Merge branch 'main' into appveyor

This commit is contained in:
Andrew Murray 2024-11-08 07:42:29 +11:00 committed by GitHub
commit 4fe42f48b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
111 changed files with 1282 additions and 556 deletions

View File

@ -18,7 +18,7 @@ environment:
TEST_OPTIONS: TEST_OPTIONS:
DEPLOY: YES DEPLOY: YES
matrix: matrix:
- PYTHON: C:/Python312 - PYTHON: C:/Python313
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python39 - PYTHON: C:/Python39

View File

@ -1 +1 @@
cibuildwheel==2.21.2 cibuildwheel==2.21.3

View File

@ -1,4 +1,4 @@
mypy==1.11.2 mypy==1.13.0
IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6 IceSpringPySideStubs-PySide6
ipython ipython

12
.github/renovate.json vendored
View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": [
"config:base" "config:recommended"
], ],
"labels": [ "labels": [
"Dependency" "Dependency"
@ -9,9 +9,13 @@
"packageRules": [ "packageRules": [
{ {
"groupName": "github-actions", "groupName": "github-actions",
"matchManagers": ["github-actions"], "matchManagers": [
"separateMajorMinor": "false" "github-actions"
],
"separateMajorMinor": false
} }
], ],
"schedule": ["on the 3rd day of the month"] "schedule": [
"on the 3rd day of the month"
]
} }

View File

@ -33,6 +33,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5

View File

@ -21,6 +21,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: pre-commit cache - name: pre-commit cache
uses: actions/cache@v4 uses: actions/cache@v4

View File

@ -6,7 +6,7 @@ on:
workflow_dispatch: workflow_dispatch:
permissions: permissions:
issues: write contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -15,6 +15,8 @@ concurrency:
jobs: jobs:
stale: stale:
if: github.repository_owner == 'python-pillow' if: github.repository_owner == 'python-pillow'
permissions:
issues: write
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -48,6 +48,8 @@ jobs:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Cygwin - name: Install Cygwin
uses: cygwin/cygwin-install-action@v4 uses: cygwin/cygwin-install-action@v4

View File

@ -46,8 +46,8 @@ jobs:
centos-stream-9-amd64, centos-stream-9-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-39-amd64,
fedora-40-amd64, fedora-40-amd64,
fedora-41-amd64,
gentoo, gentoo,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64, ubuntu-24.04-noble-amd64,
@ -65,6 +65,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
@ -102,7 +104,6 @@ jobs:
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }} token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:

View File

@ -46,6 +46,8 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up shell - name: Set up shell
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH

View File

@ -40,6 +40,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py

View File

@ -44,16 +44,20 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
persist-credentials: false
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
path: Tests\test-images path: Tests\test-images
@ -69,16 +73,14 @@ jobs:
- name: Print build system information - name: Print build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: Install Python dependencies - name: Upgrade pip
run: > run: |
python3 -m pip install python3 -m pip install --upgrade pip
coverage>=7.4.2
defusedxml - name: Install CPython dependencies
olefile if: "!contains(matrix.python-version, 'pypy')"
pyroma run: |
pytest python3 -m pip install PyQt6
pytest-cov
pytest-timeout
- name: Install dependencies - name: Install dependencies
id: install id: install
@ -178,7 +180,7 @@ jobs:
- name: Build Pillow - name: Build Pillow
run: | run: |
$FLAGS="-C raqm=vendor -C fribidi=vendor" $FLAGS="-C raqm=vendor -C fribidi=vendor"
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]"
& $env:pythonLocation\python.exe selftest.py --installed & $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh shell: pwsh

View File

@ -63,6 +63,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@ -158,7 +160,6 @@ jobs:
with: with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }} token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:

View File

