Merge branch 'main' into libavif-plugin

This commit is contained in:
Andrew Murray 2025-01-18 07:52:07 +11:00 committed by GitHub
commit ce6bf21f15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 388 additions and 382 deletions

View File

@ -1,99 +0,0 @@
skip_commits:
files:
- ".github/**/*"
- ".gitmodules"
- "docs/**/*"
- "wheels/**/*"
version: '{build}'
clone_folder: c:\pillow
init:
- ECHO %PYTHON%
#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
# Uncomment previous line to get RDP access during the build.
environment:
COVERAGE_CORE: sysmon
EXECUTABLE: python.exe
TEST_OPTIONS:
DEPLOY: YES
matrix:
- PYTHON: C:/Python313
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python39-x64
ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
install:
- '%PYTHON%\%EXECUTABLE% --version'
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-test-images.zip -oc:\
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.4.0
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
c:\pillow\winbuild\build\build_dep_all.cmd
$host.SetShouldExit(0)
- path C:\pillow\winbuild\build\bin;%PATH%
build_script:
- cd c:\pillow
- winbuild\build\build_env.cmd
- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .'
- '%PYTHON%\%EXECUTABLE% selftest.py --installed'
test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- path %PYTHON%;%PATH%
- .ci\test.cmd
after_test:
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe
- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor
matrix:
fast_finish: true
cache:
- '%LOCALAPPDATA%\pip\Cache'
artifacts:
- path: pillow\*.egg
name: egg
- path: pillow\*.whl
name: wheel
before_deploy:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .'
- ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
deploy:
provider: S3
region: us-west-2
access_key_id: AKIAIRAXC62ZNTVQJMOQ
secret_access_key:
secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi
bucket: pillow-nightly
folder: win/$(APPVEYOR_BUILD_NUMBER)/
artifact: /.*egg|wheel/
on:
APPVEYOR_REPO_NAME: python-pillow/Pillow
branch: main
deploy: YES
# Uncomment the following lines to get RDP access after the build/test and block for
# up to the timeout limit (~1hr)
#
#on_finish:
#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))

View File

@ -3,8 +3,5 @@
set -e set -e
python3 -m coverage erase python3 -m coverage erase
if [ $(uname) == "Darwin" ]; then
export CPPFLAGS="-I/usr/local/miniconda/include";
fi
make clean make clean
make install-coverage make install-coverage

View File

@ -9,7 +9,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Fork the Pillow repository. - Fork the Pillow repository.
- Create a branch from `main`. - Create a branch from `main`.
- Develop bug fixes, features, tests, etc. - Develop bug fixes, features, tests, etc.
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. - Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
- Create a pull request to pull the changes from your branch to the Pillow `main`. - Create a pull request to pull the changes from your branch to the Pillow `main`.
### Guidelines ### Guidelines
@ -17,7 +17,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Separate code commits from reformatting commits. - Separate code commits from reformatting commits.
- Provide tests for any newly added code. - Provide tests for any newly added code.
- Follow PEP 8. - Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests.
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. - Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
## Reporting Issues ## Reporting Issues

1
.github/mergify.yml vendored
View File

@ -9,7 +9,6 @@ pull_request_rules:
- status-success=Windows Test Successful - status-success=Windows Test Successful
- status-success=MinGW - status-success=MinGW
- status-success=Cygwin Test Successful - status-success=Cygwin Test Successful
- status-success=continuous-integration/appveyor/pr
actions: actions:
merge: merge:
method: merge method: merge

View File

@ -67,9 +67,9 @@ jobs:
mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python-numpy \
mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python-olefile \
mingw-w64-x86_64-python3-pip \ mingw-w64-x86_64-python-pip \
mingw-w64-x86_64-python-pytest \ mingw-w64-x86_64-python-pytest \
mingw-w64-x86_64-python-pytest-cov \ mingw-w64-x86_64-python-pytest-cov \
mingw-w64-x86_64-python-pytest-timeout \ mingw-w64-x86_64-python-pytest-timeout \

View File

@ -31,15 +31,20 @@ env:
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
architecture: ["x64"]
os: ["windows-latest"]
include:
# Test the oldest Python on 32-bit
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
timeout-minutes: 45 timeout-minutes: 45
name: Python ${{ matrix.python-version }} name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
@ -67,6 +72,7 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
architecture: ${{ matrix.architecture }}
cache: pip cache: pip
cache-dependency-path: ".github/workflows/test-windows.yml" cache-dependency-path: ".github/workflows/test-windows.yml"
@ -78,7 +84,7 @@ jobs:
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
- name: Install CPython dependencies - name: Install CPython dependencies
if: "!contains(matrix.python-version, 'pypy')" if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
run: | run: |
python3 -m pip install PyQt6 python3 -m pip install PyQt6

View File

@ -42,6 +42,7 @@ jobs:
] ]
python-version: [ python-version: [
"pypy3.10", "pypy3.10",
"3.14",
"3.13t", "3.13t",
"3.13", "3.13",
"3.12", "3.12",

View File

@ -39,7 +39,7 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds # Package versions for fresh source builds
FREETYPE_VERSION=2.13.3 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.1.0 HARFBUZZ_VERSION=10.1.0
LIBPNG_VERSION=1.6.44 LIBPNG_VERSION=1.6.45
JPEGTURBO_VERSION=3.1.0 JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3 OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3 XZ_VERSION=5.6.3

View File

@ -11,6 +11,9 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
$env:path += ";$pillow\winbuild\build\bin\" $env:path += ";$pillow\winbuild\build\bin\"
& "$venv\Scripts\activate.ps1" & "$venv\Scripts\activate.ps1"
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
& python -m pip install numpy
}
cd $pillow cd $pillow
& python -VV & python -VV
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }

View File

