Merge branch 'main' into convert_mode

This commit is contained in:
Andrew Murray 2025-09-04 21:19:41 +10:00 committed by GitHub
commit fc1680103d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
120 changed files with 1087 additions and 858 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\
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
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
# 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 ..
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

View File

@ -1 +1 @@
cibuildwheel==3.0.0
cibuildwheel==3.1.4

View File

@ -1,10 +1,11 @@
mypy==1.16.1
mypy==1.17.1
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

@ -32,7 +32,7 @@ jobs:
name: Docs
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

View File

@ -20,7 +20,7 @@ jobs:
name: Lint
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

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,6 +47,8 @@ jobs:
centos-stream-10-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
debian-13-trixie-x86,
debian-13-trixie-amd64,
fedora-41-amd64,
fedora-42-amd64,
gentoo,
@ -66,7 +68,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false

View File

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

View File

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

View File

@ -39,7 +39,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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@v5
with:
persist-credentials: false
- name: Checkout cached dependencies
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
repository: python-pillow/pillow-depends
path: winbuild\depends
- name: Checkout extra test images
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
repository: python-pillow/test-images

View File

@ -42,7 +42,6 @@ jobs:
]
python-version: [
"pypy3.11",
"pypy3.10",
"3.14t",
"3.14",
"3.13t",
@ -50,24 +49,23 @@ 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-13", 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@v5
with:
persist-credentials: false
@ -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

View File

@ -60,7 +60,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.
@ -94,16 +94,16 @@ ARCHIVE_SDIR=pillow-depends-main
# 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
HARFBUZZ_VERSION=11.3.3
LIBPNG_VERSION=1.6.50
JPEGTURBO_VERSION=3.1.2
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.8.1
ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.0
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.
ZLIB_NG_VERSION=2.2.5
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.
@ -165,7 +165,7 @@ function build_brotli {
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)
&& 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,33 +230,44 @@ 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
else
build_zlib_ng
fi
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [[ -n "$IS_MACOS" ]]; then
@ -265,13 +289,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_libpng
build_lcms2
build_openjpeg
@ -280,7 +302,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
@ -380,6 +406,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

@ -77,36 +77,36 @@ 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
cibw_arch: arm64_iphoneos
- name: "iOS arm64 simulator"
platform: ios
os: macos-latest
os: macos-14
cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator"
platform: ios
os: macos-13
cibw_arch: x86_64_iphonesimulator
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
submodules: true
@ -153,12 +153,12 @@ jobs:
- cibw_arch: ARM64
os: windows-11-arm
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Checkout extra test images
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
repository: python-pillow/test-images
@ -234,7 +234,7 @@ jobs:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
@ -256,7 +256,7 @@ jobs:
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
pattern: dist-*
path: dist
@ -278,7 +278,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
pattern: dist-*
path: dist

2
.github/zizmor.yml vendored
View File

@ -1,5 +1,5 @@
# 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:
unpinned-uses:
config:

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.2
rev: v0.12.11
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.7
rev: v21.1.0
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
@ -51,14 +51,14 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.2
rev: 0.33.3
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.12.1
hooks:
- id: zizmor
@ -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.6.0
hooks:
- id: tox-ini-fmt

View File

@ -13,6 +13,7 @@ include LICENSE
include Makefile
include tox.ini
graft Tests
graft Tests/images
graft checks
graft patches
graft src
@ -28,8 +29,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>

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.

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,10 +21,6 @@ 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)
@ -48,10 +47,6 @@ 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)
@ -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

@ -380,21 +380,28 @@ def test_palette() -> None:
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
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

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

@ -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, value):
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,
@ -731,14 +730,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()}

View File

@ -365,7 +365,6 @@ 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"
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
@ -873,7 +872,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.mode == "RGB"
assert im.size == (128, 128)
assert im.format == "TIFF"
im2 = hopper()
with hopper() as im2:
assert_image_similar(im, im2, 5)
except OSError:
captured = capfd.readouterr()

View File

@ -1,10 +1,15 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image
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 +20,13 @@ 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)

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,10 +53,7 @@ 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"):
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)
@ -81,10 +78,7 @@ 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"):
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()

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,
TiffImagePlugin,
UnidentifiedImageError,
features,
@ -389,6 +390,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")
@ -994,6 +1026,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"]
@ -1077,6 +1120,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)

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

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

@ -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,10 +28,7 @@ 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"):
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"

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)

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":
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,7 +118,8 @@ def test_text_direction_ttb_stroke() -> None:
im = Image.new(mode="RGB", size=(100, 300))
draw = ImageDraw.Draw(im)
try:
if not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
draw.text(
(27, 27),
"あい",
@ -129,9 +129,6 @@ def test_text_direction_ttb_stroke() -> None:
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")
target = "Tests/images/test_direction_ttb_stroke.png"
assert_image_similar_tofile(im, target, 19.4)
@ -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"
):
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:
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")
@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":
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":
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

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

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

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

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

View File

@ -2,14 +2,18 @@ from __future__ import annotations
import shutil
from io import BytesIO
from pathlib import Path
from typing import IO, Callable
import pytest
from PIL import GifImagePlugin, Image, JpegImagePlugin
from .helper import cjpeg_available, djpeg_available, is_win32, netpbm_available
from .helper import djpeg_available, is_win32, netpbm_available
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
from pathlib import Path
from typing import IO
TEST_JPG = "Tests/images/hopper.jpg"
TEST_GIF = "Tests/images/hopper.gif"
@ -42,11 +46,6 @@ class TestShellInjection:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
im.load_djpeg()
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
def test_save_cjpeg_filename(self, tmp_path: Path) -> None:
with Image.open(TEST_JPG) as im:
self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg)
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im:

View File

@ -1,18 +1,20 @@
#!/usr/bin/env python3
from __future__ import annotations
from typing import Any, Callable
import sys
from collections.abc import Callable
from typing import Any
import pytest
from PIL import Image
from .helper import is_win32
min_iterations = 100
max_iterations = 10000
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
pytestmark = pytest.mark.skipif(
sys.platform.startswith("win32"), reason="requires Unix or macOS"
)
def _get_mem_usage() -> float:

View File

@ -1,12 +1,11 @@
from __future__ import annotations
import sys
from io import BytesIO
import pytest
from PIL import Image
from .helper import is_win32, skip_unless_feature
from PIL import Image, features
# Limits for testing the leak
mem_limit = 1024 * 1048576
@ -15,8 +14,10 @@ iterations = int((mem_limit / stack_size) * 2)
test_file = "Tests/images/rgb_trns_ycbc.jp2"
pytestmark = [
pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"),
skip_unless_feature("jpg_2000"),
pytest.mark.skipif(
sys.platform.startswith("win32"), reason="requires Unix or macOS"
),
pytest.mark.skipif(not features.check("jpg_2000"), reason="jpg_2000 not available"),
]

View File

@ -1,10 +1,11 @@
from __future__ import annotations
import sys
from io import BytesIO
import pytest
from .helper import hopper, is_win32
from PIL import Image
iterations = 5000
@ -18,7 +19,9 @@ valgrind --tool=massif python test-installed.py -s -v checks/check_jpeg_leaks.py
"""
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
pytestmark = pytest.mark.skipif(
sys.platform.startswith("win32"), reason="requires Unix or macOS"
)
"""
pre patch:
@ -112,7 +115,7 @@ standard_chrominance_qtable = (
),
)
def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
im = hopper("RGB")
with Image.open("Tests/images/hopper.ppm") as im:
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables)
@ -173,9 +176,9 @@ def test_exif_leak() -> None:
0 +----------------------------------------------------------------------->Gi
0 11.33
"""
im = hopper("RGB")
exif = b"12345678" * 4096
with Image.open("Tests/images/hopper.ppm") as im:
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG", exif=exif)
@ -207,8 +210,7 @@ def test_base_save() -> None:
| :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@:::
0 +----------------------------------------------------------------------->Gi
0 7.882"""
im = hopper("RGB")
with Image.open("Tests/images/hopper.ppm") as im:
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG")

View File

@ -4,7 +4,6 @@ import platform
import sys
from PIL import features
from Tests.helper import is_pypy
def test_wheel_modules() -> None:
@ -25,8 +24,7 @@ def test_wheel_modules() -> None:
elif sys.platform == "ios":
# tkinter is not available on iOS
# libavif is not available on iOS (for now)
expected_modules -= {"tkinter", "avif"}
expected_modules.remove("tkinter")
assert set(features.get_supported_modules()) == expected_modules
@ -49,8 +47,6 @@ def test_wheel_features() -> None:
if sys.platform == "win32":
expected_features.remove("xcb")
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
expected_features.remove("zlib_ng")
elif sys.platform == "ios":
# Can't distribute raqm due to licensing, and there's no system version;
# fribidi and harfbuzz won't be available if raqm isn't available.

View File

@ -2,7 +2,7 @@
# install libimagequant
archive_name=libimagequant
archive_version=4.3.4
archive_version=4.4.0
archive=$archive_name-$archive_version

View File

@ -2,7 +2,7 @@
# install raqm
archive=libraqm-0.10.2
archive=libraqm-0.10.3
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -1,7 +1,7 @@
#!/bin/bash
# install webp
archive=libwebp-1.5.0
archive=libwebp-1.6.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -12,13 +12,6 @@ Deprecated features
Below are features which are considered deprecated. Where appropriate,
a :py:exc:`DeprecationWarning` is issued.
ImageDraw.getdraw hints parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
ExifTags.IFD.Makernote
^^^^^^^^^^^^^^^^^^^^^^
@ -59,6 +52,23 @@ another mode before saving::
im = Image.new("I", (1, 1))
im.convert("I;16").save("out.png")
ImageCms.ImageCmsProfile.product_name and .product_info
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 12.0.0
``ImageCms.ImageCmsProfile.product_name`` and the corresponding
``.product_info`` attributes have been deprecated, and will be removed in
Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0.
Image._show
~~~~~~~~~~~
.. deprecated:: 12.0.0
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
Use :py:meth:`~PIL.ImageShow.show` instead.
Removed features
----------------
@ -186,6 +196,7 @@ ICNS (width, height, scale) sizes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
.. versionremoved:: 12.0.0
Setting an ICNS image size to ``(width, height, scale)`` before loading has been
removed. Instead, ``load(scale)`` can be used.

View File