@ -38,16 +38,6 @@ BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0 LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0 BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
function build_openjpeg {
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v$OPENJPEG_VERSION.tar.gz openjpeg-$OPENJPEG_VERSION.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
touch openjpeg-stamp
}
fi
function build_brotli { function build_brotli {
local cmake=$(get_modern_cmake) local cmake=$(get_modern_cmake)
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
@ -101,9 +91,6 @@ function build {
build_libpng build_libpng
build_lcms2 build_lcms2
build_openjpeg build_openjpeg
if [ -f /usr/local/lib64/libopenjp2.so ]; then
cp /usr/local/lib64/libopenjp2.so /usr/local/lib
fi
ORIGINAL_CFLAGS=$CFLAGS ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -O3 -DNDEBUG" CFLAGS="$CFLAGS -O3 -DNDEBUG"
@ -131,6 +118,7 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de
untar pillow-depends-main.zip untar pillow-depends-main.zip
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
# libdeflate may cause a minimum target error when repairing the wheel
# libtiff and libxcb cause a conflict with building libtiff and libxcb # libtiff and libxcb cause a conflict with building libtiff and libxcb
# libxau and libxdmcp cause an issue on macOS < 11 # libxau and libxdmcp cause an issue on macOS < 11
# remove cairo to fix building harfbuzz on arm64 # remove cairo to fix building harfbuzz on arm64
@ -142,7 +130,7 @@ if [[ -n "$IS_MACOS" ]]; then
if [[ "$CIBW_ARCHS" == "arm64" ]]; then if [[ "$CIBW_ARCHS" == "arm64" ]]; then
brew remove --ignore-dependencies jpeg-turbo brew remove --ignore-dependencies jpeg-turbo
else else
brew remove --ignore-dependencies webp brew remove --ignore-dependencies libdeflate webp
fi fi
brew install pkg-config brew install pkg-config

View File

@ -41,7 +41,7 @@ env:
jobs: jobs:
build-1-QEMU-emulated-wheels: build-1-QEMU-emulated-wheels:
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' if: github.event_name != 'schedule'
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -61,6 +61,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false
submodules: true submodules: true
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
@ -132,6 +133,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false
submodules: true submodules: true
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
@ -152,6 +154,7 @@ jobs:
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp39-*
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@ -172,10 +175,13 @@ jobs:
- cibw_arch: ARM64 - cibw_arch: ARM64
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
path: Tests\test-images path: Tests\test-images
@ -224,6 +230,7 @@ jobs:
CIBW_CACHE_PATH: "C:\\cibw" CIBW_CACHE_PATH: "C:\\cibw"
CIBW_FREE_THREADED_SUPPORT: True CIBW_FREE_THREADED_SUPPORT: True
CIBW_PRERELEASE_PYTHONS: True CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow -v {project}:C:\pillow
@ -251,6 +258,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@ -301,3 +310,5 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with:
attestations: true

View File

@ -1,17 +1,17 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.3 rev: v0.7.2
hooks: hooks:
- id: ruff - id: ruff
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: 24.8.0 rev: 24.10.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.9 rev: 1.7.10
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
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: v18.1.8 rev: v19.1.3
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -36,7 +36,7 @@ repos:
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 rev: v5.0.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable - id: check-shebang-scripts-are-executable
@ -50,29 +50,30 @@ 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.29.2 rev: 0.29.4
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/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.9.1 rev: v1.0.0
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 2.2.1 rev: v2.5.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.19 rev: v0.22
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject
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.3.1 rev: 1.4.1
hooks: hooks:
- id: tox-ini-fmt - id: tox-ini-fmt

View File

@ -2,9 +2,51 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
11.0.0 (unreleased) 11.1.0 (unreleased)
------------------- -------------------
- Detach PyQt6 QPixmap instance before returning #8509
[radarhere]
- Corrected EMF DPI #8485
[radarhere]
- Fix IFDRational with a zero denominator #8474
[radarhere]
- Fixed disabling a feature during install #8469
[radarhere]
11.0.0 (2024-10-15)
-------------------
- Update licence to MIT-CMU #8460
[hugovk]
- Conditionally define ImageCms type hint to avoid requiring core #8197
[radarhere]
- Support writing LONG8 offsets in AppendingTiffWriter #8417
[radarhere]
- Use ImageFile.MAXBLOCK when saving TIFF images #8461
[radarhere]
- Do not close provided file handles with libtiff when saving #8458
[radarhere]
- Support ImageFilter.BuiltinFilter for I;16* images #8438
[radarhere]
- Use ImagingCore.ptr instead of ImagingCore.id #8341
[homm, radarhere, hugovk]
- Updated EPS mode when opening images without transparency #8281
[Yay295, radarhere]
- Use transparency when combining P frames from APNGs #8443
[radarhere]
- Support all resampling filters when resizing I;16* images #8422 - Support all resampling filters when resizing I;16* images #8422
[radarhere] [radarhere]

View File

@ -7,7 +7,7 @@ Pillow is the friendly PIL fork. It is
Copyright © 2010-2024 by Jeffrey A. Clark and contributors Copyright © 2010-2024 by Jeffrey A. Clark and contributors
Like PIL, Pillow is licensed under the open source HPND License: Like PIL, Pillow is licensed under the open source MIT-CMU License:
By obtaining, using, and/or copying this software and/or its associated By obtaining, using, and/or copying this software and/or its associated
documentation, you agree that you have read, understood, and will comply documentation, you agree that you have read, understood, and will comply

View File

@ -17,12 +17,10 @@ coverage:
.PHONY: doc .PHONY: doc
.PHONY: html .PHONY: html
doc html: doc html:
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs html $(MAKE) -C docs html
.PHONY: htmlview .PHONY: htmlview
htmlview: htmlview:
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs htmlview $(MAKE) -C docs htmlview
.PHONY: doccheck .PHONY: doccheck

BIN
Tests/images/eps/1.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -22,6 +22,8 @@ def test_bad() -> None:
for f in get_files("b"): for f in get_files("b"):
# Assert that there is no unclosed file warning # Assert that there is no unclosed file warning
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
try: try:
with Image.open(f) as im: with Image.open(f) as im:
im.load() im.load()

View File

@ -56,17 +56,17 @@ def test_version() -> None:
def test_webp_transparency() -> None: def test_webp_transparency() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert features.check("transp_webp") == features.check_module("webp") assert (features.check("transp_webp") or False) == features.check_module("webp")
def test_webp_mux() -> None: def test_webp_mux() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert features.check("webp_mux") == features.check_module("webp") assert (features.check("webp_mux") or False) == features.check_module("webp")
def test_webp_anim() -> None: def test_webp_anim() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert features.check("webp_anim") == features.check_module("webp") assert (features.check("webp_anim") or False) == features.check_module("webp")
@skip_unless_feature("libjpeg_turbo") @skip_unless_feature("libjpeg_turbo")

View File

@ -258,8 +258,8 @@ def test_apng_mode() -> None:
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im = im.convert("RGBA") im = im.convert("RGBA")
assert im.getpixel((0, 0)) == (255, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (255, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert im.mode == "P" assert im.mode == "P"

View File

@ -36,6 +36,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None: def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
im.close() im.close()
@ -43,6 +45,8 @@ def test_closed_file() -> None:
def test_context_manager() -> None: def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()

View File

@ -8,6 +8,7 @@ import pytest
from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features
from .helper import ( from .helper import (
assert_image_equal_tofile,
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
hopper, hopper,
@ -19,18 +20,18 @@ from .helper import (
HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript()
# Our two EPS test files (they are identical except for their bounding boxes) # Our two EPS test files (they are identical except for their bounding boxes)
FILE1 = "Tests/images/zero_bb.eps" FILE1 = "Tests/images/eps/zero_bb.eps"
FILE2 = "Tests/images/non_zero_bb.eps" FILE2 = "Tests/images/eps/non_zero_bb.eps"
# Due to palletization, we'll need to convert these to RGB after load # Due to palletization, we'll need to convert these to RGB after load
FILE1_COMPARE = "Tests/images/zero_bb.png" FILE1_COMPARE = "Tests/images/eps/zero_bb.png"
FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png" FILE1_COMPARE_SCALE2 = "Tests/images/eps/zero_bb_scale2.png"
FILE2_COMPARE = "Tests/images/non_zero_bb.png" FILE2_COMPARE = "Tests/images/eps/non_zero_bb.png"
FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" FILE2_COMPARE_SCALE2 = "Tests/images/eps/non_zero_bb_scale2.png"
# EPS test files with binary preview # EPS test files with binary preview
FILE3 = "Tests/images/binary_preview_map.eps" FILE3 = "Tests/images/eps/binary_preview_map.eps"
# Three unsigned 32bit little-endian values: # Three unsigned 32bit little-endian values:
# 0xC6D3D0C5 magic number # 0xC6D3D0C5 magic number
@ -126,6 +127,15 @@ def test_binary_header_only() -> None:
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_simple_eps_file(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file))
with Image.open(data) as img:
assert img.mode == "RGB"
assert img.size == (100, 100)
assert img.format == "EPS"
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_missing_version_comment(prefix: bytes) -> None: def test_missing_version_comment(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
@ -141,23 +151,21 @@ def test_missing_boundingbox_comment(prefix: bytes) -> None:
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment(prefix: bytes) -> None: @pytest.mark.parametrize(
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) "file_lines",
(
simple_eps_file_with_invalid_boundingbox,
simple_eps_file_with_invalid_boundingbox_valid_imagedata,
),
)
def test_invalid_boundingbox_comment(
prefix: bytes, file_lines: tuple[bytes, ...]
) -> None:
data = io.BytesIO(prefix + b"\n".join(file_lines))
with pytest.raises(OSError, match="cannot determine EPS bounding box"): with pytest.raises(OSError, match="cannot determine EPS bounding box"):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None:
data = io.BytesIO(
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
)
with Image.open(data) as img:
assert img.mode == "RGB"
assert img.size == (100, 100)
assert img.format == "EPS"
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_ascii_comment_too_long(prefix: bytes) -> None: def test_ascii_comment_too_long(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
@ -177,7 +185,7 @@ def test_load_long_binary_data(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
with Image.open(data) as img: with Image.open(data) as img:
img.load() img.load()
assert img.mode == "RGB" assert img.mode == "1"
assert img.size == (100, 100) assert img.size == (100, 100)
assert img.format == "EPS" assert img.format == "EPS"
@ -187,7 +195,7 @@ def test_load_long_binary_data(prefix: bytes) -> None:
) )
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_cmyk() -> None: def test_cmyk() -> None:
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: with Image.open("Tests/images/eps/pil_sample_cmyk.eps") as cmyk_image:
assert cmyk_image.mode == "CMYK" assert cmyk_image.mode == "CMYK"
assert cmyk_image.size == (100, 100) assert cmyk_image.size == (100, 100)
assert cmyk_image.format == "EPS" assert cmyk_image.format == "EPS"
@ -204,8 +212,8 @@ def test_cmyk() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_showpage() -> None: def test_showpage() -> None:
# See https://github.com/python-pillow/Pillow/issues/2615 # See https://github.com/python-pillow/Pillow/issues/2615
with Image.open("Tests/images/reqd_showpage.eps") as plot_image: with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
with Image.open("Tests/images/reqd_showpage.png") as target: with Image.open("Tests/images/eps/reqd_showpage.png") as target:
# should not crash/hang # should not crash/hang
plot_image.load() plot_image.load()
# fonts could be slightly different # fonts could be slightly different
@ -214,11 +222,11 @@ def test_showpage() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_transparency() -> None: def test_transparency() -> None:
with Image.open("Tests/images/reqd_showpage.eps") as plot_image: with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
plot_image.load(transparency=True) plot_image.load(transparency=True)
assert plot_image.mode == "RGBA" assert plot_image.mode == "RGBA"
with Image.open("Tests/images/reqd_showpage_transparency.png") as target: with Image.open("Tests/images/eps/reqd_showpage_transparency.png") as target:
# fonts could be slightly different # fonts could be slightly different
assert_image_similar(plot_image, target, 6) assert_image_similar(plot_image, target, 6)
@ -245,9 +253,19 @@ def test_bytesio_object() -> None:
assert_image_similar(img, image1_scale1_compare, 5) assert_image_similar(img, image1_scale1_compare, 5)
def test_1_mode() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
with Image.open("Tests/images/1.eps") as im: @pytest.mark.parametrize(
assert im.mode == "1" # These images have an "ImageData" descriptor.
"filename",
(
"Tests/images/eps/1.eps",
"Tests/images/eps/1_boundingbox_after_imagedata.eps",
"Tests/images/eps/1_second_imagedata.eps",
),
)
def test_1(filename: str) -> None:
with Image.open(filename) as im:
assert_image_equal_tofile(im, "Tests/images/eps/1.bmp")
def test_image_mode_not_supported(tmp_path: Path) -> None: def test_image_mode_not_supported(tmp_path: Path) -> None:
@ -302,7 +320,9 @@ def test_render_scale2() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) @pytest.mark.parametrize(
"filename", (FILE1, FILE2, "Tests/images/eps/illu10_preview.eps")
)
def test_resize(filename: str) -> None: def test_resize(filename: str) -> None:
with Image.open(filename) as im: with Image.open(filename) as im:
new_size = (100, 100) new_size = (100, 100)
@ -344,10 +364,10 @@ def test_readline(prefix: bytes, line_ending: bytes) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"filename", "filename",
( (
"Tests/images/illu10_no_preview.eps", "Tests/images/eps/illu10_no_preview.eps",
"Tests/images/illu10_preview.eps", "Tests/images/eps/illu10_preview.eps",
"Tests/images/illuCS6_no_preview.eps", "Tests/images/eps/illuCS6_no_preview.eps",
"Tests/images/illuCS6_preview.eps", "Tests/images/eps/illuCS6_preview.eps",
), ),
) )
def test_open_eps(filename: str) -> None: def test_open_eps(filename: str) -> None:
@ -359,7 +379,7 @@ def test_open_eps(filename: str) -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_emptyline() -> None: def test_emptyline() -> None:
# Test file includes an empty line in the header data # Test file includes an empty line in the header data
emptyline_file = "Tests/images/zero_bb_emptyline.eps" emptyline_file = "Tests/images/eps/zero_bb_emptyline.eps"
with Image.open(emptyline_file) as image: with Image.open(emptyline_file) as image:
image.load() image.load()
@ -371,7 +391,7 @@ def test_emptyline() -> None:
@pytest.mark.timeout(timeout=5) @pytest.mark.timeout(timeout=5)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file",
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
) )
def test_timeout(test_file: str) -> None: def test_timeout(test_file: str) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
@ -384,7 +404,7 @@ def test_bounding_box_in_trailer() -> None:
# Check bounding boxes are parsed in the same way # Check bounding boxes are parsed in the same way
# when specified in the header and the trailer # when specified in the header and the trailer
with ( with (
Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open("Tests/images/eps/zero_bb_trailer.eps") as trailer_image,
Image.open(FILE1) as header_image, Image.open(FILE1) as header_image,
): ):
assert trailer_image.size == header_image.size assert trailer_image.size == header_image.size
@ -392,12 +412,12 @@ def test_bounding_box_in_trailer() -> None:
def test_eof_before_bounding_box() -> None: def test_eof_before_bounding_box() -> None:
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): with Image.open("Tests/images/eps/zero_bb_eof_before_boundingbox.eps"):
pass pass
def test_invalid_data_after_eof() -> None: def test_invalid_data_after_eof() -> None:
with open("Tests/images/illuCS6_preview.eps", "rb") as f: with open("Tests/images/eps/illuCS6_preview.eps", "rb") as f:
img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255)) img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255))
with Image.open(img_bytes) as img: with Image.open(img_bytes) as img:

View File

@ -65,6 +65,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None: def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(static_test_file) im = Image.open(static_test_file)
im.load() im.load()
im.close() im.close()
@ -81,6 +83,8 @@ def test_seek_after_close() -> None:
def test_context_manager() -> None: def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
im.load() im.load()

View File

@ -46,6 +46,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None: def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(TEST_GIF) im = Image.open(TEST_GIF)
im.load() im.load()
im.close() im.close()
@ -67,6 +69,8 @@ def test_seek_after_close() -> None:
def test_context_manager() -> None: def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im.load() im.load()

View File

@ -21,6 +21,8 @@ def test_sanity() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert that there is no unclosed file warning # Assert that there is no unclosed file warning
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im.load() im.load()
assert im.mode == "RGBA" assert im.mode == "RGBA"

View File

@ -41,6 +41,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None: def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(TEST_IM) im = Image.open(TEST_IM)
im.load() im.load()
im.close() im.close()
@ -48,6 +50,8 @@ def test_closed_file() -> None:
def test_context_manager() -> None: def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
im.load() im.load()

View File

