Merge branch 'main' into mode_enums

This commit is contained in:
Andrew Murray 2025-08-28 08:36:22 +10:00 committed by GitHub
commit b33254f370
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 538 additions and 546 deletions

View File

@ -13,24 +13,21 @@ aptget_update()
return 1 return 1
fi fi
} }
if [[ $(uname) != CYGWIN* ]]; then aptget_update || aptget_update retry || aptget_update retry
aptget_update || aptget_update retry || aptget_update retry
fi
set -e 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\ ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev nasm sway wl-clipboard libopenblas-dev nasm
fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install ipython python3 -m pip install ipython
python3 -m pip install numpy
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
@ -40,36 +37,24 @@ python3 -m pip install pyroma
# fails on beta 3.14 and PyPy # fails on beta 3.14 and PyPy
python3 -m pip install --only-binary=:all: pyarrow || true python3 -m pip install --only-binary=:all: pyarrow || true
if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3
python3 -m pip install numpy 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 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 # TODO Update condition when pyqt6 supports free-threading
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi 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 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.1.2 cibuildwheel==3.1.4

View File

@ -1,4 +1,4 @@
mypy==1.17.0 mypy==1.17.1
IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6 IceSpringPySideStubs-PySide6
ipython ipython

1
.github/mergify.yml vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,11 +35,11 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.11", "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"] architecture: ["x64"]
include: include:
# Test the oldest Python on 32-bit # Test the oldest Python on 32-bit
- { python-version: "3.9", architecture: "x86" } - { python-version: "3.10", architecture: "x86" }
timeout-minutes: 45 timeout-minutes: 45
@ -47,19 +47,19 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images

View File

@ -49,24 +49,23 @@ jobs:
"3.12", "3.12",
"3.11", "3.11",
"3.10", "3.10",
"3.9",
] ]
include: include:
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 } - { python-version: "3.11", PYTHONOPTIMIZE: 2 }
# Free-threaded # Free-threaded
- { python-version: "3.14t", disable-gil: true } - { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true } - { python-version: "3.13t", disable-gil: true }
# M1 only available for 3.10+ # Intel
- { os: "macos-13", python-version: "3.9" } - { os: "macos-13", python-version: "3.10" }
exclude: exclude:
- { os: "macos-latest", python-version: "3.9" } - { os: "macos-latest", python-version: "3.10" }
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@ -112,7 +111,7 @@ jobs:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher - 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" run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build - name: Build

View File