@ -101,6 +101,28 @@ Palette
The palette mode (``P``) uses a color palette to define the actual color for
each pixel.
.. _colors:
Colors
------
To specify colors, you can use tuples with a value for each channel in the image, e.g.
``Image.new("RGB", (1, 1), (255, 0, 0))``.
If an image has a single channel, you can use a single number instead, e.g.
``Image.new("L", (1, 1), 255)``. For "F" mode images, floating point values are also
accepted. In the case of "P" mode images, these will be indexes for the color palette.
If a single value is used for an image with more than one channel, it will still be
parsed::
>>> from PIL import Image
>>> im = Image.new("RGBA", (1, 1), 0x04030201)
>>> im.getpixel((0, 0))
(1, 2, 3, 4)
Some methods accept other forms, such as color names. See :ref:`color-names`.
Info
----

View File

@ -11,7 +11,7 @@ Here is a list of PyPI projects that offer additional plugins:
* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library.
* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL.
* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images.
* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implemetation. Python bindings implemented using pybind11.
* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implementation. Python bindings implemented using pybind11.
* :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings.
* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format.
* :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text.

View File

@ -29,10 +29,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml
:alt: GitHub Actions build status (Test MinGW)
.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml
:alt: GitHub Actions build status (Test Cygwin)
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
:alt: GitHub Actions build status (Wheels)

View File

@ -44,7 +44,7 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.7.0**
* Pillow has been tested with libtiff versions **4.0-4.7.0**
* **libfreetype** provides type related services
@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.3.4**
* Pillow has been tested with libimagequant **2.6-4.4.0**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.
@ -276,10 +276,9 @@ Build options
* Config setting: ``-C parallel=n``. Can also be given
with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
multiprocessing to build the extension. Setting ``-C parallel=n``
multiprocessing to build the extensions. Setting ``-C parallel=n``
sets the number of CPUs to use to ``n``, or can disable parallel building by
using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
available, as many as are present.
using a setting of 1. By default, it uses as many CPUs as are present.
* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,

View File

@ -1,9 +1,10 @@
Python,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5
Pillow >= 11,Yes,Yes,Yes,Yes,Yes,,,,
Pillow 10.1 - 10.4,,Yes,Yes,Yes,Yes,Yes,,,
Pillow 10.0,,,Yes,Yes,Yes,Yes,,,
Pillow 9.3 - 9.5,,,Yes,Yes,Yes,Yes,Yes,,
Pillow 9.0 - 9.2,,,,Yes,Yes,Yes,Yes,,
Pillow 8.3.2 - 8.4,,,,Yes,Yes,Yes,Yes,Yes,
Pillow 8.0 - 8.3.1,,,,,Yes,Yes,Yes,Yes,
Pillow 7.0 - 7.2,,,,,,Yes,Yes,Yes,Yes
Python,3.14,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5
Pillow 12,Yes,Yes,Yes,Yes,Yes,,,,,
Pillow 11,,Yes,Yes,Yes,Yes,Yes,,,,
Pillow 10.1 - 10.4,,,Yes,Yes,Yes,Yes,Yes,,,
Pillow 10.0,,,,Yes,Yes,Yes,Yes,,,
Pillow 9.3 - 9.5,,,,Yes,Yes,Yes,Yes,Yes,,
Pillow 9.0 - 9.2,,,,,Yes,Yes,Yes,Yes,,
Pillow 8.3.2 - 8.4,,,,,Yes,Yes,Yes,Yes,Yes,
Pillow 8.0 - 8.3.1,,,,,,Yes,Yes,Yes,Yes,
Pillow 7.0 - 7.2,,,,,,,Yes,Yes,Yes,Yes

1 Python 3.14 3.13 3.12 3.11 3.10 3.9 3.8 3.7 3.6 3.5
2 Pillow >= 11 Pillow 12 Yes Yes Yes Yes Yes Yes
3 Pillow 10.1 - 10.4 Pillow 11 Yes Yes Yes Yes Yes Yes
4 Pillow 10.0 Pillow 10.1 - 10.4 Yes Yes Yes Yes Yes
5 Pillow 9.3 - 9.5 Pillow 10.0 Yes Yes Yes Yes Yes
6 Pillow 9.0 - 9.2 Pillow 9.3 - 9.5 Yes Yes Yes Yes Yes
7 Pillow 8.3.2 - 8.4 Pillow 9.0 - 9.2 Yes Yes Yes Yes Yes
8 Pillow 8.0 - 8.3.1 Pillow 8.3.2 - 8.4 Yes Yes Yes Yes Yes
9 Pillow 7.0 - 7.2 Pillow 8.0 - 8.3.1 Yes Yes Yes Yes Yes
10 Pillow 7.0 - 7.2 Yes Yes Yes Yes

View File

@ -19,45 +19,45 @@ These platforms are built and tested for every change.
+==================================+============================+=====================+
| Alpine | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Amazon Linux 2 | 3.9 | x86-64 |
| Amazon Linux 2 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Amazon Linux 2023 | 3.9 | x86-64 |
| Amazon Linux 2023 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Arch | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 |
| CentOS Stream 9 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 10 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 13 Trixie | 3.13 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 41 | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 42 | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 13 Ventura | 3.9 | x86-64 |
| macOS 13 Ventura | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | 3.14, PyPy3 | |
| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14 | arm64 |
| | PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, 3.14, PyPy3 | |
| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | 3.14, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | arm64v8, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.9 | x86 |
| Windows Server 2022 | 3.10 | x86 |
| +----------------------------+---------------------+
| | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | 3.14, PyPy3 | |
| | 3.11, 3.12, 3.13, 3.14, | x86-64 |
| | PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 |
| +----------------------------+---------------------+
| | 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+

View File