@ -541,12 +541,12 @@ class TestFileJpeg:
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )
def test_qtables(self, tmp_path: Path) -> None: def test_qtables(self) -> None:
def _n_qtables_helper(n: int, test_file: str) -> None: def _n_qtables_helper(n: int, test_file: str) -> None:
b = BytesIO()
with Image.open(test_file) as im: with Image.open(test_file) as im:
f = str(tmp_path / "temp.jpg") im.save(b, "JPEG", qtables=[[n] * 64] * n)
im.save(f, qtables=[[n] * 64] * n) with Image.open(b) as im:
with Image.open(f) as im:
assert len(im.quantization) == n assert len(im.quantization) == n
reloaded = self.roundtrip(im, qtables="keep") reloaded = self.roundtrip(im, qtables="keep")
assert im.quantization == reloaded.quantization assert im.quantization == reloaded.quantization
@ -850,6 +850,8 @@ class TestFileJpeg:
out = str(tmp_path / "out.jpg") out = str(tmp_path / "out.jpg")
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im.save(out, exif=exif) im.save(out, exif=exif)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import os import os
import re import re
from collections.abc import Generator
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -29,8 +30,16 @@ EXTRA_DIR = "Tests/images/jpeg2000"
pytestmark = skip_unless_feature("jpg_2000") pytestmark = skip_unless_feature("jpg_2000")
test_card = Image.open("Tests/images/test-card.png")
test_card.load() @pytest.fixture
def card() -> Generator[ImageFile.ImageFile, None, None]:
with Image.open("Tests/images/test-card.png") as im:
im.load()
try:
yield im
finally:
im.close()
# OpenJPEG 2.0.0 outputs this debugging message sometimes; we should # OpenJPEG 2.0.0 outputs this debugging message sometimes; we should
# ignore it---it doesn't represent a test failure. # ignore it---it doesn't represent a test failure.
@ -74,76 +83,76 @@ def test_invalid_file() -> None:
Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file)
def test_bytesio() -> None: def test_bytesio(card: ImageFile.ImageFile) -> None:
with open("Tests/images/test-card-lossless.jp2", "rb") as f: with open("Tests/images/test-card-lossless.jp2", "rb") as f:
data = BytesIO(f.read()) data = BytesIO(f.read())
with Image.open(data) as im: with Image.open(data) as im:
im.load() im.load()
assert_image_similar(im, test_card, 1.0e-3) assert_image_similar(im, card, 1.0e-3)
# These two test pre-written JPEG 2000 files that were not written with # These two test pre-written JPEG 2000 files that were not written with
# PIL (they were made using Adobe Photoshop) # PIL (they were made using Adobe Photoshop)
def test_lossless(tmp_path: Path) -> None: def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im: with Image.open("Tests/images/test-card-lossless.jp2") as im:
im.load() im.load()
outfile = str(tmp_path / "temp_test-card.png") outfile = str(tmp_path / "temp_test-card.png")
im.save(outfile) im.save(outfile)
assert_image_similar(im, test_card, 1.0e-3) assert_image_similar(im, card, 1.0e-3)
def test_lossy_tiled() -> None: def test_lossy_tiled(card: ImageFile.ImageFile) -> None:
assert_image_similar_tofile( assert_image_similar_tofile(card, "Tests/images/test-card-lossy-tiled.jp2", 2.0)
test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0
)
def test_lossless_rt() -> None: def test_lossless_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(test_card) im = roundtrip(card)
assert_image_equal(im, test_card) assert_image_equal(im, card)
def test_lossy_rt() -> None: def test_lossy_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(test_card, quality_layers=[20]) im = roundtrip(card, quality_layers=[20])
assert_image_similar(im, test_card, 2.0) assert_image_similar(im, card, 2.0)
def test_tiled_rt() -> None: def test_tiled_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(test_card, tile_size=(128, 128)) im = roundtrip(card, tile_size=(128, 128))
assert_image_equal(im, test_card) assert_image_equal(im, card)
def test_tiled_offset_rt() -> None: def test_tiled_offset_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) im = roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32))
assert_image_equal(im, test_card) assert_image_equal(im, card)
def test_tiled_offset_too_small() -> None: def test_tiled_offset_too_small(card: ImageFile.ImageFile) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32))
def test_irreversible_rt() -> None: def test_irreversible_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(test_card, irreversible=True, quality_layers=[20]) im = roundtrip(card, irreversible=True, quality_layers=[20])
assert_image_similar(im, test_card, 2.0) assert_image_similar(im, card, 2.0)
def test_prog_qual_rt() -> None: def test_prog_qual_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") im = roundtrip(card, quality_layers=[60, 40, 20], progression="LRCP")
assert_image_similar(im, test_card, 2.0) assert_image_similar(im, card, 2.0)
def test_prog_res_rt() -> None: def test_prog_res_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(test_card, num_resolutions=8, progression="RLCP") im = roundtrip(card, num_resolutions=8, progression="RLCP")
assert_image_equal(im, test_card) assert_image_equal(im, card)
@pytest.mark.parametrize("num_resolutions", range(2, 6)) @pytest.mark.parametrize("num_resolutions", range(2, 6))
def test_default_num_resolutions(num_resolutions: int) -> None: def test_default_num_resolutions(
card: ImageFile.ImageFile, num_resolutions: int
) -> None:
d = 1 << (num_resolutions - 1) d = 1 << (num_resolutions - 1)
im = test_card.resize((d - 1, d - 1)) im = card.resize((d - 1, d - 1))
with pytest.raises(OSError): with pytest.raises(OSError):
roundtrip(im, num_resolutions=num_resolutions) roundtrip(im, num_resolutions=num_resolutions)
reloaded = roundtrip(im) reloaded = roundtrip(im)
@ -205,31 +214,31 @@ def test_header_errors() -> None:
pass pass
def test_layers_type(tmp_path: Path) -> None: def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp_layers.jp2") outfile = str(tmp_path / "temp_layers.jp2")
for quality_layers in [[100, 50, 10], (100, 50, 10), None]: for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
test_card.save(outfile, quality_layers=quality_layers) card.save(outfile, quality_layers=quality_layers)
for quality_layers_str in ["quality_layers", ("100", "50", "10")]: for quality_layers_str in ["quality_layers", ("100", "50", "10")]:
with pytest.raises(ValueError): with pytest.raises(ValueError):
test_card.save(outfile, quality_layers=quality_layers_str) card.save(outfile, quality_layers=quality_layers_str)
def test_layers() -> None: def test_layers(card: ImageFile.ImageFile) -> None:
out = BytesIO() out = BytesIO()
test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP")
out.seek(0) out.seek(0)
with Image.open(out) as im: with Image.open(out) as im:
im.layers = 1 im.layers = 1
im.load() im.load()
assert_image_similar(im, test_card, 13) assert_image_similar(im, card, 13)
out.seek(0) out.seek(0)
with Image.open(out) as im: with Image.open(out) as im:
im.layers = 3 im.layers = 3
im.load() im.load()
assert_image_similar(im, test_card, 0.4) assert_image_similar(im, card, 0.4)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -245,24 +254,30 @@ def test_layers() -> None:
(None, {"no_jp2": False}, 4, b"jP"), (None, {"no_jp2": False}, 4, b"jP"),
), ),
) )
def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: def test_no_jp2(
card: ImageFile.ImageFile,
name: str,
args: dict[str, bool],
offset: int,
data: bytes,
) -> None:
out = BytesIO() out = BytesIO()
if name: if name:
out.name = name out.name = name
test_card.save(out, "JPEG2000", **args) card.save(out, "JPEG2000", **args)
out.seek(offset) out.seek(offset)
assert out.read(2) == data assert out.read(2) == data
def test_mct() -> None: def test_mct(card: ImageFile.ImageFile) -> None:
# Three component # Three component
for val in (0, 1): for val in (0, 1):
out = BytesIO() out = BytesIO()
test_card.save(out, "JPEG2000", mct=val, no_jp2=True) card.save(out, "JPEG2000", mct=val, no_jp2=True)
assert out.getvalue()[59] == val assert out.getvalue()[59] == val
with Image.open(out) as im: with Image.open(out) as im:
assert_image_similar(im, test_card, 1.0e-3) assert_image_similar(im, card, 1.0e-3)
# Single component should have MCT disabled # Single component should have MCT disabled
for val in (0, 1): for val in (0, 1):
@ -419,22 +434,22 @@ def test_comment() -> None:
pass pass
def test_save_comment() -> None: def test_save_comment(card: ImageFile.ImageFile) -> None:
for comment in ("Created by Pillow", b"Created by Pillow"): for comment in ("Created by Pillow", b"Created by Pillow"):
out = BytesIO() out = BytesIO()
test_card.save(out, "JPEG2000", comment=comment) card.save(out, "JPEG2000", comment=comment)
with Image.open(out) as im: with Image.open(out) as im:
assert im.info["comment"] == b"Created by Pillow" assert im.info["comment"] == b"Created by Pillow"
out = BytesIO() out = BytesIO()
long_comment = b" " * 65531 long_comment = b" " * 65531
test_card.save(out, "JPEG2000", comment=long_comment) card.save(out, "JPEG2000", comment=long_comment)
with Image.open(out) as im: with Image.open(out) as im:
assert im.info["comment"] == long_comment assert im.info["comment"] == long_comment
with pytest.raises(ValueError): with pytest.raises(ValueError):
test_card.save(out, "JPEG2000", comment=long_comment + b" ") card.save(out, "JPEG2000", comment=long_comment + b" ")
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -457,10 +472,10 @@ def test_crashes(test_file: str) -> None:
@skip_unless_feature_version("jpg_2000", "2.4.0") @skip_unless_feature_version("jpg_2000", "2.4.0")
def test_plt_marker() -> None: def test_plt_marker(card: ImageFile.ImageFile) -> None:
# Search the start of the codesteam for PLT # Search the start of the codesteam for PLT
out = BytesIO() out = BytesIO()
test_card.save(out, "JPEG2000", no_jp2=True, plt=True) card.save(out, "JPEG2000", no_jp2=True, plt=True)
out.seek(0) out.seek(0)
while True: while True:
marker = out.read(2) marker = out.read(2)

View File

@ -48,6 +48,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None: def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(test_files[0]) im = Image.open(test_files[0])
im.load() im.load()
im.close() im.close()
@ -63,6 +65,8 @@ def test_seek_after_close() -> None:
def test_context_manager() -> None: def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(test_files[0]) as im: with Image.open(test_files[0]) as im:
im.load() im.load()

View File

@ -338,6 +338,8 @@ class TestFilePng:
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
# Assert that there is no unclosed file warning # Assert that there is no unclosed file warning
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im.verify() im.verify()
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:

View File