@ -94,7 +94,7 @@ ARCHIVE_SDIR=pillow-depends-main
# annotations have a source code patch that is required for some platforms. If # annotations have a source code patch that is required for some platforms. If
# you change those versions, ensure the patch is also updated. # you change those versions, ensure the patch is also updated.
FREETYPE_VERSION=2.13.3 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.2.1 HARFBUZZ_VERSION=11.3.3
LIBPNG_VERSION=1.6.50 LIBPNG_VERSION=1.6.50
JPEGTURBO_VERSION=3.1.1 JPEGTURBO_VERSION=3.1.1
OPENJPEG_VERSION=2.5.3 OPENJPEG_VERSION=2.5.3
@ -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) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \ (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 . \ && 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 touch brotli-stamp
} }
@ -186,30 +186,43 @@ function build_libavif {
python3 -m pip install meson ninja 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 build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
fi fi
local build_type=MinSizeRel local build_type=MinSizeRel
local build_shared=ON
local lto=ON local lto=ON
local libavif_cmake_flags local libavif_cmake_flags
if [ -n "$IS_MACOS" ]; then if [[ -n "$IS_MACOS" ]]; then
lto=OFF lto=OFF
libavif_cmake_flags=( libavif_cmake_flags=(
-DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ -DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ -DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \ -DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \
) )
if [[ -n "$IOS_SDK" ]]; then
build_shared=OFF
fi
else else
if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then
build_type=Release build_type=Release
fi fi
libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now")
fi 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) 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 # CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject
# of libavif) that disables support for encoding high bit depth images. # of libavif) that disables support for encoding high bit depth images.
(cd $out_dir \ (cd $out_dir \
@ -217,20 +230,27 @@ function build_libavif {
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
-DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \ -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \
-DBUILD_SHARED_LIBS=ON \ -DBUILD_SHARED_LIBS=$build_shared \
-DAVIF_LIBSHARPYUV=LOCAL \ -DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \ -DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_AOM=LOCAL \ -DAVIF_CODEC_AOM=LOCAL \
-DCONFIG_AV1_HIGHBITDEPTH=0 \ -DCONFIG_AV1_HIGHBITDEPTH=0 \
-DAVIF_CODEC_AOM_DECODE=OFF \
-DAVIF_CODEC_DAV1D=LOCAL \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \ -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
-DCMAKE_C_VISIBILITY_PRESET=hidden \ -DCMAKE_C_VISIBILITY_PRESET=hidden \
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \ -DCMAKE_CXX_VISIBILITY_PRESET=hidden \
-DCMAKE_BUILD_TYPE=$build_type \ -DCMAKE_BUILD_TYPE=$build_type \
"${libavif_cmake_flags[@]}" \ "${libavif_cmake_flags[@]}" \
. \ $HOST_CMAKE_FLAGS . )
&& make install)
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 touch libavif-stamp
} }
@ -268,10 +288,7 @@ function build {
build_tiff build_tiff
fi fi
if [[ -z "$IOS_SDK" ]]; then
# Short term workaround; don't build libavif on iOS
build_libavif build_libavif
fi
build_libpng build_libpng
build_lcms2 build_lcms2
build_openjpeg build_openjpeg

View File

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

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.2 rev: v0.12.7
hooks: hooks:
- id: ruff-check - id: ruff-check
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.7 rev: v20.1.8
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -57,7 +57,7 @@ repos:
- id: check-readthedocs - id: check-readthedocs
- id: check-renovate - id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit - repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.11.0 rev: v1.11.0
hooks: hooks:
- id: zizmor - id: zizmor
@ -79,7 +79,7 @@ repos:
additional_dependencies: [trove-classifiers>=2024.10.12] additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt - repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.5.0 rev: 1.6.0
hooks: hooks:
- id: tox-ini-fmt - id: tox-ini-fmt

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 <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
alt="GitHub Actions build status (Test MinGW)" alt="GitHub Actions build status (Test MinGW)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a> 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 <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
alt="GitHub Actions build status (Test Docker)" alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>

View File

@ -10,17 +10,20 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from collections.abc import Sequence
from functools import lru_cache from functools import lru_cache
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import Any, Callable
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
from PIL import Image, ImageFile, ImageMath, features 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__) logger = logging.getLogger(__name__)
uploader = None uploader = None
@ -172,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
return pytest.mark.skipif(not features.check(feature), reason=reason) 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( def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator: ) -> pytest.MarkDecorator:

View File

@ -2,7 +2,6 @@ from __future__ import annotations
import io import io
import re import re
from typing import Callable
import pytest import pytest
@ -10,6 +9,10 @@ from PIL import features
from .helper import skip_unless_feature from .helper import skip_unless_feature
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
def test_check() -> None: def test_check() -> None:
# Check the correctness of the convenience function # Check the correctness of the convenience function

View File

@ -1,8 +1,10 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
import pytest import pytest
from PIL import GbrImagePlugin, Image from PIL import GbrImagePlugin, Image, _binary
from .helper import assert_image_equal_tofile 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") assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_invalid_file() -> None: def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
invalid_file = "Tests/images/flower.jpg" 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) GbrImagePlugin.GbrImageFile(invalid_file)
def test_unsupported_gimp_brush() -> None:
f = create_gbr_image({"color_depth": 2})
with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"):
GbrImagePlugin.GbrImageFile(f)
def test_bad_magic_number() -> None:
f = create_gbr_image({"version": 2}, magic_number=b"badm")
with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"):
GbrImagePlugin.GbrImageFile(f)
def test_L() -> None:
f = create_gbr_image()
with Image.open(f) as im:
assert im.mode == "L"

View File

@ -1,10 +1,15 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image from PIL import Image
def test_load_raw() -> None: def test_load_raw() -> None:
with Image.open("Tests/images/hopper.pcd") as im: with Image.open("Tests/images/hopper.pcd") as im:
assert im.size == (768, 512)
im.load() # should not segfault. im.load() # should not segfault.
# Note that this image was created with a resized hopper # Note that this image was created with a resized hopper
@ -15,3 +20,13 @@ def test_load_raw() -> None:
# target = hopper().resize((768,512)) # target = hopper().resize((768,512))
# assert_image_similar(im, target, 10) # 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

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

View File