@ -13,6 +13,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "pyproject.toml"
- "setup.py" - "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
@ -23,6 +24,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "pyproject.toml"
- "setup.py" - "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
@ -40,62 +42,7 @@ env:
FORCE_COLOR: 1 FORCE_COLOR: 1
jobs: jobs:
build-1-QEMU-emulated-wheels: build-native-wheels:
if: github.event_name != 'schedule'
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- pp310
- cp3{9,10,11}
- cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
- musllinux
exclude:
- { python-version: pp310, spec: musllinux }
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: true
- uses: actions/setup-python@v5
with:
python-version: "3.x"
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Install cibuildwheel
run: |
python3 -m pip install -r .ci/requirements-cibw.txt
- name: Build wheels
run: |
python3 -m cibuildwheel --output-dir wheelhouse
env:
# Build only the currently selected Linux architecture (so we can
# parallelise for speed).
CIBW_ARCHS: "aarch64"
# Likewise, select only one Python version per job to speed this up.
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
CIBW_ENABLE: cpython-prerelease
# Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
- uses: actions/upload-artifact@v4
with:
name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }}
path: ./wheelhouse/*.whl
build-2-native-wheels:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: ${{ matrix.name }} name: ${{ matrix.name }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -130,6 +77,14 @@ jobs:
cibw_arch: x86_64 cibw_arch: x86_64
build: "*manylinux*" build: "*manylinux*"
manylinux: "manylinux_2_28" manylinux: "manylinux_2_28"
- name: "manylinux2014 and musllinux aarch64"
os: ubuntu-24.04-arm
cibw_arch: aarch64
- name: "manylinux_2_28 aarch64"
os: ubuntu-24.04-arm
cibw_arch: aarch64
build: "*manylinux*"
manylinux: "manylinux_2_28"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -150,7 +105,9 @@ jobs:
env: env:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }} CIBW_BUILD: ${{ matrix.build }}
CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
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_SKIP: pp39-* CIBW_SKIP: pp39-*
@ -227,7 +184,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw" CIBW_CACHE_PATH: "C:\\cibw"
CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_SKIP: pp39-* 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
@ -273,7 +230,7 @@ jobs:
scientific-python-nightly-wheels-publish: scientific-python-nightly-wheels-publish:
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [build-2-native-wheels, windows] needs: [build-native-wheels, windows]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels name: Upload wheels to scientific-python-nightly-wheels
steps: steps:
@ -290,7 +247,7 @@ jobs:
pypi-publish: pypi-publish:
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] needs: [build-native-wheels, windows, sdist]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Upload release to PyPI name: Upload release to PyPI
environment: environment:

View File

@ -1,5 +1,8 @@
version: 2 version: 2
sphinx:
configuration: docs/conf.py
formats: [pdf] formats: [pdf]
build: build:

View File

@ -20,7 +20,6 @@ graft docs
graft _custom_build graft _custom_build
# build/src control detritus # build/src control detritus
exclude .appveyor.yml
exclude .clang-format exclude .clang-format
exclude .coveragerc exclude .coveragerc
exclude .editorconfig exclude .editorconfig

View File

@ -42,9 +42,6 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
alt="GitHub Actions build status (Test Docker)" alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
alt="AppVeyor CI build status (Windows)"
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
alt="GitHub Actions build status (Wheels)" alt="GitHub Actions build status (Wheels)"
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>

View File

@ -9,7 +9,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch. * [ ] Develop and prepare release in `main` branch.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch.
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. * [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
@ -38,7 +38,7 @@ Released as needed for security, installation or critical bug fixes.
git checkout -t remotes/origin/5.2.x git checkout -t remotes/origin/5.2.x
``` ```
* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Run pre-release check via `make release-test`. * [ ] Run pre-release check via `make release-test`.
* [ ] Create tag for release e.g.: * [ ] Create tag for release e.g.:

View File

@ -140,18 +140,11 @@ def assert_image_similar_tofile(
filename: str, filename: str,
epsilon: float, epsilon: float,
msg: str | None = None, msg: str | None = None,
mode: str | None = None,
) -> None: ) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg) assert_image_similar(a, img, epsilon, msg)
def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) == len(items), msg
def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) != len(items), msg assert items.count(items[0]) != len(items), msg
@ -327,16 +320,7 @@ def magick_command() -> list[str] | None:
return None return None
def on_appveyor() -> bool:
return "APPVEYOR" in os.environ
def on_github_actions() -> bool:
return "GITHUB_ACTIONS" in os.environ
def on_ci() -> bool: def on_ci() -> bool:
# GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ return "CI" in os.environ

View File

@ -7,7 +7,7 @@ import fuzzers
import packaging import packaging
import pytest import pytest
from PIL import Image, UnidentifiedImageError, features from PIL import Image, features
from Tests.helper import skip_unless_feature from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"): if sys.platform.startswith("win32"):
@ -32,21 +32,17 @@ def test_fuzz_images(path: str) -> None:
fuzzers.fuzz_image(f.read()) fuzzers.fuzz_image(f.read())
assert True assert True
except ( except (
# Known exceptions from Pillow
OSError, OSError,
SyntaxError, SyntaxError,
MemoryError, MemoryError,
ValueError, ValueError,
NotImplementedError, NotImplementedError,
OverflowError, OverflowError,
): # Known Image.* exceptions
# Known exceptions that are through from Pillow
assert True
except (
Image.DecompressionBombError, Image.DecompressionBombError,
Image.DecompressionBombWarning, Image.DecompressionBombWarning,
UnidentifiedImageError,
): ):
# Known Image.* exceptions
assert True assert True
finally: finally:
fuzzers.disable_decompressionbomb_error() fuzzers.disable_decompressionbomb_error()

View File

@ -307,13 +307,8 @@ def test_apng_syntax_errors() -> None:
im.load() im.load()
# we can handle this case gracefully # we can handle this case gracefully
exception = None
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
try: im.seek(im.n_frames - 1)
im.seek(im.n_frames - 1)
except Exception as e:
exception = e
assert exception is None
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
@ -405,13 +400,8 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
append_images=frames, append_images=frames,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
exception = None im.seek(im.n_frames - 1)
try: im.load()
im.seek(im.n_frames - 1)
im.load()
except Exception as e:
exception = e
assert exception is None
def test_apng_save_duration_loop(tmp_path: Path) -> None: def test_apng_save_duration_loop(tmp_path: Path) -> None:

View File

@ -253,8 +253,7 @@ def test_truncated_mask() -> None:
try: try:
with Image.open(io.BytesIO(data)) as im: with Image.open(io.BytesIO(data)) as im:
with Image.open("Tests/images/hopper_mask.png") as expected: assert im.mode == "1"
assert im.mode == "1"
# 32 bpp # 32 bpp
output = io.BytesIO() output = io.BytesIO()

View File

@ -58,10 +58,7 @@ def test_getiptcinfo_fotostation() -> None:
# Assert # Assert
assert iptc is not None assert iptc is not None
for tag in iptc.keys(): assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found"
if tag[0] == 240:
return
pytest.fail("FotoStation tag not found")
def test_getiptcinfo_zero_padding() -> None: def test_getiptcinfo_zero_padding() -> None:

View File

@ -181,7 +181,7 @@ class TestFileJpeg:
assert test(100, 200) == (100, 200) assert test(100, 200) == (100, 200)
assert test(0) is None # square pixels assert test(0) is None # square pixels
def test_dpi_jfif_cm(self): def test_dpi_jfif_cm(self) -> None:
with Image.open("Tests/images/jfif_unit_cm.jpg") as im: with Image.open("Tests/images/jfif_unit_cm.jpg") as im:
assert im.info["dpi"] == (2.54, 5.08) assert im.info["dpi"] == (2.54, 5.08)
@ -281,7 +281,10 @@ class TestFileJpeg:
assert not im2.info.get("progressive") assert not im2.info.get("progressive")
assert im3.info.get("progressive") assert im3.info.get("progressive")
assert_image_equal(im1, im3) if features.check_feature("mozjpeg"):
assert_image_similar(im1, im3, 9.39)
else:
assert_image_equal(im1, im3)
assert im1_bytes >= im3_bytes assert im1_bytes >= im3_bytes
def test_progressive_large_buffer(self, tmp_path: Path) -> None: def test_progressive_large_buffer(self, tmp_path: Path) -> None:
@ -423,8 +426,12 @@ class TestFileJpeg:
im2 = self.roundtrip(hopper(), progressive=1) im2 = self.roundtrip(hopper(), progressive=1)
im3 = self.roundtrip(hopper(), progression=1) # compatibility im3 = self.roundtrip(hopper(), progression=1) # compatibility
assert_image_equal(im1, im2) if features.check_feature("mozjpeg"):
assert_image_equal(im1, im3) assert_image_similar(im1, im2, 9.39)
assert_image_similar(im1, im3, 9.39)
else:
assert_image_equal(im1, im2)
assert_image_equal(im1, im3)
assert im2.info.get("progressive") assert im2.info.get("progressive")
assert im2.info.get("progression") assert im2.info.get("progression")
assert im3.info.get("progressive") assert im3.info.get("progressive")
@ -1030,7 +1037,7 @@ class TestFileJpeg:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.tile = [ im.tile = [
("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
] ]
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
im.load() im.load()

View File

@ -492,8 +492,7 @@ def test_plt_marker(card: ImageFile.ImageFile) -> None:
out.seek(0) out.seek(0)
while True: while True:
marker = out.read(2) marker = out.read(2)
if not marker: assert marker, "End of stream without PLT"
pytest.fail("End of stream without PLT")
jp2_boxid = _binary.i16be(marker) jp2_boxid = _binary.i16be(marker)
if jp2_boxid == 0xFF4F: if jp2_boxid == 0xFF4F:

View File

@ -36,11 +36,7 @@ class LibTiffTestCase:
im.load() im.load()
im.getdata() im.getdata()
try: assert im._compression == "group4"
assert im._compression == "group4"
except AttributeError:
print("No _compression")
print(dir(im))
# can we write it back out, in a different form. # can we write it back out, in a different form.
out = str(tmp_path / "temp.png") out = str(tmp_path / "temp.png")
@ -1146,7 +1142,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.load() im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY # Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "-9" assert str(e.value) == "decoder error -9"
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:

View File

@ -618,7 +618,7 @@ class TestFilePng:
with Image.open("Tests/images/truncated_image.png") as im: with Image.open("Tests/images/truncated_image.png") as im:
# The file is truncated # The file is truncated
with pytest.raises(OSError): with pytest.raises(OSError):
im.text() im.text
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
assert isinstance(im.text, dict) assert isinstance(im.text, dict)
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False

View File

@ -7,7 +7,7 @@ from pathlib import Path
import pytest import pytest
from PIL import Image, ImageSequence, SpiderImagePlugin from PIL import Image, SpiderImagePlugin
from .helper import assert_image_equal, hopper, is_pypy from .helper import assert_image_equal, hopper, is_pypy
@ -153,8 +153,8 @@ def test_nonstack_file() -> None:
def test_nonstack_dos() -> None: def test_nonstack_dos() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
for i, frame in enumerate(ImageSequence.Iterator(im)): with pytest.raises(EOFError):
assert i <= 1, "Non-stack DOS file test failed" im.seek(0)
# for issue #4093 # for issue #4093

View File

@ -117,10 +117,16 @@ class TestFileTiff:
def test_bigtiff_save(self, tmp_path: Path) -> None: def test_bigtiff_save(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
hopper().save(outfile, big_tiff=True) im = hopper()
im.save(outfile, big_tiff=True)
with Image.open(outfile) as im: with Image.open(outfile) as reloaded:
assert im.tag_v2._bigtiff is True assert reloaded.tag_v2._bigtiff is True
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
with Image.open(outfile) as reloaded:
assert reloaded.tag_v2._bigtiff is True
def test_seek_too_large(self) -> None: def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"): with pytest.raises(ValueError, match="Unable to seek to frame"):
@ -740,7 +746,7 @@ class TestFileTiff:
assert reread.n_frames == 3 assert reread.n_frames == 3
def test_fixoffsets(self) -> None: def test_fixoffsets(self) -> None:
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a: with TiffImagePlugin.AppendingTiffWriter(b) as a:
b.seek(0) b.seek(0)
a.fixOffsets(1, isShort=True) a.fixOffsets(1, isShort=True)
@ -753,6 +759,37 @@ class TestFileTiff:
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
a.fixOffsets(1) a.fixOffsets(1)
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**16
b.seek(0)
a.fixOffsets(1, isShort=True)
b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**32
b.seek(0)
a.fixOffsets(1, isShort=True)
b.seek(0)
a.fixOffsets(1, isLong=True)
def test_appending_tiff_writer_writelong(self) -> None:
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.writeLong(2**32 - 1)
assert b.getvalue() == data + b"\xff\xff\xff\xff"
def test_appending_tiff_writer_rewritelastshorttolong(self) -> None:
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.rewriteLastShortToLong(2**32 - 1)
assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff"
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

@ -189,8 +189,6 @@ class TestImage:
if ext == ".jp2" and not features.check_codec("jpg_2000"): if ext == ".jp2" and not features.check_codec("jpg_2000"):
pytest.skip("jpg_2000 not available") pytest.skip("jpg_2000 not available")
temp_file = str(tmp_path / ("temp." + ext)) temp_file = str(tmp_path / ("temp." + ext))
if os.path.exists(temp_file):
os.remove(temp_file)
im.save(Path(temp_file)) im.save(Path(temp_file))
def test_fp_name(self, tmp_path: Path) -> None: def test_fp_name(self, tmp_path: Path) -> None:
@ -667,7 +665,7 @@ class TestImage:
# Test illegal image mode # Test illegal image mode
with hopper() as im: with hopper() as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.remap_palette(None) im.remap_palette([])
def test_remap_palette_transparency(self) -> None: def test_remap_palette_transparency(self) -> None:
im = Image.new("P", (1, 2), (0, 0, 0)) im = Image.new("P", (1, 2), (0, 0, 0))
@ -770,7 +768,7 @@ class TestImage:
assert dict(exif) assert dict(exif)
# Test that exif data is cleared after another load # Test that exif data is cleared after another load
exif.load(None) exif.load(b"")
assert not dict(exif) assert not dict(exif)
# Test loading just the EXIF header # Test loading just the EXIF header
@ -991,6 +989,11 @@ class TestImage:
else: else:
assert im.getxmp() == {"xmpmeta": None} assert im.getxmp() == {"xmpmeta": None}
def test_get_child_images(self) -> None:
im = Image.new("RGB", (1, 1))
with pytest.warns(DeprecationWarning):
assert im.get_child_images() == []
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None: def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size) im = Image.new("RGB", size)

View File

@ -271,13 +271,25 @@ class TestImagePutPixelError:
class TestEmbeddable: class TestEmbeddable:
@pytest.mark.xfail(reason="failing test") @pytest.mark.xfail(not (sys.version_info >= (3, 13)), reason="failing test")
@pytest.mark.skipif(not is_win32(), reason="requires Windows") @pytest.mark.skipif(not is_win32(), reason="requires Windows")
def test_embeddable(self) -> None: def test_embeddable(self) -> None:
import ctypes import ctypes
from setuptools.command import build_ext from setuptools.command import build_ext
compiler = getattr(build_ext, "new_compiler")()
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
"INCLUDEPY"
).replace("include", "libs")
compiler.add_library_dir(libdir)
try:
compiler.initialize()
except Exception:
pytest.skip("Compiler could not be initialized")
with open("embed_pil.c", "w", encoding="utf-8") as fh: with open("embed_pil.c", "w", encoding="utf-8") as fh:
home = sys.prefix.replace("\\", "\\\\") home = sys.prefix.replace("\\", "\\\\")
fh.write( fh.write(
@ -305,13 +317,6 @@ int main(int argc, char* argv[])
""" """
) )
compiler = getattr(build_ext, "new_compiler")()
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
"INCLUDEPY"
).replace("include", "libs")
compiler.add_library_dir(libdir)
objects = compiler.compile(["embed_pil.c"]) objects = compiler.compile(["embed_pil.c"])
compiler.link_executable(objects, "embed_pil") compiler.link_executable(objects, "embed_pil")

View File

@ -309,7 +309,7 @@ class TestImageResize:
# Test unknown resampling filter # Test unknown resampling filter
with hopper() as im: with hopper() as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.resize((10, 10), "unknown") im.resize((10, 10), -1)
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_transposed(self) -> None: def test_transposed(self) -> None:

View File

@ -6,12 +6,11 @@ Goals
The fork author's goal is to foster and support active development of PIL through: The fork author's goal is to foster and support active development of PIL through:
- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ - Continuous integration testing via `GitHub Actions`_
- Publicized development activity on `GitHub`_ - Publicized development activity on `GitHub`_
- Regular releases to the `Python Package Index`_ - Regular releases to the `Python Package Index`_
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
.. _GitHub: https://github.com/python-pillow/Pillow .. _GitHub: https://github.com/python-pillow/Pillow
.. _Python Package Index: https://pypi.org/project/pillow/ .. _Python Package Index: https://pypi.org/project/pillow/

View File

@ -183,6 +183,16 @@ ExifTags.IFD.Makernote
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use ``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``. ``ExifTags.IFD.MakerNote``.
Image.Image.get_child_images()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.2.0
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.
Removed features Removed features
---------------- ----------------

View File

@ -33,10 +33,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml :target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml
:alt: GitHub Actions build status (Test Cygwin) :alt: GitHub Actions build status (Test Cygwin)
.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build
:target: https://ci.appveyor.com/project/python-pillow/Pillow
:alt: AppVeyor CI build status (Windows)
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg .. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
:alt: GitHub Actions build status (Wheels) :alt: GitHub Actions build status (Wheels)

View File

@ -50,12 +50,10 @@ These platforms are built and tested for every change.
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, | | Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x | | | | s390x |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2019 | 3.9 | x86-64 | | Windows Server 2019 | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 | | Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | 3.12, 3.13, PyPy3 | | | | PyPy3 | |
| +----------------------------+---------------------+
| | 3.13 | x86 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 | | | 3.12 (MinGW) | x86-64 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+

View File

@ -0,0 +1,58 @@
11.2.0
------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
Image.Image.get_child_images()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.2.0
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
TODO
^^^^
TODO
Other Changes
=============
TODO
^^^^
TODO

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
11.2.0
11.1.0 11.1.0
11.0.0 11.0.0
10.4.0 10.4.0

View File

@ -374,14 +374,10 @@ class BLP1Decoder(_BLPBaseDecoder):
image = JpegImageFile(BytesIO(data)) image = JpegImageFile(BytesIO(data))
Image._decompression_bomb_check(image.size) Image._decompression_bomb_check(image.size)
if image.mode == "CMYK": if image.mode == "CMYK":
decoder_name, extents, offset, args = image.tile[0] args = image.tile[0].args
assert isinstance(args, tuple) assert isinstance(args, tuple)
image.tile = [ image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK")) self.set_as_raw(image.convert("RGB").tobytes(), "BGR")
]
r, g, b = image.convert("RGB").split()
reversed_image = Image.merge("RGB", (b, g, r))
self.set_as_raw(reversed_image.tobytes())
class BLP2Decoder(_BLPBaseDecoder): class BLP2Decoder(_BLPBaseDecoder):

View File

@ -1556,50 +1556,10 @@ class Image:
self.getexif() self.getexif()
def get_child_images(self) -> list[ImageFile.ImageFile]: def get_child_images(self) -> list[ImageFile.ImageFile]:
child_images = [] from . import ImageFile
exif = self.getexif()
ifds = []
if ExifTags.Base.SubIFDs in exif:
subifd_offsets = exif[ExifTags.Base.SubIFDs]
if subifd_offsets:
if not isinstance(subifd_offsets, tuple):
subifd_offsets = (subifd_offsets,)
for subifd_offset in subifd_offsets:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
assert exif._info is not None
ifds.append((ifd1, exif._info.next))
offset = None deprecate("Image.Image.get_child_images", 13)
for ifd, ifd_offset in ifds: return ImageFile.ImageFile.get_child_images(self) # type: ignore[arg-type]
current_offset = self.fp.tell()
if offset is None:
offset = current_offset
fp = self.fp
if ifd is not None:
thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
if thumbnail_offset is not None:
thumbnail_offset += getattr(self, "_exif_offset", 0)
self.fp.seek(thumbnail_offset)
data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount))
fp = io.BytesIO(data)
with open(fp) as im:
from . import TiffImagePlugin
if thumbnail_offset is None and isinstance(
im, TiffImagePlugin.TiffImageFile
):
im._frame_pos = [ifd_offset]
im._seek(0)
im.load()
child_images.append(im)
if offset is not None:
self.fp.seek(offset)
return child_images
def getim(self) -> CapsuleType: def getim(self) -> CapsuleType:
""" """

View File

@ -36,7 +36,7 @@ import struct
import sys import sys
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
from . import Image from . import ExifTags, Image
from ._deprecate import deprecate from ._deprecate import deprecate
from ._util import is_path from ._util import is_path
@ -163,6 +163,57 @@ class ImageFile(Image.Image):
def _open(self) -> None: def _open(self) -> None:
pass pass
def get_child_images(self) -> list[ImageFile]:
child_images = []
exif = self.getexif()
ifds = []
if ExifTags.Base.SubIFDs in exif:
subifd_offsets = exif[ExifTags.Base.SubIFDs]
if subifd_offsets:
if not isinstance(subifd_offsets, tuple):
subifd_offsets = (subifd_offsets,)
for subifd_offset in subifd_offsets:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
assert exif._info is not None
ifds.append((ifd1, exif._info.next))
offset = None
for ifd, ifd_offset in ifds:
assert self.fp is not None
current_offset = self.fp.tell()
if offset is None:
offset = current_offset
fp = self.fp
if ifd is not None:
thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
if thumbnail_offset is not None:
thumbnail_offset += getattr(self, "_exif_offset", 0)
self.fp.seek(thumbnail_offset)
length = ifd.get(ExifTags.Base.JpegIFByteCount)
assert isinstance(length, int)
data = self.fp.read(length)
fp = io.BytesIO(data)
with Image.open(fp) as im:
from . import TiffImagePlugin
if thumbnail_offset is None and isinstance(
im, TiffImagePlugin.TiffImageFile
):
im._frame_pos = [ifd_offset]
im._seek(0)
im.load()
child_images.append(im)
if offset is not None:
assert self.fp is not None
self.fp.seek(offset)
return child_images
def get_format_mimetype(self) -> str | None: def get_format_mimetype(self) -> str | None:
if self.custom_mimetype: if self.custom_mimetype:
return self.custom_mimetype return self.custom_mimetype

View File

@ -267,7 +267,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]:
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode[0] != "F": if im.mode != "F":
im = im.convert("F") im = im.convert("F")
hdr = makeSpiderHeader(im) hdr = makeSpiderHeader(im)

View File

@ -949,7 +949,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
warnings.warn(str(msg)) warnings.warn(str(msg))
return return
def _get_ifh(self): def _get_ifh(self) -> bytes:
ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
if self._bigtiff: if self._bigtiff:
ifh += self._pack("HH", 8, 0) ifh += self._pack("HH", 8, 0)
@ -962,13 +962,16 @@ class ImageFileDirectory_v2(_IFDv2Base):
result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
entries: list[tuple[int, int, int, bytes, bytes]] = [] entries: list[tuple[int, int, int, bytes, bytes]] = []
offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4
fmt = "Q" if self._bigtiff else "L"
fmt_size = 8 if self._bigtiff else 4
offset += (
len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
)
stripoffsets = None stripoffsets = None
# pass 1: convert tags to binary format # pass 1: convert tags to binary format
# always write tags in ascending order # always write tags in ascending order
fmt = "Q" if self._bigtiff else "L"
fmt_size = 8 if self._bigtiff else 4
for tag, value in sorted(self._tags_v2.items()): for tag, value in sorted(self._tags_v2.items()):
if tag == STRIPOFFSETS: if tag == STRIPOFFSETS:
stripoffsets = len(entries) stripoffsets = len(entries)
@ -1024,7 +1027,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
) )
# -- overwrite here for multi-page -- # -- overwrite here for multi-page --
result += b"\0\0\0\0" # end of entries result += self._pack(fmt, 0) # end of entries
# pass 3: write auxiliary data to file # pass 3: write auxiliary data to file
for tag, typ, count, value, data in entries: for tag, typ, count, value, data in entries:
@ -1406,7 +1409,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.fp = None # might be shared self.fp = None # might be shared
if err < 0: if err < 0:
raise OSError(err) msg = f"decoder error {err}"
raise OSError(msg)
return Image.Image.load(self) return Image.Image.load(self)
@ -2043,20 +2047,21 @@ class AppendingTiffWriter(io.BytesIO):
self.offsetOfNewPage = 0 self.offsetOfNewPage = 0
self.IIMM = iimm = self.f.read(4) self.IIMM = iimm = self.f.read(4)
self._bigtiff = b"\x2B" in iimm
if not iimm: if not iimm:
# empty file - first page # empty file - first page
self.isFirst = True self.isFirst = True
return return
self.isFirst = False self.isFirst = False
if iimm == b"II\x2a\x00": if iimm not in PREFIXES:
self.setEndian("<")
elif iimm == b"MM\x00\x2a":
self.setEndian(">")
else:
msg = "Invalid TIFF file header" msg = "Invalid TIFF file header"
raise RuntimeError(msg) raise RuntimeError(msg)
self.setEndian("<" if iimm.startswith(II) else ">")
if self._bigtiff:
self.f.seek(4, os.SEEK_CUR)
self.skipIFDs() self.skipIFDs()
self.goToEnd() self.goToEnd()
@ -2076,11 +2081,13 @@ class AppendingTiffWriter(io.BytesIO):
msg = "IIMM of new page doesn't match IIMM of first page" msg = "IIMM of new page doesn't match IIMM of first page"
raise RuntimeError(msg) raise RuntimeError(msg)
ifd_offset = self.readLong() if self._bigtiff:
self.f.seek(4, os.SEEK_CUR)
ifd_offset = self._read(8 if self._bigtiff else 4)
ifd_offset += self.offsetOfNewPage ifd_offset += self.offsetOfNewPage
assert self.whereToWriteNewIFDOffset is not None assert self.whereToWriteNewIFDOffset is not None
self.f.seek(self.whereToWriteNewIFDOffset) self.f.seek(self.whereToWriteNewIFDOffset)
self.writeLong(ifd_offset) self._write(ifd_offset, 8 if self._bigtiff else 4)
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
self.fixIFD() self.fixIFD()
@ -2126,18 +2133,20 @@ class AppendingTiffWriter(io.BytesIO):
self.endian = endian self.endian = endian
self.longFmt = f"{self.endian}L" self.longFmt = f"{self.endian}L"
self.shortFmt = f"{self.endian}H" self.shortFmt = f"{self.endian}H"
self.tagFormat = f"{self.endian}HHL" self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
def skipIFDs(self) -> None: def skipIFDs(self) -> None:
while True: while True:
ifd_offset = self.readLong() ifd_offset = self._read(8 if self._bigtiff else 4)
if ifd_offset == 0: if ifd_offset == 0:
self.whereToWriteNewIFDOffset = self.f.tell() - 4 self.whereToWriteNewIFDOffset = self.f.tell() - (
8 if self._bigtiff else 4
)
break break
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
num_tags = self.readShort() num_tags = self._read(8 if self._bigtiff else 2)
self.f.seek(num_tags * 12, os.SEEK_CUR) self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
def write(self, data: Buffer, /) -> int: def write(self, data: Buffer, /) -> int:
return self.f.write(data) return self.f.write(data)
@ -2167,17 +2176,19 @@ class AppendingTiffWriter(io.BytesIO):
msg = f"wrote only {bytes_written} bytes but wanted {expected}" msg = f"wrote only {bytes_written} bytes but wanted {expected}"
raise RuntimeError(msg) raise RuntimeError(msg)
def rewriteLastShortToLong(self, value: int) -> None: def _rewriteLast(
self.f.seek(-2, os.SEEK_CUR) self, value: int, field_size: int, new_field_size: int = 0
bytes_written = self.f.write(struct.pack(self.longFmt, value)) ) -> None:
self._verify_bytes_written(bytes_written, 4)
def _rewriteLast(self, value: int, field_size: int) -> None:
self.f.seek(-field_size, os.SEEK_CUR) self.f.seek(-field_size, os.SEEK_CUR)
if not new_field_size:
new_field_size = field_size
bytes_written = self.f.write( bytes_written = self.f.write(
struct.pack(self.endian + self._fmt(field_size), value) struct.pack(self.endian + self._fmt(new_field_size), value)
) )
self._verify_bytes_written(bytes_written, field_size) self._verify_bytes_written(bytes_written, new_field_size)
def rewriteLastShortToLong(self, value: int) -> None:
self._rewriteLast(value, 2, 4)
def rewriteLastShort(self, value: int) -> None: def rewriteLastShort(self, value: int) -> None:
return self._rewriteLast(value, 2) return self._rewriteLast(value, 2)
@ -2185,13 +2196,17 @@ class AppendingTiffWriter(io.BytesIO):
def rewriteLastLong(self, value: int) -> None: def rewriteLastLong(self, value: int) -> None:
return self._rewriteLast(value, 4) return self._rewriteLast(value, 4)
def _write(self, value: int, field_size: int) -> None:
bytes_written = self.f.write(
struct.pack(self.endian + self._fmt(field_size), value)
)
self._verify_bytes_written(bytes_written, field_size)
def writeShort(self, value: int) -> None: def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) self._write(value, 2)
self._verify_bytes_written(bytes_written, 2)
def writeLong(self, value: int) -> None: def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value)) self._write(value, 4)
self._verify_bytes_written(bytes_written, 4)
def close(self) -> None: def close(self) -> None:
self.finalize() self.finalize()
@ -2199,24 +2214,37 @@ class AppendingTiffWriter(io.BytesIO):
self.f.close() self.f.close()
def fixIFD(self) -> None: def fixIFD(self) -> None:
num_tags = self.readShort() num_tags = self._read(8 if self._bigtiff else 2)
for i in range(num_tags): for i in range(num_tags):
tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) tag, field_type, count = struct.unpack(
self.tagFormat, self.f.read(12 if self._bigtiff else 8)
)
field_size = self.fieldSizes[field_type] field_size = self.fieldSizes[field_type]
total_size = field_size * count total_size = field_size * count
is_local = total_size <= 4 fmt_size = 8 if self._bigtiff else 4
is_local = total_size <= fmt_size
if not is_local: if not is_local:
offset = self.readLong() + self.offsetOfNewPage offset = self._read(fmt_size) + self.offsetOfNewPage
self.rewriteLastLong(offset) self._rewriteLast(offset, fmt_size)
if tag in self.Tags: if tag in self.Tags:
cur_pos = self.f.tell() cur_pos = self.f.tell()
logger.debug(
"fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d",
TiffTags.lookup(tag).name,
tag,
TYPES.get(field_type, "unknown"),
field_type,
field_size,
count,
)
if is_local: if is_local:
self._fixOffsets(count, field_size) self._fixOffsets(count, field_size)
self.f.seek(cur_pos + 4) self.f.seek(cur_pos + fmt_size)
else: else:
self.f.seek(offset) self.f.seek(offset)
self._fixOffsets(count, field_size) self._fixOffsets(count, field_size)
@ -2224,24 +2252,33 @@ class AppendingTiffWriter(io.BytesIO):
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(fmt_size, os.SEEK_CUR)
def _fixOffsets(self, count: int, field_size: int) -> None: def _fixOffsets(self, count: int, field_size: int) -> None:
for i in range(count): for i in range(count):
offset = self._read(field_size) offset = self._read(field_size)
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
if field_size == 2 and offset >= 65536:
# offset is now too large - we must convert shorts to longs new_field_size = 0
if self._bigtiff and field_size in (2, 4) and offset >= 2**32:
# offset is now too large - we must convert long to long8
new_field_size = 8
elif field_size == 2 and offset >= 2**16:
# offset is now too large - we must convert short to long
new_field_size = 4
if new_field_size:
if count != 1: if count != 1:
msg = "not implemented" msg = "not implemented"
raise RuntimeError(msg) # XXX TODO raise RuntimeError(msg) # XXX TODO
# simple case - the offset is just one and therefore it is # simple case - the offset is just one and therefore it is
# local (not referenced with another offset) # local (not referenced with another offset)
self.rewriteLastShortToLong(offset) self._rewriteLast(offset, field_size, new_field_size)
self.f.seek(-10, os.SEEK_CUR) # Move back past the new offset, past 'count', and before 'field_type'
self.writeShort(TiffTags.LONG) # rewrite the type to LONG rewind = -new_field_size - 4 - 2
self.f.seek(8, os.SEEK_CUR) self.f.seek(rewind, os.SEEK_CUR)
self.writeShort(new_field_size) # rewrite the type
self.f.seek(2 - rewind, os.SEEK_CUR)
else: else:
self._rewriteLast(offset, field_size) self._rewriteLast(offset, field_size)

View File

@ -47,6 +47,8 @@ def deprecate(
raise RuntimeError(msg) raise RuntimeError(msg)
elif when == 12: elif when == 12:
removed = "Pillow 12 (2025-10-15)" removed = "Pillow 12 (2025-10-15)"
elif when == 13:
removed = "Pillow 13 (2026-10-15)"
else: else:
msg = f"Unknown removal version: {when}. Update {__name__}?" msg = f"Unknown removal version: {when}. Update {__name__}?"
raise ValueError(msg) raise ValueError(msg)

View File

@ -128,6 +128,7 @@ features: dict[str, tuple[str, str | bool, str | None]] = {
"fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
"harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"),
"libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"),
"mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"),
"zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"),
"libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"),
"xcb": ("PIL._imaging", "HAVE_XCB", None), "xcb": ("PIL._imaging", "HAVE_XCB", None),
@ -302,7 +303,8 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
if name == "jpg": if name == "jpg":
libjpeg_turbo_version = version_feature("libjpeg_turbo") libjpeg_turbo_version = version_feature("libjpeg_turbo")
if libjpeg_turbo_version is not None: if libjpeg_turbo_version is not None:
v = "libjpeg-turbo " + libjpeg_turbo_version v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo"
v += " " + libjpeg_turbo_version
if v is None: if v is None:
v = version(name) v = version(name)
if v is not None: if v is not None:

View File

@ -76,6 +76,13 @@
#ifdef HAVE_LIBJPEG #ifdef HAVE_LIBJPEG
#include "jconfig.h" #include "jconfig.h"
#ifdef LIBJPEG_TURBO_VERSION
#define JCONFIG_INCLUDED
#ifdef __CYGWIN__
#define _BASETSD_H
#endif
#include "jpeglib.h"
#endif
#endif #endif
#ifdef HAVE_LIBZ #ifdef HAVE_LIBZ
@ -4367,6 +4374,15 @@ setup_module(PyObject *m) {
Py_INCREF(have_libjpegturbo); Py_INCREF(have_libjpegturbo);
PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo);
PyObject *have_mozjpeg;
#ifdef JPEG_C_PARAM_SUPPORTED
have_mozjpeg = Py_True;
#else
have_mozjpeg = Py_False;
#endif
Py_INCREF(have_mozjpeg);
PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg);
PyObject *have_libimagequant; PyObject *have_libimagequant;
#ifdef HAVE_LIBIMAGEQUANT #ifdef HAVE_LIBIMAGEQUANT
have_libimagequant = Py_True; have_libimagequant = Py_True;

View File

@ -339,29 +339,23 @@ text_layout_raqm(
len = PySequence_Fast_GET_SIZE(seq); len = PySequence_Fast_GET_SIZE(seq);
for (j = 0; j < len; j++) { for (j = 0; j < len; j++) {
PyObject *item = PySequence_Fast_GET_ITEM(seq, j); PyObject *item = PySequence_Fast_GET_ITEM(seq, j);
char *feature = NULL;
Py_ssize_t size = 0;
PyObject *bytes;
if (!PyUnicode_Check(item)) { if (!PyUnicode_Check(item)) {
Py_DECREF(seq); Py_DECREF(seq);
PyErr_SetString(PyExc_TypeError, "expected a string"); PyErr_SetString(PyExc_TypeError, "expected a string");
goto failed; goto failed;
} }
bytes = PyUnicode_AsUTF8String(item);
if (bytes == NULL) { Py_ssize_t size;
const char *feature = PyUnicode_AsUTF8AndSize(item, &size);
if (feature == NULL) {
Py_DECREF(seq); Py_DECREF(seq);
goto failed; goto failed;
} }
feature = PyBytes_AS_STRING(bytes);
size = PyBytes_GET_SIZE(bytes);
if (!raqm_add_font_feature(rq, feature, size)) { if (!raqm_add_font_feature(rq, feature, size)) {
Py_DECREF(seq); Py_DECREF(seq);
Py_DECREF(bytes);
PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed");
goto failed; goto failed;
} }
Py_DECREF(bytes);
} }
Py_DECREF(seq); Py_DECREF(seq);
} }

View File

@ -134,7 +134,16 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
return -1; return -1;
} }
/* Compressor configuration */ /* Compressor configuration */
#ifdef JPEG_C_PARAM_SUPPORTED
/* MozJPEG */
if (!context->progressive) {
/* Do not use MozJPEG progressive default */
jpeg_c_set_int_param(
&context->cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST
);
}
#endif
jpeg_set_defaults(&context->cinfo); jpeg_set_defaults(&context->cinfo);
/* Prevent RGB -> YCbCr conversion */ /* Prevent RGB -> YCbCr conversion */