@ -45,9 +45,7 @@ Colors
^^^^^^
To specify colors, you can use numbers or tuples just as you would use with
:py:meth:`PIL.Image.new` or :py:meth:`PIL.Image.Image.putpixel`. For “1”,
“L”, and “I” images, use integers. For “RGB” images, use a 3-tuple containing
integer values. For “F” images, use integer or floating point values.
:py:meth:`PIL.Image.new`. See :ref:`colors` for more information.
For palette images (mode “P”), use integers as color indexes. In 1.1.4 and
later, you can also use RGB 3-tuples or color names (see below). The drawing

View File

@ -74,5 +74,6 @@ Constants
---------
.. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES
.. autodata:: PIL.ImageFile.MAXBLOCK
.. autodata:: PIL.ImageFile.ERRORS
:annotation:

View File

@ -59,7 +59,7 @@ Access using negative indexes is also possible. ::
Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for
multi-band images.
multi-band images. See :ref:`colors` for more information.
:param xy: The pixel coordinate, given as (x, y).
:param color: The pixel value according to its mode,

View File

@ -53,11 +53,6 @@ on some Python versions.
An object that supports the read method.
.. py:data:: TypeGuard
:value: typing.TypeGuard
See :py:obj:`typing.TypeGuard`.
:mod:`~PIL._util` module
------------------------

View File

@ -17,6 +17,12 @@ TODO
Backwards incompatible changes
==============================
Python 3.9
^^^^^^^^^^
Pillow has dropped support for Python 3.9,
which reached end-of-life in October 2025.
ImageFile.raise_oserror
^^^^^^^^^^^^^^^^^^^^^^^
@ -110,10 +116,18 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
Deprecations
============
TODO
^^^^
Image._show
^^^^^^^^^^^
TODO
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
Use :py:meth:`~PIL.ImageShow.show` instead.
ImageCms.ImageCmsProfile.product_name and .product_info
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``ImageCms.ImageCmsProfile.product_name`` and the corresponding
``.product_info`` attributes have been deprecated, and will be removed in
Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0.
API changes
===========
@ -134,7 +148,18 @@ TODO
Other changes
=============
TODO
^^^^
Python 3.14
^^^^^^^^^^^
TODO
Pillow 11.3.0 had wheels built against Python 3.14 beta, available as a preview to help
others prepare for 3.14, and to ensure Pillow could be used immediately at the release
of 3.14.0 final (2025-10-07, :pep:`745`).
Pillow 12.0.0 now officially supports Python 3.14.
ImageMorph operations must have length 1
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Valid ImageMorph operations are 4, N, 1 and M. By limiting the length to 1 character
within Pillow, long execution times can be avoided if a user provided long pattern
strings. Reported by Jang Choi.

View File

@ -1,42 +0,0 @@
# libwebp example binaries require dependencies that aren't available for iOS builds.
# There's also no easy way to invoke the build to *exclude* the example builds.
# Since we don't need the examples anyway, remove them from the Makefile.
#
# As a point of reference, libwebp provides an XCFramework build script that involves
# 7 separate invocations of make to avoid building the examples. Patching the Makefile
# to remove the examples is a simpler approach, and one that is more compatible with
# the existing multibuild infrastructure.
#
# In the next release, it should be possible to pass --disable-libwebpexamples
# instead of applying this patch.
#
diff -ur libwebp-1.5.0-orig/Makefile.am libwebp-1.5.0/Makefile.am
--- libwebp-1.5.0-orig/Makefile.am 2024-12-20 09:17:50
+++ libwebp-1.5.0/Makefile.am 2025-01-09 11:24:17
@@ -5,5 +5,3 @@
if BUILD_EXTRAS
SUBDIRS += extras
endif
-
-SUBDIRS += examples
diff -ur libwebp-1.5.0-orig/Makefile.in libwebp-1.5.0/Makefile.in
--- libwebp-1.5.0-orig/Makefile.in 2024-12-20 09:52:53
+++ libwebp-1.5.0/Makefile.in 2025-01-09 11:24:17
@@ -156,7 +156,7 @@
unique=`for i in $$list; do \
if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
done | $(am__uniquify_input)`
-DIST_SUBDIRS = sharpyuv src imageio man extras examples
+DIST_SUBDIRS = sharpyuv src imageio man extras
am__DIST_COMMON = $(srcdir)/Makefile.in \
$(top_srcdir)/src/webp/config.h.in AUTHORS COPYING ChangeLog \
NEWS README.md ar-lib compile config.guess config.sub \
@@ -351,7 +351,7 @@
top_srcdir = @top_srcdir@
webp_libname_prefix = @webp_libname_prefix@
ACLOCAL_AMFLAGS = -I m4
-SUBDIRS = sharpyuv src imageio man $(am__append_1) examples
+SUBDIRS = sharpyuv src imageio man $(am__append_1)
EXTRA_DIST = COPYING autogen.sh
all: all-recursive

View File