@ -2,12 +2,15 @@ from __future__ import annotations
import colorsys import colorsys
import itertools import itertools
from typing import Callable
from PIL import Image from PIL import Image
from .helper import assert_image_similar, hopper 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: def int_to_float(i: int) -> float:
return i / 255 return i / 255

View File

@ -388,6 +388,37 @@ class TestImage:
assert img_colors is not None assert img_colors is not None
assert sorted(img_colors) == expected_colors 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: def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue") src = Image.new("RGBA", (128, 128), "blue")
@ -922,6 +953,17 @@ class TestImage:
reloaded_exif.load(exif.tobytes()) reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) 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: def test_exif_load_from_fp(self) -> None:
with Image.open("Tests/images/flower.jpg") as im: with Image.open("Tests/images/flower.jpg") as im:
data = im.info["exif"] data = im.info["exif"]

View File

@ -1,11 +1,16 @@
from __future__ import annotations from __future__ import annotations
import pytest 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: def test_sanity() -> None:
@ -23,10 +28,7 @@ def test_sanity() -> None:
@skip_unless_feature("libimagequant") @skip_unless_feature("libimagequant")
def test_libimagequant_quantize() -> None: def test_libimagequant_quantize() -> None:
image = hopper() image = hopper()
if is_ppc64le(): if is_ppc64le() and not has_feature_version("libimagequant", "4"):
version = features.version_feature("libimagequant")
assert version is not None
if parse_version(version) < parse_version("4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P" assert converted.mode == "P"

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import shutil
import sys import sys
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any, Literal, cast from typing import Literal, cast
import pytest import pytest
@ -31,6 +31,9 @@ except ImportError:
# Skipped via setup_module() # Skipped via setup_module()
pass pass
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any
SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc"
HAVE_PROFILE = os.path.exists(SRGB) HAVE_PROFILE = os.path.exists(SRGB)
@ -208,9 +211,10 @@ def test_exceptions() -> None:
ImageCms.getProfileName(None) # type: ignore[arg-type] ImageCms.getProfileName(None) # type: ignore[arg-type]
skip_missing() skip_missing()
# Python <= 3.9: "an integer is required (got type NoneType)" with pytest.raises(
# Python > 3.9: "'NoneType' object cannot be interpreted as an integer" ImageCms.PyCMSError,
with pytest.raises(ImageCms.PyCMSError, match="integer"): match="'NoneType' object cannot be interpreted as an integer",
):
ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type] ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type]

View File

@ -1,13 +1,10 @@
from __future__ import annotations from __future__ import annotations
import os.path import os.path
from collections.abc import Sequence
from typing import Callable
import pytest import pytest
from PIL import Image, ImageColor, ImageDraw, ImageFont, features from PIL import Image, ImageColor, ImageDraw, ImageFont, features
from PIL._typing import Coords
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -17,6 +14,12 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from PIL._typing import Coords
BLACK = (0, 0, 0) BLACK = (0, 0, 0)
WHITE = (255, 255, 255) WHITE = (255, 255, 255)
GRAY = (190, 190, 190) GRAY = (190, 190, 190)

View File

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

View File