@ -35,6 +35,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None: def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(test_file) im = Image.open(test_file)
im.load() im.load()
im.close() im.close()
@ -42,6 +44,8 @@ def test_closed_file() -> None:
def test_context_manager() -> None: def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.load() im.load()

View File

@ -34,6 +34,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None: def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
im.close() im.close()
@ -41,6 +43,8 @@ def test_closed_file() -> None:
def test_context_manager() -> None: def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()

View File

@ -37,11 +37,15 @@ def test_unclosed_file() -> None:
def test_close() -> None: def test_close() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
tar.close() tar.close()
def test_contextmanager() -> None: def test_contextmanager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"):
pass pass

View File

@ -72,6 +72,8 @@ class TestFileTiff:
def test_closed_file(self) -> None: def test_closed_file(self) -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open("Tests/images/multipage.tiff") im = Image.open("Tests/images/multipage.tiff")
im.load() im.load()
im.close() im.close()
@ -88,6 +90,8 @@ class TestFileTiff:
def test_context_manager(self) -> None: def test_context_manager(self) -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
im.load() im.load()
@ -108,10 +112,6 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/hopper.tif") assert_image_equal_tofile(im, "Tests/images/hopper.tif")
with Image.open("Tests/images/hopper_bigtiff.tif") as im: with Image.open("Tests/images/hopper_bigtiff.tif") as im:
# The data type of this file's StripOffsets tag is LONG8,
# which is not yet supported for offset data when saving multiple frames.
del im.tag_v2[273]
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
@ -732,6 +732,20 @@ class TestFileTiff:
with Image.open(mp) as reread: with Image.open(mp) as reread:
assert reread.n_frames == 3 assert reread.n_frames == 3
def test_fixoffsets(self) -> None:
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
b.seek(0)
a.fixOffsets(1, isShort=True)
b.seek(0)
a.fixOffsets(1, isLong=True)
# Neither short nor long
b.seek(0)
with pytest.raises(RuntimeError):
a.fixOffsets(1)
def test_saving_icc_profile(self, tmp_path: Path) -> None: def test_saving_icc_profile(self, tmp_path: Path) -> None:
# Tests saving TIFF with icc_profile set. # Tests saving TIFF with icc_profile set.
# At the time of writing this will only work for non-compressed tiffs # At the time of writing this will only work for non-compressed tiffs

View File

@ -191,6 +191,8 @@ class TestFileWebp:
file_path = "Tests/images/hopper.webp" file_path = "Tests/images/hopper.webp"
with Image.open(file_path) as image: with Image.open(file_path) as image:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
image.save(tmp_path / "temp.webp") image.save(tmp_path / "temp.webp")
def test_file_pointer_could_be_reused(self) -> None: def test_file_pointer_could_be_reused(self) -> None:

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import IO from typing import IO
@ -61,6 +62,12 @@ def test_load_float_dpi() -> None:
with Image.open("Tests/images/drawing.emf") as im: with Image.open("Tests/images/drawing.emf") as im:
assert im.info["dpi"] == 1423.7668161434979 assert im.info["dpi"] == 1423.7668161434979
with open("Tests/images/drawing.emf", "rb") as fp:
data = fp.read()
b = BytesIO(data[:8] + b"\x06\xFA" + data[10:])
with Image.open(b) as im:
assert im.info["dpi"][0] == 2540
def test_load_set_dpi() -> None: def test_load_set_dpi() -> None:
with Image.open("Tests/images/drawing.wmf") as im: with Image.open("Tests/images/drawing.wmf") as im:

View File

@ -737,6 +737,8 @@ class TestImage:
# Act/Assert # Act/Assert
with Image.open(test_file) as im: with Image.open(test_file) as im:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
im.save(temp_file) im.save(temp_file)
def test_no_new_file_on_error(self, tmp_path: Path) -> None: def test_no_new_file_on_error(self, tmp_path: Path) -> None:

View File

@ -35,16 +35,25 @@ from .helper import assert_image_equal, hopper
ImageFilter.UnsharpMask(10), ImageFilter.UnsharpMask(10),
), ),
) )
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) @pytest.mark.parametrize(
def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: "mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
)
def test_sanity(
filter_to_apply: ImageFilter.Filter | type[ImageFilter.Filter], mode: str
) -> None:
im = hopper(mode) im = hopper(mode)
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): if mode[0] != "I" or (
callable(filter_to_apply)
and issubclass(filter_to_apply, ImageFilter.BuiltinFilter)
):
out = im.filter(filter_to_apply) out = im.filter(filter_to_apply)
assert out.mode == im.mode assert out.mode == im.mode
assert out.size == im.size assert out.size == im.size
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) @pytest.mark.parametrize(
"mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
)
def test_sanity_error(mode: str) -> None: def test_sanity_error(mode: str) -> None:
im = hopper(mode) im = hopper(mode)
with pytest.raises(TypeError): with pytest.raises(TypeError):
@ -145,7 +154,9 @@ def test_kernel_not_enough_coefficients() -> None:
ImageFilter.Kernel((3, 3), (0, 0)) ImageFilter.Kernel((3, 3), (0, 0))
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) @pytest.mark.parametrize(
"mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
)
def test_consistency_3x3(mode: str) -> None: def test_consistency_3x3(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss.bmp") as reference: with Image.open("Tests/images/hopper_emboss.bmp") as reference:
@ -161,7 +172,9 @@ def test_consistency_3x3(mode: str) -> None:
assert_image_equal(source.filter(kernel), reference) assert_image_equal(source.filter(kernel), reference)
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) @pytest.mark.parametrize(
"mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
)
def test_consistency_5x5(mode: str) -> None: def test_consistency_5x5(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:

View File

@ -1,11 +1,19 @@
from __future__ import annotations from __future__ import annotations
import pytest
from .helper import hopper from .helper import hopper
def test_sanity() -> None: def test_sanity() -> None:
im = hopper() im = hopper()
type_repr = repr(type(im.getim()))
type_repr = repr(type(im.getim()))
assert "PyCapsule" in type_repr assert "PyCapsule" in type_repr
assert isinstance(im.im.id, int)
with pytest.warns(DeprecationWarning):
assert isinstance(im.im.id, int)
with pytest.warns(DeprecationWarning):
ptrs = dict(im.im.unsafe_ptrs)
assert ptrs.keys() == {"image8", "image32", "image"}

View File

@ -10,7 +10,7 @@ from pathlib import Path
import pytest import pytest
from PIL import Image from PIL import Image, ImageFile
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -179,7 +179,7 @@ class TestImagingCoreResize:
@pytest.fixture @pytest.fixture
def gradients_image() -> Generator[Image.Image, None, None]: def gradients_image() -> Generator[ImageFile.ImageFile, None, None]:
with Image.open("Tests/images/radial_gradients.png") as im: with Image.open("Tests/images/radial_gradients.png") as im:
im.load() im.load()
try: try:
@ -189,7 +189,7 @@ def gradients_image() -> Generator[Image.Image, None, None]:
class TestReducingGapResize: class TestReducingGapResize:
def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: def test_reducing_gap_values(self, gradients_image: ImageFile.ImageFile) -> None:
ref = gradients_image.resize( ref = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, reducing_gap=None (52, 34), Image.Resampling.BICUBIC, reducing_gap=None
) )
@ -210,7 +210,7 @@ class TestReducingGapResize:
) )
def test_reducing_gap_1( def test_reducing_gap_1(
self, self,
gradients_image: Image.Image, gradients_image: ImageFile.ImageFile,
box: tuple[float, float, float, float], box: tuple[float, float, float, float],
epsilon: float, epsilon: float,
) -> None: ) -> None:
@ -230,7 +230,7 @@ class TestReducingGapResize:
) )
def test_reducing_gap_2( def test_reducing_gap_2(
self, self,
gradients_image: Image.Image, gradients_image: ImageFile.ImageFile,
box: tuple[float, float, float, float], box: tuple[float, float, float, float],
epsilon: float, epsilon: float,
) -> None: ) -> None:
@ -250,7 +250,7 @@ class TestReducingGapResize:
) )
def test_reducing_gap_3( def test_reducing_gap_3(
self, self,
gradients_image: Image.Image, gradients_image: ImageFile.ImageFile,
box: tuple[float, float, float, float], box: tuple[float, float, float, float],
epsilon: float, epsilon: float,
) -> None: ) -> None:
@ -266,7 +266,9 @@ class TestReducingGapResize:
@pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)))
def test_reducing_gap_8( def test_reducing_gap_8(
self, gradients_image: Image.Image, box: tuple[float, float, float, float] self,
gradients_image: ImageFile.ImageFile,
box: tuple[float, float, float, float],
) -> None: ) -> None:
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize( im = gradients_image.resize(
@ -281,7 +283,7 @@ class TestReducingGapResize:
) )
def test_box_filter( def test_box_filter(
self, self,
gradients_image: Image.Image, gradients_image: ImageFile.ImageFile,
box: tuple[float, float, float, float], box: tuple[float, float, float, float],
epsilon: float, epsilon: float,
) -> None: ) -> None:

View File

@ -324,17 +324,17 @@ def test_set_lut() -> None:
def test_wrong_mode() -> None: def test_wrong_mode() -> None:
lut = ImageMorph.LutBuilder(op_name="corner").build_lut() lut = ImageMorph.LutBuilder(op_name="corner").build_lut()
imrgb = Image.new("RGB", (10, 10)) imrgb_ptr = Image.new("RGB", (10, 10)).getim()
iml = Image.new("L", (10, 10)) iml_ptr = Image.new("L", (10, 10)).getim()
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
_imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) _imagingmorph.apply(bytes(lut), imrgb_ptr, iml_ptr)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
_imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) _imagingmorph.apply(bytes(lut), iml_ptr, imrgb_ptr)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
_imagingmorph.match(bytes(lut), imrgb.im.id) _imagingmorph.match(bytes(lut), imrgb_ptr)
# Should not raise # Should not raise
_imagingmorph.match(bytes(lut), iml.im.id) _imagingmorph.match(bytes(lut), iml_ptr)

View File

@ -52,4 +52,6 @@ def test_image(mode: str) -> None:
def test_closed_file() -> None: def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
ImageQt.ImageQt("Tests/images/hopper.gif") ImageQt.ImageQt("Tests/images/hopper.gif")

View File

@ -117,5 +117,5 @@ def test_ipythonviewer() -> None:
else: else:
pytest.fail("IPythonViewer not found") pytest.fail("IPythonViewer not found")
im = hopper() with hopper() as im:
assert test_viewer.show(im) == 1 assert test_viewer.show(im) == 1

