mirror of
https://github.com/python-pillow/Pillow.git
synced 2026-02-21 22:50:28 +03:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43c12af730 | ||
|
|
4777a0b318 | ||
|
|
02764a0077 | ||
|
|
3cd69cb12f | ||
|
|
a5c9eba30a | ||
|
|
2c00c6f80e | ||
|
|
d4111967a8 | ||
|
|
f71d74eec2 | ||
|
|
1457c6032a | ||
|
|
657d0414f0 | ||
|
|
3795a1b916 | ||
|
|
913698b667 | ||
|
|
27765189c8 | ||
|
|
a15f9c6121 | ||
|
|
54ba4db542 | ||
|
|
f78663b806 | ||
|
|
657d6ea4b6 | ||
|
|
49bc134ee1 | ||
|
|
26a188c062 | ||
|
|
fd8fa7df79 | ||
|
|
18cab11437 | ||
|
|
2a2638e58f | ||
|
|
8eddb86076 | ||
|
|
1ac7691fe5 | ||
|
|
e108e646da | ||
|
|
62aa42f9da | ||
|
|
508e9c9984 | ||
|
|
095cdb3c4a | ||
|
|
7cbe8c4924 | ||
|
|
27924be4fd | ||
|
|
fc4dbc3810 | ||
|
|
0e8bb72a66 | ||
|
|
f86ad8b36d | ||
|
|
29ff5fcb55 | ||
|
|
6a5c588c5f | ||
|
|
a293273b31 | ||
|
|
6564325e43 | ||
|
|
93c8a60784 | ||
|
|
b6178303a1 | ||
|
|
d568c8d9e3 | ||
|
|
d08d7ee99e | ||
|
|
2b186fceb8 | ||
|
|
c036185514 | ||
|
|
d737687fc3 | ||
|
|
34814d8d2f | ||
|
|
3968886cf6 | ||
|
|
bc64ccbf28 | ||
|
|
76d3116ef0 | ||
|
|
a6b36f0b6b | ||
|
|
a0f51493ca | ||
|
|
a6a701c4db | ||
|
|
e08f910db4 | ||
|
|
d1974d76f7 | ||
|
|
5ea2d3a056 | ||
|
|
d23a899f23 | ||
|
|
096c479cfb | ||
|
|
7f38f980dd | ||
|
|
b06118c2b3 | ||
|
|
9c8059fdea | ||
|
|
1baf141146 | ||
|
|
6b9de40533 | ||
|
|
ef8ff756fa | ||
|
|
2e9d54887b | ||
|
|
0f4becea73 | ||
|
|
e2b87a0420 | ||
|
|
d7dfeeb7ad | ||
|
|
426ad8307d | ||
|
|
627d8743b7 | ||
|
|
dcd52ebf65 | ||
|
|
d6e0a8d174 | ||
|
|
2210714a43 | ||
|
|
3d7801417a | ||
|
|
a85d3b135d | ||
|
|
932aa68d2a | ||
|
|
fe236d77a5 | ||
|
|
bc0e2c0e61 | ||
|
|
e66dd607f0 | ||
|
|
d5d8a91597 | ||
|
|
b8351fde41 | ||
|
|
36cf82ae76 | ||
|
|
525842215f | ||
|
|
844b10f894 | ||
|
|
555fb8371c | ||
|
|
0a1d6c3c61 | ||
|
|
00ec73dfd1 | ||
|
|
e924cfd181 | ||
|
|
2360d0df17 | ||
|
|
499b796556 | ||
|
|
5b677ca1c6 | ||
|
|
b71109d435 | ||
|
|
4337139f0c | ||
|
|
72931475f2 | ||
|
|
79357a2718 | ||
|
|
3abb62ed29 | ||
|
|
d06c8b3591 | ||
|
|
6a9960e8c1 |
|
|
@ -53,7 +53,7 @@ pushd depends && ./install_imagequant.sh && popd
|
||||||
pushd depends && sudo ./install_raqm.sh && popd
|
pushd depends && sudo ./install_raqm.sh && popd
|
||||||
|
|
||||||
# libavif
|
# libavif
|
||||||
pushd depends && sudo ./install_libavif.sh && popd
|
pushd depends && ./install_libavif.sh && popd
|
||||||
|
|
||||||
# extra test images
|
# extra test images
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
cibuildwheel==3.3.0
|
cibuildwheel==3.3.1
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
mypy==1.19.0
|
mypy==1.19.1
|
||||||
arro3-compute
|
arro3-compute
|
||||||
arro3-core
|
arro3-core
|
||||||
IceSpringPySideStubs-PyQt6
|
IceSpringPySideStubs-PyQt6
|
||||||
|
|
@ -9,7 +9,6 @@ packaging
|
||||||
pyarrow-stubs
|
pyarrow-stubs
|
||||||
pybind11
|
pybind11
|
||||||
pytest
|
pytest
|
||||||
sphinx
|
|
||||||
types-atheris
|
types-atheris
|
||||||
types-defusedxml
|
types-defusedxml
|
||||||
types-olefile
|
types-olefile
|
||||||
|
|
|
||||||
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
|
|
@ -6,6 +6,7 @@
|
||||||
"labels": [
|
"labels": [
|
||||||
"Dependency"
|
"Dependency"
|
||||||
],
|
],
|
||||||
|
"minimumReleaseAge": "7 days",
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"groupName": "github-actions",
|
"groupName": "github-actions",
|
||||||
|
|
|
||||||
4
.github/workflows/cifuzz.yml
vendored
4
.github/workflows/cifuzz.yml
vendored
|
|
@ -44,13 +44,13 @@ jobs:
|
||||||
language: python
|
language: python
|
||||||
dry-run: false
|
dry-run: false
|
||||||
- name: Upload New Crash
|
- name: Upload New Crash
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
if: failure() && steps.build.outcome == 'success'
|
if: failure() && steps.build.outcome == 'success'
|
||||||
with:
|
with:
|
||||||
name: artifacts
|
name: artifacts
|
||||||
path: ./out/artifacts
|
path: ./out/artifacts
|
||||||
- name: Upload Legacy Crash
|
- name: Upload Legacy Crash
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
if: steps.run.outcome == 'success'
|
if: steps.run.outcome == 'success'
|
||||||
with:
|
with:
|
||||||
name: crash
|
name: crash
|
||||||
|
|
|
||||||
18
.github/workflows/docs.yml
vendored
18
.github/workflows/docs.yml
vendored
|
|
@ -48,19 +48,35 @@ jobs:
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
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
|
- name: Cache libimagequant
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
id: cache-libimagequant
|
id: cache-libimagequant
|
||||||
with:
|
with:
|
||||||
path: ~/cache-libimagequant
|
path: ~/cache-libimagequant
|
||||||
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
|
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
|
- name: Install Linux dependencies
|
||||||
run: |
|
run: |
|
||||||
.ci/install.sh
|
.ci/install.sh
|
||||||
env:
|
env:
|
||||||
GHA_PYTHON_VERSION: "3.x"
|
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_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
|
||||||
|
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.x"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v7
|
||||||
- name: Lint
|
- name: Lint
|
||||||
|
|
|
||||||
2
.github/workflows/test-docker.yml
vendored
2
.github/workflows/test-docker.yml
vendored
|
|
@ -83,7 +83,7 @@ jobs:
|
||||||
|
|
||||||
- name: Docker pull
|
- name: Docker pull
|
||||||
run: |
|
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
|
- name: Docker build
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
11
.github/workflows/test-windows.yml
vendored
11
.github/workflows/test-windows.yml
vendored
|
|
@ -112,7 +112,7 @@ jobs:
|
||||||
|
|
||||||
- name: Cache build
|
- name: Cache build
|
||||||
id: build-cache
|
id: build-cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: winbuild\build
|
path: winbuild\build
|
||||||
key:
|
key:
|
||||||
|
|
@ -188,8 +188,9 @@ jobs:
|
||||||
# trim ~150MB for each job
|
# trim ~150MB for each job
|
||||||
- name: Optimize build cache
|
- name: Optimize build cache
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
run: rmdir /S /Q winbuild\build\src
|
run: |
|
||||||
shell: cmd
|
rm -rf winbuild\build\src
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Build Pillow
|
- name: Build Pillow
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -206,9 +207,7 @@ jobs:
|
||||||
|
|
||||||
- name: Test Pillow
|
- name: Test Pillow
|
||||||
run: |
|
run: |
|
||||||
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
|
|
||||||
.ci\test.cmd
|
.ci\test.cmd
|
||||||
shell: cmd
|
|
||||||
|
|
||||||
- name: Prepare to upload errors
|
- name: Prepare to upload errors
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|
@ -217,7 +216,7 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Upload errors
|
- name: Upload errors
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: errors
|
name: errors
|
||||||
|
|
|
||||||
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
|
|
@ -29,6 +29,7 @@ concurrency:
|
||||||
env:
|
env:
|
||||||
COVERAGE_CORE: sysmon
|
COVERAGE_CORE: sysmon
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -90,21 +91,39 @@ jobs:
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
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
|
- name: Cache libimagequant
|
||||||
if: startsWith(matrix.os, 'ubuntu')
|
if: startsWith(matrix.os, 'ubuntu')
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
id: cache-libimagequant
|
id: cache-libimagequant
|
||||||
with:
|
with:
|
||||||
path: ~/cache-libimagequant
|
path: ~/cache-libimagequant
|
||||||
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
|
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
|
- name: Install Linux dependencies
|
||||||
if: startsWith(matrix.os, 'ubuntu')
|
if: startsWith(matrix.os, 'ubuntu')
|
||||||
run: |
|
run: |
|
||||||
.ci/install.sh
|
.ci/install.sh
|
||||||
env:
|
env:
|
||||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
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_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
|
||||||
|
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
|
||||||
|
|
||||||
- name: Install macOS dependencies
|
- name: Install macOS dependencies
|
||||||
if: startsWith(matrix.os, 'macOS')
|
if: startsWith(matrix.os, 'macOS')
|
||||||
|
|
@ -143,7 +162,7 @@ jobs:
|
||||||
mkdir -p Tests/errors
|
mkdir -p Tests/errors
|
||||||
|
|
||||||
- name: Upload errors
|
- name: Upload errors
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: errors
|
name: errors
|
||||||
|
|
|
||||||
10
.github/workflows/wheels-dependencies.sh
vendored
10
.github/workflows/wheels-dependencies.sh
vendored
|
|
@ -95,15 +95,15 @@ if [[ -n "$IOS_SDK" ]]; then
|
||||||
else
|
else
|
||||||
FREETYPE_VERSION=2.14.1
|
FREETYPE_VERSION=2.14.1
|
||||||
fi
|
fi
|
||||||
HARFBUZZ_VERSION=12.3.0
|
HARFBUZZ_VERSION=12.3.2
|
||||||
LIBPNG_VERSION=1.6.53
|
LIBPNG_VERSION=1.6.54
|
||||||
JPEGTURBO_VERSION=3.1.3
|
JPEGTURBO_VERSION=3.1.3
|
||||||
OPENJPEG_VERSION=2.5.4
|
OPENJPEG_VERSION=2.5.4
|
||||||
XZ_VERSION=5.8.2
|
XZ_VERSION=5.8.2
|
||||||
ZSTD_VERSION=1.5.7
|
ZSTD_VERSION=1.5.7
|
||||||
TIFF_VERSION=4.7.1
|
TIFF_VERSION=4.7.1
|
||||||
LCMS2_VERSION=2.17
|
LCMS2_VERSION=2.18
|
||||||
ZLIB_NG_VERSION=2.3.2
|
ZLIB_NG_VERSION=2.3.3
|
||||||
LIBWEBP_VERSION=1.6.0
|
LIBWEBP_VERSION=1.6.0
|
||||||
BZIP2_VERSION=1.0.8
|
BZIP2_VERSION=1.0.8
|
||||||
LIBXCB_VERSION=1.17.0
|
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
|
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||||
if [[ -n "$IS_MACOS" ]]; then
|
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 libXau 1.0.12 https://www.x.org/pub/individual/lib
|
||||||
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||||
else
|
else
|
||||||
|
|
|
||||||
48
.github/workflows/wheels.yml
vendored
48
.github/workflows/wheels.yml
vendored
|
|
@ -134,7 +134,7 @@ jobs:
|
||||||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v5
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: dist-${{ matrix.name }}
|
name: dist-${{ matrix.name }}
|
||||||
path: ./wheelhouse/*.whl
|
path: ./wheelhouse/*.whl
|
||||||
|
|
@ -186,24 +186,18 @@ jobs:
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
run: |
|
run: |
|
||||||
setlocal EnableDelayedExpansion
|
for f in winbuild/build/license/*; do
|
||||||
for %%f in (winbuild\build\license\*) do (
|
name=$(basename "${f%.*}")
|
||||||
set x=%%~nf
|
# Skip FriBiDi license, it is not included in the wheel.
|
||||||
rem Skip FriBiDi license, it is not included in the wheel.
|
[[ $name == fribidi* ]] && continue
|
||||||
set fribidi=!x:~0,7!
|
# Skip imagequant license, it is not included in the wheel.
|
||||||
if NOT !fribidi!==fribidi (
|
[[ $name == libimagequant* ]] && continue
|
||||||
rem Skip imagequant license, it is not included in the wheel.
|
echo "" >> LICENSE
|
||||||
set libimagequant=!x:~0,13!
|
echo "===== $name =====" >> LICENSE
|
||||||
if NOT !libimagequant!==libimagequant (
|
echo "" >> LICENSE
|
||||||
echo. >> LICENSE
|
cat "$f" >> LICENSE
|
||||||
echo ===== %%~nf ===== >> LICENSE
|
done
|
||||||
echo. >> LICENSE
|
cmd //c "winbuild\\build\\build_env.cmd && $pythonLocation\\python.exe -m cibuildwheel . --output-dir wheelhouse"
|
||||||
type %%f >> LICENSE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
call winbuild\\build\\build_env.cmd
|
|
||||||
%pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
|
|
||||||
env:
|
env:
|
||||||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||||
|
|
@ -217,16 +211,16 @@ jobs:
|
||||||
-e CI -e GITHUB_ACTIONS
|
-e CI -e GITHUB_ACTIONS
|
||||||
mcr.microsoft.com/windows/servercore:ltsc2022
|
mcr.microsoft.com/windows/servercore:ltsc2022
|
||||||
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
|
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
|
||||||
shell: cmd
|
shell: bash
|
||||||
|
|
||||||
- name: Upload wheels
|
- name: Upload wheels
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: dist-windows-${{ matrix.cibw_arch }}
|
name: dist-windows-${{ matrix.cibw_arch }}
|
||||||
path: ./wheelhouse/*.whl
|
path: ./wheelhouse/*.whl
|
||||||
|
|
||||||
- name: Upload fribidi.dll
|
- name: Upload fribidi.dll
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: fribidi-windows-${{ matrix.cibw_arch }}
|
name: fribidi-windows-${{ matrix.cibw_arch }}
|
||||||
path: winbuild\build\bin\fribidi*
|
path: winbuild\build\bin\fribidi*
|
||||||
|
|
@ -246,7 +240,7 @@ jobs:
|
||||||
|
|
||||||
- run: make sdist
|
- run: make sdist
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v5
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: dist-sdist
|
name: dist-sdist
|
||||||
path: dist/*.tar.gz
|
path: dist/*.tar.gz
|
||||||
|
|
@ -256,7 +250,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Count dists
|
name: Count dists
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v6
|
- uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
pattern: dist-*
|
pattern: dist-*
|
||||||
path: dist
|
path: dist
|
||||||
|
|
@ -275,13 +269,13 @@ 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@v6
|
- uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
pattern: dist-!(sdist)*
|
pattern: dist-!(sdist)*
|
||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Upload wheels to scientific-python-nightly-wheels
|
- 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:
|
with:
|
||||||
artifacts_path: dist
|
artifacts_path: dist
|
||||||
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
|
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
|
||||||
|
|
@ -297,7 +291,7 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v6
|
- uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
pattern: dist-*
|
pattern: dist-*
|
||||||
path: dist
|
path: dist
|
||||||
|
|
|
||||||
2
.github/zizmor.yml
vendored
2
.github/zizmor.yml
vendored
|
|
@ -1,7 +1,5 @@
|
||||||
# https://docs.zizmor.sh/configuration/
|
# https://docs.zizmor.sh/configuration/
|
||||||
rules:
|
rules:
|
||||||
obfuscation:
|
|
||||||
disable: true
|
|
||||||
unpinned-uses:
|
unpinned-uses:
|
||||||
config:
|
config:
|
||||||
policies:
|
policies:
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.7
|
rev: v0.14.14
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args: [--exit-non-zero-on-fix]
|
args: [--exit-non-zero-on-fix]
|
||||||
|
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 25.11.0
|
rev: 26.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/PyCQA/bandit
|
||||||
rev: 1.9.2
|
rev: 1.9.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: bandit
|
- id: bandit
|
||||||
args: [--severity-level=high]
|
args: [--severity-level=high]
|
||||||
files: ^src/
|
files: ^src/
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: v1.5.5
|
rev: v1.5.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: remove-tabs
|
- id: remove-tabs
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||||
rev: v21.1.6
|
rev: v21.1.8
|
||||||
hooks:
|
hooks:
|
||||||
- id: clang-format
|
- id: clang-format
|
||||||
types: [c]
|
types: [c]
|
||||||
|
|
@ -51,14 +51,14 @@ repos:
|
||||||
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
|
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||||
|
|
||||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||||
rev: 0.35.0
|
rev: 0.36.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-github-workflows
|
- id: check-github-workflows
|
||||||
- id: check-readthedocs
|
- id: check-readthedocs
|
||||||
- id: check-renovate
|
- id: check-renovate
|
||||||
|
|
||||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||||
rev: v1.18.0
|
rev: v1.22.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@ repos:
|
||||||
- id: sphinx-lint
|
- id: sphinx-lint
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: v2.11.1
|
rev: v2.12.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
|
|
||||||
|
|
@ -76,10 +76,10 @@ repos:
|
||||||
rev: v0.24.1
|
rev: v0.24.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: validate-pyproject
|
- 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
|
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||||
rev: 1.7.0
|
rev: 1.7.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: tox-ini-fmt
|
- id: tox-ini-fmt
|
||||||
|
|
||||||
|
|
|
||||||
BIN
Tests/images/imagedraw_rounded_rectangle_radius.png
Normal file
BIN
Tests/images/imagedraw_rounded_rectangle_radius.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 456 B |
BIN
Tests/images/pal8rletrns.png
Normal file
BIN
Tests/images/pal8rletrns.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
|
|
@ -213,7 +213,7 @@ INT32 = DataShape(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
|
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]
|
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||||
if dtype == fl_uint8_4_type:
|
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))
|
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
|
||||||
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
|
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]
|
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||||
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
|
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ def test_multiblock_l_image() -> None:
|
||||||
img = Image.new("L", size, 128)
|
img = Image.new("L", size, 128)
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
(schema, arr) = img.__arrow_c_array__()
|
schema, arr = img.__arrow_c_array__()
|
||||||
|
|
||||||
|
|
||||||
def test_multiblock_rgba_image() -> None:
|
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))
|
img = Image.new("RGBA", size, (128, 127, 126, 125))
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
(schema, arr) = img.__arrow_c_array__()
|
schema, arr = img.__arrow_c_array__()
|
||||||
|
|
||||||
|
|
||||||
def test_multiblock_l_schema() -> None:
|
def test_multiblock_l_schema() -> None:
|
||||||
|
|
@ -114,7 +114,7 @@ def test_singleblock_l_image() -> None:
|
||||||
img = Image.new("L", size, 128)
|
img = Image.new("L", size, 128)
|
||||||
assert img.im.isblock()
|
assert img.im.isblock()
|
||||||
|
|
||||||
(schema, arr) = img.__arrow_c_array__()
|
schema, arr = img.__arrow_c_array__()
|
||||||
assert schema
|
assert schema
|
||||||
assert arr
|
assert arr
|
||||||
|
|
||||||
|
|
@ -130,7 +130,7 @@ def test_singleblock_rgba_image() -> None:
|
||||||
img = Image.new("RGBA", size, (128, 127, 126, 125))
|
img = Image.new("RGBA", size, (128, 127, 126, 125))
|
||||||
assert img.im.isblock()
|
assert img.im.isblock()
|
||||||
|
|
||||||
(schema, arr) = img.__arrow_c_array__()
|
schema, arr = img.__arrow_c_array__()
|
||||||
assert schema
|
assert schema
|
||||||
assert arr
|
assert arr
|
||||||
Image.core.set_use_block_allocator(0)
|
Image.core.set_use_block_allocator(0)
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,11 @@ def test_rle8_eof(file_name: str, length: int) -> None:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rle_delta() -> None:
|
||||||
|
with Image.open("Tests/images/bmp/q/pal8rletrns.bmp") as im:
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/pal8rletrns.png")
|
||||||
|
|
||||||
|
|
||||||
def test_unsupported_bmp_bitfields_layout() -> None:
|
def test_unsupported_bmp_bitfields_layout() -> None:
|
||||||
fp = io.BytesIO(
|
fp = io.BytesIO(
|
||||||
o32(40) # header size
|
o32(40) # header size
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
|
||||||
assert reloaded.getpixel((0, 0)) == 255
|
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(
|
@pytest.mark.parametrize(
|
||||||
"path, mode",
|
"path, mode",
|
||||||
(
|
(
|
||||||
|
|
@ -399,7 +407,7 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
|
||||||
b = BytesIO()
|
b = BytesIO()
|
||||||
GifImagePlugin._save_netpbm(img_rgb, b, tempfile)
|
GifImagePlugin._save_netpbm(img_rgb, b, tempfile)
|
||||||
with Image.open(tempfile) as reloaded:
|
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")
|
@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()
|
b = BytesIO()
|
||||||
GifImagePlugin._save_netpbm(img_l, b, tempfile)
|
GifImagePlugin._save_netpbm(img_l, b, tempfile)
|
||||||
with Image.open(tempfile) as reloaded:
|
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:
|
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:
|
# with open('Tests/images/gif_header_data.pkl', 'wb') as f:
|
||||||
# pickle.dump((h, d), f, 1)
|
# pickle.dump((h, d), f, 1)
|
||||||
with open("Tests/images/gif_header_data.pkl", "rb") as f:
|
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 h == h_target
|
||||||
assert d == d_target
|
assert d == d_target
|
||||||
|
|
|
||||||
|
|
@ -590,9 +590,7 @@ class TestFileJpeg:
|
||||||
assert im2.quantization == {0: bounds_qtable}
|
assert im2.quantization == {0: bounds_qtable}
|
||||||
|
|
||||||
# values from wizard.txt in jpeg9-a src package.
|
# values from wizard.txt in jpeg9-a src package.
|
||||||
standard_l_qtable = [
|
standard_l_qtable = [int(s) for s in """
|
||||||
int(s)
|
|
||||||
for s in """
|
|
||||||
16 11 10 16 24 40 51 61
|
16 11 10 16 24 40 51 61
|
||||||
12 12 14 19 26 58 60 55
|
12 12 14 19 26 58 60 55
|
||||||
14 13 16 24 40 57 69 56
|
14 13 16 24 40 57 69 56
|
||||||
|
|
@ -601,14 +599,9 @@ class TestFileJpeg:
|
||||||
24 35 55 64 81 104 113 92
|
24 35 55 64 81 104 113 92
|
||||||
49 64 78 87 103 121 120 101
|
49 64 78 87 103 121 120 101
|
||||||
72 92 95 98 112 100 103 99
|
72 92 95 98 112 100 103 99
|
||||||
""".split(
|
""".split(None)]
|
||||||
None
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
standard_chrominance_qtable = [
|
standard_chrominance_qtable = [int(s) for s in """
|
||||||
int(s)
|
|
||||||
for s in """
|
|
||||||
17 18 24 47 99 99 99 99
|
17 18 24 47 99 99 99 99
|
||||||
18 21 26 66 99 99 99 99
|
18 21 26 66 99 99 99 99
|
||||||
24 26 56 99 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
|
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(
|
""".split(None)]
|
||||||
None
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
for quality in range(101):
|
for quality in range(101):
|
||||||
qtable_from_qtable_quality = self.roundtrip(
|
qtable_from_qtable_quality = self.roundtrip(
|
||||||
|
|
|
||||||
|
|
@ -738,7 +738,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
buffer_io.seek(0)
|
buffer_io.seek(0)
|
||||||
|
|
||||||
with Image.open(buffer_io) as saved_im:
|
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()
|
||||||
save_bytesio("raw")
|
save_bytesio("raw")
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,14 @@ def test_sanity(tmp_path: Path) -> None:
|
||||||
im.save(f)
|
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:
|
def test_p_4_planes() -> None:
|
||||||
with Image.open("Tests/images/p_4_planes.pcx") as im:
|
with Image.open("Tests/images/p_4_planes.pcx") as im:
|
||||||
assert im.getpixel((0, 0)) == 3
|
assert im.getpixel((0, 0)) == 3
|
||||||
|
|
@ -119,36 +127,36 @@ def test_large_count(tmp_path: Path) -> None:
|
||||||
_roundtrip(tmp_path, im)
|
_roundtrip(tmp_path, im)
|
||||||
|
|
||||||
|
|
||||||
def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None:
|
def _test_buffer_overflow(
|
||||||
_last = ImageFile.MAXBLOCK
|
tmp_path: Path, im: Image.Image, monkeypatch: pytest.MonkeyPatch
|
||||||
ImageFile.MAXBLOCK = size
|
) -> None:
|
||||||
try:
|
monkeypatch.setattr(ImageFile, "MAXBLOCK", 1024)
|
||||||
_roundtrip(tmp_path, im)
|
_roundtrip(tmp_path, im)
|
||||||
finally:
|
|
||||||
ImageFile.MAXBLOCK = _last
|
|
||||||
|
|
||||||
|
|
||||||
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))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
assert px is not None
|
assert px is not None
|
||||||
for y in range(4):
|
for y in range(4):
|
||||||
for x in range(256):
|
for x in range(256):
|
||||||
px[x, y] = x % 128
|
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))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
assert px is not None
|
assert px is not None
|
||||||
for y in range(5):
|
for y in range(5):
|
||||||
for x in range(256):
|
for x in range(256):
|
||||||
px[x, y] = x % 128
|
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))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
assert px is not None
|
assert px is not None
|
||||||
|
|
@ -157,10 +165,10 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
|
||||||
px[x, y] = x % 128
|
px[x, y] = x % 128
|
||||||
for x in range(8):
|
for x in range(8):
|
||||||
px[x, 4] = 16
|
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))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
assert px is not None
|
assert px is not None
|
||||||
|
|
@ -168,10 +176,10 @@ def test_break_one_at_end(tmp_path: Path) -> None:
|
||||||
for x in range(256):
|
for x in range(256):
|
||||||
px[x, y] = x % 128
|
px[x, y] = x % 128
|
||||||
px[0, 3] = 128 + 64
|
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))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
assert px is not None
|
assert px is not None
|
||||||
|
|
@ -181,10 +189,10 @@ def test_break_many_at_end(tmp_path: Path) -> None:
|
||||||
for x in range(4):
|
for x in range(4):
|
||||||
px[x * 2, 3] = 128 + 64
|
px[x * 2, 3] = 128 + 64
|
||||||
px[x + 256 - 4, 3] = 0
|
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))
|
im = Image.new("L", (257, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
assert px is not None
|
assert px is not None
|
||||||
|
|
@ -193,4 +201,4 @@ def test_break_padding(tmp_path: Path) -> None:
|
||||||
px[x, y] = x % 128
|
px[x, y] = x % 128
|
||||||
for x in range(5):
|
for x in range(5):
|
||||||
px[x, 3] = 0
|
px[x, 3] = 0
|
||||||
_test_buffer_overflow(tmp_path, im)
|
_test_buffer_overflow(tmp_path, im, monkeypatch)
|
||||||
|
|
|
||||||
|
|
@ -654,21 +654,17 @@ class TestFilePng:
|
||||||
with pytest.raises(SyntaxError, match="Unknown compression method"):
|
with pytest.raises(SyntaxError, match="Unknown compression method"):
|
||||||
PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png")
|
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
|
# This image has been manually hexedited
|
||||||
# so that the IDAT chunk has padding at the end
|
# so that the IDAT chunk has padding at the end
|
||||||
# Set MAXBLOCK to the length of the actual data
|
# Set MAXBLOCK to the length of the actual data
|
||||||
# so that the decoder finishes reading before the chunk ends
|
# so that the decoder finishes reading before the chunk ends
|
||||||
MAXBLOCK = ImageFile.MAXBLOCK
|
monkeypatch.setattr(ImageFile, "MAXBLOCK", 45)
|
||||||
ImageFile.MAXBLOCK = 45
|
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
||||||
|
|
||||||
with Image.open("Tests/images/padded_idat.png") as im:
|
with Image.open("Tests/images/padded_idat.png") as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
ImageFile.MAXBLOCK = MAXBLOCK
|
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
|
||||||
|
|
||||||
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
|
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ def test_seek_tell() -> None:
|
||||||
|
|
||||||
im.seek(2)
|
im.seek(2)
|
||||||
layer_number = im.tell()
|
layer_number = im.tell()
|
||||||
assert layer_number == 2
|
assert layer_number == 2
|
||||||
|
|
||||||
|
|
||||||
def test_seek_eoferror() -> None:
|
def test_seek_eoferror() -> None:
|
||||||
|
|
@ -138,7 +138,7 @@ def test_icc_profile() -> None:
|
||||||
assert "icc_profile" in im.info
|
assert "icc_profile" in im.info
|
||||||
|
|
||||||
icc_profile = im.info["icc_profile"]
|
icc_profile = im.info["icc_profile"]
|
||||||
assert len(icc_profile) == 3144
|
assert len(icc_profile) == 3144
|
||||||
|
|
||||||
|
|
||||||
def test_no_icc_profile() -> None:
|
def test_no_icc_profile() -> None:
|
||||||
|
|
@ -158,17 +158,16 @@ def test_combined_larger_than_size() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_file,raises",
|
"test_file",
|
||||||
[
|
[
|
||||||
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
"Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd",
|
||||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
"Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_crashes(test_file: str, raises: type[Exception]) -> None:
|
def test_crashes(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with pytest.raises(OSError):
|
||||||
with pytest.raises(raises):
|
with Image.open(test_file):
|
||||||
with Image.open(f):
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@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:
|
def test_layer_crashes(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with Image.open(test_file) as im:
|
||||||
with Image.open(f) as im:
|
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
with pytest.raises(SyntaxError):
|
||||||
with pytest.raises(SyntaxError):
|
im.layers
|
||||||
im.layers
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ from .helper import assert_image_equal, hopper, is_pypy
|
||||||
TEST_FILE = "Tests/images/hopper.spider"
|
TEST_FILE = "Tests/images/hopper.spider"
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_module() -> None:
|
||||||
|
del Image.EXTENSION[".spider"]
|
||||||
|
|
||||||
|
|
||||||
def test_sanity() -> None:
|
def test_sanity() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
@ -64,6 +68,14 @@ def test_save(tmp_path: Path) -> None:
|
||||||
assert im2.format == "SPIDER"
|
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:
|
def test_tempfile() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ def test_load_raw() -> None:
|
||||||
# Currently, support for WMF/EMF is Windows-only
|
# Currently, support for WMF/EMF is Windows-only
|
||||||
im.load()
|
im.load()
|
||||||
# Compare to reference rendering
|
# 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
|
# Test basic WMF open and rendering
|
||||||
with Image.open("Tests/images/drawing.wmf") as im:
|
with Image.open("Tests/images/drawing.wmf") as im:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont, _util
|
from PIL import Image, ImageDraw, ImageFont, _util
|
||||||
|
|
||||||
from .helper import PillowLeakTestCase, features, skip_unless_feature
|
from .helper import PillowLeakTestCase, features, skip_unless_feature
|
||||||
|
|
@ -7,11 +9,7 @@ from .helper import PillowLeakTestCase, features, skip_unless_feature
|
||||||
original_core = ImageFont.core
|
original_core = ImageFont.core
|
||||||
|
|
||||||
|
|
||||||
class TestTTypeFontLeak(PillowLeakTestCase):
|
class TestFontLeak(PillowLeakTestCase):
|
||||||
# fails at iteration 3 in main
|
|
||||||
iterations = 10
|
|
||||||
mem_limit = 4096 # k
|
|
||||||
|
|
||||||
def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None:
|
def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None:
|
||||||
im = Image.new("RGB", (255, 255), "white")
|
im = Image.new("RGB", (255, 255), "white")
|
||||||
draw = ImageDraw.ImageDraw(im)
|
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")
|
@skip_unless_feature("freetype2")
|
||||||
def test_leak(self) -> None:
|
def test_leak(self) -> None:
|
||||||
ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
|
ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
|
||||||
self._test_font(ttype)
|
self._test_font(ttype)
|
||||||
|
|
||||||
|
|
||||||
class TestDefaultFontLeak(TestTTypeFontLeak):
|
class TestDefaultFontLeak(TestFontLeak):
|
||||||
# fails at iteration 37 in main
|
# fails at iteration 37 in main
|
||||||
iterations = 100
|
iterations = 100
|
||||||
mem_limit = 1024 # k
|
mem_limit = 1024 # k
|
||||||
|
|
||||||
def test_leak(self) -> None:
|
def test_leak(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
if features.check_module("freetype2"):
|
if features.check_module("freetype2"):
|
||||||
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
monkeypatch.setattr(
|
||||||
try:
|
ImageFont,
|
||||||
default_font = ImageFont.load_default()
|
"core",
|
||||||
finally:
|
_util.DeferredError(ImportError("Disabled for testing")),
|
||||||
ImageFont.core = original_core
|
)
|
||||||
|
default_font = ImageFont.load_default()
|
||||||
self._test_font(default_font)
|
self._test_font(default_font)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal_tofile,
|
assert_image_equal_tofile,
|
||||||
assert_image_similar_tofile,
|
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -73,14 +72,14 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (130, 30), "white")
|
im = Image.new("L", (130, 30), "white")
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
draw.text((0, 0), message, "black", font=font)
|
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:
|
def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None:
|
||||||
tempname = save_font(request, tmp_path)
|
tempname = save_font(request, tmp_path)
|
||||||
font = ImageFont.load(tempname)
|
font = ImageFont.load(tempname)
|
||||||
for i in range(255):
|
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 ox == 0
|
||||||
assert oy == 0
|
assert oy == 0
|
||||||
assert dy == 20
|
assert dy == 20
|
||||||
|
|
@ -100,7 +99,7 @@ def _test_high_characters(
|
||||||
im = Image.new("L", (750, 30), "white")
|
im = Image.new("L", (750, 30), "white")
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
draw.text((0, 0), message, "black", font=font)
|
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:
|
def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None:
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal_tofile,
|
assert_image_equal_tofile,
|
||||||
assert_image_similar_tofile,
|
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -85,7 +84,7 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) ->
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
message = charsets[encoding]["message"].encode(encoding)
|
message = charsets[encoding]["message"].encode(encoding)
|
||||||
draw.text((0, 0), message, "black", font=font)
|
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"))
|
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
|
||||||
|
|
@ -95,7 +94,7 @@ def test_textsize(
|
||||||
tempname = save_font(request, tmp_path, encoding)
|
tempname = save_font(request, tmp_path, encoding)
|
||||||
font = ImageFont.load(tempname)
|
font = ImageFont.load(tempname)
|
||||||
for i in range(255):
|
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 ox == 0
|
||||||
assert oy == 0
|
assert oy == 0
|
||||||
assert dy == 20
|
assert dy == 20
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ def linear_gradient() -> Image.Image:
|
||||||
im = Image.linear_gradient(mode="L")
|
im = Image.linear_gradient(mode="L")
|
||||||
im90 = im.rotate(90)
|
im90 = im.rotate(90)
|
||||||
|
|
||||||
(px, h) = im.size
|
px, h = im.size
|
||||||
|
|
||||||
r = Image.new("L", (px * 3, h))
|
r = Image.new("L", (px * 3, h))
|
||||||
g = r.copy()
|
g = r.copy()
|
||||||
|
|
@ -54,7 +54,7 @@ def to_xxx_colorsys(
|
||||||
) -> Image.Image:
|
) -> Image.Image:
|
||||||
# convert the hard way using the library colorsys routines.
|
# convert the hard way using the library colorsys routines.
|
||||||
|
|
||||||
(r, g, b) = im.split()
|
r, g, b = im.split()
|
||||||
|
|
||||||
conv_func = int_to_float
|
conv_func = int_to_float
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -456,9 +456,11 @@ class TestImage:
|
||||||
# Assert
|
# Assert
|
||||||
assert len(Image.ID) == id_length
|
assert len(Image.ID) == id_length
|
||||||
|
|
||||||
def test_registered_extensions_uninitialized(self) -> None:
|
def test_registered_extensions_uninitialized(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
Image._initialized = 0
|
monkeypatch.setattr(Image, "_initialized", 0)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
Image.registered_extensions()
|
Image.registered_extensions()
|
||||||
|
|
@ -466,6 +468,9 @@ class TestImage:
|
||||||
# Assert
|
# Assert
|
||||||
assert Image._initialized == 2
|
assert Image._initialized == 2
|
||||||
|
|
||||||
|
for extension in Image.EXTENSION:
|
||||||
|
assert extension in Image._EXTENSION_PLUGIN
|
||||||
|
|
||||||
def test_registered_extensions(self) -> None:
|
def test_registered_extensions(self) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
# Open an image to trigger plugin registration
|
# Open an image to trigger plugin registration
|
||||||
|
|
|
||||||
|
|
@ -278,8 +278,7 @@ class TestEmbeddable:
|
||||||
|
|
||||||
with open("embed_pil.c", "w", encoding="utf-8") as fh:
|
with open("embed_pil.c", "w", encoding="utf-8") as fh:
|
||||||
home = sys.prefix.replace("\\", "\\\\")
|
home = sys.prefix.replace("\\", "\\\\")
|
||||||
fh.write(
|
fh.write(f"""
|
||||||
f"""
|
|
||||||
#include "Python.h"
|
#include "Python.h"
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
|
|
@ -300,8 +299,7 @@ int main(int argc, char* argv[])
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}}
|
}}
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
objects = compiler.compile(["embed_pil.c"])
|
objects = compiler.compile(["embed_pil.c"])
|
||||||
compiler.link_executable(objects, "embed_pil")
|
compiler.link_executable(objects, "embed_pil")
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ class TestImageTransform:
|
||||||
|
|
||||||
def test_extent(self) -> None:
|
def test_extent(self) -> None:
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
(w, h) = im.size
|
w, h = im.size
|
||||||
transformed = im.transform(
|
transformed = im.transform(
|
||||||
im.size,
|
im.size,
|
||||||
Image.Transform.EXTENT,
|
Image.Transform.EXTENT,
|
||||||
|
|
@ -72,7 +72,7 @@ class TestImageTransform:
|
||||||
def test_quad(self) -> None:
|
def test_quad(self) -> None:
|
||||||
# one simple quad transform, equivalent to scale & crop upper left quad
|
# one simple quad transform, equivalent to scale & crop upper left quad
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
(w, h) = im.size
|
w, h = im.size
|
||||||
transformed = im.transform(
|
transformed = im.transform(
|
||||||
im.size,
|
im.size,
|
||||||
Image.Transform.QUAD,
|
Image.Transform.QUAD,
|
||||||
|
|
@ -99,7 +99,7 @@ class TestImageTransform:
|
||||||
)
|
)
|
||||||
def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None:
|
def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
(w, h) = im.size
|
w, h = im.size
|
||||||
transformed = im.transform(
|
transformed = im.transform(
|
||||||
im.size,
|
im.size,
|
||||||
Image.Transform.EXTENT,
|
Image.Transform.EXTENT,
|
||||||
|
|
@ -112,7 +112,7 @@ class TestImageTransform:
|
||||||
def test_mesh(self) -> None:
|
def test_mesh(self) -> None:
|
||||||
# this should be a checkerboard of halfsized hoppers in ul, lr
|
# this should be a checkerboard of halfsized hoppers in ul, lr
|
||||||
im = hopper("RGBA")
|
im = hopper("RGBA")
|
||||||
(w, h) = im.size
|
w, h = im.size
|
||||||
transformed = im.transform(
|
transformed = im.transform(
|
||||||
im.size,
|
im.size,
|
||||||
Image.Transform.MESH,
|
Image.Transform.MESH,
|
||||||
|
|
@ -174,7 +174,7 @@ class TestImageTransform:
|
||||||
|
|
||||||
def test_alpha_premult_transform(self) -> None:
|
def test_alpha_premult_transform(self) -> None:
|
||||||
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
|
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
|
||||||
(w, h) = im.size
|
w, h = im.size
|
||||||
return im.transform(
|
return im.transform(
|
||||||
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
|
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
|
||||||
)
|
)
|
||||||
|
|
@ -216,7 +216,7 @@ class TestImageTransform:
|
||||||
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
|
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
|
||||||
def test_nearest_transform(self, mode: str) -> None:
|
def test_nearest_transform(self, mode: str) -> None:
|
||||||
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
|
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
|
||||||
(w, h) = im.size
|
w, h = im.size
|
||||||
return im.transform(
|
return im.transform(
|
||||||
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST
|
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"))
|
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
|
||||||
def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None:
|
def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None:
|
||||||
with hopper() as im:
|
with hopper() as im:
|
||||||
(w, h) = im.size
|
w, h = im.size
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type]
|
im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,10 +68,22 @@ def test_sanity() -> None:
|
||||||
draw.rectangle(list(range(4)))
|
draw.rectangle(list(range(4)))
|
||||||
|
|
||||||
|
|
||||||
def test_valueerror() -> None:
|
def test_new_color() -> None:
|
||||||
with Image.open("Tests/images/chi.gif") as im:
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
draw = ImageDraw.Draw(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))
|
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:
|
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(
|
@pytest.mark.parametrize(
|
||||||
"xy, radius, type",
|
"xy, radius, type",
|
||||||
[
|
[
|
||||||
|
|
@ -1461,21 +1485,15 @@ def test_stroke_multiline() -> None:
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("freetype2")
|
@skip_unless_feature("freetype2")
|
||||||
def test_setting_default_font() -> None:
|
def test_setting_default_font(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
# Arrange
|
|
||||||
im = Image.new("RGB", (100, 250))
|
im = Image.new("RGB", (100, 250))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
assert isinstance(draw.getfont(), ImageFont.load_default().__class__)
|
||||||
|
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
|
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
|
||||||
|
monkeypatch.setattr(ImageDraw.ImageDraw, "font", font)
|
||||||
# Act
|
assert draw.getfont() == font
|
||||||
ImageDraw.ImageDraw.font = font
|
|
||||||
|
|
||||||
# Assert
|
|
||||||
try:
|
|
||||||
assert draw.getfont() == font
|
|
||||||
finally:
|
|
||||||
ImageDraw.ImageDraw.font = None
|
|
||||||
assert isinstance(draw.getfont(), ImageFont.load_default().__class__)
|
|
||||||
|
|
||||||
|
|
||||||
def test_default_font_size() -> None:
|
def test_default_font_size() -> None:
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK
|
||||||
|
|
||||||
|
|
||||||
class TestImageFile:
|
class TestImageFile:
|
||||||
def test_parser(self) -> None:
|
def test_parser(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
def roundtrip(format: str) -> tuple[Image.Image, Image.Image]:
|
def roundtrip(format: str) -> tuple[Image.Image, Image.Image]:
|
||||||
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
|
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
|
||||||
if format in ("MSP", "XBM"):
|
if format in ("MSP", "XBM"):
|
||||||
|
|
@ -55,12 +55,9 @@ class TestImageFile:
|
||||||
assert_image_equal(*roundtrip("IM"))
|
assert_image_equal(*roundtrip("IM"))
|
||||||
assert_image_equal(*roundtrip("MSP"))
|
assert_image_equal(*roundtrip("MSP"))
|
||||||
if features.check("zlib"):
|
if features.check("zlib"):
|
||||||
try:
|
# force multiple blocks in PNG driver
|
||||||
# force multiple blocks in PNG driver
|
monkeypatch.setattr(ImageFile, "MAXBLOCK", 8192)
|
||||||
ImageFile.MAXBLOCK = 8192
|
assert_image_equal(*roundtrip("PNG"))
|
||||||
assert_image_equal(*roundtrip("PNG"))
|
|
||||||
finally:
|
|
||||||
ImageFile.MAXBLOCK = MAXBLOCK
|
|
||||||
assert_image_equal(*roundtrip("PPM"))
|
assert_image_equal(*roundtrip("PPM"))
|
||||||
assert_image_equal(*roundtrip("TIFF"))
|
assert_image_equal(*roundtrip("TIFF"))
|
||||||
assert_image_equal(*roundtrip("XBM"))
|
assert_image_equal(*roundtrip("XBM"))
|
||||||
|
|
@ -120,14 +117,11 @@ class TestImageFile:
|
||||||
assert (128, 128) == p.image.size
|
assert (128, 128) == p.image.size
|
||||||
|
|
||||||
@skip_unless_feature("zlib")
|
@skip_unless_feature("zlib")
|
||||||
def test_safeblock(self) -> None:
|
def test_safeblock(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
im1 = hopper()
|
im1 = hopper()
|
||||||
|
|
||||||
try:
|
monkeypatch.setattr(ImageFile, "SAFEBLOCK", 1)
|
||||||
ImageFile.SAFEBLOCK = 1
|
im2 = fromstring(tostring(im1, "PNG"))
|
||||||
im2 = fromstring(tostring(im1, "PNG"))
|
|
||||||
finally:
|
|
||||||
ImageFile.SAFEBLOCK = SAFEBLOCK
|
|
||||||
|
|
||||||
assert_image_equal(im1, im2)
|
assert_image_equal(im1, im2)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,20 +38,18 @@ def test_invalid_mode() -> None:
|
||||||
font._load_pilfont_data(fp, im)
|
font._load_pilfont_data(fp, im)
|
||||||
|
|
||||||
|
|
||||||
def test_without_freetype() -> None:
|
def test_without_freetype(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
original_core = ImageFont.core
|
|
||||||
if features.check_module("freetype2"):
|
if features.check_module("freetype2"):
|
||||||
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
monkeypatch.setattr(
|
||||||
try:
|
ImageFont, "core", _util.DeferredError(ImportError("Disabled for testing"))
|
||||||
with pytest.raises(ImportError):
|
)
|
||||||
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
|
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):
|
with pytest.raises(ImportError):
|
||||||
ImageFont.load_default(size=14)
|
ImageFont.load_default(size=14)
|
||||||
finally:
|
|
||||||
ImageFont.core = original_core
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("font", fonts)
|
@pytest.mark.parametrize("font", fonts)
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@ def string_to_img(image_string: str) -> Image.Image:
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
|
||||||
A = string_to_img(
|
A = string_to_img("""
|
||||||
"""
|
|
||||||
.......
|
.......
|
||||||
.......
|
.......
|
||||||
..111..
|
..111..
|
||||||
|
|
@ -31,8 +30,7 @@ A = string_to_img(
|
||||||
..111..
|
..111..
|
||||||
.......
|
.......
|
||||||
.......
|
.......
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def img_to_string(im: Image.Image) -> str:
|
def img_to_string(im: Image.Image) -> str:
|
||||||
|
|
@ -231,15 +229,15 @@ def test_negate() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_incorrect_mode() -> None:
|
def test_incorrect_mode() -> None:
|
||||||
im = hopper()
|
|
||||||
mop = ImageMorph.MorphOp(op_name="erosion8")
|
mop = ImageMorph.MorphOp(op_name="erosion8")
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
|
with hopper() as im:
|
||||||
mop.apply(im)
|
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
|
||||||
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
|
mop.apply(im)
|
||||||
mop.match(im)
|
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
|
||||||
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
|
mop.match(im)
|
||||||
mop.get_on_pixels(im)
|
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
|
||||||
|
mop.get_on_pixels(im)
|
||||||
|
|
||||||
|
|
||||||
def test_add_patterns() -> None:
|
def test_add_patterns() -> None:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImagePalette
|
from PIL import Image, ImagePalette, PaletteFile
|
||||||
|
|
||||||
from .helper import assert_image_equal, assert_image_equal_tofile
|
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)
|
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:
|
def test_invalid_palette() -> None:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
ImagePalette.load("Tests/images/hopper.jpg")
|
ImagePalette.load("Tests/images/hopper.jpg")
|
||||||
|
|
||||||
|
b = BytesIO(b"1" * 101)
|
||||||
|
with pytest.raises(SyntaxError, match="bad palette file"):
|
||||||
|
PaletteFile.PaletteFile(b)
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ if is_win32():
|
||||||
|
|
||||||
def test_pointer(tmp_path: Path) -> None:
|
def test_pointer(tmp_path: Path) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
(width, height) = im.size
|
width, height = im.size
|
||||||
opath = tmp_path / "temp.png"
|
opath = tmp_path / "temp.png"
|
||||||
imdib = ImageWin.Dib(im)
|
imdib = ImageWin.Dib(im)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ INT32 = DataShape(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
|
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]
|
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||||
if dtype == fl_uint8_4_type:
|
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))
|
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
|
||||||
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
|
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]
|
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||||
arr = nanoarrow.Array(
|
arr = nanoarrow.Array(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, PSDraw
|
from PIL import Image, PSDraw
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -47,21 +49,16 @@ def test_draw_postscript(tmp_path: Path) -> None:
|
||||||
assert os.path.getsize(tempfile) > 0
|
assert os.path.getsize(tempfile) > 0
|
||||||
|
|
||||||
|
|
||||||
def test_stdout() -> None:
|
def test_stdout(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
# Temporarily redirect stdout
|
# Temporarily redirect stdout
|
||||||
old_stdout = sys.stdout
|
|
||||||
|
|
||||||
class MyStdOut:
|
class MyStdOut:
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
|
|
||||||
mystdout = MyStdOut()
|
mystdout = MyStdOut()
|
||||||
|
|
||||||
sys.stdout = mystdout
|
monkeypatch.setattr(sys, "stdout", mystdout)
|
||||||
|
|
||||||
ps = PSDraw.PSDraw()
|
ps = PSDraw.PSDraw()
|
||||||
_create_document(ps)
|
_create_document(ps)
|
||||||
|
|
||||||
# Reset stdout
|
|
||||||
sys.stdout = old_stdout
|
|
||||||
|
|
||||||
assert mystdout.buffer.getvalue() != b""
|
assert mystdout.buffer.getvalue() != b""
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ INT32 = DataShape(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
|
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]
|
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||||
arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
|
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:
|
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]
|
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||||
arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
|
arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,15 @@ import pytest
|
||||||
|
|
||||||
from PIL import __version__
|
from PIL import __version__
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from importlib.metadata import PackageMetadata
|
||||||
|
|
||||||
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
|
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.
|
# 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 was a utility method in pyroma 4.3.3; it was removed in 5.0.
|
||||||
# This implementation is constructed from the relevant logic from
|
# 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,
|
# upstream to Pyroma as https://github.com/regebro/pyroma/pull/116,
|
||||||
# so it may be possible to simplify this test in future.
|
# so it may be possible to simplify this test in future.
|
||||||
data = {}
|
data = {}
|
||||||
for key in set(md.keys()):
|
for key in set(md):
|
||||||
value = md.get_all(key)
|
value = md.get_all(key)
|
||||||
key = pyroma.projectdata.normalize(key)
|
key = pyroma.projectdata.normalize(key)
|
||||||
|
|
||||||
if len(value) == 1:
|
if value is not None and len(value) == 1:
|
||||||
value = value[0]
|
first_value = value[0]
|
||||||
if value.strip() == "UNKNOWN":
|
if first_value.strip() != "UNKNOWN":
|
||||||
continue
|
data[key] = first_value
|
||||||
|
else:
|
||||||
data[key] = value
|
data[key] = value
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
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:
|
def check_upload_equal() -> None:
|
||||||
|
|
@ -12,4 +12,4 @@ def check_upload_equal() -> None:
|
||||||
def check_upload_similar() -> None:
|
def check_upload_similar() -> None:
|
||||||
result = hopper("P").convert("RGB")
|
result = hopper("P").convert("RGB")
|
||||||
target = hopper("RGB")
|
target = hopper("RGB")
|
||||||
assert_image_similar(result, target, 0)
|
assert_image_equal(result, target)
|
||||||
|
|
|
||||||
|
|
@ -3,66 +3,88 @@ set -eo pipefail
|
||||||
|
|
||||||
version=1.3.0
|
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
|
# Copy cached files into place
|
||||||
# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971
|
sudo cp ~/cache-libavif/lib/* $LIBDIR/
|
||||||
patch -p1 < ../libavif-svt4.patch
|
sudo cp -r ~/cache-libavif/include/avif /usr/include/
|
||||||
|
|
||||||
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
|
|
||||||
PREFIX=$(brew --prefix)
|
|
||||||
else
|
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
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,30 @@
|
||||||
|
|
||||||
archive=libwebp-1.6.0
|
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
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import subprocess
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sphinx.application import Sphinx
|
from typing import Any
|
||||||
|
|
||||||
DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+")
|
DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+")
|
||||||
VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n")
|
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]
|
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])):
|
if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])):
|
||||||
old_title = m.group(1)
|
old_title = m.group(1)
|
||||||
|
|
||||||
|
|
@ -43,6 +43,6 @@ def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None:
|
||||||
source[0] = result
|
source[0] = result
|
||||||
|
|
||||||
|
|
||||||
def setup(app: Sphinx) -> dict[str, bool]:
|
def setup(app: Any) -> dict[str, bool]:
|
||||||
app.connect("source-read", add_date)
|
app.connect("source-read", add_date)
|
||||||
return {"parallel_read_safe": True}
|
return {"parallel_read_safe": True}
|
||||||
|
|
|
||||||
|
|
@ -828,16 +828,6 @@ PCX
|
||||||
|
|
||||||
Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data.
|
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
|
Opening
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
|
@ -1081,12 +1071,19 @@ following parameters can also be set:
|
||||||
PPM
|
PPM
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
|
Pillow reads and writes PBM, PGM, PPM, PNM and PFM files containing ``1``, ``L``, ``I``,
|
||||||
``RGB`` data.
|
``RGB`` or ``F`` data.
|
||||||
|
|
||||||
"Raw" (P4 to P6) formats can be read, and are used when writing.
|
"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
|
QOI
|
||||||
^^^
|
^^^
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ Many of Pillow's features require external libraries:
|
||||||
* **littlecms** provides color management
|
* **littlecms** provides color management
|
||||||
|
|
||||||
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
|
* 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.
|
* **libwebp** provides the WebP format.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 |
|
||||||
| | PyPy3 | |
|
| | 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 |
|
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||||
| | | versions | | Pillow version | | processors |
|
| | | 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 | |
|
| | 3.9 | 11.3.0 | |
|
||||||
+----------------------------------+-----------------------------+------------------+--------------+
|
+----------------------------------+-----------------------------+------------------+--------------+
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
Security
|
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,
|
Check that tile extents do not use negative x or y offsets when decoding or encoding,
|
||||||
|
|
|
||||||
|
|
@ -112,14 +112,6 @@ test-requires = [
|
||||||
]
|
]
|
||||||
xbuild-tools = [ ]
|
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]
|
[tool.cibuildwheel.ios]
|
||||||
# Disable platform guessing on iOS, and disable raqm (since there won't be a
|
# 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)
|
# 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...)
|
# There's no numpy wheel for iOS (yet...)
|
||||||
test-requires = [ ]
|
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]]
|
[[tool.cibuildwheel.overrides]]
|
||||||
# iOS environment is isolated by cibuildwheel, but needs the dependencies
|
# iOS environment is isolated by cibuildwheel, but needs the dependencies
|
||||||
select = "*_iphoneos"
|
select = "*_iphoneos"
|
||||||
|
|
@ -217,6 +217,7 @@ testpaths = [
|
||||||
python_version = "3.10"
|
python_version = "3.10"
|
||||||
pretty = true
|
pretty = true
|
||||||
disallow_any_generics = true
|
disallow_any_generics = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
enable_error_code = "ignore-without-code"
|
enable_error_code = "ignore-without-code"
|
||||||
extra_checks = true
|
extra_checks = true
|
||||||
follow_imports = "silent"
|
follow_imports = "silent"
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ def testimage() -> None:
|
||||||
('R', 'G', 'B')
|
('R', 'G', 'B')
|
||||||
>>> im.getbbox()
|
>>> im.getbbox()
|
||||||
(0, 0, 128, 128)
|
(0, 0, 128, 128)
|
||||||
>>> len(im.getdata())
|
>>> len(im.get_flattened_data())
|
||||||
16384
|
16384
|
||||||
>>> im.getextrema()
|
>>> im.getextrema()
|
||||||
((0, 255), (0, 255), (0, 255))
|
((0, 255), (0, 255), (0, 255))
|
||||||
|
|
|
||||||
9
setup.py
9
setup.py
|
|
@ -363,7 +363,6 @@ class pil_build_ext(build_ext):
|
||||||
("disable-platform-guessing", None, "Disable platform guessing"),
|
("disable-platform-guessing", None, "Disable platform guessing"),
|
||||||
("debug", None, "Debug logging"),
|
("debug", None, "Debug logging"),
|
||||||
]
|
]
|
||||||
+ [("add-imaging-libs=", None, "Add libs to _imaging build")]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -374,7 +373,6 @@ class pil_build_ext(build_ext):
|
||||||
self.disable_platform_guessing = self.check_configuration(
|
self.disable_platform_guessing = self.check_configuration(
|
||||||
"platform-guessing", "disable"
|
"platform-guessing", "disable"
|
||||||
)
|
)
|
||||||
self.add_imaging_libs = ""
|
|
||||||
build_ext.initialize_options(self)
|
build_ext.initialize_options(self)
|
||||||
for x in self.feature:
|
for x in self.feature:
|
||||||
setattr(self, f"disable_{x}", self.check_configuration(x, "disable"))
|
setattr(self, f"disable_{x}", self.check_configuration(x, "disable"))
|
||||||
|
|
@ -901,7 +899,6 @@ class pil_build_ext(build_ext):
|
||||||
# core library
|
# core library
|
||||||
|
|
||||||
libs: list[str | bool | None] = []
|
libs: list[str | bool | None] = []
|
||||||
libs.extend(self.add_imaging_libs.split())
|
|
||||||
defs: list[tuple[str, str | None]] = []
|
defs: list[tuple[str, str | None]] = []
|
||||||
if feature.get("tiff"):
|
if feature.get("tiff"):
|
||||||
libs.append(feature.get("tiff"))
|
libs.append(feature.get("tiff"))
|
||||||
|
|
@ -1092,7 +1089,11 @@ ext_modules = [
|
||||||
Extension("PIL._webp", ["src/_webp.c"]),
|
Extension("PIL._webp", ["src/_webp.c"]),
|
||||||
Extension("PIL._avif", ["src/_avif.c"]),
|
Extension("PIL._avif", ["src/_avif.c"]),
|
||||||
Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.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"]),
|
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"""
|
"""
|
||||||
Parse X Bitmap Distribution Format (BDF)
|
Parse X Bitmap Distribution Format (BDF)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import BinaryIO
|
from typing import BinaryIO
|
||||||
|
|
|
||||||
|
|
@ -369,7 +369,7 @@ class BmpRleDecoder(ImageFile.PyDecoder):
|
||||||
bytes_read = self.fd.read(2)
|
bytes_read = self.fd.read(2)
|
||||||
if len(bytes_read) < 2:
|
if len(bytes_read) < 2:
|
||||||
break
|
break
|
||||||
right, up = self.fd.read(2)
|
right, up = bytes_read
|
||||||
data += b"\x00" * (right + up * self.state.xsize)
|
data += b"\x00" * (right + up * self.state.xsize)
|
||||||
x = len(data) % self.state.xsize
|
x = len(data) % self.state.xsize
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
def _open(self) -> None:
|
def _open(self) -> None:
|
||||||
assert self.fp is not 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"
|
# go to offset - start of "%!PS"
|
||||||
self.fp.seek(offset)
|
self.fp.seek(offset)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
This module provides constants and clear-text names for various
|
This module provides constants and clear-text names for various
|
||||||
well-known EXIF tags.
|
well-known EXIF tags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
size = i32(s, 4), i32(s, 8)
|
size = i32(s, 4), i32(s, 8)
|
||||||
# tilecount = i32(s, 12)
|
# tilecount = i32(s, 12)
|
||||||
tilesize = i32(s, 16), i32(s, 20)
|
xtile, ytile = i32(s, 16), i32(s, 20)
|
||||||
# channels = i32(s, 24)
|
# channels = i32(s, 24)
|
||||||
offset = i32(s, 28)
|
offset = i32(s, 28)
|
||||||
length = i32(s, 32)
|
length = i32(s, 32)
|
||||||
|
|
@ -156,7 +156,6 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
x = y = 0
|
x = y = 0
|
||||||
xsize, ysize = size
|
xsize, ysize = size
|
||||||
xtile, ytile = tilesize
|
|
||||||
self.tile = []
|
self.tile = []
|
||||||
|
|
||||||
for i in range(0, len(s), length):
|
for i in range(0, len(s), length):
|
||||||
|
|
@ -224,7 +223,7 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||||
msg = "unknown/invalid compression"
|
msg = "unknown/invalid compression"
|
||||||
raise OSError(msg)
|
raise OSError(msg)
|
||||||
|
|
||||||
x = x + xtile
|
x += xtile
|
||||||
if x >= xsize:
|
if x >= xsize:
|
||||||
x, y = 0, y + ytile
|
x, y = 0, y + ytile
|
||||||
if y >= ysize:
|
if y >= ysize:
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
implementation is provided for convenience and demonstrational
|
implementation is provided for convenience and demonstrational
|
||||||
purposes only.
|
purposes only.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
|
|
||||||
|
|
@ -937,7 +937,13 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
|
||||||
:param info: encoderinfo
|
:param info: encoderinfo
|
||||||
:returns: list of indexes of palette entries in use, or None
|
: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.
|
# Potentially expensive operation.
|
||||||
|
|
||||||
# The palette saves 3 bytes per color not used, but palette
|
# The palette saves 3 bytes per color not used, but palette
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ Stuff to translate curve segments to palette values (derived from
|
||||||
the corresponding code in GIMP, written by Federico Mena Quintero.
|
the corresponding code in GIMP, written by Federico Mena Quintero.
|
||||||
See the GIMP distribution for more information.)
|
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
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ def read_32t(
|
||||||
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
||||||
) -> dict[str, Image.Image]:
|
) -> dict[str, Image.Image]:
|
||||||
# The 128x128 icon seems to have an extra header for some reason.
|
# The 128x128 icon seems to have an extra header for some reason.
|
||||||
(start, length) = start_length
|
start, length = start_length
|
||||||
fobj.seek(start)
|
fobj.seek(start)
|
||||||
sig = fobj.read(4)
|
sig = fobj.read(4)
|
||||||
if sig != b"\x00\x00\x00\x00":
|
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
|
Read a 32bit RGB icon resource. Seems to be either uncompressed or
|
||||||
an RLE packbits-like scheme.
|
an RLE packbits-like scheme.
|
||||||
"""
|
"""
|
||||||
(start, length) = start_length
|
start, length = start_length
|
||||||
fobj.seek(start)
|
fobj.seek(start)
|
||||||
pixel_size = (size[0] * size[2], size[1] * size[2])
|
pixel_size = (size[0] * size[2], size[1] * size[2])
|
||||||
sizesq = pixel_size[0] * pixel_size[1]
|
sizesq = pixel_size[0] * pixel_size[1]
|
||||||
|
|
@ -111,7 +111,7 @@ def read_mk(
|
||||||
def read_png_or_jpeg2000(
|
def read_png_or_jpeg2000(
|
||||||
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
|
||||||
) -> dict[str, Image.Image]:
|
) -> dict[str, Image.Image]:
|
||||||
(start, length) = start_length
|
start, length = start_length
|
||||||
fobj.seek(start)
|
fobj.seek(start)
|
||||||
sig = fobj.read(12)
|
sig = fobj.read(12)
|
||||||
|
|
||||||
|
|
|
||||||
158
src/PIL/Image.py
158
src/PIL/Image.py
|
|
@ -323,10 +323,112 @@ def getmodebands(mode: str) -> int:
|
||||||
|
|
||||||
_initialized = 0
|
_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:
|
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.
|
It is called when opening or saving images.
|
||||||
"""
|
"""
|
||||||
|
|
@ -382,11 +484,10 @@ def init() -> bool:
|
||||||
if _initialized >= 2:
|
if _initialized >= 2:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
parent_name = __name__.rpartition(".")[0]
|
|
||||||
for plugin in _plugins:
|
for plugin in _plugins:
|
||||||
try:
|
try:
|
||||||
logger.debug("Importing %s", plugin)
|
logger.debug("Importing %s", plugin)
|
||||||
__import__(f"{parent_name}.{plugin}", globals(), locals(), [])
|
__import__(f"{__spec__.parent}.{plugin}", globals(), locals(), [])
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger.debug("Image: failed to import %s: %s", plugin, e)
|
logger.debug("Image: failed to import %s: %s", plugin, e)
|
||||||
|
|
||||||
|
|
@ -892,7 +993,9 @@ class Image:
|
||||||
else:
|
else:
|
||||||
self.im.putpalettealphas(self.info["transparency"])
|
self.im.putpalettealphas(self.info["transparency"])
|
||||||
self.palette.mode = "RGBA"
|
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.palette = self.im.getpalette(
|
||||||
self.palette.mode, self.palette.mode
|
self.palette.mode, self.palette.mode
|
||||||
)
|
)
|
||||||
|
|
@ -2443,7 +2546,7 @@ class Image:
|
||||||
]
|
]
|
||||||
|
|
||||||
def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]:
|
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
|
return a * x + b * y + c, d * x + e * y + f
|
||||||
|
|
||||||
matrix[2], matrix[5] = transform(
|
matrix[2], matrix[5] = transform(
|
||||||
|
|
@ -2533,12 +2636,20 @@ class Image:
|
||||||
# only set the name for metadata purposes
|
# only set the name for metadata purposes
|
||||||
filename = os.fspath(fp.name)
|
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()
|
# Try importing only the plugin for this extension first
|
||||||
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
|
if not _import_plugin_for_extension(ext):
|
||||||
|
preinit()
|
||||||
|
|
||||||
if not format:
|
|
||||||
if ext not in EXTENSION:
|
if ext not in EXTENSION:
|
||||||
init()
|
init()
|
||||||
try:
|
try:
|
||||||
|
|
@ -3378,7 +3489,7 @@ def fromarrow(
|
||||||
msg = "arrow_c_array interface not found"
|
msg = "arrow_c_array interface not found"
|
||||||
raise ValueError(msg)
|
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)
|
_im = core.new_arrow(mode, size, schema_capsule, array_capsule)
|
||||||
if _im:
|
if _im:
|
||||||
return Image()._new(_im)
|
return Image()._new(_im)
|
||||||
|
|
@ -3522,7 +3633,11 @@ def open(
|
||||||
|
|
||||||
prefix = fp.read(16)
|
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] = []
|
warning_messages: list[str] = []
|
||||||
|
|
||||||
|
|
@ -3558,14 +3673,19 @@ def open(
|
||||||
im = _open_core(fp, filename, prefix, formats)
|
im = _open_core(fp, filename, prefix, formats)
|
||||||
|
|
||||||
if im is None and formats is ID:
|
if im is None and formats is ID:
|
||||||
checked_formats = ID.copy()
|
# Try preinit (few common plugins) then init (all plugins)
|
||||||
if init():
|
for loader in (preinit, init):
|
||||||
im = _open_core(
|
checked_formats = ID.copy()
|
||||||
fp,
|
loader()
|
||||||
filename,
|
if formats != checked_formats:
|
||||||
prefix,
|
im = _open_core(
|
||||||
tuple(format for format in formats if format not in checked_formats),
|
fp,
|
||||||
)
|
filename,
|
||||||
|
prefix,
|
||||||
|
tuple(f for f in formats if f not in checked_formats),
|
||||||
|
)
|
||||||
|
if im is not None:
|
||||||
|
break
|
||||||
|
|
||||||
if im:
|
if im:
|
||||||
im._exclusive_fp = exclusive_fp
|
im._exclusive_fp = exclusive_fp
|
||||||
|
|
|
||||||
|
|
@ -487,7 +487,7 @@ class ImageDraw:
|
||||||
|
|
||||||
if full_x:
|
if full_x:
|
||||||
self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
|
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)
|
self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
|
||||||
if not full_x and not full_y:
|
if not full_x and not full_y:
|
||||||
left = [x0, y0, x0 + r, y1]
|
left = [x0, y0, x0 + r, y1]
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
.. seealso:: :py:mod:`PIL.ImageDraw`
|
.. seealso:: :py:mod:`PIL.ImageDraw`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, AnyStr, BinaryIO
|
from typing import Any, AnyStr, BinaryIO
|
||||||
|
|
@ -117,7 +118,7 @@ class Draw:
|
||||||
|
|
||||||
def settransform(self, offset: tuple[float, float]) -> None:
|
def settransform(self, offset: tuple[float, float]) -> None:
|
||||||
"""Sets a transformation offset."""
|
"""Sets a transformation offset."""
|
||||||
(xoffset, yoffset) = offset
|
xoffset, yoffset = offset
|
||||||
self.transform = (1, 0, xoffset, 0, 1, yoffset)
|
self.transform = (1, 0, xoffset, 0, 1, yoffset)
|
||||||
|
|
||||||
def arc(
|
def arc(
|
||||||
|
|
|
||||||
|
|
@ -579,10 +579,7 @@ class Parser:
|
||||||
pass # not enough data
|
pass # not enough data
|
||||||
else:
|
else:
|
||||||
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
|
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
|
||||||
if flag or len(im.tile) != 1:
|
if not flag and len(im.tile) == 1:
|
||||||
# custom load code, or multiple tiles
|
|
||||||
self.decode = None
|
|
||||||
else:
|
|
||||||
# initialize decoder
|
# initialize decoder
|
||||||
im.load_prepare()
|
im.load_prepare()
|
||||||
d, e, o, a = im.tile[0]
|
d, e, o, a = im.tile[0]
|
||||||
|
|
@ -801,9 +798,9 @@ class PyCodec:
|
||||||
self.im = im
|
self.im = im
|
||||||
|
|
||||||
if extents:
|
if extents:
|
||||||
(x0, y0, x1, y1) = extents
|
x0, y0, x1, y1 = extents
|
||||||
else:
|
else:
|
||||||
(x0, y0, x1, y1) = (0, 0, 0, 0)
|
x0, y0, x1, y1 = (0, 0, 0, 0)
|
||||||
|
|
||||||
if x0 == 0 and x1 == 0:
|
if x0 == 0 and x1 == 0:
|
||||||
self.state.xsize, self.state.ysize = self.im.size
|
self.state.xsize, self.state.ysize = self.im.size
|
||||||
|
|
@ -814,7 +811,7 @@ class PyCodec:
|
||||||
self.state.ysize = y1 - y0
|
self.state.ysize = y1 - y0
|
||||||
|
|
||||||
if self.state.xsize <= 0 or self.state.ysize <= 0:
|
if self.state.xsize <= 0 or self.state.ysize <= 0:
|
||||||
msg = "Size cannot be negative"
|
msg = "Size must be positive"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -940,9 +940,7 @@ def load_default_imagefont() -> ImageFont:
|
||||||
f = ImageFont()
|
f = ImageFont()
|
||||||
f._load_pilfont_data(
|
f._load_pilfont_data(
|
||||||
# courB08
|
# courB08
|
||||||
BytesIO(
|
BytesIO(base64.b64decode(b"""
|
||||||
base64.b64decode(
|
|
||||||
b"""
|
|
||||||
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
|
@ -1034,13 +1032,8 @@ AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
|
||||||
pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
|
pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
|
||||||
AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
|
AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
|
||||||
+QAGAAIAzgAKANUAEw==
|
+QAGAAIAzgAKANUAEw==
|
||||||
"""
|
""")),
|
||||||
)
|
Image.open(BytesIO(base64.b64decode(b"""
|
||||||
),
|
|
||||||
Image.open(
|
|
||||||
BytesIO(
|
|
||||||
base64.b64decode(
|
|
||||||
b"""
|
|
||||||
iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
|
iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
|
||||||
Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
|
Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
|
||||||
M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
|
M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
|
||||||
|
|
@ -1064,10 +1057,7 @@ evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
|
||||||
AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
|
AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
|
||||||
Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
|
Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
|
||||||
w7IkEbzhVQAAAABJRU5ErkJggg==
|
w7IkEbzhVQAAAABJRU5ErkJggg==
|
||||||
"""
|
"""))),
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
@ -1088,9 +1078,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
|
||||||
"""
|
"""
|
||||||
if isinstance(core, ModuleType) or size is not None:
|
if isinstance(core, ModuleType) or size is not None:
|
||||||
return truetype(
|
return truetype(
|
||||||
BytesIO(
|
BytesIO(base64.b64decode(b"""
|
||||||
base64.b64decode(
|
|
||||||
b"""
|
|
||||||
AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA
|
AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA
|
||||||
AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA
|
AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA
|
||||||
MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh
|
MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh
|
||||||
|
|
@ -1311,9 +1299,7 @@ ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA
|
||||||
gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC
|
gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC
|
||||||
YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA
|
YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA
|
||||||
AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
|
AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
|
||||||
"""
|
""")),
|
||||||
)
|
|
||||||
),
|
|
||||||
10 if size is None else size,
|
10 if size is None else size,
|
||||||
layout_engine=Layout.BASIC,
|
layout_engine=Layout.BASIC,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ class MspDecoder(ImageFile.PyDecoder):
|
||||||
runtype = row[idx]
|
runtype = row[idx]
|
||||||
idx += 1
|
idx += 1
|
||||||
if runtype == 0:
|
if runtype == 0:
|
||||||
(runcount, runval) = struct.unpack_from("Bc", row, idx)
|
runcount, runval = struct.unpack_from("Bc", row, idx)
|
||||||
img.write(runval * runcount)
|
img.write(runval * runcount)
|
||||||
idx += 2
|
idx += 2
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ class PSDraw:
|
||||||
Draws text at the given position. You must use
|
Draws text at the given position. You must use
|
||||||
:py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
|
: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"("))
|
||||||
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,)))
|
self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,)))
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,10 @@ SAVE = {
|
||||||
|
|
||||||
|
|
||||||
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
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:
|
try:
|
||||||
version, bits, planes, rawmode = SAVE[im.mode]
|
version, bits, planes, rawmode = SAVE[im.mode]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | No
|
||||||
|
|
||||||
def makeSpiderHeader(im: Image.Image) -> list[bytes]:
|
def makeSpiderHeader(im: Image.Image) -> list[bytes]:
|
||||||
nsam, nrow = im.size
|
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)
|
labrec = int(1024 / lenbyt)
|
||||||
if 1024 % lenbyt != 0:
|
if 1024 % lenbyt != 0:
|
||||||
labrec += 1
|
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:
|
def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
# get the filename extension and register it with Image
|
# get the filename extension and register it with Image
|
||||||
filename_ext = os.path.splitext(filename)[1]
|
if filename_ext := os.path.splitext(filename)[1]:
|
||||||
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
|
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
|
||||||
Image.register_extension(SpiderImageFile.format, ext)
|
Image.register_extension(SpiderImageFile.format, ext)
|
||||||
_save(im, fp, filename)
|
_save(im, fp, filename)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()`.
|
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.
|
To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ if hasattr(Image.core, "drawwmf"):
|
||||||
|
|
||||||
class WmfHandler(ImageFile.StubHandler):
|
class WmfHandler(ImageFile.StubHandler):
|
||||||
def open(self, im: ImageFile.StubImageFile) -> None:
|
def open(self, im: ImageFile.StubImageFile) -> None:
|
||||||
im._mode = "RGB"
|
|
||||||
self.bbox = im.info["wmf_bbox"]
|
self.bbox = im.info["wmf_bbox"]
|
||||||
|
|
||||||
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
|
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
|
|
||||||
"""Binary input/output support routines."""
|
"""Binary input/output support routines."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from struct import pack, unpack_from
|
from struct import pack, unpack_from
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Master version for Pillow
|
# Master version for Pillow
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__version__ = "12.1.1"
|
__version__ = "12.2.0.dev0"
|
||||||
|
|
|
||||||
|
|
@ -485,7 +485,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
|
||||||
frame = image;
|
frame = image;
|
||||||
} else {
|
} else {
|
||||||
frame = avifImageCreateEmpty();
|
frame = avifImageCreateEmpty();
|
||||||
if (image == NULL) {
|
if (frame == NULL) {
|
||||||
PyErr_SetString(PyExc_ValueError, "Image creation failed");
|
PyErr_SetString(PyExc_ValueError, "Image creation failed");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,17 +116,17 @@ V = {
|
||||||
"BROTLI": "1.2.0",
|
"BROTLI": "1.2.0",
|
||||||
"FREETYPE": "2.14.1",
|
"FREETYPE": "2.14.1",
|
||||||
"FRIBIDI": "1.0.16",
|
"FRIBIDI": "1.0.16",
|
||||||
"HARFBUZZ": "12.3.0",
|
"HARFBUZZ": "12.3.2",
|
||||||
"JPEGTURBO": "3.1.3",
|
"JPEGTURBO": "3.1.3",
|
||||||
"LCMS2": "2.17",
|
"LCMS2": "2.18",
|
||||||
"LIBAVIF": "1.3.0",
|
"LIBAVIF": "1.3.0",
|
||||||
"LIBIMAGEQUANT": "4.4.1",
|
"LIBIMAGEQUANT": "4.4.1",
|
||||||
"LIBPNG": "1.6.53",
|
"LIBPNG": "1.6.54",
|
||||||
"LIBWEBP": "1.6.0",
|
"LIBWEBP": "1.6.0",
|
||||||
"OPENJPEG": "2.5.4",
|
"OPENJPEG": "2.5.4",
|
||||||
"TIFF": "4.7.1",
|
"TIFF": "4.7.1",
|
||||||
"XZ": "5.8.2",
|
"XZ": "5.8.2",
|
||||||
"ZLIBNG": "2.3.2",
|
"ZLIBNG": "2.3.3",
|
||||||
}
|
}
|
||||||
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
|
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user