@ -57,3 +57,13 @@ def test_constant() -> None:
assert st.rms[0] == 128 assert st.rms[0] == 128
assert st.var[0] == 0 assert st.var[0] == 0
assert st.stddev[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 = numpy.array(data, dtype=dtype)
a.shape = TEST_IMAGE_SIZE a.shape = TEST_IMAGE_SIZE
i = Image.fromarray(a) i = Image.fromarray(a)
if list(i.getdata()) != data: assert list(i.getdata()) == data
print("data mismatch for", dtype)
else: else:
data = list(range(100)) data = list(range(100))
a = numpy.array([[x] * bands for x in data], dtype=dtype) a = numpy.array([[x] * bands for x in data], dtype=dtype)
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
i = Image.fromarray(a) i = Image.fromarray(a)
if list(i.getchannel(0).getdata()) != list(range(100)): assert list(i.getchannel(0).getdata()) == list(range(100))
print("data mismatch for", dtype)
return i return i
# Check supported 1-bit integer formats # Check supported 1-bit integer formats

View File

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

View File

@ -1,13 +1,15 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import ImageQt from PIL import ImageQt
from .helper import assert_image_equal, assert_image_equal_tofile, hopper 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( pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed" 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) src = hopper(mode)
data = ImageQt.toqimage(src) data = ImageQt.toqimage(src)
assert isinstance(data, QImage) # type: ignore[arg-type, misc] assert isinstance(data, QImage)
assert not data.isNull() assert not data.isNull()
# reload directly from the qimage # reload directly from the qimage

View File

@ -2,8 +2,6 @@ from __future__ import annotations
import shutil import shutil
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import IO, Callable
import pytest import pytest
@ -11,6 +9,12 @@ from PIL import GifImagePlugin, Image, JpegImagePlugin
from .helper import 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_JPG = "Tests/images/hopper.jpg"
TEST_GIF = "Tests/images/hopper.gif" TEST_GIF = "Tests/images/hopper.gif"

View File

@ -1,18 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable import sys
from collections.abc import Callable
from typing import Any
import pytest import pytest
from PIL import Image from PIL import Image
from .helper import is_win32
min_iterations = 100 min_iterations = 100
max_iterations = 10000 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: def _get_mem_usage() -> float:

View File

@ -1,12 +1,11 @@
from __future__ import annotations from __future__ import annotations
import sys
from io import BytesIO from io import BytesIO
import pytest import pytest
from PIL import Image from PIL import Image, features
from .helper import is_win32, skip_unless_feature
# Limits for testing the leak # Limits for testing the leak
mem_limit = 1024 * 1048576 mem_limit = 1024 * 1048576
@ -15,8 +14,10 @@ iterations = int((mem_limit / stack_size) * 2)
test_file = "Tests/images/rgb_trns_ycbc.jp2" test_file = "Tests/images/rgb_trns_ycbc.jp2"
pytestmark = [ pytestmark = [
pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), pytest.mark.skipif(
skip_unless_feature("jpg_2000"), 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 from __future__ import annotations
import sys
from io import BytesIO from io import BytesIO
import pytest import pytest
from .helper import hopper, is_win32 from PIL import Image
iterations = 5000 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: pre patch:
@ -112,7 +115,7 @@ standard_chrominance_qtable = (
), ),
) )
def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: 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): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables) im.save(test_output, "JPEG", qtables=qtables)
@ -173,9 +176,9 @@ def test_exif_leak() -> None:
0 +----------------------------------------------------------------------->Gi 0 +----------------------------------------------------------------------->Gi
0 11.33 0 11.33
""" """
im = hopper("RGB")
exif = b"12345678" * 4096 exif = b"12345678" * 4096
with Image.open("Tests/images/hopper.ppm") as im:
for _ in range(iterations): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG", exif=exif) im.save(test_output, "JPEG", exif=exif)
@ -207,8 +210,7 @@ def test_base_save() -> None:
| :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@:::
0 +----------------------------------------------------------------------->Gi 0 +----------------------------------------------------------------------->Gi
0 7.882""" 0 7.882"""
im = hopper("RGB") with Image.open("Tests/images/hopper.ppm") as im:
for _ in range(iterations): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG") im.save(test_output, "JPEG")

View File

@ -25,8 +25,7 @@ def test_wheel_modules() -> None:
elif sys.platform == "ios": elif sys.platform == "ios":
# tkinter is not available on iOS # tkinter is not available on iOS
# libavif is not available on iOS (for now) expected_modules.remove("tkinter")
expected_modules -= {"tkinter", "avif"}
assert set(features.get_supported_modules()) == expected_modules assert set(features.get_supported_modules()) == expected_modules

View File

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

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:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library.
* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL. * :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL.
* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images. * :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-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings.
* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format. * :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. * :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 :target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml
:alt: GitHub Actions build status (Test MinGW) :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 .. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
:alt: GitHub Actions build status (Wheels) :alt: GitHub Actions build status (Wheels)

View File

@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **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 * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.

View File

@ -19,45 +19,45 @@ These platforms are built and tested for every change.
+==================================+============================+=====================+ +==================================+============================+=====================+
| Alpine | 3.12 | x86-64 | | 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 | | 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 | | CentOS Stream 10 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, 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 41 | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 42 | 3.13 | x86-64 | | Fedora 42 | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Gentoo | 3.12 | 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 | | macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14 | arm64 |
| | 3.14, PyPy3 | | | | PyPy3 | |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 | | Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | 3.12, 3.13, 3.14, PyPy3 | | | | 3.14, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.12 | arm64v8, ppc64le, | | | 3.12 | arm64v8, ppc64le, |
| | | s390x | | | | s390x |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.9 | x86 | | Windows Server 2022 | 3.10 | x86 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.10, 3.11, 3.12, 3.13, | x86-64 | | | 3.11, 3.12, 3.13, 3.14, | x86-64 |
| | 3.14, PyPy3 | | | | PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 | | | 3.12 (MinGW) | x86-64 |
| +----------------------------+---------------------+
| | 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+