View File

@ -264,4 +264,6 @@ def test_no_resource_warning_for_numpy_array() -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
# Act/Assert # Act/Assert
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error")
array(im) array(im)

View File

@ -56,10 +56,10 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
), ),
("Tests/images/hopper.tif", None), ("Tests/images/hopper.tif", None),
("Tests/images/test-card.png", None), ("Tests/images/test-card.png", None),
("Tests/images/zero_bb.png", None), ("Tests/images/eps/zero_bb.png", None),
("Tests/images/zero_bb_scale2.png", None), ("Tests/images/eps/zero_bb_scale2.png", None),
("Tests/images/non_zero_bb.png", None), ("Tests/images/eps/non_zero_bb.png", None),
("Tests/images/non_zero_bb_scale2.png", None), ("Tests/images/eps/non_zero_bb_scale2.png", None),
("Tests/images/p_trns_single.png", None), ("Tests/images/p_trns_single.png", None),
("Tests/images/pil123p.png", None), ("Tests/images/pil123p.png", None),
("Tests/images/itxt_chunks.png", None), ("Tests/images/itxt_chunks.png", None),

View File

@ -46,7 +46,7 @@ clean:
-rm -rf $(BUILDDIR)/* -rm -rf $(BUILDDIR)/*
install-sphinx: install-sphinx:
$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph $(PYTHON) -m pip install -e ..[docs]
.PHONY: html .PHONY: html
html: html:

View File

@ -18,7 +18,7 @@ The fork author's goal is to foster and support active development of PIL throug
License License
------- -------
Like PIL, Pillow is `licensed under the open source HPND License <https://raw.githubusercontent.com/python-pillow/Pillow/main/LICENSE>`_ Like PIL, Pillow is `licensed under the open source MIT-CMU License <https://raw.githubusercontent.com/python-pillow/Pillow/main/LICENSE>`_
Why a fork? Why a fork?
----------- -----------

View File

@ -22,7 +22,7 @@ import PIL
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "7.3" needs_sphinx = "8.1"
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -121,7 +121,7 @@ nitpicky = True
# generating warnings in “nitpicky mode”. Note that type should include the domain name # generating warnings in “nitpicky mode”. Note that type should include the domain name
# if present. Example entries would be ('py:func', 'int') or # if present. Example entries would be ('py:func', 'int') or
# ('envvar', 'LD_LIBRARY_PATH'). # ('envvar', 'LD_LIBRARY_PATH').
nitpick_ignore = [("py:class", "_io.BytesIO")] nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")]
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------
@ -338,8 +338,6 @@ linkcheck_allowed_redirects = {
# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
_repo = "https://github.com/python-pillow/Pillow/" _repo = "https://github.com/python-pillow/Pillow/"
extlinks = { extlinks = {
"cve": ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s"),
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
"issue": (_repo + "issues/%s", "#%s"), "issue": (_repo + "issues/%s", "#%s"),
"pr": (_repo + "pull/%s", "#%s"), "pr": (_repo + "pull/%s", "#%s"),
"pypi": ("https://pypi.org/project/%s/", "%s"), "pypi": ("https://pypi.org/project/%s/", "%s"),

View File

@ -165,6 +165,16 @@ Specific WebP Feature Checks
``True`` if the WebP module is installed, until they are removed in Pillow ``True`` if the WebP module is installed, until they are removed in Pillow
12.0.0 (2025-10-15). 12.0.0 (2025-10-15).
Get internal pointers to objects
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been
deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object.
Removed features Removed features
---------------- ----------------

View File

@ -692,6 +692,30 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
you fail to do this, you will get errors about not being able to load the you fail to do this, you will get errors about not being able to load the
``_imaging`` DLL). ``_imaging`` DLL).
MPO
^^^
Pillow reads and writes Multi Picture Object (MPO) files. When first opened, it loads
the primary image. The :py:meth:`~PIL.Image.Image.seek` and
:py:meth:`~PIL.Image.Image.tell` methods may be used to read other pictures from the
file. The pictures are zero-indexed and random access is supported.
.. _mpo-saving:
Saving
~~~~~~
When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default
only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following
option will also be available.
**append_images**
A list of images to append as additional pictures. Each of the
images in the list can be single or multiframe images.
.. versionadded:: 9.3.0
MSP MSP
^^^ ^^^
@ -1435,30 +1459,6 @@ Note that there may be an embedded gamma of 2.2 in MIC files.
To enable MIC support, you must install :pypi:`olefile`. To enable MIC support, you must install :pypi:`olefile`.
MPO
^^^
Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary
image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell`
methods may be used to read other pictures from the file. The pictures are
zero-indexed and random access is supported.
.. _mpo-saving:
Saving
~~~~~~
When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default
only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following
option will also be available.
**append_images**
A list of images to append as additional pictures. Each of the
images in the list can be single or multiframe images.
.. versionadded:: 9.3.0
PCD PCD
^^^ ^^^

View File

@ -29,10 +29,10 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 | | Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 39 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | x86-64 | | Fedora 40 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 41 | 3.13 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.12 | x86-64 | | Gentoo | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 13 Ventura | 3.9 | x86-64 | | macOS 13 Ventura | 3.9 | x86-64 |
@ -53,7 +53,7 @@ These platforms are built and tested for every change.
| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 | | Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | | | | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.12 | x86 | | | 3.13 | x86 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 | | | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
@ -75,7 +75,9 @@ 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 15 Sequoia | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | | macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm |
| +----------------------------+------------------+ |
| | 3.8 | 10.4.0 | |
+----------------------------------+----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | | macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm |
+----------------------------------+----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
@ -148,7 +150,7 @@ These platforms have been reported to work at the versions mentioned.
+----------------------------------+----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
+----------------------------------+----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | | Windows 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm64 |
+----------------------------------+----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | | Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 |
+----------------------------------+----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+

View File

@ -19,7 +19,7 @@ Example: Parse an image
from PIL import ImageFile from PIL import ImageFile
fp = open("hopper.pgm", "rb") fp = open("hopper.ppm", "rb")
p = ImageFile.Parser() p = ImageFile.Parser()

View File

@ -1,19 +1,6 @@
11.0.0 11.0.0
------ ------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes Backwards Incompatible Changes
============================== ==============================
@ -73,6 +60,16 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/
Get internal pointers to objects
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been
deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object.
ICNS (width, height, scale) sizes ICNS (width, height, scale) sizes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -149,7 +146,7 @@ Python 3.13
Pillow 10.4.0 had wheels built against Python 3.13 beta, available as a preview to help Pillow 10.4.0 had wheels built against Python 3.13 beta, available as a preview to help
others prepare for 3.13, and to ensure Pillow could be used immediately at the release others prepare for 3.13, and to ensure Pillow could be used immediately at the release
of 3.13.0 final (2024-10-01, :pep:`719`). of 3.13.0 final (2024-10-07, :pep:`719`).
Pillow 11.0.0 now officially supports Python 3.13. Pillow 11.0.0 now officially supports Python 3.13.

View File

@ -14,14 +14,14 @@ readme = "README.md"
keywords = [ keywords = [
"Imaging", "Imaging",
] ]
license = { text = "HPND" } license = { text = "MIT-CMU" }
authors = [ authors = [
{ name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, { name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
] ]
requires-python = ">=3.9" requires-python = ">=3.9"
classifiers = [ classifiers = [
"Development Status :: 6 - Mature", "Development Status :: 6 - Mature",
"License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", "License :: OSI Approved :: CMU License (MIT-CMU)",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
@ -43,7 +43,7 @@ dynamic = [
optional-dependencies.docs = [ optional-dependencies.docs = [
"furo", "furo",
"olefile", "olefile",
"sphinx>=7.3", "sphinx>=8.1",
"sphinx-copybutton", "sphinx-copybutton",
"sphinx-inline-tabs", "sphinx-inline-tabs",
"sphinxext-opengraph", "sphinxext-opengraph",
@ -56,7 +56,7 @@ optional-dependencies.mic = [
] ]
optional-dependencies.tests = [ optional-dependencies.tests = [
"check-manifest", "check-manifest",
"coverage", "coverage>=7.4.2",
"defusedxml", "defusedxml",
"markdown2", "markdown2",
"olefile", "olefile",
@ -65,6 +65,7 @@ optional-dependencies.tests = [
"pytest", "pytest",
"pytest-cov", "pytest-cov",
"pytest-timeout", "pytest-timeout",
"trove-classifiers>=2024.10.12",
] ]
optional-dependencies.typing = [ optional-dependencies.typing = [
"typing-extensions; python_version<'3.10'", "typing-extensions; python_version<'3.10'",

View File

@ -389,7 +389,7 @@ class pil_build_ext(build_ext):
pass pass
for x in self.feature: for x in self.feature:
if getattr(self, f"disable_{x}"): if getattr(self, f"disable_{x}"):
setattr(self.feature, x, False) self.feature.set(x, False)
self.feature.required.discard(x) self.feature.required.discard(x)
_dbg("Disabling %s", x) _dbg("Disabling %s", x)
if getattr(self, f"enable_{x}"): if getattr(self, f"enable_{x}"):
@ -1001,7 +1001,7 @@ def debug_build() -> bool:
return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD
files = ["src/_imaging.c"] files: list[str | os.PathLike[str]] = ["src/_imaging.c"]
for src_file in _IMAGING: for src_file in _IMAGING:
files.append("src/" + src_file + ".c") files.append("src/" + src_file + ".c")
for src_file in _LIB_IMAGING: for src_file in _LIB_IMAGING:

View File

@ -121,7 +121,13 @@ def Ghostscript(
lengthfile -= len(s) lengthfile -= len(s)
f.write(s) f.write(s)
device = "pngalpha" if transparency else "ppmraw" if transparency:
# "RGBA"
device = "pngalpha"
else:
# "pnmraw" automatically chooses between
# PBM ("1"), PGM ("L"), and PPM ("RGB").
device = "pnmraw"
# Build Ghostscript command # Build Ghostscript command
command = [ command = [
@ -151,8 +157,9 @@ def Ghostscript(
startupinfo = subprocess.STARTUPINFO() startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
subprocess.check_call(command, startupinfo=startupinfo) subprocess.check_call(command, startupinfo=startupinfo)
out_im = Image.open(outfile) with Image.open(outfile) as out_im:
out_im.load() out_im.load()
return out_im.im.copy()
finally: finally:
try: try:
os.unlink(outfile) os.unlink(outfile)
@ -161,10 +168,6 @@ def Ghostscript(
except OSError: except OSError:
pass pass
im = out_im.im.copy()
out_im.close()
return im
def _accept(prefix: bytes) -> bool: def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
@ -191,6 +194,11 @@ class EpsImageFile(ImageFile.ImageFile):
self._mode = "RGB" self._mode = "RGB"
# When reading header comments, the first comment is used.
# When reading trailer comments, the last comment is used.
bounding_box: list[int] | None = None
imagedata_size: tuple[int, int] | None = None
byte_arr = bytearray(255) byte_arr = bytearray(255)
bytes_mv = memoryview(byte_arr) bytes_mv = memoryview(byte_arr)
bytes_read = 0 bytes_read = 0
@ -211,8 +219,8 @@ class EpsImageFile(ImageFile.ImageFile):
msg = 'EPS header missing "%%BoundingBox" comment' msg = 'EPS header missing "%%BoundingBox" comment'
raise SyntaxError(msg) raise SyntaxError(msg)
def _read_comment(s: str) -> bool: def read_comment(s: str) -> bool:
nonlocal reading_trailer_comments nonlocal bounding_box, reading_trailer_comments
try: try:
m = split.match(s) m = split.match(s)
except re.error as e: except re.error as e:
@ -227,18 +235,12 @@ class EpsImageFile(ImageFile.ImageFile):
if k == "BoundingBox": if k == "BoundingBox":
if v == "(atend)": if v == "(atend)":
reading_trailer_comments = True reading_trailer_comments = True
elif not self.tile or (trailer_reached and reading_trailer_comments): elif not bounding_box or (trailer_reached and reading_trailer_comments):
try: try:
# Note: The DSC spec says that BoundingBox # Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers # fields should be integers, but some drivers
# put floating point values there anyway. # put floating point values there anyway.
box = [int(float(i)) for i in v.split()] bounding_box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1]
self.tile = [
ImageFile._Tile(
"eps", (0, 0) + self.size, offset, (length, box)
)
]
except Exception: except Exception:
pass pass
return True return True
@ -289,7 +291,7 @@ class EpsImageFile(ImageFile.ImageFile):
continue continue
s = str(bytes_mv[:bytes_read], "latin-1") s = str(bytes_mv[:bytes_read], "latin-1")
if not _read_comment(s): if not read_comment(s):
m = field.match(s) m = field.match(s)
if m: if m:
k = m.group(1) k = m.group(1)
@ -308,6 +310,12 @@ class EpsImageFile(ImageFile.ImageFile):
# Check for an "ImageData" descriptor # Check for an "ImageData" descriptor
# https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
# If we've already read an "ImageData" descriptor,
# don't read another one.
if imagedata_size:
bytes_read = 0
continue
# Values: # Values:
# columns # columns
# rows # rows
@ -333,22 +341,35 @@ class EpsImageFile(ImageFile.ImageFile):
else: else:
break break
self._size = columns, rows # Parse the columns and rows after checking the bit depth and mode
return # in case the bit depth and/or mode are invalid.
imagedata_size = columns, rows
elif bytes_mv[:5] == b"%%EOF": elif bytes_mv[:5] == b"%%EOF":
break break
elif trailer_reached and reading_trailer_comments: elif trailer_reached and reading_trailer_comments:
# Load EPS trailer # Load EPS trailer
s = str(bytes_mv[:bytes_read], "latin-1") s = str(bytes_mv[:bytes_read], "latin-1")
_read_comment(s) read_comment(s)
elif bytes_mv[:9] == b"%%Trailer": elif bytes_mv[:9] == b"%%Trailer":
trailer_reached = True trailer_reached = True
bytes_read = 0 bytes_read = 0
if not self.tile: # A "BoundingBox" is always required,
# even if an "ImageData" descriptor size exists.
if not bounding_box:
msg = "cannot determine EPS bounding box" msg = "cannot determine EPS bounding box"
raise OSError(msg) raise OSError(msg)
# An "ImageData" size takes precedence over the "BoundingBox".
self._size = imagedata_size or (
bounding_box[2] - bounding_box[0],
bounding_box[3] - bounding_box[1],
)
self.tile = [
ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
]
def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
s = fp.read(4) s = fp.read(4)

View File

@ -103,7 +103,6 @@ class GifImageFile(ImageFile.ImageFile):
self.info["version"] = s[:6] self.info["version"] = s[:6]
self._size = i16(s, 6), i16(s, 8) self._size = i16(s, 6), i16(s, 8)
self.tile = []
flags = s[10] flags = s[10]
bits = (flags & 7) + 1 bits = (flags & 7) + 1

View File

@ -225,12 +225,7 @@ if TYPE_CHECKING:
from IPython.lib.pretty import PrettyPrinter from IPython.lib.pretty import PrettyPrinter
from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard
if sys.version_info >= (3, 13):
from types import CapsuleType
else:
CapsuleType = object
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
str, str,

View File

@ -31,6 +31,10 @@ from ._typing import SupportsRead
try: try:
from . import _imagingcms as core from . import _imagingcms as core
_CmsProfileCompatible = Union[
str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile"
]
except ImportError as ex: except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing # Allow error import for doc purposes, but error out when accessing
# anything in core. # anything in core.
@ -349,19 +353,17 @@ class ImageCmsTransform(Image.ImagePointHandler):
return self.apply(im) return self.apply(im)
def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image:
im.load()
if imOut is None: if imOut is None:
imOut = Image.new(self.output_mode, im.size, None) imOut = Image.new(self.output_mode, im.size, None)
self.transform.apply(im.im.id, imOut.im.id) self.transform.apply(im.getim(), imOut.getim())
imOut.info["icc_profile"] = self.output_profile.tobytes() imOut.info["icc_profile"] = self.output_profile.tobytes()
return imOut return imOut
def apply_in_place(self, im: Image.Image) -> Image.Image: def apply_in_place(self, im: Image.Image) -> Image.Image:
im.load()
if im.mode != self.output_mode: if im.mode != self.output_mode:
msg = "mode mismatch" msg = "mode mismatch"
raise ValueError(msg) # wrong output mode raise ValueError(msg) # wrong output mode
self.transform.apply(im.im.id, im.im.id) self.transform.apply(im.getim(), im.getim())
im.info["icc_profile"] = self.output_profile.tobytes() im.info["icc_profile"] = self.output_profile.tobytes()
return im return im
@ -391,10 +393,6 @@ def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile |
# pyCMS compatible layer # pyCMS compatible layer
# --------------------------------------------------------------------. # --------------------------------------------------------------------.
_CmsProfileCompatible = Union[
str, SupportsRead[bytes], core.CmsProfile, ImageCmsProfile
]
class PyCMSError(Exception): class PyCMSError(Exception):
"""(pyCMS) Exception class. """(pyCMS) Exception class.

View File

@ -59,13 +59,12 @@ class _Operand:
if im2 is None: if im2 is None:
# unary operation # unary operation
out = Image.new(mode or im_1.mode, im_1.size, None) out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load()
try: try:
op = getattr(_imagingmath, f"{op}_{im_1.mode}") op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e raise TypeError(msg) from e
_imagingmath.unop(op, out.im.id, im_1.im.id) _imagingmath.unop(op, out.getim(), im_1.getim())
else: else:
# binary operation # binary operation
im_2 = self.__fixup(im2) im_2 = self.__fixup(im2)
@ -86,14 +85,12 @@ class _Operand:
if im_2.size != size: if im_2.size != size:
im_2 = im_2.crop((0, 0) + size) im_2 = im_2.crop((0, 0) + size)
out = Image.new(mode or im_1.mode, im_1.size, None) out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load()
im_2.load()
try: try:
op = getattr(_imagingmath, f"{op}_{im_1.mode}") op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e raise TypeError(msg) from e
_imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) _imagingmath.binop(op, out.getim(), im_1.getim(), im_2.getim())
return _Operand(out) return _Operand(out)
# unary operators # unary operators
@ -176,10 +173,10 @@ class _Operand:
return self.apply("rshift", self, other) return self.apply("rshift", self, other)
# logical # logical
def __eq__(self, other): def __eq__(self, other: _Operand | float) -> _Operand: # type: ignore[override]
return self.apply("eq", self, other) return self.apply("eq", self, other)
def __ne__(self, other): def __ne__(self, other: _Operand | float) -> _Operand: # type: ignore[override]
return self.apply("ne", self, other) return self.apply("ne", self, other)
def __lt__(self, other: _Operand | float) -> _Operand: def __lt__(self, other: _Operand | float) -> _Operand:

View File

@ -213,7 +213,7 @@ class MorphOp:
msg = "Image mode must be L" msg = "Image mode must be L"
raise ValueError(msg) raise ValueError(msg)
outimage = Image.new(image.mode, image.size, None) outimage = Image.new(image.mode, image.size, None)
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim())
return count, outimage return count, outimage
def match(self, image: Image.Image) -> list[tuple[int, int]]: def match(self, image: Image.Image) -> list[tuple[int, int]]:
@ -229,7 +229,7 @@ class MorphOp:
if image.mode != "L": if image.mode != "L":
msg = "Image mode must be L" msg = "Image mode must be L"
raise ValueError(msg) raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id) return _imagingmorph.match(bytes(self.lut), image.getim())
def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
"""Get a list of all turned on pixels in a binary image """Get a list of all turned on pixels in a binary image
@ -240,7 +240,7 @@ class MorphOp:
if image.mode != "L": if image.mode != "L":
msg = "Image mode must be L" msg = "Image mode must be L"
raise ValueError(msg) raise ValueError(msg)
return _imagingmorph.get_on_pixels(image.im.id) return _imagingmorph.get_on_pixels(image.getim())
def load_lut(self, filename: str) -> None: def load_lut(self, filename: str) -> None:
"""Load an operator from an mrl file""" """Load an operator from an mrl file"""

View File

@ -213,4 +213,7 @@ def toqimage(im: Image.Image | str | QByteArray) -> ImageQt:
def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap:
qimage = toqimage(im) qimage = toqimage(im)
return getattr(QPixmap, "fromImage")(qimage) pixmap = getattr(QPixmap, "fromImage")(qimage)
if qt_version == "6":
pixmap.detach()
return pixmap

View File

@ -32,23 +32,12 @@ from typing import TYPE_CHECKING, Any, cast
from . import Image, ImageFile from . import Image, ImageFile
if TYPE_CHECKING:
from ._typing import CapsuleType
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Check for Tkinter interface hooks # Check for Tkinter interface hooks
_pilbitmap_ok = None
def _pilbitmap_check() -> int:
global _pilbitmap_ok
if _pilbitmap_ok is None:
try:
im = Image.new("1", (1, 1))
tkinter.BitmapImage(data=f"PIL:{im.im.id}")
_pilbitmap_ok = 1
except tkinter.TclError:
_pilbitmap_ok = 0
return _pilbitmap_ok
def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
source = None source = None
@ -62,18 +51,18 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
def _pyimagingtkcall( def _pyimagingtkcall(
command: str, photo: PhotoImage | tkinter.PhotoImage, id: int command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType
) -> None: ) -> None:
tk = photo.tk tk = photo.tk
try: try:
tk.call(command, photo, id) tk.call(command, photo, repr(ptr))
except tkinter.TclError: except tkinter.TclError:
# activate Tkinter hook # activate Tkinter hook
# may raise an error if it cannot attach to Tkinter # may raise an error if it cannot attach to Tkinter
from . import _imagingtk from . import _imagingtk
_imagingtk.tkinit(tk.interpaddr()) _imagingtk.tkinit(tk.interpaddr())
tk.call(command, photo, id) tk.call(command, photo, repr(ptr))
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -142,7 +131,10 @@ class PhotoImage:
self.paste(image) self.paste(image)
def __del__(self) -> None: def __del__(self) -> None:
name = self.__photo.name try:
name = self.__photo.name
except AttributeError:
return
self.__photo.name = None self.__photo.name = None
try: try:
self.__photo.tk.call("image", "delete", name) self.__photo.tk.call("image", "delete", name)
@ -185,15 +177,14 @@ class PhotoImage:
the bitmap image. the bitmap image.
""" """
# convert to blittable # convert to blittable
im.load() ptr = im.getim()
image = im.im image = im.im
if image.isblock() and im.mode == self.__mode: if not image.isblock() or im.mode != self.__mode:
block = image block = Image.core.new_block(self.__mode, im.size)
else:
block = image.new_block(self.__mode, im.size)
image.convert2(block, image) # convert directly between buffers image.convert2(block, image) # convert directly between buffers
ptr = block.ptr
_pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) _pyimagingtkcall("PyImagingPhoto", self.__photo, ptr)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -225,18 +216,13 @@ class BitmapImage:
self.__mode = image.mode self.__mode = image.mode
self.__size = image.size self.__size = image.size
if _pilbitmap_check(): self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw)
# fast way (requires the pilbitmap booster patch)
image.load()
kw["data"] = f"PIL:{image.im.id}"
self.__im = image # must keep a reference
else:
# slow but safe way
kw["data"] = image.tobitmap()
self.__photo = tkinter.BitmapImage(**kw)
def __del__(self) -> None: def __del__(self) -> None:
name = self.__photo.name try:
name = self.__photo.name
except AttributeError:
return
self.__photo.name = None self.__photo.name = None
try: try:
self.__photo.tk.call("image", "delete", name) self.__photo.tk.call("image", "delete", name)
@ -273,9 +259,8 @@ class BitmapImage:
def getimage(photo: PhotoImage) -> Image.Image: def getimage(photo: PhotoImage) -> Image.Image:
"""Copies the contents of a PhotoImage to a PIL image memory.""" """Copies the contents of a PhotoImage to a PIL image memory."""
im = Image.new("RGBA", (photo.width(), photo.height())) im = Image.new("RGBA", (photo.width(), photo.height()))
block = im.im
_pyimagingtkcall("PyImagingPhotoGet", photo, block.id) _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim())
return im return im

View File

@ -1063,6 +1063,12 @@ class PngImageFile(ImageFile.ImageFile):
"RGBA", self.info["transparency"] "RGBA", self.info["transparency"]
) )
else: else:
if self.im.mode == "P" and "transparency" in self.info:
t = self.info["transparency"]
if isinstance(t, bytes):
updated.putpalettealphas(t)
elif isinstance(t, int):
updated.putpalettealpha(t)
mask = updated.convert("RGBA") mask = updated.convert("RGBA")
self._prev_im.paste(updated, self.dispose_extent, mask) self._prev_im.paste(updated, self.dispose_extent, mask)
self.im = self._prev_im self.im = self._prev_im

View File

@ -294,7 +294,7 @@ def _accept(prefix: bytes) -> bool:
def _limit_rational( def _limit_rational(
val: float | Fraction | IFDRational, max_val: int val: float | Fraction | IFDRational, max_val: int
) -> tuple[IntegralLike, IntegralLike]: ) -> tuple[IntegralLike, IntegralLike]:
inv = abs(float(val)) > 1 inv = abs(val) > 1
n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
return n_d[::-1] if inv else n_d return n_d[::-1] if inv else n_d
@ -685,22 +685,33 @@ class ImageFileDirectory_v2(_IFDv2Base):
else: else:
self.tagtype[tag] = TiffTags.UNDEFINED self.tagtype[tag] = TiffTags.UNDEFINED
if all(isinstance(v, IFDRational) for v in values): if all(isinstance(v, IFDRational) for v in values):
self.tagtype[tag] = ( for v in values:
TiffTags.RATIONAL assert isinstance(v, IFDRational)
if all(v >= 0 for v in values) if v < 0:
else TiffTags.SIGNED_RATIONAL self.tagtype[tag] = TiffTags.SIGNED_RATIONAL
) break
elif all(isinstance(v, int) for v in values):
if all(0 <= v < 2**16 for v in values):
self.tagtype[tag] = TiffTags.SHORT
elif all(-(2**15) < v < 2**15 for v in values):
self.tagtype[tag] = TiffTags.SIGNED_SHORT
else: else:
self.tagtype[tag] = ( self.tagtype[tag] = TiffTags.RATIONAL
TiffTags.LONG elif all(isinstance(v, int) for v in values):
if all(v >= 0 for v in values) short = True
else TiffTags.SIGNED_LONG signed_short = True
) long = True
for v in values:
assert isinstance(v, int)
if short and not (0 <= v < 2**16):
short = False
if signed_short and not (-(2**15) < v < 2**15):
signed_short = False
if long and v < 0:
long = False
if short:
self.tagtype[tag] = TiffTags.SHORT
elif signed_short:
self.tagtype[tag] = TiffTags.SIGNED_SHORT
elif long:
self.tagtype[tag] = TiffTags.LONG
else:
self.tagtype[tag] = TiffTags.SIGNED_LONG
elif all(isinstance(v, float) for v in values): elif all(isinstance(v, float) for v in values):
self.tagtype[tag] = TiffTags.DOUBLE self.tagtype[tag] = TiffTags.DOUBLE
elif all(isinstance(v, str) for v in values): elif all(isinstance(v, str) for v in values):
@ -718,7 +729,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)
if not is_ifd: if not is_ifd:
values = tuple(info.cvt_enum(value) for value in values) values = tuple(
info.cvt_enum(value) if isinstance(value, str) else value
for value in values
)
dest = self._tags_v1 if legacy_api else self._tags_v2 dest = self._tags_v1 if legacy_api else self._tags_v2
@ -1193,11 +1207,11 @@ class TiffImageFile(ImageFile.ImageFile):
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self._seek(frame) self._seek(frame)
# Create a new core image object on second and if self._im is not None and (
# subsequent frames in the image. Image may be self.im.size != self._tile_size or self.im.mode != self.mode
# different size/mode. ):
Image._decompression_bomb_check(self._tile_size) # The core image will no longer be used
self.im = Image.core.new(self.mode, self._tile_size) self._im = None
def _seek(self, frame: int) -> None: def _seek(self, frame: int) -> None:
self.fp = self._fp self.fp = self._fp
@ -1279,6 +1293,7 @@ class TiffImageFile(ImageFile.ImageFile):
def load_prepare(self) -> None: def load_prepare(self) -> None:
if self._im is None: if self._im is None:
Image._decompression_bomb_check(self._tile_size)
self.im = Image.core.new(self.mode, self._tile_size) self.im = Image.core.new(self.mode, self._tile_size)
ImageFile.ImageFile.load_prepare(self) ImageFile.ImageFile.load_prepare(self)
@ -1864,7 +1879,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if hasattr(fp, "fileno"): if hasattr(fp, "fileno"):
try: try:
fp.seek(0) fp.seek(0)
_fp = os.dup(fp.fileno()) _fp = fp.fileno()
except io.UnsupportedOperation: except io.UnsupportedOperation:
pass pass
@ -1937,17 +1952,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
encoder.setimage(im.im, (0, 0) + im.size) encoder.setimage(im.im, (0, 0) + im.size)
while True: while True:
# undone, change to self.decodermaxblock: errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:]
errcode, data = encoder.encode(16 * 1024)[1:]
if not _fp: if not _fp:
fp.write(data) fp.write(data)
if errcode: if errcode:
break break
if _fp:
try:
os.close(_fp)
except OSError:
pass
if errcode < 0: if errcode < 0:
msg = f"encoder error {errcode} when writing image file" msg = f"encoder error {errcode} when writing image file"
raise OSError(msg) raise OSError(msg)
@ -2121,13 +2130,24 @@ class AppendingTiffWriter(io.BytesIO):
def write(self, data: Buffer, /) -> int: def write(self, data: Buffer, /) -> int:
return self.f.write(data) return self.f.write(data)
def readShort(self) -> int: def _fmt(self, field_size: int) -> str:
(value,) = struct.unpack(self.shortFmt, self.f.read(2)) try:
return {2: "H", 4: "L", 8: "Q"}[field_size]
except KeyError:
msg = "offset is not supported"
raise RuntimeError(msg)
def _read(self, field_size: int) -> int:
(value,) = struct.unpack(
self.endian + self._fmt(field_size), self.f.read(field_size)
)
return value return value
def readShort(self) -> int:
return self._read(2)
def readLong(self) -> int: def readLong(self) -> int:
(value,) = struct.unpack(self.longFmt, self.f.read(4)) return self._read(4)
return value
@staticmethod @staticmethod
def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: def _verify_bytes_written(bytes_written: int | None, expected: int) -> None:
@ -2140,15 +2160,18 @@ class AppendingTiffWriter(io.BytesIO):
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
self._verify_bytes_written(bytes_written, 4) self._verify_bytes_written(bytes_written, 4)
def _rewriteLast(self, value: int, field_size: int) -> None:
self.f.seek(-field_size, os.SEEK_CUR)
bytes_written = self.f.write(
struct.pack(self.endian + self._fmt(field_size), value)
)
self._verify_bytes_written(bytes_written, field_size)
def rewriteLastShort(self, value: int) -> None: def rewriteLastShort(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) return self._rewriteLast(value, 2)
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
self._verify_bytes_written(bytes_written, 2)
def rewriteLastLong(self, value: int) -> None: def rewriteLastLong(self, value: int) -> None:
self.f.seek(-4, os.SEEK_CUR) return self._rewriteLast(value, 4)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
self._verify_bytes_written(bytes_written, 4)
def writeShort(self, value: int) -> None: def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
@ -2180,32 +2203,22 @@ class AppendingTiffWriter(io.BytesIO):
cur_pos = self.f.tell() cur_pos = self.f.tell()
if is_local: if is_local:
self.fixOffsets( self._fixOffsets(count, field_size)
count, isShort=(field_size == 2), isLong=(field_size == 4)
)
self.f.seek(cur_pos + 4) self.f.seek(cur_pos + 4)
else: else:
self.f.seek(offset) self.f.seek(offset)
self.fixOffsets( self._fixOffsets(count, field_size)
count, isShort=(field_size == 2), isLong=(field_size == 4)
)
self.f.seek(cur_pos) self.f.seek(cur_pos)
elif is_local: elif is_local:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(4, os.SEEK_CUR)
def fixOffsets( def _fixOffsets(self, count: int, field_size: int) -> None:
self, count: int, isShort: bool = False, isLong: bool = False
) -> None:
if not isShort and not isLong:
msg = "offset is neither short nor long"
raise RuntimeError(msg)
for i in range(count): for i in range(count):
offset = self.readShort() if isShort else self.readLong() offset = self._read(field_size)
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
if isShort and offset >= 65536: if field_size == 2 and offset >= 65536:
# offset is now too large - we must convert shorts to longs # offset is now too large - we must convert shorts to longs
if count != 1: if count != 1:
msg = "not implemented" msg = "not implemented"
@ -2217,10 +2230,19 @@ class AppendingTiffWriter(io.BytesIO):
self.f.seek(-10, os.SEEK_CUR) self.f.seek(-10, os.SEEK_CUR)
self.writeShort(TiffTags.LONG) # rewrite the type to LONG self.writeShort(TiffTags.LONG) # rewrite the type to LONG
self.f.seek(8, os.SEEK_CUR) self.f.seek(8, os.SEEK_CUR)
elif isShort:
self.rewriteLastShort(offset)
else: else:
self.rewriteLastLong(offset) self._rewriteLast(offset, field_size)
def fixOffsets(
self, count: int, isShort: bool = False, isLong: bool = False
) -> None:
if isShort:
field_size = 2
elif isLong:
field_size = 4
else:
field_size = 0
return self._fixOffsets(count, field_size)
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:

View File

@ -60,7 +60,6 @@ class WebPImageFile(ImageFile.ImageFile):
self.is_animated = self.n_frames > 1 self.is_animated = self.n_frames > 1
self._mode = "RGB" if mode == "RGBX" else mode self._mode = "RGB" if mode == "RGBX" else mode
self.rawmode = mode self.rawmode = mode
self.tile = []
# Attempt to read ICC / EXIF / XMP chunks from file # Attempt to read ICC / EXIF / XMP chunks from file
icc_profile = self._decoder.get_chunk("ICCP") icc_profile = self._decoder.get_chunk("ICCP")

View File

@ -128,7 +128,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
size = x1 - x0, y1 - y0 size = x1 - x0, y1 - y0
# calculate dots per inch from bbox and frame # calculate dots per inch from bbox and frame
xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0])
ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1])
self.info["wmf_bbox"] = x0, y0, x1, y1 self.info["wmf_bbox"] = x0, y0, x1, y1

View File

@ -2,6 +2,8 @@ import datetime
import sys import sys
from typing import Literal, SupportsFloat, TypedDict from typing import Literal, SupportsFloat, TypedDict
from ._typing import CapsuleType
littlecms_version: str | None littlecms_version: str | None
_Tuple3f = tuple[float, float, float] _Tuple3f = tuple[float, float, float]
@ -108,7 +110,7 @@ class CmsProfile:
def is_intent_supported(self, intent: int, direction: int, /) -> int: ... def is_intent_supported(self, intent: int, direction: int, /) -> int: ...
class CmsTransform: class CmsTransform:
def apply(self, id_in: int, id_out: int) -> int: ... def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ...
def profile_open(profile: str, /) -> CmsProfile: ... def profile_open(profile: str, /) -> CmsProfile: ...
def profile_frombytes(profile: bytes, /) -> CmsProfile: ... def profile_frombytes(profile: bytes, /) -> CmsProfile: ...

View File

@ -15,6 +15,11 @@ if TYPE_CHECKING:
except (ImportError, AttributeError): except (ImportError, AttributeError):
pass pass
if sys.version_info >= (3, 13):
from types import CapsuleType
else:
CapsuleType = object
if sys.version_info >= (3, 12): if sys.version_info >= (3, 12):
from collections.abc import Buffer from collections.abc import Buffer
else: else:

View File

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

View File

@ -146,10 +146,11 @@ def check_feature(feature: str) -> bool | None:
module, flag, ver = features[feature] module, flag, ver = features[feature]
if isinstance(flag, bool):
deprecate(f'check_feature("{feature}")', 12)
try: try:
imported_module = __import__(module, fromlist=["PIL"]) imported_module = __import__(module, fromlist=["PIL"])
if isinstance(flag, bool): if isinstance(flag, bool):
deprecate(f'check_feature("{feature}")', 12)
return flag return flag
return getattr(imported_module, flag) return getattr(imported_module, flag)
except ModuleNotFoundError: except ModuleNotFoundError:

View File

@ -56,19 +56,34 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK;
static Imaging static Imaging
ImagingFind(const char *name) { ImagingFind(const char *name) {
Py_ssize_t id; PyObject *capsule;
int direct_pointer = 0;
const char *expected = "capsule object \"" IMAGING_MAGIC "\" at 0x";
/* FIXME: use CObject instead? */ if (name[0] == '<') {
#if defined(_WIN64) name++;
id = _atoi64(name); } else {
#else // Special case for PyPy, where the string representation of a Capsule
id = atol(name); // refers directly to the pointer itself, not to the PyCapsule object.
#endif direct_pointer = 1;
if (!id) { }
if (strncmp(name, expected, strlen(expected))) {
return NULL; return NULL;
} }
return (Imaging)id; capsule = (PyObject *)strtoull(name + strlen(expected), NULL, 16);
if (direct_pointer) {
return (Imaging)capsule;
}
if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
return (Imaging)PyCapsule_GetPointer(capsule, IMAGING_MAGIC);
} }
static int static int

