Compare commits

..

94 Commits
12.1.1 ... main

Author SHA1 Message Date
Andrew Murray
02764a0077
Correct error check when encoding AVIF images (#9442)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-19 14:09:59 +02:00
Andrew Murray
3cd69cb12f
Specify platform when pulling docker image (#9440)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-17 09:57:29 +02:00
Andrew Murray
a5c9eba30a
Fix unexpected error when saving zero dimension images (#9391)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-16 12:57:27 +02:00
Hugo van Kemenade
2c00c6f80e
GHA: Cache libavif and webp builds for Ubuntu (#9437)
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-02-16 12:29:42 +02:00
Hugo van Kemenade
d4111967a8
Merge PFM documentation into PPM (#9434) 2026-02-14 00:38:40 +02:00
Andrew Murray
f71d74eec2
Use versionadded
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-02-13 18:29:41 +11:00
Hugo van Kemenade
1457c6032a
Use uppercase format ID for PALM (#9435) 2026-02-12 15:13:27 +02:00
Andrew Murray
657d0414f0 Merge PFM into PPM 2026-02-12 21:51:01 +11:00
Andrew Murray
3795a1b916 Use uppercase format id 2026-02-12 21:47:04 +11:00
Hugo van Kemenade
913698b667
Update macOS tested Pillow versions (#9431) 2026-02-11 18:52:47 +02:00
Andrew Murray
27765189c8 Updated macOS tested Pillow versions 2026-02-11 23:51:33 +11:00
Hugo van Kemenade
a15f9c6121
Fix CVE number (#9430) 2026-02-11 22:48:11 +11:00
Andrew Murray
54ba4db542
Fix OOB Write with invalid tile extents (#9427)
Co-authored-by: Eric Soroos <eric-github@soroos.net>
2026-02-11 10:24:50 +11:00
Andrew Murray
f78663b806
CI: Disable pip upgrade warning (#9424) 2026-02-09 22:16:01 +11:00
Hugo van Kemenade
657d6ea4b6 CI: Disable pip upgrade warning 2026-02-09 11:07:07 +02:00
Hugo van Kemenade
49bc134ee1
Use assert_image_equal* when similarity is zero (#9421) 2026-02-08 14:20:25 +02:00
Hugo van Kemenade
26a188c062
Simplify code in FpxImagePlugin.py (#9423) 2026-02-07 14:36:32 +02:00
Andrew Murray
fd8fa7df79 Simplified code 2026-02-07 11:19:18 +11:00
Andrew Murray
18cab11437 Use assert_image_equal* when similarity is zero 2026-02-06 08:34:13 +11:00
Hugo van Kemenade
2a2638e58f
Update harfbuzz to 12.3.2 (#9402) 2026-02-04 18:34:10 +02:00
Hugo van Kemenade
8eddb86076
Updated zlib-ng to 2.3.3 (#9418) 2026-02-04 18:33:31 +02:00
Andrew Murray
1ac7691fe5 Updated zlib-ng to 2.3.3 2026-02-04 20:39:31 +11:00
Andrew Murray
e108e646da
Updated lcms2 to 2.18 (#9387) 2026-02-04 08:57:34 +11:00
Andrew Murray
62aa42f9da
Update dependency cibuildwheel to v3.3.1 (#9416) 2026-02-03 18:23:26 +11:00
renovate[bot]
508e9c9984
Update dependency cibuildwheel to v3.3.1 2026-02-03 01:32:07 +00:00
Hugo van Kemenade
095cdb3c4a
[pre-commit.ci] pre-commit autoupdate (#9415) 2026-02-02 21:46:00 +01:00
pre-commit-ci[bot]
7cbe8c4924 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-02-02 17:17:55 +00:00
pre-commit-ci[bot]
27924be4fd
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.10 → v0.14.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.10...v0.14.14)
- [github.com/psf/black-pre-commit-mirror: 25.12.0 → 26.1.0](https://github.com/psf/black-pre-commit-mirror/compare/25.12.0...26.1.0)
- [github.com/PyCQA/bandit: 1.9.2 → 1.9.3](https://github.com/PyCQA/bandit/compare/1.9.2...1.9.3)
- [github.com/Lucas-C/pre-commit-hooks: v1.5.5 → v1.5.6](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.5.5...v1.5.6)
- [github.com/python-jsonschema/check-jsonschema: 0.36.0 → 0.36.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.0...0.36.1)
- [github.com/zizmorcore/zizmor-pre-commit: v1.19.0 → v1.22.0](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.19.0...v1.22.0)
- [github.com/tox-dev/pyproject-fmt: v2.11.1 → v2.12.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.11.1...v2.12.1)
2026-02-02 17:17:12 +00:00
Andrew Murray
fc4dbc3810
Remove unnecessary code in WmfHandler (#9411)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-01-30 17:41:44 +02:00
Andrew Murray
0e8bb72a66
Patch libavif for svt-av1 4.0 compatibility (#9413) 2026-01-30 23:25:42 +11:00
Hugo van Kemenade
f86ad8b36d Patch libavif for svt-av1 4.0 compatibility 2026-01-29 23:26:20 +01:00
Andrew Murray
29ff5fcb55
Use monkeypatch (#9406)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
2026-01-27 23:43:14 +02:00
Andrew Murray
6a5c588c5f
Fix docstring typo (#9407) 2026-01-27 08:58:11 +11:00
Hugo van Kemenade
a293273b31 Fix docstring typo 2026-01-26 16:10:37 +02:00
Andrew Murray
6564325e43
Encode using latin-1 in PSDraw text() to match the latin-1 specification in setfont() (#9403) 2026-01-26 13:25:27 +11:00
Andrew Murray
93c8a60784
Lazy import only required plugin: open 2.3-15.6x & save 2.2-9x faster (#9398) 2026-01-26 13:25:14 +11:00
Andrew Murray
b6178303a1
Improve error message (#9392)
Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2026-01-25 23:01:02 +02:00
Hugo van Kemenade
d568c8d9e3
Check ext is not empty during save (#145) 2026-01-25 14:46:11 +02:00
Andrew Murray
d08d7ee99e Check ext is not empty during save 2026-01-25 22:55:19 +11:00
Hugo van Kemenade
2b186fceb8 Use __spec__.parent instead of calculating each time 2026-01-24 23:02:39 +02:00
Hugo van Kemenade
c036185514
Ensure lower before checking if ext in EXTENSION
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-01-24 22:48:41 +02:00
Andrew Murray
d737687fc3 Updated harfbuzz to 12.3.2 2026-01-25 06:45:13 +11:00
Hugo van Kemenade
34814d8d2f
Improve wording
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-01-24 12:52:49 +02:00
Hugo van Kemenade
3968886cf6
format overrides file extension when saving (#144) 2026-01-24 11:08:37 +02:00
Andrew Murray
bc64ccbf28
Updated libpng to 1.6.54 (#9397) 2026-01-24 12:16:14 +11:00
Andrew Murray
76d3116ef0 Added logger messages to match init() 2026-01-24 09:44:31 +11:00
Andrew Murray
a6b36f0b6b format overrides file extension when saving 2026-01-24 09:44:31 +11:00
Andrew Murray
a0f51493ca Refer to lazy importing, as lazy loading of images is separate 2026-01-24 09:44:31 +11:00
Steve Dougherty
a6a701c4db
Match PSDraw text() encoding to the latin-1 specification in setfont()
Without this, characters that are in latin-1 but reflected differently in UTF-8 will not be properly rendered. For example,"ó" becomes "ó".
2026-01-21 06:01:00 -05:00
Hugo van Kemenade
e08f910db4
Improve PaletteFile coverage (#9396) 2026-01-20 13:04:44 +02:00
Hugo van Kemenade
d1974d76f7
Updated MinGW Python version (#9400) 2026-01-20 10:56:17 +02:00
Andrew Murray
5ea2d3a056 Updated MinGW Python version 2026-01-20 18:16:34 +11:00
Hugo van Kemenade
d23a899f23
Link to m from _imagingmath, except on Windows (#9393) 2026-01-19 12:21:29 +02:00
Hugo van Kemenade
096c479cfb
If plugin has already been imported and registered the extension, return early
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2026-01-19 11:28:42 +02:00
Hugo van Kemenade
7f38f980dd
Check that _EXTENSION_PLUGIN contains all registered extensions (#143) 2026-01-19 11:21:00 +02:00
Andrew Murray
b06118c2b3 Do not register empty extension 2026-01-19 17:24:28 +11:00
Andrew Murray
9c8059fdea Cleanup .spider extension registered by test code during save 2026-01-19 17:18:30 +11:00
Andrew Murray
1baf141146 Check that _EXTENSION_PLUGIN contains all registered extensions 2026-01-19 17:13:43 +11:00
Hugo van Kemenade
6b9de40533 Lazy import only required plugin 2026-01-18 22:59:28 +02:00
Andrew Murray
ef8ff756fa Updated libpng to 1.6.54 2026-01-15 12:10:01 +11:00
Andrew Murray
2e9d54887b Improved coverage 2026-01-14 19:42:18 +11:00
Andrew Murray
0f4becea73 Link to m from _imagingmath, except on Windows 2026-01-13 16:26:08 +11:00
Hugo van Kemenade
e2b87a0420
Fix joining rounded rectangle corners (#9384) 2026-01-12 12:21:06 +02:00
Andrew Murray
d7dfeeb7ad Updated lcms2 to 2.18 2026-01-10 06:46:04 +11:00
Andrew Murray
426ad8307d Fix joining rounded rectangle corners 2026-01-08 19:27:19 +11:00
Hugo van Kemenade
627d8743b7
Simplify test code (#9382) 2026-01-06 14:21:49 +02:00
Andrew Murray
dcd52ebf65 Simplified code 2026-01-06 09:56:56 +11:00
Andrew Murray
d6e0a8d174
[pre-commit.ci] pre-commit autoupdate (#9381) 2026-01-06 09:33:57 +11:00
pre-commit-ci[bot]
2210714a43
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.7 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.7...v0.14.10)
- [github.com/psf/black-pre-commit-mirror: 25.11.0 → 25.12.0](https://github.com/psf/black-pre-commit-mirror/compare/25.11.0...25.12.0)
- [github.com/pre-commit/mirrors-clang-format: v21.1.6 → v21.1.8](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.6...v21.1.8)
- [github.com/python-jsonschema/check-jsonschema: 0.35.0 → 0.36.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.35.0...0.36.0)
- [github.com/zizmorcore/zizmor-pre-commit: v1.18.0 → v1.19.0](https://github.com/zizmorcore/zizmor-pre-commit/compare/v1.18.0...v1.19.0)
- [github.com/tox-dev/tox-ini-fmt: 1.7.0 → 1.7.1](https://github.com/tox-dev/tox-ini-fmt/compare/1.7.0...1.7.1)
2026-01-05 17:20:09 +00:00
Hugo van Kemenade
3d7801417a
Move from deprecated getdata to get_flattened_data (#9373) 2026-01-04 15:36:32 +02:00
Andrew Murray
a85d3b135d
Only update Python palette when loading an image if rawmode was different (#9309) 2026-01-04 06:20:56 +11:00
Andrew Murray
932aa68d2a
Add seven-day cooldown to Renovate (#9380) 2026-01-04 05:22:19 +11:00
Hugo van Kemenade
fe236d77a5 Add seven-day cooldown to Renovate 2026-01-03 11:32:19 +02:00
Andrew Murray
bc0e2c0e61
Remove add-imaging-libs option from setup.py (#9378)
Co-authored-by: Alexander Karpinsky <homm86@gmail.com>
2026-01-03 20:18:57 +11:00
Hugo van Kemenade
e66dd607f0
Update xorgproto to 2025.1 (#9379) 2026-01-03 10:56:55 +02:00
Hugo van Kemenade
d5d8a91597
Replace shell: cmd with shell: bash (#9359) 2026-01-03 10:12:48 +02:00
Andrew Murray
b8351fde41
Added type hints to map_metadata_keys() (#9337) 2026-01-03 17:08:17 +11:00
Andrew Murray
36cf82ae76 Updated xorgproto to 2025.1 2026-01-03 16:25:37 +11:00
renovate[bot]
525842215f
Update dependency mypy to v1.19.1 (#9374) 2026-01-03 13:59:38 +11:00
renovate[bot]
844b10f894
Update github-actions (#9375) 2026-01-03 13:55:50 +11:00
Andrew Murray
555fb8371c Move from deprecated getdata to get_flattened_data 2026-01-03 08:16:37 +11:00
Hugo van Kemenade
0a1d6c3c61
Remove Sphinx dependency from mypy (#9370) 2026-01-02 18:30:53 +02:00
mergify[bot]
00ec73dfd1
Fix unclosed file warning (#9371) 2026-01-02 12:33:25 +00:00
Andrew Murray
e924cfd181 Fix unclosed file warning 2026-01-02 21:32:22 +11:00
Hugo van Kemenade
2360d0df17 Revert "Use minimum supported Python version for Lint (#9364)"
This reverts commit 900636e7db.
2026-01-02 12:31:22 +02:00
Hugo van Kemenade
499b796556 Remove Sphinx dependency from mypy 2026-01-02 12:30:14 +02:00
Andrew Murray
5b677ca1c6 Assert palette is not None 2026-01-02 20:31:47 +11:00
Andrew Murray
b71109d435
Merge branch 'main' into load_palette 2026-01-02 20:21:23 +11:00
Andrew Murray
4337139f0c 12.2.0.dev0 version bump 2026-01-02 20:16:49 +11:00
Hugo van Kemenade
72931475f2 Replace shell: cmd with shell: bash 2025-12-29 14:57:25 +02:00
Andrew Murray
79357a2718 Revert "Disable https://docs.zizmor.sh/audits/#obfuscation"
This reverts commit 9342e209b2.
2025-12-29 14:44:12 +02:00
Andrew Murray
3abb62ed29 Do not use cmd shell 2025-12-29 14:44:03 +02:00
Andrew Murray
d06c8b3591 Test drawing a new color onto a dirty palette 2025-11-27 13:12:42 +11:00
Andrew Murray
6a9960e8c1 Only update Python palette if rawmode was different to the mode 2025-11-25 23:40:34 +11:00
77 changed files with 626 additions and 396 deletions

View File

@ -53,7 +53,7 @@ pushd depends && ./install_imagequant.sh && popd
pushd depends && sudo ./install_raqm.sh && popd
# libavif
pushd depends && sudo ./install_libavif.sh && popd
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -1 +1 @@
cibuildwheel==3.3.0
cibuildwheel==3.3.1

View File

@ -1,4 +1,4 @@
mypy==1.19.0
mypy==1.19.1
arro3-compute
arro3-core
IceSpringPySideStubs-PyQt6
@ -9,7 +9,6 @@ packaging
pyarrow-stubs
pybind11
pytest
sphinx
types-atheris
types-defusedxml
types-olefile

View File

@ -6,6 +6,7 @@
"labels": [
"Dependency"
],
"minimumReleaseAge": "7 days",
"packageRules": [
{
"groupName": "github-actions",

View File

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

View File

@ -48,19 +48,35 @@ jobs:
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Cache libavif
uses: actions/cache@v5
id: cache-libavif
with:
path: ~/cache-libavif
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
- name: Cache libimagequant
uses: actions/cache@v4
uses: actions/cache@v5
id: cache-libimagequant
with:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Cache libwebp
uses: actions/cache@v5
id: cache-libwebp
with:
path: ~/cache-libwebp
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
- name: Install Linux dependencies
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: "3.x"
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Build
run: |

View File

@ -23,7 +23,7 @@ jobs:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: "3.x"
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Lint

View File

@ -83,7 +83,7 @@ jobs:
- name: Docker pull
run: |
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
- name: Docker build
run: |

View File

@ -112,7 +112,7 @@ jobs:
- name: Cache build
id: build-cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: winbuild\build
key:
@ -188,8 +188,9 @@ jobs:
# trim ~150MB for each job
- name: Optimize build cache
if: steps.build-cache.outputs.cache-hit != 'true'
run: rmdir /S /Q winbuild\build\src
shell: cmd
run: |
rm -rf winbuild\build\src
shell: bash
- name: Build Pillow
run: |
@ -206,9 +207,7 @@ jobs:
- name: Test Pillow
run: |
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
.ci\test.cmd
shell: cmd
- name: Prepare to upload errors
if: failure()
@ -217,7 +216,7 @@ jobs:
shell: bash
- name: Upload errors
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
if: failure()
with:
name: errors

View File

@ -29,6 +29,7 @@ concurrency:
env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1
PIP_DISABLE_PIP_VERSION_CHECK: 1
jobs:
build:
@ -90,21 +91,39 @@ jobs:
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Cache libavif
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v5
id: cache-libavif
with:
path: ~/cache-libavif
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
- name: Cache libimagequant
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v4
uses: actions/cache@v5
id: cache-libimagequant
with:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Cache libwebp
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v5
id: cache-libwebp
with:
path: ~/cache-libwebp
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
- name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu')
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Install macOS dependencies
if: startsWith(matrix.os, 'macOS')
@ -143,7 +162,7 @@ jobs:
mkdir -p Tests/errors
- name: Upload errors
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
if: failure()
with:
name: errors

View File

@ -95,15 +95,15 @@ if [[ -n "$IOS_SDK" ]]; then
else
FREETYPE_VERSION=2.14.1
fi
HARFBUZZ_VERSION=12.3.0
LIBPNG_VERSION=1.6.53
HARFBUZZ_VERSION=12.3.2
LIBPNG_VERSION=1.6.54
JPEGTURBO_VERSION=3.1.3
OPENJPEG_VERSION=2.5.4
XZ_VERSION=5.8.2
ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.1
LCMS2_VERSION=2.17
ZLIB_NG_VERSION=2.3.2
LCMS2_VERSION=2.18
ZLIB_NG_VERSION=2.3.3
LIBWEBP_VERSION=1.6.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
@ -267,7 +267,7 @@ function build {
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [[ -n "$IS_MACOS" ]]; then
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
else

View File

@ -134,7 +134,7 @@ jobs:
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: dist-${{ matrix.name }}
path: ./wheelhouse/*.whl
@ -186,24 +186,18 @@ jobs:
- name: Build wheels
run: |
setlocal EnableDelayedExpansion
for %%f in (winbuild\build\license\*) do (
set x=%%~nf
rem Skip FriBiDi license, it is not included in the wheel.
set fribidi=!x:~0,7!
if NOT !fribidi!==fribidi (
rem Skip imagequant license, it is not included in the wheel.
set libimagequant=!x:~0,13!
if NOT !libimagequant!==libimagequant (
echo. >> LICENSE
echo ===== %%~nf ===== >> LICENSE
echo. >> LICENSE
type %%f >> LICENSE
)
)
)
call winbuild\\build\\build_env.cmd
%pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
for f in winbuild/build/license/*; do
name=$(basename "${f%.*}")
# Skip FriBiDi license, it is not included in the wheel.
[[ $name == fribidi* ]] && continue
# Skip imagequant license, it is not included in the wheel.
[[ $name == libimagequant* ]] && continue
echo "" >> LICENSE
echo "===== $name =====" >> LICENSE
echo "" >> LICENSE
cat "$f" >> LICENSE
done
cmd //c "winbuild\\build\\build_env.cmd && $pythonLocation\\python.exe -m cibuildwheel . --output-dir wheelhouse"
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
@ -217,16 +211,16 @@ jobs:
-e CI -e GITHUB_ACTIONS
mcr.microsoft.com/windows/servercore:ltsc2022
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
shell: cmd
shell: bash
- name: Upload wheels
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl
- name: Upload fribidi.dll
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: fribidi-windows-${{ matrix.cibw_arch }}
path: winbuild\build\bin\fribidi*
@ -246,7 +240,7 @@ jobs:
- run: make sdist
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: dist-sdist
path: dist/*.tar.gz
@ -256,7 +250,7 @@ jobs:
runs-on: ubuntu-latest
name: Count dists
steps:
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
pattern: dist-*
path: dist
@ -275,13 +269,13 @@ jobs:
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
pattern: dist-!(sdist)*
path: dist
merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2
uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
@ -297,7 +291,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
pattern: dist-*
path: dist

2
.github/zizmor.yml vendored
View File

@ -1,7 +1,5 @@
# https://docs.zizmor.sh/configuration/
rules:
obfuscation:
disable: true
unpinned-uses:
config:
policies:

View File

@ -1,30 +1,30 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.7
rev: v0.14.14
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.11.0
rev: 26.1.0
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.9.2
rev: 1.9.3
hooks:
- id: bandit
args: [--severity-level=high]
files: ^src/
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.5
rev: v1.5.6
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v21.1.6
rev: v21.1.8
hooks:
- id: clang-format
types: [c]
@ -51,14 +51,14 @@ repos:
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.35.0
rev: 0.36.1
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.18.0
rev: v1.22.0
hooks:
- id: zizmor
@ -68,7 +68,7 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.11.1
rev: v2.12.1
hooks:
- id: pyproject-fmt
@ -76,10 +76,10 @@ repos:
rev: v0.24.1
hooks:
- id: validate-pyproject
additional_dependencies: [tomli, trove-classifiers>=2024.10.12]
additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.7.0
rev: 1.7.1
hooks:
- id: tox-ini-fmt

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

View File

@ -213,7 +213,7 @@ INT32 = DataShape(
),
)
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
dtype, elt, elts_per_pixel = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
if dtype == fl_uint8_4_type:
@ -239,7 +239,7 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non
)
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
(dtype, elt, elts_per_pixel) = data_tp
dtype, elt, elts_per_pixel = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)

View File

@ -68,7 +68,7 @@ def test_multiblock_l_image() -> None:
img = Image.new("L", size, 128)
with pytest.raises(ValueError):
(schema, arr) = img.__arrow_c_array__()
schema, arr = img.__arrow_c_array__()
def test_multiblock_rgba_image() -> None:
@ -79,7 +79,7 @@ def test_multiblock_rgba_image() -> None:
img = Image.new("RGBA", size, (128, 127, 126, 125))
with pytest.raises(ValueError):
(schema, arr) = img.__arrow_c_array__()
schema, arr = img.__arrow_c_array__()
def test_multiblock_l_schema() -> None:
@ -114,7 +114,7 @@ def test_singleblock_l_image() -> None:
img = Image.new("L", size, 128)
assert img.im.isblock()
(schema, arr) = img.__arrow_c_array__()
schema, arr = img.__arrow_c_array__()
assert schema
assert arr
@ -130,7 +130,7 @@ def test_singleblock_rgba_image() -> None:
img = Image.new("RGBA", size, (128, 127, 126, 125))
assert img.im.isblock()
(schema, arr) = img.__arrow_c_array__()
schema, arr = img.__arrow_c_array__()
assert schema
assert arr
Image.core.set_use_block_allocator(0)

View File

@ -310,6 +310,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
assert reloaded.getpixel((0, 0)) == 255
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = BytesIO()
im = Image.new("RGB", size)
with pytest.raises(SystemError):
im.save(b, "GIF")
@pytest.mark.parametrize(
"path, mode",
(
@ -399,7 +407,7 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
b = BytesIO()
GifImagePlugin._save_netpbm(img_rgb, b, tempfile)
with Image.open(tempfile) as reloaded:
assert_image_similar(img_rgb, reloaded.convert("RGB"), 0)
assert_image_equal(img_rgb, reloaded.convert("RGB"))
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
@ -411,7 +419,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
b = BytesIO()
GifImagePlugin._save_netpbm(img_l, b, tempfile)
with Image.open(tempfile) as reloaded:
assert_image_similar(img_l, reloaded.convert("L"), 0)
assert_image_equal(img_l, reloaded.convert("L"))
def test_seek() -> None:
@ -1433,7 +1441,7 @@ def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None:
# with open('Tests/images/gif_header_data.pkl', 'wb') as f:
# pickle.dump((h, d), f, 1)
with open("Tests/images/gif_header_data.pkl", "rb") as f:
(h_target, d_target) = pickle.load(f)
h_target, d_target = pickle.load(f)
assert h == h_target
assert d == d_target

View File

@ -590,9 +590,7 @@ class TestFileJpeg:
assert im2.quantization == {0: bounds_qtable}
# values from wizard.txt in jpeg9-a src package.
standard_l_qtable = [
int(s)
for s in """
standard_l_qtable = [int(s) for s in """
16 11 10 16 24 40 51 61
12 12 14 19 26 58 60 55
14 13 16 24 40 57 69 56
@ -601,14 +599,9 @@ class TestFileJpeg:
24 35 55 64 81 104 113 92
49 64 78 87 103 121 120 101
72 92 95 98 112 100 103 99
""".split(
None
)
]
""".split(None)]
standard_chrominance_qtable = [
int(s)
for s in """
standard_chrominance_qtable = [int(s) for s in """
17 18 24 47 99 99 99 99
18 21 26 66 99 99 99 99
24 26 56 99 99 99 99 99
@ -617,10 +610,7 @@ class TestFileJpeg:
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
""".split(
None
)
]
""".split(None)]
for quality in range(101):
qtable_from_qtable_quality = self.roundtrip(

View File

@ -738,7 +738,7 @@ class TestFileLibTiff(LibTiffTestCase):
buffer_io.seek(0)
with Image.open(buffer_io) as saved_im:
assert_image_similar(pilim, saved_im, 0)
assert_image_equal(pilim, saved_im)
save_bytesio()
save_bytesio("raw")

View File

@ -37,6 +37,14 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f)
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = io.BytesIO()
im = Image.new("1", size)
with pytest.raises(ValueError):
im.save(b, "PCX")
def test_p_4_planes() -> None:
with Image.open("Tests/images/p_4_planes.pcx") as im:
assert im.getpixel((0, 0)) == 3
@ -119,36 +127,36 @@ def test_large_count(tmp_path: Path) -> None:
_roundtrip(tmp_path, im)
def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None:
_last = ImageFile.MAXBLOCK
ImageFile.MAXBLOCK = size
try:
_roundtrip(tmp_path, im)
finally:
ImageFile.MAXBLOCK = _last
def _test_buffer_overflow(
tmp_path: Path, im: Image.Image, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(ImageFile, "MAXBLOCK", 1024)
_roundtrip(tmp_path, im)
def test_break_in_count_overflow(tmp_path: Path) -> None:
def test_break_in_count_overflow(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_one_in_loop(tmp_path: Path) -> None:
def test_break_one_in_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_many_in_loop(tmp_path: Path) -> None:
def test_break_many_in_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
@ -157,10 +165,10 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
px[x, y] = x % 128
for x in range(8):
px[x, 4] = 16
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_one_at_end(tmp_path: Path) -> None:
def test_break_one_at_end(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
@ -168,10 +176,10 @@ def test_break_one_at_end(tmp_path: Path) -> None:
for x in range(256):
px[x, y] = x % 128
px[0, 3] = 128 + 64
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_many_at_end(tmp_path: Path) -> None:
def test_break_many_at_end(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
@ -181,10 +189,10 @@ def test_break_many_at_end(tmp_path: Path) -> None:
for x in range(4):
px[x * 2, 3] = 128 + 64
px[x + 256 - 4, 3] = 0
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)
def test_break_padding(tmp_path: Path) -> None:
def test_break_padding(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("L", (257, 5))
px = im.load()
assert px is not None
@ -193,4 +201,4 @@ def test_break_padding(tmp_path: Path) -> None:
px[x, y] = x % 128
for x in range(5):
px[x, 3] = 0
_test_buffer_overflow(tmp_path, im)
_test_buffer_overflow(tmp_path, im, monkeypatch)

View File

@ -654,21 +654,17 @@ class TestFilePng:
with pytest.raises(SyntaxError, match="Unknown compression method"):
PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png")
def test_padded_idat(self) -> None:
def test_padded_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
# This image has been manually hexedited
# so that the IDAT chunk has padding at the end
# Set MAXBLOCK to the length of the actual data
# so that the decoder finishes reading before the chunk ends
MAXBLOCK = ImageFile.MAXBLOCK
ImageFile.MAXBLOCK = 45
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "MAXBLOCK", 45)
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open("Tests/images/padded_idat.png") as im:
im.load()
ImageFile.MAXBLOCK = MAXBLOCK
ImageFile.LOAD_TRUNCATED_IMAGES = False
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
@pytest.mark.parametrize(

View File

@ -100,7 +100,7 @@ def test_seek_tell() -> None:
im.seek(2)
layer_number = im.tell()
assert layer_number == 2
assert layer_number == 2
def test_seek_eoferror() -> None:
@ -138,7 +138,7 @@ def test_icc_profile() -> None:
assert "icc_profile" in im.info
icc_profile = im.info["icc_profile"]
assert len(icc_profile) == 3144
assert len(icc_profile) == 3144
def test_no_icc_profile() -> None:
@ -158,17 +158,16 @@ def test_combined_larger_than_size() -> None:
@pytest.mark.parametrize(
"test_file,raises",
"test_file",
[
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
"Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd",
"Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd",
],
)
def test_crashes(test_file: str, raises: type[Exception]) -> None:
with open(test_file, "rb") as f:
with pytest.raises(raises):
with Image.open(f):
pass
def test_crashes(test_file: str) -> None:
with pytest.raises(OSError):
with Image.open(test_file):
pass
@pytest.mark.parametrize(
@ -179,11 +178,10 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None:
],
)
def test_layer_crashes(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
with pytest.raises(SyntaxError):
im.layers
with Image.open(test_file) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
with pytest.raises(SyntaxError):
im.layers
@pytest.mark.parametrize(

View File

@ -14,6 +14,10 @@ from .helper import assert_image_equal, hopper, is_pypy
TEST_FILE = "Tests/images/hopper.spider"
def teardown_module() -> None:
del Image.EXTENSION[".spider"]
def test_sanity() -> None:
with Image.open(TEST_FILE) as im:
im.load()
@ -64,6 +68,14 @@ def test_save(tmp_path: Path) -> None:
assert im2.format == "SPIDER"
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = BytesIO()
im = Image.new("1", size)
with pytest.raises(SystemError):
im.save(b, "SPIDER")
def test_tempfile() -> None:
# Arrange
im = hopper()

View File

@ -18,7 +18,7 @@ def test_load_raw() -> None:
# Currently, support for WMF/EMF is Windows-only
im.load()
# Compare to reference rendering
assert_image_similar_tofile(im, "Tests/images/drawing_emf_ref.png", 0)
assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref.png")
# Test basic WMF open and rendering
with Image.open("Tests/images/drawing.wmf") as im:

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import pytest
from PIL import Image, ImageDraw, ImageFont, _util
from .helper import PillowLeakTestCase, features, skip_unless_feature
@ -7,11 +9,7 @@ from .helper import PillowLeakTestCase, features, skip_unless_feature
original_core = ImageFont.core
class TestTTypeFontLeak(PillowLeakTestCase):
# fails at iteration 3 in main
iterations = 10
mem_limit = 4096 # k
class TestFontLeak(PillowLeakTestCase):
def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None:
im = Image.new("RGB", (255, 255), "white")
draw = ImageDraw.ImageDraw(im)
@ -21,23 +19,29 @@ class TestTTypeFontLeak(PillowLeakTestCase):
)
)
class TestTTypeFontLeak(TestFontLeak):
# fails at iteration 3 in main
iterations = 10
mem_limit = 4096 # k
@skip_unless_feature("freetype2")
def test_leak(self) -> None:
ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
self._test_font(ttype)
class TestDefaultFontLeak(TestTTypeFontLeak):
class TestDefaultFontLeak(TestFontLeak):
# fails at iteration 37 in main
iterations = 100
mem_limit = 1024 # k
def test_leak(self) -> None:
def test_leak(self, monkeypatch: pytest.MonkeyPatch) -> None:
if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try:
default_font = ImageFont.load_default()
finally:
ImageFont.core = original_core
monkeypatch.setattr(
ImageFont,
"core",
_util.DeferredError(ImportError("Disabled for testing")),
)
default_font = ImageFont.load_default()
self._test_font(default_font)

View File

@ -10,7 +10,6 @@ from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
from .helper import (
assert_image_equal_tofile,
assert_image_similar_tofile,
skip_unless_feature,
)
@ -73,14 +72,14 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None:
im = Image.new("L", (130, 30), "white")
draw = ImageDraw.Draw(im)
draw.text((0, 0), message, "black", font=font)
assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0)
assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png")
def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None:
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)
for i in range(255):
(ox, oy, dx, dy) = font.getbbox(chr(i))
ox, oy, dx, dy = font.getbbox(chr(i))
assert ox == 0
assert oy == 0
assert dy == 20
@ -100,7 +99,7 @@ def _test_high_characters(
im = Image.new("L", (750, 30), "white")
draw = ImageDraw.Draw(im)
draw.text((0, 0), message, "black", font=font)
assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0)
assert_image_equal_tofile(im, "Tests/images/high_ascii_chars.png")
def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None:

View File

@ -10,7 +10,6 @@ from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
from .helper import (
assert_image_equal_tofile,
assert_image_similar_tofile,
skip_unless_feature,
)
@ -85,7 +84,7 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) ->
draw = ImageDraw.Draw(im)
message = charsets[encoding]["message"].encode(encoding)
draw.text((0, 0), message, "black", font=font)
assert_image_similar_tofile(im, charsets[encoding]["image1"], 0)
assert_image_equal_tofile(im, charsets[encoding]["image1"])
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
@ -95,7 +94,7 @@ def test_textsize(
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
for i in range(255):
(ox, oy, dx, dy) = font.getbbox(bytearray([i]))
ox, oy, dx, dy = font.getbbox(bytearray([i]))
assert ox == 0
assert oy == 0
assert dy == 20

View File

@ -29,7 +29,7 @@ def linear_gradient() -> Image.Image:
im = Image.linear_gradient(mode="L")
im90 = im.rotate(90)
(px, h) = im.size
px, h = im.size
r = Image.new("L", (px * 3, h))
g = r.copy()
@ -54,7 +54,7 @@ def to_xxx_colorsys(
) -> Image.Image:
# convert the hard way using the library colorsys routines.
(r, g, b) = im.split()
r, g, b = im.split()
conv_func = int_to_float

View File

@ -456,9 +456,11 @@ class TestImage:
# Assert
assert len(Image.ID) == id_length
def test_registered_extensions_uninitialized(self) -> None:
def test_registered_extensions_uninitialized(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# Arrange
Image._initialized = 0
monkeypatch.setattr(Image, "_initialized", 0)
# Act
Image.registered_extensions()
@ -466,6 +468,9 @@ class TestImage:
# Assert
assert Image._initialized == 2
for extension in Image.EXTENSION:
assert extension in Image._EXTENSION_PLUGIN
def test_registered_extensions(self) -> None:
# Arrange
# Open an image to trigger plugin registration

View File

@ -278,8 +278,7 @@ class TestEmbeddable:
with open("embed_pil.c", "w", encoding="utf-8") as fh:
home = sys.prefix.replace("\\", "\\\\")
fh.write(
f"""
fh.write(f"""
#include "Python.h"
int main(int argc, char* argv[])
@ -300,8 +299,7 @@ int main(int argc, char* argv[])
return 0;
}}
"""
)
""")
objects = compiler.compile(["embed_pil.c"])
compiler.link_executable(objects, "embed_pil")

View File

@ -56,7 +56,7 @@ class TestImageTransform:
def test_extent(self) -> None:
im = hopper("RGB")
(w, h) = im.size
w, h = im.size
transformed = im.transform(
im.size,
Image.Transform.EXTENT,
@ -72,7 +72,7 @@ class TestImageTransform:
def test_quad(self) -> None:
# one simple quad transform, equivalent to scale & crop upper left quad
im = hopper("RGB")
(w, h) = im.size
w, h = im.size
transformed = im.transform(
im.size,
Image.Transform.QUAD,
@ -99,7 +99,7 @@ class TestImageTransform:
)
def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None:
im = hopper(mode)
(w, h) = im.size
w, h = im.size
transformed = im.transform(
im.size,
Image.Transform.EXTENT,
@ -112,7 +112,7 @@ class TestImageTransform:
def test_mesh(self) -> None:
# this should be a checkerboard of halfsized hoppers in ul, lr
im = hopper("RGBA")
(w, h) = im.size
w, h = im.size
transformed = im.transform(
im.size,
Image.Transform.MESH,
@ -174,7 +174,7 @@ class TestImageTransform:
def test_alpha_premult_transform(self) -> None:
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
(w, h) = im.size
w, h = im.size
return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
)
@ -216,7 +216,7 @@ class TestImageTransform:
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
def test_nearest_transform(self, mode: str) -> None:
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
(w, h) = im.size
w, h = im.size
return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST
)
@ -255,7 +255,7 @@ class TestImageTransform:
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None:
with hopper() as im:
(w, h) = im.size
w, h = im.size
with pytest.raises(ValueError):
im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type]

View File

@ -68,10 +68,22 @@ def test_sanity() -> None:
draw.rectangle(list(range(4)))
def test_valueerror() -> None:
def test_new_color() -> None:
with Image.open("Tests/images/chi.gif") as im:
draw = ImageDraw.Draw(im)
assert im.palette is not None
assert len(im.palette.colors) == 249
# Test drawing a new color onto the palette
draw.line((0, 0), fill=(0, 0, 0))
assert im.palette is not None
assert len(im.palette.colors) == 250
assert im.palette.dirty
# Test drawing another new color, now that the palette is dirty
draw.point((0, 0), fill=(1, 0, 0))
assert len(im.palette.colors) == 251
assert im.convert("RGB").getpixel((0, 0)) == (1, 0, 0)
def test_mode_mismatch() -> None:
@ -883,6 +895,18 @@ def test_rounded_rectangle_joined_x_different_corners() -> None:
)
def test_rounded_rectangle_radius() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGB")
# Act
draw.rounded_rectangle((25, 25, 75, 75), 24, fill="red", outline="green", width=5)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle_radius.png")
@pytest.mark.parametrize(
"xy, radius, type",
[
@ -1461,21 +1485,15 @@ def test_stroke_multiline() -> None:
@skip_unless_feature("freetype2")
def test_setting_default_font() -> None:
# Arrange
def test_setting_default_font(monkeypatch: pytest.MonkeyPatch) -> None:
im = Image.new("RGB", (100, 250))
draw = ImageDraw.Draw(im)
assert isinstance(draw.getfont(), ImageFont.load_default().__class__)
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
# Act
ImageDraw.ImageDraw.font = font
# Assert
try:
assert draw.getfont() == font
finally:
ImageDraw.ImageDraw.font = None
assert isinstance(draw.getfont(), ImageFont.load_default().__class__)
monkeypatch.setattr(ImageDraw.ImageDraw, "font", font)
assert draw.getfont() == font
def test_default_font_size() -> None:

View File

@ -31,7 +31,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK
class TestImageFile:
def test_parser(self) -> None:
def test_parser(self, monkeypatch: pytest.MonkeyPatch) -> None:
def roundtrip(format: str) -> tuple[Image.Image, Image.Image]:
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
if format in ("MSP", "XBM"):
@ -55,12 +55,9 @@ class TestImageFile:
assert_image_equal(*roundtrip("IM"))
assert_image_equal(*roundtrip("MSP"))
if features.check("zlib"):
try:
# force multiple blocks in PNG driver
ImageFile.MAXBLOCK = 8192
assert_image_equal(*roundtrip("PNG"))
finally:
ImageFile.MAXBLOCK = MAXBLOCK
# force multiple blocks in PNG driver
monkeypatch.setattr(ImageFile, "MAXBLOCK", 8192)
assert_image_equal(*roundtrip("PNG"))
assert_image_equal(*roundtrip("PPM"))
assert_image_equal(*roundtrip("TIFF"))
assert_image_equal(*roundtrip("XBM"))
@ -120,14 +117,11 @@ class TestImageFile:
assert (128, 128) == p.image.size
@skip_unless_feature("zlib")
def test_safeblock(self) -> None:
def test_safeblock(self, monkeypatch: pytest.MonkeyPatch) -> None:
im1 = hopper()
try:
ImageFile.SAFEBLOCK = 1
im2 = fromstring(tostring(im1, "PNG"))
finally:
ImageFile.SAFEBLOCK = SAFEBLOCK
monkeypatch.setattr(ImageFile, "SAFEBLOCK", 1)
im2 = fromstring(tostring(im1, "PNG"))
assert_image_equal(im1, im2)

View File

@ -38,20 +38,18 @@ def test_invalid_mode() -> None:
font._load_pilfont_data(fp, im)
def test_without_freetype() -> None:
original_core = ImageFont.core
def test_without_freetype(monkeypatch: pytest.MonkeyPatch) -> None:
if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try:
with pytest.raises(ImportError):
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
monkeypatch.setattr(
ImageFont, "core", _util.DeferredError(ImportError("Disabled for testing"))
)
with pytest.raises(ImportError):
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
assert isinstance(ImageFont.load_default(), ImageFont.ImageFont)
assert isinstance(ImageFont.load_default(), ImageFont.ImageFont)
with pytest.raises(ImportError):
ImageFont.load_default(size=14)
finally:
ImageFont.core = original_core
with pytest.raises(ImportError):
ImageFont.load_default(size=14)
@pytest.mark.parametrize("font", fonts)

View File

@ -22,8 +22,7 @@ def string_to_img(image_string: str) -> Image.Image:
return im
A = string_to_img(
"""
A = string_to_img("""
.......
.......
..111..
@ -31,8 +30,7 @@ A = string_to_img(
..111..
.......
.......
"""
)
""")
def img_to_string(im: Image.Image) -> str:
@ -231,15 +229,15 @@ def test_negate() -> None:
def test_incorrect_mode() -> None:
im = hopper()
mop = ImageMorph.MorphOp(op_name="erosion8")
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
mop.apply(im)
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
mop.match(im)
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
mop.get_on_pixels(im)
with hopper() as im:
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
mop.apply(im)
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
mop.match(im)
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
mop.get_on_pixels(im)
def test_add_patterns() -> None:

View File

@ -1,10 +1,11 @@
from __future__ import annotations
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image, ImagePalette
from PIL import Image, ImagePalette, PaletteFile
from .helper import assert_image_equal, assert_image_equal_tofile
@ -202,6 +203,19 @@ def test_2bit_palette(tmp_path: Path) -> None:
assert_image_equal_tofile(img, outfile)
def test_getpalette() -> None:
b = BytesIO(b"0 1\n1 2 3 4")
p = PaletteFile.PaletteFile(b)
palette, rawmode = p.getpalette()
assert palette[:6] == b"\x01\x01\x01\x02\x03\x04"
assert rawmode == "RGB"
def test_invalid_palette() -> None:
with pytest.raises(OSError):
ImagePalette.load("Tests/images/hopper.jpg")
b = BytesIO(b"1" * 101)
with pytest.raises(SyntaxError, match="bad palette file"):
PaletteFile.PaletteFile(b)

View File

@ -87,7 +87,7 @@ if is_win32():
def test_pointer(tmp_path: Path) -> None:
im = hopper()
(width, height) = im.size
width, height = im.size
opath = tmp_path / "temp.png"
imdib = ImageWin.Dib(im)

View File

@ -208,7 +208,7 @@ INT32 = DataShape(
),
)
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
dtype, elt, elts_per_pixel = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
if dtype == fl_uint8_4_type:
@ -241,7 +241,7 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non
)
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
(dtype, elt, elts_per_pixel) = data_tp
dtype, elt, elts_per_pixel = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = nanoarrow.Array(

View File

@ -5,6 +5,8 @@ import sys
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image, PSDraw
@ -47,21 +49,16 @@ def test_draw_postscript(tmp_path: Path) -> None:
assert os.path.getsize(tempfile) > 0
def test_stdout() -> None:
def test_stdout(monkeypatch: pytest.MonkeyPatch) -> None:
# Temporarily redirect stdout
old_stdout = sys.stdout
class MyStdOut:
buffer = BytesIO()
mystdout = MyStdOut()
sys.stdout = mystdout
monkeypatch.setattr(sys, "stdout", mystdout)
ps = PSDraw.PSDraw()
_create_document(ps)
# Reset stdout
sys.stdout = old_stdout
assert mystdout.buffer.getvalue() != b""

View File

@ -211,7 +211,7 @@ INT32 = DataShape(
),
)
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
dtype, elt, elts_per_pixel = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
@ -238,7 +238,7 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non
),
)
def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
dtype, elt, elts_per_pixel = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype)

View File

@ -6,10 +6,15 @@ import pytest
from PIL import __version__
TYPE_CHECKING = False
if TYPE_CHECKING:
from importlib.metadata import PackageMetadata
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
def map_metadata_keys(md):
def map_metadata_keys(md: PackageMetadata) -> dict[str, str | list[str] | None]:
# 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
@ -17,16 +22,16 @@ def map_metadata_keys(md):
# 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()):
for key in set(md):
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
if value is not None and len(value) == 1:
first_value = value[0]
if first_value.strip() != "UNKNOWN":
data[key] = first_value
else:
data[key] = value
return data

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from .helper import assert_image_equal, assert_image_similar, hopper
from .helper import assert_image_equal, hopper
def check_upload_equal() -> None:
@ -12,4 +12,4 @@ def check_upload_equal() -> None:
def check_upload_similar() -> None:
result = hopper("P").convert("RGB")
target = hopper("RGB")
assert_image_similar(result, target, 0)
assert_image_equal(result, target)

View File

@ -3,66 +3,88 @@ set -eo pipefail
version=1.3.0
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
if [[ "$GHA_LIBAVIF_CACHE_HIT" == "true" ]]; then
pushd libavif-$version
LIBDIR=/usr/lib/x86_64-linux-gnu
# Apply patch for SVT-AV1 4.0 compatibility
# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971
patch -p1 < ../libavif-svt4.patch
# Copy cached files into place
sudo cp ~/cache-libavif/lib/* $LIBDIR/
sudo cp -r ~/cache-libavif/include/avif /usr/include/
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
PREFIX=$(brew --prefix)
else
PREFIX=/usr
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
pushd libavif-$version
# Apply patch for SVT-AV1 4.0 compatibility
# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971
patch -p1 < ../libavif-svt4.patch
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
PREFIX=$(brew --prefix)
else
PREFIX=/usr
fi
PKGCONFIG=${PKGCONFIG:-pkg-config}
LIBAVIF_CMAKE_FLAGS=()
HAS_DECODER=0
HAS_ENCODER=0
if $PKGCONFIG --exists aom; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
HAS_ENCODER=1
HAS_DECODER=1
fi
if $PKGCONFIG --exists dav1d; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists rav1e; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
HAS_ENCODER=1
fi
if $PKGCONFIG --exists SvtAv1Enc; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
HAS_ENCODER=1
fi
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
fi
cmake \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_MACOSX_RPATH=OFF \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
"${LIBAVIF_CMAKE_FLAGS[@]}" \
.
sudo make install
if [ -n "$GITHUB_ACTIONS" ] && [ "$(uname)" != "Darwin" ]; then
# Copy to cache
LIBDIR=/usr/lib/x86_64-linux-gnu
rm -rf ~/cache-libavif
mkdir -p ~/cache-libavif/lib
mkdir -p ~/cache-libavif/include
cp $LIBDIR/libavif.so* ~/cache-libavif/lib/
cp -r /usr/include/avif ~/cache-libavif/include/
fi
popd
fi
PKGCONFIG=${PKGCONFIG:-pkg-config}
LIBAVIF_CMAKE_FLAGS=()
HAS_DECODER=0
HAS_ENCODER=0
if $PKGCONFIG --exists aom; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
HAS_ENCODER=1
HAS_DECODER=1
fi
if $PKGCONFIG --exists dav1d; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists rav1e; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
HAS_ENCODER=1
fi
if $PKGCONFIG --exists SvtAv1Enc; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
HAS_ENCODER=1
fi
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
fi
cmake \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_MACOSX_RPATH=OFF \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
"${LIBAVIF_CMAKE_FLAGS[@]}" \
.
make install
popd

View File

@ -3,10 +3,30 @@
archive=libwebp-1.6.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
if [[ "$GHA_LIBWEBP_CACHE_HIT" == "true" ]]; then
pushd $archive
# Copy cached files into place
sudo cp ~/cache-libwebp/lib/* /usr/lib/
sudo cp -r ~/cache-libwebp/include/webp /usr/include/
./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install
else
popd
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
pushd $archive
./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install
if [ -n "$GITHUB_ACTIONS" ]; then
# Copy to cache
rm -rf ~/cache-libwebp
mkdir -p ~/cache-libwebp/lib
mkdir -p ~/cache-libwebp/include
cp /usr/lib/libwebp*.so* /usr/lib/libwebp*.a ~/cache-libwebp/lib/
cp /usr/lib/libsharpyuv.so* /usr/lib/libsharpyuv.a ~/cache-libwebp/lib/
cp -r /usr/include/webp ~/cache-libwebp/include/
fi
popd
fi

View File

@ -11,7 +11,7 @@ import subprocess
TYPE_CHECKING = False
if TYPE_CHECKING:
from sphinx.application import Sphinx
from typing import Any
DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+")
VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n")
@ -28,7 +28,7 @@ def get_date_for(git_version: str) -> str | None:
return out.split()[0]
def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None:
def add_date(app: Any, doc_name: str, source: list[str]) -> None:
if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])):
old_title = m.group(1)
@ -43,6 +43,6 @@ def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None:
source[0] = result
def setup(app: Sphinx) -> dict[str, bool]:
def setup(app: Any) -> dict[str, bool]:
app.connect("source-read", add_date)
return {"parallel_read_safe": True}

View File

@ -828,16 +828,6 @@ PCX
Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data.
PFM
^^^
.. versionadded:: 10.3.0
Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files
containing ``F`` data.
Color (PF format) PFM files are not supported.
Opening
~~~~~~~
@ -1081,12 +1071,19 @@ following parameters can also be set:
PPM
^^^
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
``RGB`` data.
Pillow reads and writes PBM, PGM, PPM, PNM and PFM files containing ``1``, ``L``, ``I``,
``RGB`` or ``F`` data.
"Raw" (P4 to P6) formats can be read, and are used when writing.
Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well.
.. versionadded:: 9.2.0
"Plain" (P1 to P3) formats can be read.
.. versionadded:: 10.3.0
Grayscale (Pf format) Portable FloatMap (PFM) files containing
``F`` data can be read and used when writing.
Color (PF format) PFM files are not supported.
QOI
^^^

View File

@ -51,7 +51,7 @@ Many of Pillow's features require external libraries:
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
above uses liblcms2. Tested with **1.19** and **2.7-2.17**.
above uses liblcms2. Tested with **1.19** and **2.7-2.18**.
* **libwebp** provides the WebP format.

View File

@ -57,7 +57,7 @@ These platforms are built and tested for every change.
| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 |
| | PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 |
| | 3.13 (MinGW) | x86-64 |
+----------------------------------+----------------------------+---------------------+
@ -75,7 +75,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+=============================+==================+==============+
| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.0.0 |arm |
| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.1.1 |arm |
| +-----------------------------+------------------+ |
| | 3.9 | 11.3.0 | |
+----------------------------------+-----------------------------+------------------+--------------+

View File

@ -4,7 +4,7 @@
Security
========
:cve:`2021-25289`: Fix OOB write with invalid tile extents
:cve:`2026-25990`: Fix OOB write with invalid tile extents
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Check that tile extents do not use negative x or y offsets when decoding or encoding,

View File

@ -112,14 +112,6 @@ test-requires = [
]
xbuild-tools = [ ]
[tool.cibuildwheel.macos]
# Disable platform guessing on macOS to avoid picking up Homebrew etc.
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
[tool.cibuildwheel.macos.environment]
# Isolate macOS build environment from Homebrew etc.
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
[tool.cibuildwheel.ios]
# Disable platform guessing on iOS, and disable raqm (since there won't be a
# vendor version, and we can't distribute it due to licensing)
@ -139,6 +131,14 @@ test-command = [
# There's no numpy wheel for iOS (yet...)
test-requires = [ ]
[tool.cibuildwheel.macos]
# Disable platform guessing on macOS to avoid picking up Homebrew etc.
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
[tool.cibuildwheel.macos.environment]
# Isolate macOS build environment from Homebrew etc.
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
[[tool.cibuildwheel.overrides]]
# iOS environment is isolated by cibuildwheel, but needs the dependencies
select = "*_iphoneos"
@ -217,6 +217,7 @@ testpaths = [
python_version = "3.10"
pretty = true
disallow_any_generics = true
disallow_untyped_defs = true
enable_error_code = "ignore-without-code"
extra_checks = true
follow_imports = "silent"

View File

@ -76,7 +76,7 @@ def testimage() -> None:
('R', 'G', 'B')
>>> im.getbbox()
(0, 0, 128, 128)
>>> len(im.getdata())
>>> len(im.get_flattened_data())
16384
>>> im.getextrema()
((0, 255), (0, 255), (0, 255))

View File

@ -363,7 +363,6 @@ class pil_build_ext(build_ext):
("disable-platform-guessing", None, "Disable platform guessing"),
("debug", None, "Debug logging"),
]
+ [("add-imaging-libs=", None, "Add libs to _imaging build")]
)
@staticmethod
@ -374,7 +373,6 @@ class pil_build_ext(build_ext):
self.disable_platform_guessing = self.check_configuration(
"platform-guessing", "disable"
)
self.add_imaging_libs = ""
build_ext.initialize_options(self)
for x in self.feature:
setattr(self, f"disable_{x}", self.check_configuration(x, "disable"))
@ -901,7 +899,6 @@ class pil_build_ext(build_ext):
# core library
libs: list[str | bool | None] = []
libs.extend(self.add_imaging_libs.split())
defs: list[tuple[str, str | None]] = []
if feature.get("tiff"):
libs.append(feature.get("tiff"))
@ -1092,7 +1089,11 @@ ext_modules = [
Extension("PIL._webp", ["src/_webp.c"]),
Extension("PIL._avif", ["src/_avif.c"]),
Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]),
Extension("PIL._imagingmath", ["src/_imagingmath.c"]),
Extension(
"PIL._imagingmath",
["src/_imagingmath.c"],
libraries=None if sys.platform == "win32" else ["m"],
),
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
]

View File

@ -20,6 +20,7 @@
"""
Parse X Bitmap Distribution Format (BDF)
"""
from __future__ import annotations
from typing import BinaryIO

View File

@ -190,7 +190,7 @@ class EpsImageFile(ImageFile.ImageFile):
def _open(self) -> None:
assert self.fp is not None
(length, offset) = self._find_offset(self.fp)
length, offset = self._find_offset(self.fp)
# go to offset - start of "%!PS"
self.fp.seek(offset)

View File

@ -13,6 +13,7 @@
This module provides constants and clear-text names for various
well-known EXIF tags.
"""
from __future__ import annotations
from enum import IntEnum

View File

@ -141,7 +141,7 @@ class FpxImageFile(ImageFile.ImageFile):
size = i32(s, 4), i32(s, 8)
# tilecount = i32(s, 12)
tilesize = i32(s, 16), i32(s, 20)
xtile, ytile = i32(s, 16), i32(s, 20)
# channels = i32(s, 24)
offset = i32(s, 28)
length = i32(s, 32)
@ -156,7 +156,6 @@ class FpxImageFile(ImageFile.ImageFile):
x = y = 0
xsize, ysize = size
xtile, ytile = tilesize
self.tile = []
for i in range(0, len(s), length):
@ -224,7 +223,7 @@ class FpxImageFile(ImageFile.ImageFile):
msg = "unknown/invalid compression"
raise OSError(msg)
x = x + xtile
x += xtile
if x >= xsize:
x, y = 0, y + ytile
if y >= ysize:

View File

@ -25,6 +25,7 @@
implementation is provided for convenience and demonstrational
purposes only.
"""
from __future__ import annotations
from typing import IO

View File

@ -937,7 +937,13 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
:param info: encoderinfo
:returns: list of indexes of palette entries in use, or None
"""
if im.mode in ("P", "L") and info and info.get("optimize"):
if (
im.mode in ("P", "L")
and info
and info.get("optimize")
and im.width != 0
and im.height != 0
):
# Potentially expensive operation.
# The palette saves 3 bytes per color not used, but palette

View File

@ -18,6 +18,7 @@ Stuff to translate curve segments to palette values (derived from
the corresponding code in GIMP, written by Federico Mena Quintero.
See the GIMP distribution for more information.)
"""
from __future__ import annotations
from math import log, pi, sin, sqrt

View File

@ -42,7 +42,7 @@ def read_32t(
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
) -> dict[str, Image.Image]:
# The 128x128 icon seems to have an extra header for some reason.
(start, length) = start_length
start, length = start_length
fobj.seek(start)
sig = fobj.read(4)
if sig != b"\x00\x00\x00\x00":
@ -58,7 +58,7 @@ def read_32(
Read a 32bit RGB icon resource. Seems to be either uncompressed or
an RLE packbits-like scheme.
"""
(start, length) = start_length
start, length = start_length
fobj.seek(start)
pixel_size = (size[0] * size[2], size[1] * size[2])
sizesq = pixel_size[0] * pixel_size[1]
@ -111,7 +111,7 @@ def read_mk(
def read_png_or_jpeg2000(
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
) -> dict[str, Image.Image]:
(start, length) = start_length
start, length = start_length
fobj.seek(start)
sig = fobj.read(12)

View File

@ -323,10 +323,112 @@ def getmodebands(mode: str) -> int:
_initialized = 0
# Mapping from file extension to plugin module name for lazy importing
_EXTENSION_PLUGIN: dict[str, str] = {
# Common formats (preinit)
".bmp": "BmpImagePlugin",
".dib": "BmpImagePlugin",
".gif": "GifImagePlugin",
".jfif": "JpegImagePlugin",
".jpe": "JpegImagePlugin",
".jpg": "JpegImagePlugin",
".jpeg": "JpegImagePlugin",
".pbm": "PpmImagePlugin",
".pgm": "PpmImagePlugin",
".pnm": "PpmImagePlugin",
".ppm": "PpmImagePlugin",
".pfm": "PpmImagePlugin",
".png": "PngImagePlugin",
".apng": "PngImagePlugin",
# Less common formats (init)
".avif": "AvifImagePlugin",
".avifs": "AvifImagePlugin",
".blp": "BlpImagePlugin",
".bufr": "BufrStubImagePlugin",
".cur": "CurImagePlugin",
".dcx": "DcxImagePlugin",
".dds": "DdsImagePlugin",
".ps": "EpsImagePlugin",
".eps": "EpsImagePlugin",
".fit": "FitsImagePlugin",
".fits": "FitsImagePlugin",
".fli": "FliImagePlugin",
".flc": "FliImagePlugin",
".fpx": "FpxImagePlugin",
".ftc": "FtexImagePlugin",
".ftu": "FtexImagePlugin",
".gbr": "GbrImagePlugin",
".grib": "GribStubImagePlugin",
".h5": "Hdf5StubImagePlugin",
".hdf": "Hdf5StubImagePlugin",
".icns": "IcnsImagePlugin",
".ico": "IcoImagePlugin",
".im": "ImImagePlugin",
".iim": "IptcImagePlugin",
".jp2": "Jpeg2KImagePlugin",
".j2k": "Jpeg2KImagePlugin",
".jpc": "Jpeg2KImagePlugin",
".jpf": "Jpeg2KImagePlugin",
".jpx": "Jpeg2KImagePlugin",
".j2c": "Jpeg2KImagePlugin",
".mic": "MicImagePlugin",
".mpg": "MpegImagePlugin",
".mpeg": "MpegImagePlugin",
".mpo": "MpoImagePlugin",
".msp": "MspImagePlugin",
".palm": "PalmImagePlugin",
".pcd": "PcdImagePlugin",
".pcx": "PcxImagePlugin",
".pdf": "PdfImagePlugin",
".pxr": "PixarImagePlugin",
".psd": "PsdImagePlugin",
".qoi": "QoiImagePlugin",
".bw": "SgiImagePlugin",
".rgb": "SgiImagePlugin",
".rgba": "SgiImagePlugin",
".sgi": "SgiImagePlugin",
".ras": "SunImagePlugin",
".tga": "TgaImagePlugin",
".icb": "TgaImagePlugin",
".vda": "TgaImagePlugin",
".vst": "TgaImagePlugin",
".tif": "TiffImagePlugin",
".tiff": "TiffImagePlugin",
".webp": "WebPImagePlugin",
".wmf": "WmfImagePlugin",
".emf": "WmfImagePlugin",
".xbm": "XbmImagePlugin",
".xpm": "XpmImagePlugin",
}
def _import_plugin_for_extension(ext: str | bytes) -> bool:
"""Import only the plugin needed for a specific file extension."""
if not ext:
return False
if isinstance(ext, bytes):
ext = ext.decode()
ext = ext.lower()
if ext in EXTENSION:
return True
plugin = _EXTENSION_PLUGIN.get(ext)
if plugin is None:
return False
try:
logger.debug("Importing %s", plugin)
__import__(f"{__spec__.parent}.{plugin}", globals(), locals(), [])
return True
except ImportError as e:
logger.debug("Image: failed to import %s: %s", plugin, e)
return False
def preinit() -> None:
"""
Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers.
Explicitly loads BMP, GIF, JPEG, PPM and PNG file format drivers.
It is called when opening or saving images.
"""
@ -382,11 +484,10 @@ def init() -> bool:
if _initialized >= 2:
return False
parent_name = __name__.rpartition(".")[0]
for plugin in _plugins:
try:
logger.debug("Importing %s", plugin)
__import__(f"{parent_name}.{plugin}", globals(), locals(), [])
__import__(f"{__spec__.parent}.{plugin}", globals(), locals(), [])
except ImportError as e:
logger.debug("Image: failed to import %s: %s", plugin, e)
@ -892,7 +993,9 @@ class Image:
else:
self.im.putpalettealphas(self.info["transparency"])
self.palette.mode = "RGBA"
else:
elif self.palette.mode != mode:
# If the palette rawmode is different to the mode,
# then update the Python palette data
self.palette.palette = self.im.getpalette(
self.palette.mode, self.palette.mode
)
@ -2443,7 +2546,7 @@ class Image:
]
def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]:
(a, b, c, d, e, f) = matrix
a, b, c, d, e, f = matrix
return a * x + b * y + c, d * x + e * y + f
matrix[2], matrix[5] = transform(
@ -2533,12 +2636,20 @@ class Image:
# only set the name for metadata purposes
filename = os.fspath(fp.name)
preinit()
if format:
preinit()
else:
filename_ext = os.path.splitext(filename)[1].lower()
ext = (
filename_ext.decode()
if isinstance(filename_ext, bytes)
else filename_ext
)
filename_ext = os.path.splitext(filename)[1].lower()
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
# Try importing only the plugin for this extension first
if not _import_plugin_for_extension(ext):
preinit()
if not format:
if ext not in EXTENSION:
init()
try:
@ -3378,7 +3489,7 @@ def fromarrow(
msg = "arrow_c_array interface not found"
raise ValueError(msg)
(schema_capsule, array_capsule) = obj.__arrow_c_array__()
schema_capsule, array_capsule = obj.__arrow_c_array__()
_im = core.new_arrow(mode, size, schema_capsule, array_capsule)
if _im:
return Image()._new(_im)
@ -3522,7 +3633,11 @@ def open(
prefix = fp.read(16)
preinit()
# Try to import just the plugin needed for this file extension
# before falling back to preinit() which imports common plugins
ext = os.path.splitext(filename)[1] if filename else ""
if not _import_plugin_for_extension(ext):
preinit()
warning_messages: list[str] = []
@ -3558,14 +3673,19 @@ def open(
im = _open_core(fp, filename, prefix, formats)
if im is None and formats is ID:
checked_formats = ID.copy()
if init():
im = _open_core(
fp,
filename,
prefix,
tuple(format for format in formats if format not in checked_formats),
)
# Try preinit (few common plugins) then init (all plugins)
for loader in (preinit, init):
checked_formats = ID.copy()
loader()
if formats != checked_formats:
im = _open_core(
fp,
filename,
prefix,
tuple(f for f in formats if f not in checked_formats),
)
if im is not None:
break
if im:
im._exclusive_fp = exclusive_fp

View File

@ -487,7 +487,7 @@ class ImageDraw:
if full_x:
self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
elif x1 - r - 1 > x0 + r + 1:
elif x1 - r - 1 >= x0 + r + 1:
self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
if not full_x and not full_y:
left = [x0, y0, x0 + r, y1]

View File

@ -22,6 +22,7 @@
.. seealso:: :py:mod:`PIL.ImageDraw`
"""
from __future__ import annotations
from typing import Any, AnyStr, BinaryIO
@ -117,7 +118,7 @@ class Draw:
def settransform(self, offset: tuple[float, float]) -> None:
"""Sets a transformation offset."""
(xoffset, yoffset) = offset
xoffset, yoffset = offset
self.transform = (1, 0, xoffset, 0, 1, yoffset)
def arc(

View File

@ -801,9 +801,9 @@ class PyCodec:
self.im = im
if extents:
(x0, y0, x1, y1) = extents
x0, y0, x1, y1 = extents
else:
(x0, y0, x1, y1) = (0, 0, 0, 0)
x0, y0, x1, y1 = (0, 0, 0, 0)
if x0 == 0 and x1 == 0:
self.state.xsize, self.state.ysize = self.im.size
@ -814,7 +814,7 @@ class PyCodec:
self.state.ysize = y1 - y0
if self.state.xsize <= 0 or self.state.ysize <= 0:
msg = "Size cannot be negative"
msg = "Size must be positive"
raise ValueError(msg)
if (

View File

@ -940,9 +940,7 @@ def load_default_imagefont() -> ImageFont:
f = ImageFont()
f._load_pilfont_data(
# courB08
BytesIO(
base64.b64decode(
b"""
BytesIO(base64.b64decode(b"""
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
@ -1034,13 +1032,8 @@ AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
+QAGAAIAzgAKANUAEw==
"""
)
),
Image.open(
BytesIO(
base64.b64decode(
b"""
""")),
Image.open(BytesIO(base64.b64decode(b"""
iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
@ -1064,10 +1057,7 @@ evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
w7IkEbzhVQAAAABJRU5ErkJggg==
"""
)
)
),
"""))),
)
return f
@ -1088,9 +1078,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""
if isinstance(core, ModuleType) or size is not None:
return truetype(
BytesIO(
base64.b64decode(
b"""
BytesIO(base64.b64decode(b"""
AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA
AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA
MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh
@ -1311,9 +1299,7 @@ ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA
gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC
YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA
AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
"""
)
),
""")),
10 if size is None else size,
layout_engine=Layout.BASIC,
)

View File

@ -140,7 +140,7 @@ class MspDecoder(ImageFile.PyDecoder):
runtype = row[idx]
idx += 1
if runtype == 0:
(runcount, runval) = struct.unpack_from("Bc", row, idx)
runcount, runval = struct.unpack_from("Bc", row, idx)
img.write(runval * runcount)
idx += 2
else:

View File

@ -100,7 +100,8 @@ class PSDraw:
Draws text at the given position. You must use
:py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
"""
text_bytes = bytes(text, "UTF-8")
# The font is loaded as ISOLatin1Encoding, so use latin-1 here.
text_bytes = bytes(text, "latin-1")
text_bytes = b"\\(".join(text_bytes.split(b"("))
text_bytes = b"\\)".join(text_bytes.split(b")"))
self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,)))

View File

@ -210,8 +210,8 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
#
# --------------------------------------------------------------------
Image.register_save("Palm", _save)
Image.register_save("PALM", _save)
Image.register_extension("Palm", ".palm")
Image.register_extension("PALM", ".palm")
Image.register_mime("Palm", "image/palm")
Image.register_mime("PALM", "image/palm")

View File

@ -146,6 +146,10 @@ SAVE = {
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.width == 0 or im.height == 0:
msg = "Cannot write empty image as PCX"
raise ValueError(msg)
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:

View File

@ -244,7 +244,7 @@ def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | No
def makeSpiderHeader(im: Image.Image) -> list[bytes]:
nsam, nrow = im.size
lenbyt = nsam * 4 # There are labrec records in the header
lenbyt = max(1, nsam) * 4 # There are labrec records in the header
labrec = int(1024 / lenbyt)
if 1024 % lenbyt != 0:
labrec += 1
@ -290,9 +290,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# get the filename extension and register it with Image
filename_ext = os.path.splitext(filename)[1]
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
Image.register_extension(SpiderImageFile.format, ext)
if filename_ext := os.path.splitext(filename)[1]:
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
Image.register_extension(SpiderImageFile.format, ext)
_save(im, fp, filename)

View File

@ -22,6 +22,7 @@ and has been tested with a few sample files found using google.
is not registered for use with :py:func:`PIL.Image.open()`.
To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead.
"""
from __future__ import annotations
from typing import IO

View File

@ -45,7 +45,6 @@ if hasattr(Image.core, "drawwmf"):
class WmfHandler(ImageFile.StubHandler):
def open(self, im: ImageFile.StubImageFile) -> None:
im._mode = "RGB"
self.bbox = im.info["wmf_bbox"]
def load(self, im: ImageFile.StubImageFile) -> Image.Image:

View File

@ -13,6 +13,7 @@
"""Binary input/output support routines."""
from __future__ import annotations
from struct import pack, unpack_from

View File

@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
__version__ = "12.1.1"
__version__ = "12.2.0.dev0"

View File

@ -485,7 +485,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
frame = image;
} else {
frame = avifImageCreateEmpty();
if (image == NULL) {
if (frame == NULL) {
PyErr_SetString(PyExc_ValueError, "Image creation failed");
return NULL;
}

View File

@ -116,17 +116,17 @@ V = {
"BROTLI": "1.2.0",
"FREETYPE": "2.14.1",
"FRIBIDI": "1.0.16",
"HARFBUZZ": "12.3.0",
"HARFBUZZ": "12.3.2",
"JPEGTURBO": "3.1.3",
"LCMS2": "2.17",
"LCMS2": "2.18",
"LIBAVIF": "1.3.0",
"LIBIMAGEQUANT": "4.4.1",
"LIBPNG": "1.6.53",
"LIBPNG": "1.6.54",
"LIBWEBP": "1.6.0",
"OPENJPEG": "2.5.4",
"TIFF": "4.7.1",
"XZ": "5.8.2",
"ZLIBNG": "2.3.2",
"ZLIBNG": "2.3.3",
}
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])