View File

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

View File

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

View File

@ -17,6 +17,12 @@ TODO
Backwards incompatible changes 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 ImageFile.raise_oserror
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -20,11 +20,10 @@ license-files = [ "LICENSE" ]
authors = [ authors = [
{ name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, { name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
] ]
requires-python = ">=3.9" requires-python = ">=3.10"
classifiers = [ classifiers = [
"Development Status :: 6 - Mature", "Development Status :: 6 - Mature",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
@ -76,9 +75,6 @@ optional-dependencies.tests = [
"trove-classifiers>=2024.10.12", "trove-classifiers>=2024.10.12",
] ]
optional-dependencies.typing = [
"typing-extensions; python_version<'3.10'",
]
optional-dependencies.xmp = [ optional-dependencies.xmp = [
"defusedxml", "defusedxml",
] ]
@ -189,8 +185,8 @@ lint.ignore = [
"PT011", # pytest-raises-too-broad "PT011", # pytest-raises-too-broad
"PT012", # pytest-raises-with-multiple-statements "PT012", # pytest-raises-with-multiple-statements
"PT017", # pytest-assert-in-except "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 "PYI034", # flake8-pyi: typing.Self added in Python 3.11
"UP038", # pyupgrade: deprecated rule
] ]
lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [
"I002", "I002",
@ -216,7 +212,7 @@ testpaths = [
] ]
[tool.mypy] [tool.mypy]
python_version = "3.9" python_version = "3.10"
pretty = true pretty = true
disallow_any_generics = true disallow_any_generics = true
enable_error_code = "ignore-without-code" enable_error_code = "ignore-without-code"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
def _accept(prefix: bytes) -> bool: 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): class GribStubImageFile(ImageFile.StubImageFile):

View File

@ -38,10 +38,9 @@ import struct
import sys import sys
import tempfile import tempfile
import warnings import warnings
from collections.abc import Callable, Iterator, MutableMapping, Sequence from collections.abc import MutableMapping
from enum import IntEnum from enum import IntEnum
from types import ModuleType from typing import IO, Protocol, cast
from typing import IO, Any, Literal, Protocol, cast
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -64,6 +63,12 @@ try:
except ImportError: except ImportError:
ElementTree = None 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__) logger = logging.getLogger(__name__)
@ -98,7 +103,6 @@ try:
raise ImportError(msg) raise ImportError(msg)
except ImportError as v: 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 # Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"): if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for # The _imaging C module is present, but not compiled for
@ -3566,9 +3570,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image:
""" """
Alpha composite im2 over im1. Alpha composite im2 over im1.
:param im1: The first image. Must have mode RGBA. :param im1: The first image. Must have mode RGBA or LA.
:param im2: The second image. Must have mode RGBA, and the same size as :param im2: The second image. Must have the same mode and size as the first image.
the first image.
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
""" """
@ -4215,6 +4218,8 @@ class Exif(_ExifBase):
del self._info[tag] del self._info[tag]
else: else:
del self._data[tag] del self._data[tag]
if tag in self._ifds:
del self._ifds[tag]
def __iter__(self) -> Iterator[int]: def __iter__(self) -> Iterator[int]:
keys = set(self._data) keys = set(self._data)

View File

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

View File

@ -46,6 +46,18 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAXBLOCK = 65536 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 SAFEBLOCK = 1024 * 1024

View File

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

View File

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

View File

@ -17,11 +17,15 @@
from __future__ import annotations from __future__ import annotations
import builtins import builtins
from types import CodeType
from typing import Any, Callable
from . import Image, _imagingmath 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: class _Operand:
"""Wraps an image operand, providing standard operators""" """Wraps an image operand, providing standard operators"""

View File

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

View File

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

View File

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

View File

@ -42,7 +42,6 @@ import subprocess
import sys import sys
import tempfile import tempfile
import warnings import warnings
from typing import IO, Any
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -53,6 +52,8 @@ from .JpegPresets import presets
TYPE_CHECKING = False TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import IO, Any
from .MpoImagePlugin import MpoImageFile from .MpoImagePlugin import MpoImageFile
# #

View File