View File

@ -1664,6 +1664,7 @@ static struct {
{"RGBA", "RGBaXX", 48, unpackRGBaskip2}, {"RGBA", "RGBaXX", 48, unpackRGBaskip2},
{"RGBA", "RGBa;16L", 64, unpackRGBa16L}, {"RGBA", "RGBa;16L", 64, unpackRGBa16L},
{"RGBA", "RGBa;16B", 64, unpackRGBa16B}, {"RGBA", "RGBa;16B", 64, unpackRGBa16B},
{"RGBA", "BGR", 24, ImagingUnpackBGR},
{"RGBA", "BGRa", 32, unpackBGRa}, {"RGBA", "BGRa", 32, unpackBGRa},
{"RGBA", "RGBA;I", 32, unpackRGBAI}, {"RGBA", "RGBA;I", 32, unpackRGBAI},
{"RGBA", "RGBA;L", 32, unpackRGBAL}, {"RGBA", "RGBA;L", 32, unpackRGBAL},

View File

@ -11,10 +11,11 @@ For more extensive info, see the [Windows build instructions](build.rst).
* Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires NASM for libjpeg-turbo, a required dependency when using this script.
* Requires CMake 3.15 or newer (available as Visual Studio component). * Requires CMake 3.15 or newer (available as Visual Studio component).
* Tested on Windows Server 2019 with Visual Studio 2019 Community and Visual Studio 2022 Community (AppVeyor). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server
* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). 2019 with Visual Studio 2019 Enterprise (GitHub Actions).
Here's an example script to build on Windows:
The following is a simplified version of the script used on AppVeyor:
``` ```
set PYTHON=C:\Python39\bin set PYTHON=C:\Python39\bin
cd /D C:\Pillow\winbuild cd /D C:\Pillow\winbuild