@ -1,6 +1,7 @@
[build-system]
build-backend = "backend"
requires = [
"pybind11",
"setuptools>=77",
]
backend-path = [
@ -19,15 +20,15 @@ license-files = [ "LICENSE" ]
authors = [
{ name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
]
requires-python = ">=3.9"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 6 - Mature",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Multimedia :: Graphics",
@ -66,7 +67,7 @@ optional-dependencies.tests = [
"markdown2",
"olefile",
"packaging",
"pyroma",
"pyroma>=5",
"pytest",
"pytest-cov",
"pytest-timeout",
@ -74,9 +75,6 @@ optional-dependencies.tests = [
"trove-classifiers>=2024.10.12",
]
optional-dependencies.typing = [
"typing-extensions; python_version<'3.10'",
]
optional-dependencies.xmp = [
"defusedxml",
]
@ -187,8 +185,8 @@ lint.ignore = [
"PT011", # pytest-raises-too-broad
"PT012", # pytest-raises-with-multiple-statements
"PT017", # pytest-assert-in-except
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
"UP038", # pyupgrade: deprecated rule
]
lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [
"I002",
@ -205,7 +203,7 @@ lint.isort.required-imports = [
]
[tool.pyproject-fmt]
max_supported_python = "3.13"
max_supported_python = "3.14"
[tool.pytest.ini_options]
addopts = "-ra --color=auto"
@ -214,7 +212,7 @@ testpaths = [
]
[tool.mypy]
python_version = "3.9"
python_version = "3.10"
pretty = true
disallow_any_generics = true
enable_error_code = "ignore-without-code"

View File

@ -17,9 +17,20 @@ import sys
import warnings
from collections.abc import Iterator
from pybind11.setup_helpers import ParallelCompile
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
configuration: dict[str, list[str]] = {}
# parse configuration from _custom_build/backend.py
while sys.argv[-1].startswith("--pillow-configuration="):
_, key, value = sys.argv.pop().split("=", 2)
configuration.setdefault(key, []).append(value)
default = int(configuration.get("parallel", ["0"])[-1])
ParallelCompile("MAX_CONCURRENCY", default).install()
def get_version() -> str:
version_file = "src/PIL/_version.py"
@ -27,9 +38,6 @@ def get_version() -> str:
return f.read().split('"')[1]
configuration: dict[str, list[str]] = {}
PILLOW_VERSION = get_version()
AVIF_ROOT = None
FREETYPE_ROOT = None
@ -386,9 +394,7 @@ class pil_build_ext(build_ext):
cpu_count = os.cpu_count()
if cpu_count is not None:
try:
self.parallel = int(
os.environ.get("MAX_CONCURRENCY", min(4, cpu_count))
)
self.parallel = int(os.environ.get("MAX_CONCURRENCY", cpu_count))
except TypeError:
pass
for x in self.feature:
@ -1083,11 +1089,6 @@ ext_modules = [
]
# parse configuration from _custom_build/backend.py
while sys.argv[-1].startswith("--pillow-configuration="):
_, key, value = sys.argv.pop().split("=", 2)
configuration.setdefault(key, []).append(value)
try:
setup(
cmdclass={"build_ext": pil_build_ext},

View File

@ -30,7 +30,7 @@ from ._util import DeferredError
def _accept(prefix: bytes) -> bool:
return (
len(prefix) >= 6
len(prefix) >= 16
and i16(prefix, 4) in [0xAF11, 0xAF12]
and i16(prefix, 14) in [0, 3] # flags
)

View File

@ -54,7 +54,7 @@ class GbrImageFile(ImageFile.ImageFile):
width = i32(self.fp.read(4))
height = i32(self.fp.read(4))
color_depth = i32(self.fp.read(4))
if width <= 0 or height <= 0:
if width == 0 or height == 0:
msg = "not a GIMP brush"
raise SyntaxError(msg)
if color_depth not in (1, 4):
@ -71,7 +71,7 @@ class GbrImageFile(ImageFile.ImageFile):
raise SyntaxError(msg)
self.info["spacing"] = i32(self.fp.read(4))
comment = self.fp.read(comment_length)[:-1]
self.info["comment"] = self.fp.read(comment_length)[:-1]
if color_depth == 1:
self._mode = "L"
@ -80,8 +80,6 @@ class GbrImageFile(ImageFile.ImageFile):
self._size = width, height
self.info["comment"] = comment
# Image might not be small
Image._decompression_bomb_check(self.size)

View File

@ -31,7 +31,7 @@ import os
import subprocess
from enum import IntEnum
from functools import cached_property
from typing import IO, Any, Literal, NamedTuple, Union, cast
from typing import Any, NamedTuple, cast
from . import (
Image,
@ -49,6 +49,8 @@ from ._util import DeferredError
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import IO, Literal
from . import _imaging
from ._typing import Buffer
@ -535,7 +537,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
return im.convert("L")
_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]
_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette
def _normalize_palette(

View File

@ -21,10 +21,14 @@ See the GIMP distribution for more information.)
from __future__ import annotations
from math import log, pi, sin, sqrt
from typing import IO, Callable
from ._binary import o8
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
from typing import IO
EPSILON = 1e-10
"""""" # Enable auto-doc for data member

View File

@ -17,7 +17,10 @@ from __future__ import annotations
import re
from io import BytesIO
from typing import IO
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import IO
class GimpPaletteFile:

View File

@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"GRIB") and prefix[7] == 1
return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1
class GribStubImageFile(ImageFile.StubImageFile):

View File

@ -38,10 +38,9 @@ import struct
import sys
import tempfile
import warnings
from collections.abc import Callable, Iterator, MutableMapping, Sequence
from collections.abc import MutableMapping
from enum import IntEnum
from types import ModuleType
from typing import IO, Any, Literal, Protocol, cast
from typing import IO, Protocol, cast
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
@ -64,6 +63,12 @@ try:
except ImportError:
ElementTree = None
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Iterator, Sequence
from types import ModuleType
from typing import Any, Literal
logger = logging.getLogger(__name__)
@ -98,7 +103,6 @@ try:
raise ImportError(msg)
except ImportError as v:
core = DeferredError.new(ImportError("The _imaging C module is not installed."))
# Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for
@ -1732,7 +1736,8 @@ class Image:
Instead of an image, the source can be a integer or tuple
containing pixel values. The method then fills the region
with the given color. When creating RGB images, you can
also use color strings as supported by the ImageColor module.
also use color strings as supported by the ImageColor module. See
:ref:`colors` for more information.
If a mask is given, this method updates only the regions
indicated by the mask. You can use either "1", "L", "LA", "RGBA"
@ -1988,7 +1993,8 @@ class Image:
sequence ends. The scale and offset values are used to adjust the
sequence values: **pixel = value*scale + offset**.
:param data: A flattened sequence object.
:param data: A flattened sequence object. See :ref:`colors` for more
information about values.
:param scale: An optional scale value. The default is 1.0.
:param offset: An optional offset value. The default is 0.0.
"""
@ -2047,7 +2053,7 @@ class Image:
Modifies the pixel at the given position. The color is given as
a single numerical value for single-band images, and a tuple for
multi-band images. In addition to this, RGB and RGBA tuples are
accepted for P and PA images.
accepted for P and PA images. See :ref:`colors` for more information.
Note that this method is relatively slow. For more extensive changes,
use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
@ -2694,7 +2700,9 @@ class Image:
:param title: Optional title to use for the image window, where possible.
"""
_show(self, title=title)
from . import ImageShow
ImageShow.show(self, title)
def split(self) -> tuple[Image, ...]:
"""
@ -3123,12 +3131,12 @@ def new(
:param mode: The mode to use for the new image. See:
:ref:`concept-modes`.
:param size: A 2-tuple, containing (width, height) in pixels.
:param color: What color to use for the image. Default is black.
If given, this should be a single integer or floating point value
for single-band modes, and a tuple for multi-band modes (one value
per band). When creating RGB or HSV images, you can also use color
strings as supported by the ImageColor module. If the color is
None, the image is not initialised.
:param color: What color to use for the image. Default is black. If given,
this should be a single integer or floating point value for single-band
modes, and a tuple for multi-band modes (one value per band). When
creating RGB or HSV images, you can also use color strings as supported
by the ImageColor module. See :ref:`colors` for more information. If the
color is None, the image is not initialised.
:returns: An :py:class:`~PIL.Image.Image` object.
"""
@ -3632,9 +3640,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image:
"""
Alpha composite im2 over im1.
:param im1: The first image. Must have mode RGBA.
:param im2: The second image. Must have mode RGBA, and the same size as
the first image.
:param im1: The first image. Must have mode RGBA or LA.
:param im2: The second image. Must have the same mode and size as the first image.
:returns: An :py:class:`~PIL.Image.Image` object.
"""
@ -3860,6 +3867,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None:
def _show(image: Image, **options: Any) -> None:
from . import ImageShow
deprecate("Image._show", 13, "ImageShow.show")
ImageShow.show(image, **options)
@ -4281,6 +4289,8 @@ class Exif(_ExifBase):
del self._info[tag]
else:
del self._data[tag]
if tag in self._ifds:
del self._ifds[tag]
def __iter__(self) -> Iterator[int]:
keys = set(self._data)

View File

@ -23,9 +23,10 @@ import operator
import sys
from enum import IntEnum, IntFlag
from functools import reduce
from typing import Literal, SupportsFloat, SupportsInt, Union
from typing import Any, Literal, SupportsFloat, SupportsInt, Union
from . import Image
from ._deprecate import deprecate
from ._typing import SupportsRead
try:
@ -233,9 +234,7 @@ class ImageCmsProfile:
low-level profile object
"""
self.filename = None
self.product_name = None # profile.product_name
self.product_info = None # profile.product_info
self.filename: str | None = None
if isinstance(profile, str):
if sys.platform == "win32":
@ -256,6 +255,13 @@ class ImageCmsProfile:
msg = "Invalid type for Profile" # type: ignore[unreachable]
raise TypeError(msg)
def __getattr__(self, name: str) -> Any:
if name in ("product_name", "product_info"):
deprecate(f"ImageCms.ImageCmsProfile.{name}", 13)
return None
msg = f"'{self.__class__.__name__}' object has no attribute '{name}'"
raise AttributeError(msg)
def tobytes(self) -> bytes:
"""
Returns the profile in a format suitable for embedding in

View File

@ -34,20 +34,23 @@ from __future__ import annotations
import math
import struct
from collections.abc import Sequence
from types import ModuleType
from typing import Any, AnyStr, Callable, Union, cast
from typing import cast
from . import Image, ImageColor
from ._typing import Coords
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
from types import ModuleType
from typing import Any, AnyStr
from . import ImageDraw2, ImageFont
from ._typing import Coords
# experimental access to the outline API
Outline: Callable[[], Image.core._Outline] = Image.core.outline
TYPE_CHECKING = False
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont
_Ink = Union[float, tuple[int, ...], str]
_Ink = float | tuple[int, ...] | str
"""
A simple 2D drawing interface for PIL images.

View File

@ -46,6 +46,18 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
MAXBLOCK = 65536
"""
By default, Pillow processes image data in blocks. This helps to prevent excessive use
of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``.
When reading an image, this is the number of bytes to read at once.
When writing an image, this is the number of bytes to write at once.
If the image width times 4 is greater, then that will be used instead.
Plugins may also set a greater number.
User code may set this to another number.
"""
SAFEBLOCK = 1024 * 1024

View File

@ -19,11 +19,14 @@ from __future__ import annotations
import abc
import functools
from collections.abc import Sequence
from types import ModuleType
from typing import Any, Callable, cast
from typing import cast
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
from types import ModuleType
from typing import Any
from . import _imaging
from ._typing import NumpyArray

View File

@ -671,11 +671,7 @@ class FreeTypeFont:
:returns: A list of the named styles in a variation font.
:exception OSError: If the font is not a variation font.
"""
try:
names = self.font.getvarnames()
except AttributeError as e:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
return [name.replace(b"\x00", b"") for name in names]
def set_variation_by_name(self, name: str | bytes) -> None:
@ -702,11 +698,7 @@ class FreeTypeFont:
:returns: A list of the axes in a variation font.
:exception OSError: If the font is not a variation font.
"""
try:
axes = self.font.getvaraxes()
except AttributeError as e:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
for axis in axes:
if axis["name"]:
axis["name"] = axis["name"].replace(b"\x00", b"")
@ -717,11 +709,7 @@ class FreeTypeFont:
:param axes: A list of values for each axis.
:exception OSError: If the font is not a variation font.
"""
try:
self.font.setvaraxes(axes)
except AttributeError as e:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
class TransposedFont:

View File

@ -17,11 +17,15 @@
from __future__ import annotations
import builtins
from types import CodeType
from typing import Any, Callable
from . import Image, _imagingmath
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
from types import CodeType
from typing import Any
class _Operand:
"""Wraps an image operand, providing standard operators"""

View File

@ -150,7 +150,7 @@ class LutBuilder:
# Parse and create symmetries of the patterns strings
for p in self.patterns:
m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
if not m:
msg = 'Syntax error in pattern "' + p + '"'
raise Exception(msg)

View File

@ -19,23 +19,18 @@ from __future__ import annotations
import sys
from io import BytesIO
from typing import Any, Callable, Union
from . import Image
from ._util import is_path
TYPE_CHECKING = False
if TYPE_CHECKING:
import PyQt6
import PySide6
from collections.abc import Callable
from typing import Any
from . import ImageFile
QBuffer: type
QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray]
QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
qt_version: str | None
qt_versions = [
@ -49,11 +44,15 @@ for version, qt_module in qt_versions:
try:
qRgba: Callable[[int, int, int, int], int]
if qt_module == "PyQt6":
from PyQt6.QtCore import QBuffer, QIODevice
from PyQt6.QtCore import QBuffer, QByteArray, QIODevice
from PyQt6.QtGui import QImage, QPixmap, qRgba
elif qt_module == "PySide6":
from PySide6.QtCore import QBuffer, QIODevice
from PySide6.QtGui import QImage, QPixmap, qRgba
from PySide6.QtCore import ( # type: ignore[assignment]
QBuffer,
QByteArray,
QIODevice,
)
from PySide6.QtGui import QImage, QPixmap, qRgba # type: ignore[assignment]
except (ImportError, RuntimeError):
continue
qt_is_installed = True
@ -183,7 +182,7 @@ def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]:
if qt_is_installed:
class ImageQt(QImage): # type: ignore[misc]
class ImageQt(QImage):
def __init__(self, im: Image.Image | str | QByteArray) -> None:
"""
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage

View File

@ -16,10 +16,12 @@
##
from __future__ import annotations
from typing import Callable
from . import Image
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
class Iterator:
"""

View File

@ -120,7 +120,7 @@ class Stat:
@cached_property
def mean(self) -> list[float]:
"""Average (arithmetic mean) pixel level for each band in the image."""
return [self.sum[i] / self.count[i] for i in self.bands]
return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands]
@cached_property
def median(self) -> list[int]:
@ -141,13 +141,20 @@ class Stat:
@cached_property
def rms(self) -> list[float]:
"""RMS (root-mean-square) for each band in the image."""
return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
return [
math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0
for i in self.bands
]
@cached_property
def var(self) -> list[float]:
"""Variance for each band in the image."""
return [
(
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
if self.count[i]
else 0
)
for i in self.bands
]

View File

@ -34,10 +34,6 @@ def _i(c: bytes) -> int:
return i32((b"\0\0\0\0" + c)[-4:])
def _i8(c: int | bytes) -> int:
return c if isinstance(c, int) else c[0]
##
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
@ -100,16 +96,18 @@ class IptcImageFile(ImageFile.ImageFile):
# mode
layers = self.info[(3, 60)][0]
component = self.info[(3, 60)][1]
if (3, 65) in self.info:
id = self.info[(3, 65)][0] - 1
else:
id = 0
if layers == 1 and not component:
self._mode = "L"
elif layers == 3 and component:
self._mode = "RGB"[id]
band = None
else:
if layers == 3 and component:
self._mode = "RGB"
elif layers == 4 and component:
self._mode = "CMYK"[id]
self._mode = "CMYK"
if (3, 65) in self.info:
band = self.info[(3, 65)][0] - 1
else:
band = 0
# size
self._size = self.getint((3, 20)), self.getint((3, 30))
@ -124,16 +122,16 @@ class IptcImageFile(ImageFile.ImageFile):
# tile
if tag == (8, 10):
self.tile = [
ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression)
ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band))
]
def load(self) -> Image.core.PixelAccess | None:
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
return ImageFile.ImageFile.load(self)
if self.tile:
args = self.tile[0].args
assert isinstance(args, tuple)
compression, band = args
offset, compression = self.tile[0][2:]
self.fp.seek(offset)
self.fp.seek(self.tile[0].offset)
# Copy image data to temporary file
o = BytesIO()
@ -153,10 +151,15 @@ class IptcImageFile(ImageFile.ImageFile):
size -= len(s)
with Image.open(o) as _im:
if band is not None:
bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
bands[band] = _im
_im = Image.merge(self.mode, bands)
else:
_im.load()
self.im = _im.im
self.tile = []
return Image.Image.load(self)
return ImageFile.ImageFile.load(self)
Image.register_open(IptcImageFile.format, IptcImageFile)