@ -32,7 +32,7 @@ class PcdImageFile(ImageFile.ImageFile):
assert self.fp is not None assert self.fp is not None
self.fp.seek(2048) self.fp.seek(2048)
s = self.fp.read(2048) s = self.fp.read(1539)
if not s.startswith(b"PCD_"): if not s.startswith(b"PCD_"):
msg = "not a PCD file" msg = "not a PCD file"
@ -46,14 +46,13 @@ class PcdImageFile(ImageFile.ImageFile):
self.tile_post_rotate = -90 self.tile_post_rotate = -90
self._mode = "RGB" 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)] self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]
def load_end(self) -> None: def load_end(self) -> None:
if self.tile_post_rotate: if self.tile_post_rotate:
# Handle rotated PCDs # Handle rotated PCDs
self.im = self.im.rotate(self.tile_post_rotate) self.im = self.im.rotate(self.tile_post_rotate)
self._size = self.im.size
# #

View File

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

View File

@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
def _accept(prefix: bytes) -> bool: 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 re
import time import time
import zlib 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 # 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" ]" 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): class PdfDict(_DictBase):
def __setattr__(self, key: str, value: Any) -> None: def __setattr__(self, key: str, value: Any) -> None:
if key == "data": if key == "data":

View File

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

View File

@ -47,7 +47,7 @@ MODES = {
def _accept(prefix: bytes) -> bool: 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 os
import struct import struct
import warnings import warnings
from collections.abc import Iterator, MutableMapping from collections.abc import Callable, MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational 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 . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._typing import StrOrBytesPath
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
from .TiffTags import TYPES from .TiffTags import TYPES
TYPE_CHECKING = False TYPE_CHECKING = False
if TYPE_CHECKING: 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__) logger = logging.getLogger(__name__)

View File

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
from typing import IO, Any
from . import Image, ImageFile from . import Image, ImageFile
@ -12,6 +11,9 @@ try:
except ImportError: except ImportError:
SUPPORTED = False SUPPORTED = False
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import IO, Any
_VP8_MODES_BY_IDENTIFIER = { _VP8_MODES_BY_IDENTIFIER = {
b"VP8 ": "RGB", b"VP8 ": "RGB",

View File

@ -80,7 +80,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
format_description = "Windows Metafile" format_description = "Windows Metafile"
def _open(self) -> None: def _open(self) -> None:
# check placable header # check placeable header
s = self.fp.read(44) s = self.fp.read(44)
if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"):

View File

@ -1,14 +1,14 @@
import datetime import datetime
import sys import sys
from typing import Literal, SupportsFloat, TypedDict from typing import Literal, SupportsFloat, TypeAlias, TypedDict
from ._typing import CapsuleType from ._typing import CapsuleType
littlecms_version: str | None littlecms_version: str | None
_Tuple3f = tuple[float, float, float] _Tuple3f: TypeAlias = tuple[float, float, float]
_Tuple2x3f = tuple[_Tuple3f, _Tuple3f] _Tuple2x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f]
_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f] _Tuple3x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f, _Tuple3f]
class _IccMeasurementCondition(TypedDict): class _IccMeasurementCondition(TypedDict):
observer: int observer: int

View File

@ -1,4 +1,5 @@
from typing import Any, Callable from collections.abc import Callable
from typing import Any
from . import ImageFont, _imaging from . import ImageFont, _imaging

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import os import os
import sys import sys
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Protocol, TypeVar, Union from typing import Any, Protocol, TypeVar
TYPE_CHECKING = False TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
@ -12,8 +12,8 @@ if TYPE_CHECKING:
try: try:
import numpy.typing as npt import numpy.typing as npt
NumpyArray = npt.NDArray[Any] # requires numpy>=1.21 NumpyArray = npt.NDArray[Any]
except (ImportError, AttributeError): except ImportError:
pass pass
if sys.version_info >= (3, 13): if sys.version_info >= (3, 13):
@ -26,19 +26,8 @@ if sys.version_info >= (3, 12):
else: else:
Buffer = Any Buffer = Any
if sys.version_info >= (3, 10):
from typing import TypeGuard
else:
try:
from typing_extensions import TypeGuard
except ImportError:
class TypeGuard: # type: ignore[no-redef] Coords = Sequence[float] | Sequence[Sequence[float]]
def __class_getitem__(cls, item: Any) -> type[bool]:
return bool
Coords = Union[Sequence[float], Sequence[Sequence[float]]]
_T_co = TypeVar("_T_co", covariant=True) _T_co = TypeVar("_T_co", covariant=True)
@ -48,7 +37,7 @@ class SupportsRead(Protocol[_T_co]):
def read(self, length: int = ..., /) -> _T_co: ... def read(self, length: int = ..., /) -> _T_co: ...
StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] StrOrBytesPath = str | bytes | os.PathLike[str] | os.PathLike[bytes]
__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] __all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead"]