View File

@ -3670,15 +3670,12 @@ static struct PyMethodDef methods[] = {
/* Unsharpmask extension */ /* Unsharpmask extension */
{"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS},
{"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS},
{"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, {"box_blur", (PyCFunction)_box_blur, METH_VARARGS},
/* Special effects */ /* Special effects */
{"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS},
/* Misc. */ /* Misc. */
{"new_block", (PyCFunction)_new_block, METH_VARARGS},
{"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS},
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
@ -3703,16 +3700,40 @@ _getattr_bands(ImagingObject *self, void *closure) {
static PyObject * static PyObject *
_getattr_id(ImagingObject *self, void *closure) { _getattr_id(ImagingObject *self, void *closure) {
if (PyErr_WarnEx(
PyExc_DeprecationWarning,
"id property is deprecated and will be removed in Pillow 12 (2025-10-15)",
1
) < 0) {
return NULL;
}
return PyLong_FromSsize_t((Py_ssize_t)self->image); return PyLong_FromSsize_t((Py_ssize_t)self->image);
} }
static void
_ptr_destructor(PyObject *capsule) {
PyObject *self = (PyObject *)PyCapsule_GetContext(capsule);
Py_DECREF(self);
}
static PyObject * static PyObject *
_getattr_ptr(ImagingObject *self, void *closure) { _getattr_ptr(ImagingObject *self, void *closure) {
return PyCapsule_New(self->image, IMAGING_MAGIC, NULL); PyObject *capsule = PyCapsule_New(self->image, IMAGING_MAGIC, _ptr_destructor);
Py_INCREF(self);
PyCapsule_SetContext(capsule, self);
return capsule;
} }
static PyObject * static PyObject *
_getattr_unsafe_ptrs(ImagingObject *self, void *closure) { _getattr_unsafe_ptrs(ImagingObject *self, void *closure) {
if (PyErr_WarnEx(
PyExc_DeprecationWarning,
"unsafe_ptrs property is deprecated and will be removed in Pillow 12 "
"(2025-10-15)",
1
) < 0) {
return NULL;
}
return Py_BuildValue( return Py_BuildValue(
"(sn)(sn)(sn)", "(sn)(sn)(sn)",
"image8", "image8",
@ -4194,6 +4215,7 @@ static PyMethodDef functions[] = {
{"blend", (PyCFunction)_blend, METH_VARARGS}, {"blend", (PyCFunction)_blend, METH_VARARGS},
{"fill", (PyCFunction)_fill, METH_VARARGS}, {"fill", (PyCFunction)_fill, METH_VARARGS},
{"new", (PyCFunction)_new, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS},
{"new_block", (PyCFunction)_new_block, METH_VARARGS},
{"merge", (PyCFunction)_merge, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS},
/* Functions */ /* Functions */

View File

@ -531,23 +531,24 @@ buildProofTransform(PyObject *self, PyObject *args) {
static PyObject * static PyObject *
cms_transform_apply(CmsTransformObject *self, PyObject *args) { cms_transform_apply(CmsTransformObject *self, PyObject *args) {
Py_ssize_t idIn; PyObject *i0, *i1;
Py_ssize_t idOut;
Imaging im; Imaging im;
Imaging imOut; Imaging imOut;
int result; if (!PyArg_ParseTuple(args, "OO:apply", &i0, &i1)) {
if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) {
return NULL; return NULL;
} }
im = (Imaging)idIn; if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) ||
imOut = (Imaging)idOut; !PyCapsule_IsValid(i1, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
result = pyCMSdoTransform(im, imOut, self->transform); im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
return Py_BuildValue("i", result); return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform));
} }
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */

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