View File

@ -6,7 +6,7 @@ Building Pillow on Windows
be sufficient. be sufficient.
This page describes the steps necessary to build Pillow using the same This page describes the steps necessary to build Pillow using the same
scripts used on GitHub Actions and AppVeyor CIs. scripts used on GitHub Actions CI.
Prerequisites Prerequisites
------------- -------------
@ -113,7 +113,7 @@ directory.
Example Example
------- -------
The following is a simplified version of the script used on AppVeyor:: Here's an example script to build on Windows::
set PYTHON=C:\Python39\bin set PYTHON=C:\Python39\bin
cd /D C:\Pillow\winbuild cd /D C:\Pillow\winbuild

View File

@ -116,7 +116,7 @@ V = {
"HARFBUZZ": "10.1.0", "HARFBUZZ": "10.1.0",
"JPEGTURBO": "3.1.0", "JPEGTURBO": "3.1.0",
"LCMS2": "2.16", "LCMS2": "2.16",
"LIBPNG": "1.6.44", "LIBPNG": "1.6.45",
"LIBWEBP": "1.5.0", "LIBWEBP": "1.5.0",
"OPENJPEG": "2.5.3", "OPENJPEG": "2.5.3",
"TIFF": "4.6.0", "TIFF": "4.6.0",
@ -124,7 +124,6 @@ V = {
"ZLIBNG": "2.2.3", "ZLIBNG": "2.2.3",
"LIBAVIF": "1.1.1", "LIBAVIF": "1.1.1",
} }
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
@ -242,8 +241,8 @@ DEPS: dict[str, dict[str, Any]] = {
}, },
"libpng": { "libpng": {
"url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/"
f"lpng{V['LIBPNG_DOTLESS']}.zip/download", f"FILENAME/download",
"filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", "filename": f"libpng-{V['LIBPNG']}.tar.gz",
"license": "LICENSE", "license": "LICENSE",
"build": [ "build": [
*cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"),