View File

@ -1,9 +1,12 @@
from __future__ import annotations from __future__ import annotations
import os import os
from typing import Any, NoReturn
from ._typing import StrOrBytesPath, TypeGuard TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any, NoReturn, TypeGuard
from ._typing import StrOrBytesPath
def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: def is_path(f: Any) -> TypeGuard[StrOrBytesPath]:

View File

@ -1231,8 +1231,6 @@ glyph_error:
return NULL; return NULL;
} }
#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \
(FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1)
static PyObject * static PyObject *
font_getvarnames(FontObject *self) { font_getvarnames(FontObject *self) {
int error; int error;
@ -1442,7 +1440,6 @@ font_setvaraxes(FontObject *self, PyObject *args) {
Py_RETURN_NONE; Py_RETURN_NONE;
} }
#endif
static void static void
font_dealloc(FontObject *self) { font_dealloc(FontObject *self) {
@ -1461,13 +1458,10 @@ static PyMethodDef font_methods[] = {
{"render", (PyCFunction)font_render, METH_VARARGS}, {"render", (PyCFunction)font_render, METH_VARARGS},
{"getsize", (PyCFunction)font_getsize, METH_VARARGS}, {"getsize", (PyCFunction)font_getsize, METH_VARARGS},
{"getlength", (PyCFunction)font_getlength, METH_VARARGS}, {"getlength", (PyCFunction)font_getlength, METH_VARARGS},
#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \
(FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1)
{"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS},
{"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS},
{"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS},
{"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS},
#endif
{NULL, NULL} {NULL, NULL}
}; };

View File

@ -25,13 +25,12 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) {
int x, y; int x, y;
/* Check arguments */ /* Check arguments */
if (!imDst || !imSrc || imDst->mode != IMAGING_MODE_RGBA || if (!imDst || !imSrc ||
imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) { (imDst->mode != IMAGING_MODE_RGBA && imDst->mode != IMAGING_MODE_LA)) {
return ImagingError_ModeError(); return ImagingError_ModeError();
} }
if (imDst->mode != imSrc->mode || imDst->type != imSrc->type || if (imDst->mode != imSrc->mode || imDst->xsize != imSrc->xsize ||
imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize ||
imDst->ysize != imSrc->ysize) { imDst->ysize != imSrc->ysize) {
return ImagingError_Mismatch(); return ImagingError_Mismatch();
} }

View File

@ -3,7 +3,7 @@ requires =
tox>=4.2 tox>=4.2
env_list = env_list =
lint lint
py{py3, 314, 313, 312, 311, 310, 39} py{py3, 314, 313, 312, 311, 310}
[testenv] [testenv]
deps = deps =
@ -29,7 +29,5 @@ commands =
skip_install = true skip_install = true
deps = deps =
-r .ci/requirements-mypy.txt -r .ci/requirements-mypy.txt
extras =
typing
commands = commands =
mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs} mypy conftest.py selftest.py setup.py checks docs src winbuild Tests {posargs}

@ -1 +1 @@
Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155 Subproject commit 64739327166fcad1fa41ad9b23fa910fa244c84f

View File

@ -116,11 +116,11 @@ V = {
"BROTLI": "1.1.0", "BROTLI": "1.1.0",
"FREETYPE": "2.13.3", "FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16", "FRIBIDI": "1.0.16",
"HARFBUZZ": "11.2.1", "HARFBUZZ": "11.3.3",
"JPEGTURBO": "3.1.1", "JPEGTURBO": "3.1.1",
"LCMS2": "2.17", "LCMS2": "2.17",
"LIBAVIF": "1.3.0", "LIBAVIF": "1.3.0",
"LIBIMAGEQUANT": "4.3.4", "LIBIMAGEQUANT": "4.4.0",
"LIBPNG": "1.6.50", "LIBPNG": "1.6.50",
"LIBWEBP": "1.6.0", "LIBWEBP": "1.6.0",
"OPENJPEG": "2.5.3", "OPENJPEG": "2.5.3",