View File

@ -18,11 +18,15 @@ from __future__ import annotations
import io
import os
import struct
from collections.abc import Callable
from typing import IO, cast
from typing import cast
from . import Image, ImageFile, ImagePalette, _binary
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
from typing import IO
class BoxReader:
"""

View File

@ -42,7 +42,6 @@ import subprocess
import sys
import tempfile
import warnings
from typing import IO, Any
from . import Image, ImageFile
from ._binary import i16be as i16
@ -53,6 +52,8 @@ from .JpegPresets import presets
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import IO, Any
from .MpoImagePlugin import MpoImageFile
#
@ -845,16 +846,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
)
def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# ALTERNATIVE: handle JPEGs via the IJG command line utilities.
tempfile = im._dump()
subprocess.check_call(["cjpeg", "-outfile", filename, tempfile])
try:
os.unlink(tempfile)
except OSError:
pass
##
# Factory for making JPEG and MPO instances
def jpeg_factory(

View File

@ -32,7 +32,7 @@ class PcdImageFile(ImageFile.ImageFile):
assert self.fp is not None
self.fp.seek(2048)
s = self.fp.read(2048)
s = self.fp.read(1539)
if not s.startswith(b"PCD_"):
msg = "not a PCD file"
@ -46,14 +46,13 @@ class PcdImageFile(ImageFile.ImageFile):
self.tile_post_rotate = -90
self._mode = "RGB"
self._size = 768, 512 # FIXME: not correct for rotated images!
self._size = (512, 768) if orientation in (1, 3) else (768, 512)
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]
def load_end(self) -> None:
if self.tile_post_rotate:
# Handle rotated PCDs
self.im = self.im.rotate(self.tile_post_rotate)
self._size = self.im.size
#

View File

@ -18,7 +18,6 @@
from __future__ import annotations
import io
from typing import BinaryIO, Callable
from . import FontFile, Image
from ._binary import i8
@ -27,6 +26,11 @@ from ._binary import i16le as l16
from ._binary import i32be as b32
from ._binary import i32le as l32
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
from typing import BinaryIO
# --------------------------------------------------------------------
# declarations

View File

@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
def _accept(prefix: bytes) -> bool:
return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
return len(prefix) >= 2 and prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
##

View File

@ -8,7 +8,15 @@ import os
import re
import time
import zlib
from typing import IO, Any, NamedTuple, Union
from typing import Any, NamedTuple
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import IO
_DictBase = collections.UserDict[str | bytes, Any]
else:
_DictBase = collections.UserDict
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@ -251,13 +259,6 @@ class PdfArray(list[Any]):
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
TYPE_CHECKING = False
if TYPE_CHECKING:
_DictBase = collections.UserDict[Union[str, bytes], Any]
else:
_DictBase = collections.UserDict
class PdfDict(_DictBase):
def __setattr__(self, key: str, value: Any) -> None:
if key == "data":

View File

@ -38,9 +38,8 @@ import re
import struct
import warnings
import zlib
from collections.abc import Callable
from enum import IntEnum
from typing import IO, Any, NamedTuple, NoReturn, cast
from typing import IO, NamedTuple, cast
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16
@ -53,6 +52,9 @@ from ._util import DeferredError
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
from typing import Any, NoReturn
from . import _imaging
logger = logging.getLogger(__name__)

View File

@ -47,7 +47,7 @@ MODES = {
def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"P") and prefix[1] in b"0123456fy"
return len(prefix) >= 2 and prefix.startswith(b"P") and prefix[1] in b"0123456fy"
##

View File

@ -47,22 +47,24 @@ import math
import os
import struct
import warnings
from collections.abc import Iterator, MutableMapping
from collections.abc import Callable, MutableMapping
from fractions import Fraction
from numbers import Number, Rational
from typing import IO, Any, Callable, NoReturn, cast
from typing import IO, Any, cast
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
from ._typing import StrOrBytesPath
from ._util import DeferredError, is_path
from .TiffTags import TYPES
TYPE_CHECKING = False
if TYPE_CHECKING:
from ._typing import Buffer, IntegralLike
from collections.abc import Iterator
from typing import NoReturn
from ._typing import Buffer, IntegralLike, StrOrBytesPath
logger = logging.getLogger(__name__)

View File

@ -49,8 +49,7 @@ class WalImageFile(ImageFile.ImageFile):
# strings are null-terminated
self.info["name"] = header[:32].split(b"\0", 1)[0]
next_name = header[56 : 56 + 32].split(b"\0", 1)[0]
if next_name:
if next_name := header[56 : 56 + 32].split(b"\0", 1)[0]:
self.info["next_name"] = next_name
def load(self) -> Image.core.PixelAccess | None:

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