From 327e13ffd0d4999fb9cd63d12407f08f20710677 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Sun, 6 Oct 2024 00:57:58 +0300
Subject: [PATCH 01/76] Stop testing on AppVeyor
---
.appveyor.yml | 99 -----------------------------------------
.github/CONTRIBUTING.md | 4 +-
.github/mergify.yml | 1 -
MANIFEST.in | 1 -
README.md | 3 --
RELEASING.md | 4 +-
Tests/helper.py | 9 ----
docs/about.rst | 3 +-
docs/index.rst | 4 --
winbuild/README.md | 4 +-
winbuild/build.rst | 4 +-
11 files changed, 9 insertions(+), 127 deletions(-)
delete mode 100644 .appveyor.yml
diff --git a/.appveyor.yml b/.appveyor.yml
deleted file mode 100644
index 781ad4a4b..000000000
--- a/.appveyor.yml
+++ /dev/null
@@ -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'))
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index d03fcf0d9..c372da7d2 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -9,7 +9,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Fork the Pillow repository.
- Create a branch from `main`.
- 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`.
### Guidelines
@@ -17,7 +17,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Separate code commits from reformatting commits.
- Provide tests for any newly added code.
- 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.
- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged.
diff --git a/.github/mergify.yml b/.github/mergify.yml
index 3c2066137..9bb089615 100644
--- a/.github/mergify.yml
+++ b/.github/mergify.yml
@@ -9,7 +9,6 @@ pull_request_rules:
- status-success=Windows Test Successful
- status-success=MinGW
- status-success=Cygwin Test Successful
- - status-success=continuous-integration/appveyor/pr
actions:
merge:
method: merge
diff --git a/MANIFEST.in b/MANIFEST.in
index af25dfd2d..48085b82e 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -20,7 +20,6 @@ graft docs
graft _custom_build
# build/src control detritus
-exclude .appveyor.yml
exclude .clang-format
exclude .coveragerc
exclude .editorconfig
diff --git a/README.md b/README.md
index 5bbebaccb..c6e61453f 100644
--- a/README.md
+++ b/README.md
@@ -42,9 +42,6 @@ As of 2019, Pillow development is
-
diff --git a/RELEASING.md b/RELEASING.md
index 9e6ec5dd4..490f6d6bd 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -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
* [ ] 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.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`.
@@ -40,7 +40,7 @@ Released as needed for security, installation or critical bug fixes.
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`.
-* [ ] 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`
* [ ] Run pre-release check via `make release-test`.
* [ ] Create tag for release e.g.:
diff --git a/Tests/helper.py b/Tests/helper.py
index d6a93a803..cf1ef1997 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -327,16 +327,7 @@ def magick_command() -> list[str] | 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:
- # GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ
diff --git a/docs/about.rst b/docs/about.rst
index c51ddebd0..7df895b8f 100644
--- a/docs/about.rst
+++ b/docs/about.rst
@@ -6,12 +6,11 @@ Goals
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`_
- Regular releases to the `Python Package Index`_
.. _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
.. _Python Package Index: https://pypi.org/project/pillow/
diff --git a/docs/index.rst b/docs/index.rst
index 18f5c3d13..689088d48 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -33,10 +33,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more
Date: Sun, 6 Oct 2024 20:16:55 +0300
Subject: [PATCH 02/76] Test the oldest Python on 32-bit Windows 2019
---
.github/workflows/test-windows.yml | 14 ++++++++++----
docs/installation/platform-support.rst | 4 +---
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index f6d0aeb1d..2e76c05ae 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -31,15 +31,20 @@ env:
jobs:
build:
- runs-on: windows-latest
+ runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
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"]
+ architecture: ["x64"]
+ os: ["windows-latest"]
+ include:
+ # Test the oldest Python on 32-bit
+ - { python-version: "3.9", architecture: "x86", os: "windows-2019" }
timeout-minutes: 30
- name: Python ${{ matrix.python-version }}
+ name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
steps:
- name: Checkout Pillow
@@ -63,6 +68,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
+ architecture: ${{ matrix.architecture }}
cache: pip
cache-dependency-path: ".github/workflows/test-windows.yml"
@@ -81,7 +87,7 @@ jobs:
pytest-timeout
- name: Install CPython dependencies
- if: "!contains(matrix.python-version, 'pypy')"
+ if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
run: >
python3 -m pip install
PyQt6
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index a0bada7b4..cd196e4f8 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -48,13 +48,11 @@ These platforms are built and tested for every change.
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | 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 |
| | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+
-| | 3.13 | x86 |
-| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+
| | 3.9 (Cygwin) | x86-64 |
From a262b1991b439574578b60fac932b7065e02f0b6 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Thu, 7 Nov 2024 12:54:28 +0200
Subject: [PATCH 03/76] Update winbuild/README.md
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
---
winbuild/README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/winbuild/README.md b/winbuild/README.md
index 0d3ec8d8a..c474f12ce 100644
--- a/winbuild/README.md
+++ b/winbuild/README.md
@@ -11,7 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst).
* Requires Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script.
* Requires CMake 3.15 or newer (available as Visual Studio component).
-* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
+* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server
+ 2019 with Visual Studio 2019 Enterprise (GitHub Actions).
Here's an example script to build on Windows:
From 09bf28e9d7dbe20c13dc05ebe62a9e706abd273c Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Thu, 7 Nov 2024 15:46:08 +0200
Subject: [PATCH 04/76] Update platform support
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
---
docs/installation/platform-support.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index 4e8620fc4..373708a61 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -50,8 +50,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Windows Server 2019 | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+
-| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 |
-| | 3.12, 3.13, PyPy3 | |
+| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 |
+| | PyPy3 | |
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+
From 7dcf4d8ab319348239d4f8695c6d3366bc257cbf Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 25 Nov 2024 16:43:21 +1100
Subject: [PATCH 05/76] Added logging to fixIFD()
---
src/PIL/TiffImagePlugin.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 6bf39b75a..a246994ef 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -2202,6 +2202,12 @@ class AppendingTiffWriter(io.BytesIO):
if tag in self.Tags:
cur_pos = self.f.tell()
+ tagname = TiffTags.lookup(tag).name
+ typname = TYPES.get(field_type, "unknown")
+ msg = f"fixIFD: {tagname} ({tag}) - type: {typname} ({field_type})"
+ msg += f"- type size: {field_size} - count: {count}"
+ logger.debug(msg)
+
if is_local:
self._fixOffsets(count, field_size)
self.f.seek(cur_pos + 4)
From 167ed55d8b43de26c5ce01c239ce848062e5e995 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 30 Dec 2024 19:37:38 +1100
Subject: [PATCH 06/76] Use elif
---
src/PIL/TiffImagePlugin.py | 21 ++++++++++-----------
1 file changed, 10 insertions(+), 11 deletions(-)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 16c521bea..2ab0b7ebe 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1559,17 +1559,6 @@ class TiffImageFile(ImageFile.ImageFile):
# fillorder==2 modes have a corresponding
# fillorder=1 mode
self._mode, rawmode = OPEN_INFO[key]
- # libtiff always returns the bytes in native order.
- # we're expecting image byte order. So, if the rawmode
- # contains I;16, we need to convert from native to image
- # byte order.
- if rawmode == "I;16":
- rawmode = "I;16N"
- if ";16B" in rawmode:
- rawmode = rawmode.replace(";16B", ";16N")
- if ";16L" in rawmode:
- rawmode = rawmode.replace(";16L", ";16N")
-
# YCbCr images with new jpeg compression with pixels in one plane
# unpacked straight into RGB values
if (
@@ -1578,6 +1567,16 @@ class TiffImageFile(ImageFile.ImageFile):
and self._planar_configuration == 1
):
rawmode = "RGB"
+ # libtiff always returns the bytes in native order.
+ # we're expecting image byte order. So, if the rawmode
+ # contains I;16, we need to convert from native to image
+ # byte order.
+ elif rawmode == "I;16":
+ rawmode = "I;16N"
+ elif ";16B" in rawmode:
+ rawmode = rawmode.replace(";16B", ";16N")
+ elif ";16L" in rawmode:
+ rawmode = rawmode.replace(";16L", ";16N")
# Offset in the tile tuple is 0, we go from 0,0 to
# w,h, and we only do this once -- eds
From 7cee64ad1b1cbd558cdb01edaa9444f60467947b Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 30 Dec 2024 19:45:46 +1100
Subject: [PATCH 07/76] Use endswith
---
src/PIL/TiffImagePlugin.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 2ab0b7ebe..ab760c8fb 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1573,10 +1573,8 @@ class TiffImageFile(ImageFile.ImageFile):
# byte order.
elif rawmode == "I;16":
rawmode = "I;16N"
- elif ";16B" in rawmode:
- rawmode = rawmode.replace(";16B", ";16N")
- elif ";16L" in rawmode:
- rawmode = rawmode.replace(";16L", ";16N")
+ elif rawmode.endswith(";16B") or rawmode.endswith(";16L"):
+ rawmode = rawmode[:-1] + "N"
# Offset in the tile tuple is 0, we go from 0,0 to
# w,h, and we only do this once -- eds
From 050caa9cae5a5844e934e7ec29c0c5bc42537e32 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 30 Dec 2024 21:14:23 +1100
Subject: [PATCH 08/76] Restored Makernote as a deprecated enum
---
docs/deprecations.rst | 8 ++++++++
docs/releasenotes/11.1.0.rst | 7 ++++---
src/PIL/ExifTags.py | 1 +
3 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 25607e27c..80966ca36 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -175,6 +175,14 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object.
+ExifTags.IFD.Makernote
+^^^^^^^^^^^^^^^^^^^^^^
+
+.. deprecated:: 11.1.0
+
+``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
+``ExifTags.IFD.MakerNote``.
+
Removed features
----------------
diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst
index c5d0afd58..57a8eef40 100644
--- a/docs/releasenotes/11.1.0.rst
+++ b/docs/releasenotes/11.1.0.rst
@@ -23,10 +23,11 @@ TODO
Deprecations
============
-TODO
-^^^^
+ExifTags.IFD.Makernote
+^^^^^^^^^^^^^^^^^^^^^^
-TODO
+``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
+``ExifTags.IFD.MakerNote``.
API Changes
===========
diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py
index 207d4de4e..2280d5ce8 100644
--- a/src/PIL/ExifTags.py
+++ b/src/PIL/ExifTags.py
@@ -353,6 +353,7 @@ class IFD(IntEnum):
Exif = 0x8769
GPSInfo = 0x8825
MakerNote = 0x927C
+ Makernote = 0x927C # Deprecated
Interop = 0xA005
IFD1 = -1
From 2ac383028a1983bb2bee27cd8998c25c81e93e49 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 31 Dec 2024 01:26:13 +1100
Subject: [PATCH 09/76] Allow saving as BigTIFF
---
Tests/test_file_tiff.py | 7 +++++
docs/handbook/image-file-formats.rst | 3 ++
src/PIL/TiffImagePlugin.py | 44 +++++++++++++++++-----------
3 files changed, 37 insertions(+), 17 deletions(-)
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 6f51d4651..df2c4ebea 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -115,6 +115,13 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
+ def test_bigtiff_save(self, tmp_path: Path) -> None:
+ outfile = str(tmp_path / "temp.tif")
+ hopper().save(outfile, bigtiff=True)
+
+ with Image.open(outfile) as im:
+ assert im.tag_v2._bigtiff is True
+
def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif")
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 2ea49282e..d956d12d1 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -1208,6 +1208,9 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 8.4.0
+**bigtiff**
+ If true, the image will be saved as a BigTIFF.
+
**compression**
A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index ab760c8fb..013f34a4f 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __init__(
self,
- ifh: bytes = b"II\052\0\0\0\0\0",
+ ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00",
prefix: bytes | None = None,
group: int | None = None,
) -> None:
@@ -949,16 +949,26 @@ class ImageFileDirectory_v2(_IFDv2Base):
warnings.warn(str(msg))
return
+ def _get_ifh(self):
+ ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
+ if self._bigtiff:
+ ifh += self._pack("HH", 8, 0)
+ ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8)
+
+ return ifh
+
def tobytes(self, offset: int = 0) -> bytes:
# FIXME What about tagdata?
- result = self._pack("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]] = []
- offset = offset + len(result) + len(self._tags_v2) * 12 + 4
+ offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4
stripoffsets = None
# pass 1: convert tags to binary format
# 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()):
if tag == STRIPOFFSETS:
stripoffsets = len(entries)
@@ -966,11 +976,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
if is_ifd:
- if self._endian == "<":
- ifh = b"II\x2A\x00\x08\x00\x00\x00"
- else:
- ifh = b"MM\x00\x2A\x00\x00\x00\x08"
- ifd = ImageFileDirectory_v2(ifh, group=tag)
+ ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
values = self._tags_v2[tag]
for ifd_tag, ifd_value in values.items():
ifd[ifd_tag] = ifd_value
@@ -993,10 +999,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
else:
count = len(values)
# figure out if data fits into the entry
- if len(data) <= 4:
- entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
+ if len(data) <= fmt_size:
+ entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
else:
- entries.append((tag, typ, count, self._pack("L", offset), data))
+ entries.append((tag, typ, count, self._pack(fmt, offset), data))
offset += (len(data) + 1) // 2 * 2 # pad to word
# update strip offset data to point beyond auxiliary data
@@ -1007,13 +1013,15 @@ class ImageFileDirectory_v2(_IFDv2Base):
values = [val + offset for val in handler(self, data, self.legacy_api)]
data = self._write_dispatch[typ](self, *values)
else:
- value = self._pack("L", self._unpack("L", value)[0] + offset)
+ value = self._pack(fmt, self._unpack(fmt, value)[0] + offset)
entries[stripoffsets] = tag, typ, count, value, data
# pass 2: write entries to file
for tag, typ, count, value, data in entries:
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
- result += self._pack("HHL4s", tag, typ, count, value)
+ result += self._pack(
+ "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value
+ )
# -- overwrite here for multi-page --
result += b"\0\0\0\0" # end of entries
@@ -1028,8 +1036,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def save(self, fp: IO[bytes]) -> int:
if fp.tell() == 0: # skip TIFF header on subsequent pages
- # tiff header -- PIL always starts the first IFD at offset 8
- fp.write(self._prefix + self._pack("HL", 42, 8))
+ fp.write(self._get_ifh())
offset = fp.tell()
result = self.tobytes(offset)
@@ -1680,10 +1687,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
msg = f"cannot write mode {im.mode} as TIFF"
raise OSError(msg) from e
- ifd = ImageFileDirectory_v2(prefix=prefix)
-
encoderinfo = im.encoderinfo
encoderconfig = im.encoderconfig
+
+ ifd = ImageFileDirectory_v2(prefix=prefix)
+ if encoderinfo.get("bigtiff"):
+ ifd._bigtiff = True
+
try:
compression = encoderinfo["compression"]
except KeyError:
From 8bdcadcbe999c9a2becd6aa2997eb4d74f8ddf2b Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 31 Dec 2024 10:16:00 +1100
Subject: [PATCH 10/76] Renamed argument to big_tiff
---
Tests/test_file_tiff.py | 2 +-
docs/handbook/image-file-formats.rst | 2 +-
src/PIL/TiffImagePlugin.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index df2c4ebea..dedd48c20 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -117,7 +117,7 @@ class TestFileTiff:
def test_bigtiff_save(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
- hopper().save(outfile, bigtiff=True)
+ hopper().save(outfile, big_tiff=True)
with Image.open(outfile) as im:
assert im.tag_v2._bigtiff is True
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index d956d12d1..4a220aae6 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -1208,7 +1208,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 8.4.0
-**bigtiff**
+**big_tiff**
If true, the image will be saved as a BigTIFF.
**compression**
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 013f34a4f..61eb15243 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1691,7 +1691,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderconfig = im.encoderconfig
ifd = ImageFileDirectory_v2(prefix=prefix)
- if encoderinfo.get("bigtiff"):
+ if encoderinfo.get("big_tiff"):
ifd._bigtiff = True
try:
From e27115ee8da08ace01308b6cf6f66ccb75bda360 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 30 Dec 2024 23:31:05 +0000
Subject: [PATCH 11/76] Update dependency mypy to v1.14.1
---
.ci/requirements-mypy.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt
index cd1b1a1a1..10e59b885 100644
--- a/.ci/requirements-mypy.txt
+++ b/.ci/requirements-mypy.txt
@@ -1,4 +1,4 @@
-mypy==1.14.0
+mypy==1.14.1
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
From 1de617fbe725dcf0862b0d036e3d9cffe05b089f Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 31 Dec 2024 11:13:14 +1100
Subject: [PATCH 12/76] Added release notes
---
docs/handbook/image-file-formats.rst | 2 ++
docs/releasenotes/11.1.0.rst | 26 +++++++-------------------
2 files changed, 9 insertions(+), 19 deletions(-)
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 4a220aae6..a915ee4e2 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -1211,6 +1211,8 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
**big_tiff**
If true, the image will be saved as a BigTIFF.
+ .. versionadded:: 11.1.0
+
**compression**
A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression
diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst
index 27264d99a..1505310fa 100644
--- a/docs/releasenotes/11.1.0.rst
+++ b/docs/releasenotes/11.1.0.rst
@@ -1,25 +1,6 @@
11.1.0
------
-Security
-========
-
-TODO
-^^^^
-
-TODO
-
-:cve:`YYYY-XXXXX`: TODO
-^^^^^^^^^^^^^^^^^^^^^^^
-
-TODO
-
-Backwards Incompatible Changes
-==============================
-
-TODO
-^^^^
-
Deprecations
============
@@ -66,6 +47,13 @@ zlib library, and what version of zlib-ng is being used::
features.check_feature("zlib_ng") # True or False
features.version_feature("zlib_ng") # "2.2.2" for example, or None
+Saving TIFF as BigTIFF
+^^^^^^^^^^^^^^^^^^^^^^
+
+TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument::
+
+ im.save("out.tiff", big_tiff=True)
+
Other Changes
=============
From f91b111fac15e7e10be7323b291a15e238ba25b5 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 31 Dec 2024 20:42:49 +1100
Subject: [PATCH 13/76] Removed pre-C99 definitions
---
src/libImaging/ImPlatform.h | 30 ------------------------------
1 file changed, 30 deletions(-)
diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h
index c9b7e43b4..2ce282241 100644
--- a/src/libImaging/ImPlatform.h
+++ b/src/libImaging/ImPlatform.h
@@ -44,8 +44,6 @@
defines their own types with the same names, so we need to be able to undef
ours before including the JPEG code. */
-#if __STDC_VERSION__ >= 199901L /* C99+ */
-
#include
#define INT8 int8_t
@@ -55,34 +53,6 @@
#define INT32 int32_t
#define UINT32 uint32_t
-#else /* < C99 */
-
-#define INT8 signed char
-
-#if SIZEOF_SHORT == 2
-#define INT16 short
-#elif SIZEOF_INT == 2
-#define INT16 int
-#else
-#error Cannot find required 16-bit integer type
-#endif
-
-#if SIZEOF_SHORT == 4
-#define INT32 short
-#elif SIZEOF_INT == 4
-#define INT32 int
-#elif SIZEOF_LONG == 4
-#define INT32 long
-#else
-#error Cannot find required 32-bit integer type
-#endif
-
-#define UINT8 unsigned char
-#define UINT16 unsigned INT16
-#define UINT32 unsigned INT32
-
-#endif /* < C99 */
-
#endif /* not WIN */
/* assume IEEE; tweak if necessary (patches are welcome) */
From d42f22baafca30050f4fc8b6bafcc39ef624d685 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 31 Dec 2024 21:38:05 +1100
Subject: [PATCH 14/76] Added release notes
---
docs/releasenotes/11.1.0.rst | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst
index 27264d99a..aec7633a2 100644
--- a/docs/releasenotes/11.1.0.rst
+++ b/docs/releasenotes/11.1.0.rst
@@ -80,6 +80,11 @@ Saving JPEG 2000 CMYK images
With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files.
+Minimum C version
+^^^^^^^^^^^^^^^^^
+
+C99 is now the minimum version of C required to compile Pillow from source.
+
zlib-ng in wheels
^^^^^^^^^^^^^^^^^
From 06e02cc1d98dbcfb09839da080ee8eb318baa4ba Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 31 Dec 2024 20:48:55 +1100
Subject: [PATCH 15/76] Added compile-time mozjpeg feature flag
---
src/PIL/features.py | 4 +++-
src/_imaging.c | 16 ++++++++++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/PIL/features.py b/src/PIL/features.py
index 3645e3def..ae7ea4255 100644
--- a/src/PIL/features.py
+++ b/src/PIL/features.py
@@ -127,6 +127,7 @@ features: dict[str, tuple[str, str | bool, str | None]] = {
"fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
"harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_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"),
"libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"),
"xcb": ("PIL._imaging", "HAVE_XCB", None),
@@ -300,7 +301,8 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
if name == "jpg":
libjpeg_turbo_version = version_feature("libjpeg_turbo")
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:
v = version(name)
if v is not None:
diff --git a/src/_imaging.c b/src/_imaging.c
index 5d6d97bed..00772d012 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -76,6 +76,13 @@
#ifdef HAVE_LIBJPEG
#include "jconfig.h"
+#ifdef LIBJPEG_TURBO_VERSION
+#define JCONFIG_INCLUDED
+#ifdef __CYGWIN__
+#define _BASETSD_H
+#endif
+#include "jpeglib.h"
+#endif
#endif
#ifdef HAVE_LIBZ
@@ -4367,6 +4374,15 @@ setup_module(PyObject *m) {
Py_INCREF(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;
#ifdef HAVE_LIBIMAGEQUANT
have_libimagequant = Py_True;
From ae59b039564eefdebec4ea67a712a115d0e1ab67 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 31 Dec 2024 21:40:12 +1100
Subject: [PATCH 16/76] Do not use MozJPEG progressive default
---
Tests/test_file_jpeg.py | 13 ++++++++++---
src/libImaging/JpegEncode.c | 11 ++++++++++-
2 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index bf0dec4b8..d4c0636c6 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -281,7 +281,10 @@ class TestFileJpeg:
assert not im2.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
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
@@ -424,8 +427,12 @@ class TestFileJpeg:
im2 = self.roundtrip(hopper(), progressive=1)
im3 = self.roundtrip(hopper(), progression=1) # compatibility
- assert_image_equal(im1, im2)
- assert_image_equal(im1, im3)
+ if features.check_feature("mozjpeg"):
+ 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("progression")
assert im3.info.get("progressive")
diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c
index 4372d51d5..3c11eac22 100644
--- a/src/libImaging/JpegEncode.c
+++ b/src/libImaging/JpegEncode.c
@@ -134,7 +134,16 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
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);
/* Prevent RGB -> YCbCr conversion */
From e34427167ddbaeece43490c4054c1e17fa21d77b Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 31 Dec 2024 23:26:09 +1100
Subject: [PATCH 17/76] Added CentOS Stream 10
---
.github/workflows/test-docker.yml | 1 +
docs/installation/platform-support.rst | 2 ++
2 files changed, 3 insertions(+)
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index cc5f9d4a5..4b01a10e4 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -44,6 +44,7 @@ jobs:
amazon-2023-amd64,
arch,
centos-stream-9-amd64,
+ centos-stream-10-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-40-amd64,
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index 35f863374..3741c5956 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -27,6 +27,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
+| CentOS Stream 10 | 3.12 | x86-64 |
++----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | x86-64 |
From d626e6ab9f37c6bc27036982e282d42012ed0cab Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 1 Jan 2025 09:07:41 +1100
Subject: [PATCH 18/76] text is a property
---
Tests/test_file_png.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index 974e1e75f..d87883279 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -618,7 +618,7 @@ class TestFilePng:
with Image.open("Tests/images/truncated_image.png") as im:
# The file is truncated
with pytest.raises(OSError):
- im.text()
+ im.text
ImageFile.LOAD_TRUNCATED_IMAGES = True
assert isinstance(im.text, dict)
ImageFile.LOAD_TRUNCATED_IMAGES = False
From 8d78cfcc5a798c59a193b80e200d9845992326ab Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 1 Jan 2025 09:10:16 +1100
Subject: [PATCH 19/76] Added return types
---
Tests/test_file_jpeg.py | 2 +-
src/PIL/TiffImagePlugin.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index bf0dec4b8..52fc9239c 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -181,7 +181,7 @@ class TestFileJpeg:
assert test(100, 200) == (100, 200)
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:
assert im.info["dpi"] == (2.54, 5.08)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 61eb15243..93ad89032 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -949,7 +949,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
warnings.warn(str(msg))
return
- def _get_ifh(self):
+ def _get_ifh(self) -> bytes:
ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
if self._bigtiff:
ifh += self._pack("HH", 8, 0)
From beda2b6e8d20050a16fbc261753fd30e410fba93 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 1 Jan 2025 10:49:24 +1100
Subject: [PATCH 20/76] Removed unused image open
---
Tests/test_file_ico.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index 37770498a..e81aae669 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -253,8 +253,7 @@ def test_truncated_mask() -> None:
try:
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
output = io.BytesIO()
From b89cc09944b4add584967bf1fa21208e92442def Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 1 Jan 2025 12:22:55 +1100
Subject: [PATCH 21/76] Corrected BLP1 alpha depth handling
---
Tests/test_file_blp.py | 1 +
src/PIL/BlpImagePlugin.py | 43 ++++++++++++++++++++++++---------------
2 files changed, 28 insertions(+), 16 deletions(-)
diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py
index 1e2f20c40..1f32be9c1 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -19,6 +19,7 @@ def test_load_blp1() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
+ assert im.mode == "RGBA"
im.load()
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 2d03af9d7..0d882fe96 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -260,18 +260,21 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self) -> None:
self.magic = self.fp.read(4)
- self.fp.seek(5, os.SEEK_CUR)
- (self._blp_alpha_depth,) = struct.unpack(" None:
assert im.palette is not None
fp.write(struct.pack("
Date: Wed, 1 Jan 2025 22:58:04 +1100
Subject: [PATCH 22/76] Do not reread start of header in decoder
---
src/PIL/BlpImagePlugin.py | 127 +++++++++++++++++++-------------------
1 file changed, 62 insertions(+), 65 deletions(-)
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 0d882fe96..c932b3b9c 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -259,24 +259,36 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self) -> None:
self.magic = self.fp.read(4)
-
- if self.magic == b"BLP1":
- self.fp.seek(4, os.SEEK_CUR)
- (self._blp_alpha_depth,) = struct.unpack(" tuple[int, int]:
try:
- self._read_blp_header()
+ self._read_header()
self._load()
except struct.error as e:
msg = "Truncated BLP file"
@@ -295,28 +307,9 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
def _load(self) -> None:
pass
- def _read_blp_header(self) -> None:
- assert self.fd is not None
- self.fd.seek(4)
- (self._blp_compression,) = struct.unpack(" None:
+ self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
+ self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length: int) -> bytes:
assert self.fd is not None
@@ -332,9 +325,11 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a))
return ret
- def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
+ def _read_bgra(
+ self, palette: list[tuple[int, int, int, int]], alpha: bool
+ ) -> bytearray:
data = bytearray()
- _data = BytesIO(self._safe_read(self._blp_lengths[0]))
+ _data = BytesIO(self._safe_read(self._lengths[0]))
while True:
try:
(offset,) = struct.unpack(" None:
- if self._blp_compression == Format.JPEG:
+ self._compression, self._encoding, alpha = self.args
+
+ if self._compression == Format.JPEG:
self._decode_jpeg_stream()
- elif self._blp_compression == 1:
- if self._blp_encoding in (4, 5):
+ elif self._compression == 1:
+ if self._encoding in (4, 5):
palette = self._read_palette()
- data = self._read_bgra(palette)
+ data = self._read_bgra(palette, alpha)
self.set_as_raw(data)
else:
- msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
+ msg = f"Unsupported BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg)
else:
- msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
+ msg = f"Unsupported BLP compression {repr(self._encoding)}"
raise BLPFormatError(msg)
def _decode_jpeg_stream(self) -> None:
@@ -371,8 +368,8 @@ class BLP1Decoder(_BLPBaseDecoder):
(jpeg_header_size,) = struct.unpack(" None:
+ self._compression, self._encoding, alpha, self._alpha_encoding = self.args
+
palette = self._read_palette()
assert self.fd is not None
- self.fd.seek(self._blp_offsets[0])
+ self.fd.seek(self._offsets[0])
- if self._blp_compression == 1:
+ if self._compression == 1:
# Uncompressed or DirectX compression
- if self._blp_encoding == Encoding.UNCOMPRESSED:
- data = self._read_bgra(palette)
+ if self._encoding == Encoding.UNCOMPRESSED:
+ data = self._read_bgra(palette, alpha)
- elif self._blp_encoding == Encoding.DXT:
+ elif self._encoding == Encoding.DXT:
data = bytearray()
- if self._blp_alpha_encoding == AlphaEncoding.DXT1:
- linesize = (self.size[0] + 3) // 4 * 8
- for yb in range((self.size[1] + 3) // 4):
- for d in decode_dxt1(
- self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
- ):
+ if self._alpha_encoding == AlphaEncoding.DXT1:
+ linesize = (self.state.xsize + 3) // 4 * 8
+ for yb in range((self.state.ysize + 3) // 4):
+ for d in decode_dxt1(self._safe_read(linesize), alpha):
data += d
- elif self._blp_alpha_encoding == AlphaEncoding.DXT3:
- linesize = (self.size[0] + 3) // 4 * 16
- for yb in range((self.size[1] + 3) // 4):
+ elif self._alpha_encoding == AlphaEncoding.DXT3:
+ linesize = (self.state.xsize + 3) // 4 * 16
+ for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt3(self._safe_read(linesize)):
data += d
- elif self._blp_alpha_encoding == AlphaEncoding.DXT5:
- linesize = (self.size[0] + 3) // 4 * 16
- for yb in range((self.size[1] + 3) // 4):
+ elif self._alpha_encoding == AlphaEncoding.DXT5:
+ linesize = (self.state.xsize + 3) // 4 * 16
+ for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt5(self._safe_read(linesize)):
data += d
else:
- msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
+ msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
raise BLPFormatError(msg)
else:
- msg = f"Unknown BLP encoding {repr(self._blp_encoding)}"
+ msg = f"Unknown BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg)
else:
- msg = f"Unknown BLP compression {repr(self._blp_compression)}"
+ msg = f"Unknown BLP compression {repr(self._compression)}"
raise BLPFormatError(msg)
self.set_as_raw(data)
From 5d998d3fedb06666ae680e3ebe3f3547a9059727 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 1 Jan 2025 23:38:24 +1100
Subject: [PATCH 23/76] Improved coverage
---
Tests/test_file_blp.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py
index 1f32be9c1..9f2de8f98 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
-from PIL import Image
+from PIL import BlpImagePlugin, Image
from .helper import (
assert_image_equal,
@@ -38,6 +38,13 @@ def test_load_blp2_dxt1a() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
+def test_invalid_file() -> None:
+ invalid_file = "Tests/images/flower.jpg"
+
+ with pytest.raises(BlpImagePlugin.BLPFormatError):
+ BlpImagePlugin.BlpImageFile(invalid_file)
+
+
def test_save(tmp_path: Path) -> None:
f = str(tmp_path / "temp.blp")
From f636cb8c156f53cb3acd3ebf7164113850df3f27 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 2 Jan 2025 10:28:51 +1100
Subject: [PATCH 24/76] Updated freetype to 2.13.3
---
.github/workflows/wheels-dependencies.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 4e0fad79f..9059c04a4 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -37,7 +37,7 @@ fi
ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
-FREETYPE_VERSION=2.13.2
+FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.1.0
LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.1.0
From 4c1aed801e43c6b307e7135279ca1dbc02bbf052 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 2 Jan 2025 16:00:59 +1100
Subject: [PATCH 25/76] 11.1.0 version bump
---
src/PIL/_version.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 0807f949c..9938a0afc 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
-__version__ = "11.1.0.dev0"
+__version__ = "11.1.0"
From 57786a252b2e3abd63242800ab06511bb315b2d8 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 2 Jan 2025 19:04:18 +1100
Subject: [PATCH 26/76] 11.2.0.dev0 version bump
---
src/PIL/_version.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 9938a0afc..e93c7887b 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
-__version__ = "11.1.0"
+__version__ = "11.2.0.dev0"
From 6b4619c4f5998d8d40de32de7b17b664d9b8a0db Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 2 Jan 2025 20:46:58 +1100
Subject: [PATCH 27/76] Updated macOS tested Pillow versions
---
docs/installation/platform-support.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index 3741c5956..756194679 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -77,7 +77,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+
-| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm |
+| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
| +----------------------------+------------------+ |
| | 3.8 | 10.4.0 | |
+----------------------------------+----------------------------+------------------+--------------+
From ade15fcdd3c9f41606ce560c4b5fdeb01f0025e2 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Thu, 2 Jan 2025 12:46:24 +0200
Subject: [PATCH 28/76] Upgrade zlib-ng to 2.2.3
---
.github/workflows/wheels-dependencies.sh | 2 +-
winbuild/build_prepare.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 4e0fad79f..e89db5020 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -50,7 +50,7 @@ if [[ -n "$IS_MACOS" ]]; then
else
GIFLIB_VERSION=5.2.1
fi
-ZLIB_NG_VERSION=2.2.2
+ZLIB_NG_VERSION=2.2.3
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 0674a9a15..75d6aa1bd 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -121,7 +121,7 @@ V = {
"OPENJPEG": "2.5.3",
"TIFF": "4.6.0",
"XZ": "5.6.3",
- "ZLIBNG": "2.2.2",
+ "ZLIBNG": "2.2.3",
}
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
From 2d7597ac6a431d283a65d1d17622a6d8f9918010 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 2 Jan 2025 22:50:25 +1100
Subject: [PATCH 29/76] Updated to giflib 5.2.2 on Linux
---
.github/workflows/wheels-dependencies.sh | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 4e0fad79f..71609a6f4 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -45,11 +45,7 @@ OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
-if [[ -n "$IS_MACOS" ]]; then
- GIFLIB_VERSION=5.2.2
-else
- GIFLIB_VERSION=5.2.1
-fi
+GIFLIB_VERSION=5.2.2
ZLIB_NG_VERSION=2.2.2
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
@@ -139,6 +135,14 @@ function build {
CFLAGS="$CFLAGS -O3 -DNDEBUG"
if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
+ # For giflib 5.2.2
+ elif [ -n "$IS_ALPINE" ]; then
+ apk add imagemagick
+ else
+ if [[ "$MB_ML_VER" == "_2_28" ]]; then
+ yum install -y epel-release
+ fi
+ yum install -y ImageMagick
fi
build_libwebp
CFLAGS=$ORIGINAL_CFLAGS
From 1678f7f2155beafa594c3561179f4069f9318d35 Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Thu, 2 Jan 2025 17:38:21 +0100
Subject: [PATCH 30/76] Add overloads for exif_transpose
---
src/PIL/ImageOps.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index bb29cc0d3..fef1d7328 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -22,7 +22,7 @@ import functools
import operator
import re
from collections.abc import Sequence
-from typing import Protocol, cast
+from typing import Literal, Protocol, cast, overload
from . import ExifTags, Image, ImagePalette
@@ -673,6 +673,16 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
return _lut(image, lut)
+@overload
+def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
+
+
+@overload
+def exif_transpose(
+ image: Image.Image, *, in_place: Literal[False] = False
+) -> Image.Image: ...
+
+
def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
"""
If an image has an EXIF Orientation tag, other than 1, transpose the image
From 1d771ff4a40e8eb9a38c150d18767cddf01c8a47 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Fri, 3 Jan 2025 10:26:47 +1100
Subject: [PATCH 31/76] Do not call yum on cifuzz
---
.github/workflows/wheels-dependencies.sh | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 1ebc49a88..ceb7911be 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -99,7 +99,7 @@ function build_harfbuzz {
function build {
build_xz
- if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
+ if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel
fi
build_zlib_ng
@@ -138,6 +138,8 @@ function build {
# For giflib 5.2.2
elif [ -n "$IS_ALPINE" ]; then
apk add imagemagick
+ elif [ -n "$SANITIZER" ]; then
+ apt-get install -y imagemagick
else
if [[ "$MB_ML_VER" == "_2_28" ]]; then
yum install -y epel-release
From d12e78badf1fc4a102b4bec044eb12a6bfd5d0aa Mon Sep 17 00:00:00 2001
From: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Date: Fri, 3 Jan 2025 11:00:19 +1100
Subject: [PATCH 32/76] Removed exif_transpose return type checks
---
Tests/test_file_jpeg.py | 1 -
Tests/test_imageops.py | 6 ------
2 files changed, 7 deletions(-)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index bf0dec4b8..dd62460bb 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -353,7 +353,6 @@ class TestFileJpeg:
assert exif.get_ifd(0x8825) == {}
transposed = ImageOps.exif_transpose(im)
- assert transposed is not None
exif = transposed.getexif()
assert exif.get_ifd(0x8825) == {}
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index 2fb2a60b6..7262f29e6 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -405,7 +405,6 @@ def test_exif_transpose() -> None:
else:
original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im)
- assert transposed_im is not None
assert_image_similar(base_im, transposed_im, 17)
if orientation_im is base_im:
assert "exif" not in im.info
@@ -417,7 +416,6 @@ def test_exif_transpose() -> None:
# Repeat the operation to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im)
- assert transposed_im2 is not None
assert_image_equal(transposed_im2, transposed_im)
check(base_im)
@@ -433,7 +431,6 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
- assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
transposed_im._reload_exif()
@@ -446,14 +443,12 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
- assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
# Orientation set directly on Image.Exif
im = hopper()
im.getexif()[0x0112] = 3
transposed_im = ImageOps.exif_transpose(im)
- assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
@@ -464,7 +459,6 @@ def test_exif_transpose_xml_without_xmp() -> None:
del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
- assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
From 036db2da87dba7283207b8b61cf9ca131d1223a3 Mon Sep 17 00:00:00 2001
From: "Harm.van.den.brand@alliander.com"
Date: Thu, 2 Jan 2025 16:47:24 +0100
Subject: [PATCH 33/76] OSError caused by decode error should use string
argument to be in line with rest of module
---
Tests/test_file_libtiff.py | 2 +-
src/PIL/TiffImagePlugin.py | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 9c49b1534..49d71aca7 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -1146,7 +1146,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.load()
# 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"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 61eb15243..bbbd656c6 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1406,7 +1406,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.fp = None # might be shared
if err < 0:
- raise OSError(err)
+ msg = f"decoder error {err}"
+ raise OSError(msg)
return Image.Image.load(self)
From cce0f5b653abcdf99a7bbba6757f000b3fc4cd7e Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 4 Jan 2025 10:34:59 +1100
Subject: [PATCH 34/76] Removed giflib as webp dependency
---
.github/workflows/wheels-dependencies.sh | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 1ebc49a88..05167d969 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -45,7 +45,6 @@ OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
-GIFLIB_VERSION=5.2.2
ZLIB_NG_VERSION=2.2.3
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
@@ -135,16 +134,10 @@ function build {
CFLAGS="$CFLAGS -O3 -DNDEBUG"
if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
- # For giflib 5.2.2
- elif [ -n "$IS_ALPINE" ]; then
- apk add imagemagick
- else
- if [[ "$MB_ML_VER" == "_2_28" ]]; then
- yum install -y epel-release
- fi
- yum install -y ImageMagick
fi
- build_libwebp
+ build_simple libwebp $LIBWEBP_VERSION \
+ https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
+ --enable-libwebpmux --enable-libwebpdemux
CFLAGS=$ORIGINAL_CFLAGS
build_brotli
From bd56a956594445c9b2e0bd5004f1b5c1a3f96b38 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 1 Jan 2025 12:43:50 +1100
Subject: [PATCH 35/76] Use namedtuple _replace
---
src/PIL/BlpImagePlugin.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index c932b3b9c..b8a95db87 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -374,11 +374,9 @@ class BLP1Decoder(_BLPBaseDecoder):
image = JpegImageFile(BytesIO(data))
Image._decompression_bomb_check(image.size)
if image.mode == "CMYK":
- decoder_name, extents, offset, args = image.tile[0]
+ args = image.tile[0].args
assert isinstance(args, tuple)
- image.tile = [
- ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK"))
- ]
+ image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
r, g, b = image.convert("RGB").split()
reversed_image = Image.merge("RGB", (b, g, r))
self.set_as_raw(reversed_image.tobytes())
From 73a383fa7211adf5ed8ffa43288e6bc47daa125e Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sun, 5 Jan 2025 06:11:54 +1100
Subject: [PATCH 36/76] Use rawmode instead of splitting and merging
---
src/PIL/BlpImagePlugin.py | 4 +---
src/libImaging/Unpack.c | 1 +
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index b8a95db87..8585a8e60 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -377,9 +377,7 @@ class BLP1Decoder(_BLPBaseDecoder):
args = image.tile[0].args
assert isinstance(args, tuple)
image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
- r, g, b = image.convert("RGB").split()
- reversed_image = Image.merge("RGB", (b, g, r))
- self.set_as_raw(reversed_image.tobytes())
+ self.set_as_raw(image.convert("RGB").tobytes(), "BGR")
class BLP2Decoder(_BLPBaseDecoder):
diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c
index e9203fe4d..9c3ee2665 100644
--- a/src/libImaging/Unpack.c
+++ b/src/libImaging/Unpack.c
@@ -1664,6 +1664,7 @@ static struct {
{"RGBA", "RGBaXX", 48, unpackRGBaskip2},
{"RGBA", "RGBa;16L", 64, unpackRGBa16L},
{"RGBA", "RGBa;16B", 64, unpackRGBa16B},
+ {"RGBA", "BGR", 24, ImagingUnpackBGR},
{"RGBA", "BGRa", 32, unpackBGRa},
{"RGBA", "RGBA;I", 32, unpackRGBAI},
{"RGBA", "RGBA;L", 32, unpackRGBAL},
From 4ecf8cbd75051c7213a433f80b6a9f24e4367311 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sun, 5 Jan 2025 14:49:34 +1100
Subject: [PATCH 37/76] Simplified code
---
src/_imagingft.c | 14 ++++----------
1 file changed, 4 insertions(+), 10 deletions(-)
diff --git a/src/_imagingft.c b/src/_imagingft.c
index d38279f3e..3a65007a5 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -339,29 +339,23 @@ text_layout_raqm(
len = PySequence_Fast_GET_SIZE(seq);
for (j = 0; j < len; j++) {
PyObject *item = PySequence_Fast_GET_ITEM(seq, j);
- char *feature = NULL;
- Py_ssize_t size = 0;
- PyObject *bytes;
-
if (!PyUnicode_Check(item)) {
Py_DECREF(seq);
PyErr_SetString(PyExc_TypeError, "expected a string");
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);
goto failed;
}
- feature = PyBytes_AS_STRING(bytes);
- size = PyBytes_GET_SIZE(bytes);
if (!raqm_add_font_feature(rq, feature, size)) {
Py_DECREF(seq);
- Py_DECREF(bytes);
PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed");
goto failed;
}
- Py_DECREF(bytes);
}
Py_DECREF(seq);
}
From 7708e4b524aca2e7d56fcc75eee59834622f75e8 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 6 Jan 2025 20:30:47 +1100
Subject: [PATCH 38/76] Improved Docker coverage reporting
---
.ci/after_success.sh | 6 +-----
.github/workflows/test-docker.yml | 6 +++---
2 files changed, 4 insertions(+), 8 deletions(-)
diff --git a/.ci/after_success.sh b/.ci/after_success.sh
index c71546f00..6da27b975 100755
--- a/.ci/after_success.sh
+++ b/.ci/after_success.sh
@@ -2,8 +2,4 @@
# gather the coverage data
python3 -m pip install coverage
-if [[ $MATRIX_DOCKER ]]; then
- python3 -m coverage xml --ignore-errors
-else
- python3 -m coverage xml
-fi
+python3 -m coverage xml
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 4b01a10e4..bebb9cda2 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -90,15 +90,15 @@ jobs:
- name: After success
run: |
- PATH="$PATH:~/.local/bin"
docker start pillow_container
+ sudo docker cp pillow_container:/Pillow /Pillow
+ sudo chown -R runner /Pillow
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
docker stop pillow_container
sudo mkdir -p $pil_path
sudo cp src/PIL/*.py $pil_path
+ cd /Pillow
.ci/after_success.sh
- env:
- MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage
uses: codecov/codecov-action@v5
From b1749dff08ab96a05234e1492759011ef54cbd59 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 6 Jan 2025 17:35:41 +0000
Subject: [PATCH 39/76] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.4 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.8.6)
- [github.com/pre-commit/mirrors-clang-format: v19.1.5 → v19.1.6](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.5...v19.1.6)
- [github.com/woodruffw/zizmor-pre-commit: v0.10.0 → v1.0.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v0.10.0...v1.0.0)
---
.pre-commit-config.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b76f92ec0..20fa7d04f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.8.4
+ rev: v0.8.6
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
@@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
- rev: v19.1.5
+ rev: v19.1.6
hooks:
- id: clang-format
types: [c]
@@ -57,7 +57,7 @@ repos:
- id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v0.10.0
+ rev: v1.0.0
hooks:
- id: zizmor
From 618339e2d2e9313ab8f2da0d78efec25477c4b43 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 6 Jan 2025 07:28:38 +1100
Subject: [PATCH 40/76] Allow saving multiple frames as BigTIFF
---
Tests/test_file_tiff.py | 19 +++++++++--
src/PIL/TiffImagePlugin.py | 69 +++++++++++++++++++++++---------------
2 files changed, 58 insertions(+), 30 deletions(-)
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index dedd48c20..c4a334881 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -117,10 +117,16 @@ class TestFileTiff:
def test_bigtiff_save(self, tmp_path: Path) -> None:
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:
- assert im.tag_v2._bigtiff is True
+ with Image.open(outfile) as reloaded:
+ 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:
with pytest.raises(ValueError, match="Unable to seek to frame"):
@@ -753,6 +759,13 @@ class TestFileTiff:
with pytest.raises(RuntimeError):
a.fixOffsets(1)
+ 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_saving_icc_profile(self, tmp_path: Path) -> None:
# Tests saving TIFF with icc_profile set.
# At the time of writing this will only work for non-compressed tiffs
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 61eb15243..5dd56d92b 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -962,13 +962,16 @@ class ImageFileDirectory_v2(_IFDv2Base):
result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
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
# pass 1: convert tags to binary format
# 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()):
if tag == STRIPOFFSETS:
stripoffsets = len(entries)
@@ -1024,7 +1027,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
)
# -- 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
for tag, typ, count, value, data in entries:
@@ -2043,20 +2046,21 @@ class AppendingTiffWriter(io.BytesIO):
self.offsetOfNewPage = 0
self.IIMM = iimm = self.f.read(4)
+ self._bigtiff = b"\x2B" in iimm
if not iimm:
# empty file - first page
self.isFirst = True
return
self.isFirst = False
- if iimm == b"II\x2a\x00":
- self.setEndian("<")
- elif iimm == b"MM\x00\x2a":
- self.setEndian(">")
- else:
+ if iimm not in PREFIXES:
msg = "Invalid TIFF file header"
raise RuntimeError(msg)
+ self.setEndian("<" if iimm.startswith(II) else ">")
+
+ if self._bigtiff:
+ self.f.seek(4, os.SEEK_CUR)
self.skipIFDs()
self.goToEnd()
@@ -2076,11 +2080,13 @@ class AppendingTiffWriter(io.BytesIO):
msg = "IIMM of new page doesn't match IIMM of first page"
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
assert self.whereToWriteNewIFDOffset is not None
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.fixIFD()
@@ -2126,18 +2132,20 @@ class AppendingTiffWriter(io.BytesIO):
self.endian = endian
self.longFmt = f"{self.endian}L"
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:
while True:
- ifd_offset = self.readLong()
+ ifd_offset = self._read(8 if self._bigtiff else 4)
if ifd_offset == 0:
- self.whereToWriteNewIFDOffset = self.f.tell() - 4
+ self.whereToWriteNewIFDOffset = self.f.tell() - (
+ 8 if self._bigtiff else 4
+ )
break
self.f.seek(ifd_offset)
- num_tags = self.readShort()
- self.f.seek(num_tags * 12, os.SEEK_CUR)
+ num_tags = self._read(8 if self._bigtiff else 2)
+ self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
def write(self, data: Buffer, /) -> int:
return self.f.write(data)
@@ -2185,13 +2193,17 @@ class AppendingTiffWriter(io.BytesIO):
def rewriteLastLong(self, value: int) -> None:
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:
- bytes_written = self.f.write(struct.pack(self.shortFmt, value))
- self._verify_bytes_written(bytes_written, 2)
+ self._write(value, 2)
def writeLong(self, value: int) -> None:
- bytes_written = self.f.write(struct.pack(self.longFmt, value))
- self._verify_bytes_written(bytes_written, 4)
+ self._write(value, 4)
def close(self) -> None:
self.finalize()
@@ -2199,24 +2211,27 @@ class AppendingTiffWriter(io.BytesIO):
self.f.close()
def fixIFD(self) -> None:
- num_tags = self.readShort()
+ num_tags = self._read(8 if self._bigtiff else 2)
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]
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:
- offset = self.readLong() + self.offsetOfNewPage
- self.rewriteLastLong(offset)
+ offset = self._read(fmt_size) + self.offsetOfNewPage
+ self._rewriteLast(offset, fmt_size)
if tag in self.Tags:
cur_pos = self.f.tell()
if is_local:
self._fixOffsets(count, field_size)
- self.f.seek(cur_pos + 4)
+ self.f.seek(cur_pos + fmt_size)
else:
self.f.seek(offset)
self._fixOffsets(count, field_size)
@@ -2224,7 +2239,7 @@ class AppendingTiffWriter(io.BytesIO):
elif is_local:
# 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:
for i in range(count):
From a8381c619de0c244785377322ed8bf115a899146 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 6 Jan 2025 07:28:51 +1100
Subject: [PATCH 41/76] Allow upgrading LONG to LONG8
---
Tests/test_file_tiff.py | 26 ++++++++++++++++++++++++-
src/PIL/TiffImagePlugin.py | 39 ++++++++++++++++++++++++--------------
2 files changed, 50 insertions(+), 15 deletions(-)
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index c4a334881..757d3f96a 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -746,7 +746,7 @@ class TestFileTiff:
assert reread.n_frames == 3
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:
b.seek(0)
a.fixOffsets(1, isShort=True)
@@ -759,6 +759,23 @@ class TestFileTiff:
with pytest.raises(RuntimeError):
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)
@@ -766,6 +783,13 @@ class TestFileTiff:
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:
# Tests saving TIFF with icc_profile set.
# At the time of writing this will only work for non-compressed tiffs
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 5dd56d92b..8179b7f5b 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -2175,17 +2175,19 @@ class AppendingTiffWriter(io.BytesIO):
msg = f"wrote only {bytes_written} bytes but wanted {expected}"
raise RuntimeError(msg)
- def rewriteLastShortToLong(self, value: int) -> None:
- self.f.seek(-2, os.SEEK_CUR)
- bytes_written = self.f.write(struct.pack(self.longFmt, value))
- self._verify_bytes_written(bytes_written, 4)
-
- def _rewriteLast(self, value: int, field_size: int) -> None:
+ def _rewriteLast(
+ self, value: int, field_size: int, new_field_size: int = 0
+ ) -> None:
self.f.seek(-field_size, os.SEEK_CUR)
+ if not new_field_size:
+ new_field_size = field_size
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:
return self._rewriteLast(value, 2)
@@ -2245,18 +2247,27 @@ class AppendingTiffWriter(io.BytesIO):
for i in range(count):
offset = self._read(field_size)
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:
msg = "not implemented"
raise RuntimeError(msg) # XXX TODO
# simple case - the offset is just one and therefore it is
# local (not referenced with another offset)
- self.rewriteLastShortToLong(offset)
- self.f.seek(-10, os.SEEK_CUR)
- self.writeShort(TiffTags.LONG) # rewrite the type to LONG
- self.f.seek(8, os.SEEK_CUR)
+ self._rewriteLast(offset, field_size, new_field_size)
+ # Move back past the new offset, past 'count', and before 'field_type'
+ rewind = -new_field_size - 4 - 2
+ self.f.seek(rewind, os.SEEK_CUR)
+ self.writeShort(new_field_size) # rewrite the type
+ self.f.seek(2 - rewind, os.SEEK_CUR)
else:
self._rewriteLast(offset, field_size)
From aef6df2d04bfe86b4a69ab7d93786ea55a3e7340 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 7 Jan 2025 21:33:57 +1100
Subject: [PATCH 42/76] Use ImageFile._Tile
---
Tests/test_file_jpeg.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index dd62460bb..526c6a5b6 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -1030,7 +1030,7 @@ class TestFileJpeg:
with Image.open(TEST_FILE) as im:
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
im.load()
From f36c66746705245dec44b225868bce727dca0385 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 7 Jan 2025 22:24:08 +1100
Subject: [PATCH 43/76] Improved test coverage
---
Tests/test_file_spider.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py
index 4cafda865..713db848d 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -7,7 +7,7 @@ from pathlib import Path
import pytest
-from PIL import Image, ImageSequence, SpiderImagePlugin
+from PIL import Image, SpiderImagePlugin
from .helper import assert_image_equal, hopper, is_pypy
@@ -153,8 +153,8 @@ def test_nonstack_file() -> None:
def test_nonstack_dos() -> None:
with Image.open(TEST_FILE) as im:
- for i, frame in enumerate(ImageSequence.Iterator(im)):
- assert i <= 1, "Non-stack DOS file test failed"
+ with pytest.raises(EOFError):
+ im.seek(0)
# for issue #4093
From 86b8e1e45fa1d425aafa444024003eb3b75d8a9d Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 8 Jan 2025 10:19:09 +1100
Subject: [PATCH 44/76] Updated libpng to 1.6.45
---
.github/workflows/wheels-dependencies.sh | 2 +-
winbuild/build_prepare.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 58621bca1..410255b7e 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -39,7 +39,7 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.1.0
-LIBPNG_VERSION=1.6.44
+LIBPNG_VERSION=1.6.45
JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 75d6aa1bd..912579ce7 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -116,7 +116,7 @@ V = {
"HARFBUZZ": "10.1.0",
"JPEGTURBO": "3.1.0",
"LCMS2": "2.16",
- "LIBPNG": "1.6.44",
+ "LIBPNG": "1.6.45",
"LIBWEBP": "1.5.0",
"OPENJPEG": "2.5.3",
"TIFF": "4.6.0",
From ee2b8c525632f76bde730805d11d19bbb22f1b2b Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 8 Jan 2025 10:26:21 +1100
Subject: [PATCH 45/76] Switch to .tar.gz for libpng
---
winbuild/build_prepare.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 912579ce7..b9695d1d8 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -123,7 +123,6 @@ V = {
"XZ": "5.6.3",
"ZLIBNG": "2.2.3",
}
-V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
@@ -241,8 +240,8 @@ DEPS: dict[str, dict[str, Any]] = {
},
"libpng": {
"url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/"
- f"lpng{V['LIBPNG_DOTLESS']}.zip/download",
- "filename": f"lpng{V['LIBPNG_DOTLESS']}.zip",
+ f"FILENAME/download",
+ "filename": f"libpng-{V['LIBPNG']}.tar.gz",
"license": "LICENSE",
"build": [
*cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"),
From f281eb9b469320f29006d7454d9c38a974ad65c1 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Wed, 8 Jan 2025 18:27:20 +1100
Subject: [PATCH 46/76] Trigger from changes in pyproject.toml
---
.github/workflows/wheels.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 3b22ee98a..fd89f7585 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -13,6 +13,7 @@ on:
paths:
- ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
+ - "pyproject.toml"
- "setup.py"
- "wheels/*"
- "winbuild/build_prepare.py"
@@ -23,6 +24,7 @@ on:
paths:
- ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
+ - "pyproject.toml"
- "setup.py"
- "wheels/*"
- "winbuild/build_prepare.py"
From 84c8e38b2d88d0c4fb733d9c6e44e6af13e2e05e Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 8 Jan 2025 07:38:51 +0000
Subject: [PATCH 47/76] Update cygwin/cygwin-install-action action to v5
---
.github/workflows/test-cygwin.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 5b0a03946..abfeaa77f 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -52,7 +52,7 @@ jobs:
persist-credentials: false
- name: Install Cygwin
- uses: cygwin/cygwin-install-action@v4
+ uses: cygwin/cygwin-install-action@v5
with:
packages: >
gcc-g++
From 2eb112329e4df9ca4ea15235c2743da8f6b0eab8 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 9 Jan 2025 08:32:42 +1100
Subject: [PATCH 48/76] Use python-pip instead of python3-pip
---
.github/workflows/test-mingw.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index a1d6ba61c..6b1b36cb0 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -68,7 +68,7 @@ jobs:
mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-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-cov \
mingw-w64-x86_64-python-pytest-timeout \
From 440b09e831873624f9e843adf658633b25983e77 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 9 Jan 2025 14:32:17 +1100
Subject: [PATCH 49/76] Removed unused mode argument from
assert_image_similar_tofile
---
Tests/helper.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/Tests/helper.py b/Tests/helper.py
index d6a93a803..3f45498bb 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -140,11 +140,8 @@ def assert_image_similar_tofile(
filename: str,
epsilon: float,
msg: str | None = None,
- mode: str | None = None,
) -> None:
with Image.open(filename) as img:
- if mode:
- img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg)
From aa686894a63cb2f15349e9592eedb95aebfc6f1f Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 9 Jan 2025 14:32:46 +1100
Subject: [PATCH 50/76] Removed unused assert_all_same
---
Tests/helper.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/Tests/helper.py b/Tests/helper.py
index 3f45498bb..126644c15 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -145,10 +145,6 @@ def assert_image_similar_tofile(
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:
assert items.count(items[0]) != len(items), msg
From f938af5c3cc9dd7c48f8a06a7af4870f1faf2e76 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 9 Jan 2025 14:38:07 +1100
Subject: [PATCH 51/76] Do not catch exception only to assert it is None
---
Tests/test_file_apng.py | 16 +++-------------
1 file changed, 3 insertions(+), 13 deletions(-)
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index ee6c867c3..9d5154fca 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -307,13 +307,8 @@ def test_apng_syntax_errors() -> None:
im.load()
# we can handle this case gracefully
- exception = None
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
- try:
- im.seek(im.n_frames - 1)
- except Exception as e:
- exception = e
- assert exception is None
+ im.seek(im.n_frames - 1)
with pytest.raises(OSError):
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,
)
with Image.open(test_file) as im:
- exception = None
- try:
- im.seek(im.n_frames - 1)
- im.load()
- except Exception as e:
- exception = e
- assert exception is None
+ im.seek(im.n_frames - 1)
+ im.load()
def test_apng_save_duration_loop(tmp_path: Path) -> None:
From a34a9cd6d1d4b346573fca7336f1cb82918af4b3 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 9 Jan 2025 14:49:48 +1100
Subject: [PATCH 52/76] Improved test coverage
---
Tests/test_file_iptc.py | 5 +----
Tests/test_file_jpeg2k.py | 3 +--
Tests/test_file_libtiff.py | 6 +-----
Tests/test_image.py | 2 --
4 files changed, 3 insertions(+), 13 deletions(-)
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index 8a7c59fb1..c6c0c1aab 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -58,10 +58,7 @@ def test_getiptcinfo_fotostation() -> None:
# Assert
assert iptc is not None
- for tag in iptc.keys():
- if tag[0] == 240:
- return
- pytest.fail("FotoStation tag not found")
+ assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found"
def test_getiptcinfo_zero_padding() -> None:
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index dbc2e49ec..711e988df 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -492,8 +492,7 @@ def test_plt_marker(card: ImageFile.ImageFile) -> None:
out.seek(0)
while True:
marker = out.read(2)
- if not marker:
- pytest.fail("End of stream without PLT")
+ assert marker, "End of stream without PLT"
jp2_boxid = _binary.i16be(marker)
if jp2_boxid == 0xFF4F:
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 49d71aca7..18dd11182 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -36,11 +36,7 @@ class LibTiffTestCase:
im.load()
im.getdata()
- try:
- assert im._compression == "group4"
- except AttributeError:
- print("No _compression")
- print(dir(im))
+ assert im._compression == "group4"
# can we write it back out, in a different form.
out = str(tmp_path / "temp.png")
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 092bc07f6..fe43cea40 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -189,8 +189,6 @@ class TestImage:
if ext == ".jp2" and not features.check_codec("jpg_2000"):
pytest.skip("jpg_2000 not available")
temp_file = str(tmp_path / ("temp." + ext))
- if os.path.exists(temp_file):
- os.remove(temp_file)
im.save(Path(temp_file))
def test_fp_name(self, tmp_path: Path) -> None:
From 4d14991604d34f73aa5d426340adf773c81cdc6a Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 9 Jan 2025 14:58:58 +1100
Subject: [PATCH 53/76] Corrected argument types
---
Tests/test_image.py | 4 ++--
Tests/test_image_resize.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 092bc07f6..a72694ec1 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -667,7 +667,7 @@ class TestImage:
# Test illegal image mode
with hopper() as im:
with pytest.raises(ValueError):
- im.remap_palette(None)
+ im.remap_palette([])
def test_remap_palette_transparency(self) -> None:
im = Image.new("P", (1, 2), (0, 0, 0))
@@ -770,7 +770,7 @@ class TestImage:
assert dict(exif)
# Test that exif data is cleared after another load
- exif.load(None)
+ exif.load(b"")
assert not dict(exif)
# Test loading just the EXIF header
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index 57fcf9a34..1166371b8 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -309,7 +309,7 @@ class TestImageResize:
# Test unknown resampling filter
with hopper() as im:
with pytest.raises(ValueError):
- im.resize((10, 10), "unknown")
+ im.resize((10, 10), -1)
@skip_unless_feature("libtiff")
def test_transposed(self) -> None:
From 8603d6512a2e6f534d6e0fb4a9307bc3edd4f0fa Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Thu, 9 Jan 2025 14:22:29 +0200
Subject: [PATCH 54/76] Use python-numpy and python-olefile instead of
python3-numpy and python3-olefile
---
.github/workflows/test-mingw.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index 6b1b36cb0..bb6d7dc37 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -66,8 +66,8 @@ jobs:
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
- mingw-w64-x86_64-python3-numpy \
- mingw-w64-x86_64-python3-olefile \
+ mingw-w64-x86_64-python-numpy \
+ mingw-w64-x86_64-python-olefile \
mingw-w64-x86_64-python-pip \
mingw-w64-x86_64-python-pytest \
mingw-w64-x86_64-python-pytest-cov \
From 0d93c030a576e30d3991bf5547f2e0c3e47889a9 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Fri, 10 Jan 2025 19:10:42 +1100
Subject: [PATCH 55/76] Test passes in Python 3.13
---
Tests/test_image_access.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index bb30b462d..6d8b4d355 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -271,7 +271,7 @@ class TestImagePutPixelError:
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")
def test_embeddable(self) -> None:
import ctypes
From 64bfdff6c8111225a1c7e4480376738123f2cb15 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Fri, 10 Jan 2025 21:51:33 +1100
Subject: [PATCH 56/76] Only F mode starts with F
---
src/PIL/SpiderImagePlugin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 3a87d009a..b26e1a996 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -267,7 +267,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]:
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")
hdr = makeSpiderHeader(im)
From 7166a09538bbadbeb9d02f1d9af0c8d70022a80b Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 11 Jan 2025 18:57:41 +1100
Subject: [PATCH 57/76] Skip test_embeddable if compiler cannot be initialized
---
Tests/test_image_access.py | 19 ++++++++++++-------
1 file changed, 12 insertions(+), 7 deletions(-)
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index 6d8b4d355..14a5e2e7b 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -278,6 +278,18 @@ class TestEmbeddable:
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:
home = sys.prefix.replace("\\", "\\\\")
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"])
compiler.link_executable(objects, "embed_pil")
From 5ad98e7abb19710cfb0c6c70ad52b543b1c5769b Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 13 Jan 2025 07:54:43 +1100
Subject: [PATCH 58/76] Moved get_child_images()
---
src/PIL/Image.py | 46 ------------------------------------------
src/PIL/ImageFile.py | 48 +++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 47 insertions(+), 47 deletions(-)
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index dff3d063b..0648161be 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1553,52 +1553,6 @@ class Image:
self._exif._loaded = False
self.getexif()
- def get_child_images(self) -> list[ImageFile.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:
- 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:
"""
Returns a capsule that points to the internal image memory.
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 5d0f87a9f..b2a44cb4d 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -36,7 +36,7 @@ import struct
import sys
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
-from . import Image
+from . import ExifTags, Image
from ._deprecate import deprecate
from ._util import is_path
@@ -163,6 +163,52 @@ class ImageFile(Image.Image):
def _open(self) -> None:
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:
+ 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 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:
+ self.fp.seek(offset)
+ return child_images
+
def get_format_mimetype(self) -> str | None:
if self.custom_mimetype:
return self.custom_mimetype
From 34762ded7500a20f938f98250df6e650608cd57e Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 13 Jan 2025 07:57:28 +1100
Subject: [PATCH 59/76] Assert JpegIFByteCount is int
---
src/PIL/ImageFile.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index b2a44cb4d..f905b34b6 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -191,7 +191,10 @@ class ImageFile(Image.Image):
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))
+
+ 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 a922126ed7495466b2193c6cf582ade11f0f8fe5 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 13 Jan 2025 07:57:50 +1100
Subject: [PATCH 60/76] Assert fp is not None
---
src/PIL/ImageFile.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index f905b34b6..3476e48ff 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -180,6 +180,7 @@ class ImageFile(Image.Image):
ifds.append((ifd1, exif._info.next))
offset = None
+ assert self.fp is not None
for ifd, ifd_offset in ifds:
current_offset = self.fp.tell()
if offset is None:
From a4018d192cf8a305c3da622a53df7d144d11432c Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 13 Jan 2025 21:07:32 +1100
Subject: [PATCH 61/76] Added Sphinx configuration key
---
.readthedocs.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.readthedocs.yml b/.readthedocs.yml
index def6282dd..3e03c76ea 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -1,5 +1,8 @@
version: 2
+sphinx:
+ configuration: docs/conf.py
+
formats: [pdf]
build:
From 2ce2ff297c9d3577e3bcc8f723107f661590afd2 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Mon, 13 Jan 2025 20:37:26 +1100
Subject: [PATCH 62/76] Test Python 3.14 pre-release
---
.github/workflows/test-windows.yml | 2 +-
.github/workflows/test.yml | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index d905a3925..b76b00eaa 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]
+ python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
timeout-minutes: 30
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 83a696f5f..e3efe0b59 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -42,6 +42,7 @@ jobs:
]
python-version: [
"pypy3.10",
+ "3.14",
"3.13t",
"3.13",
"3.12",
From 0f2c554c698266ec0ba464f21a616cd0eda9a7fb Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 14 Jan 2025 21:03:20 +1100
Subject: [PATCH 63/76] Improved comment
---
Tests/oss-fuzz/test_fuzzers.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py
index 90eb8713a..50d1467fc 100644
--- a/Tests/oss-fuzz/test_fuzzers.py
+++ b/Tests/oss-fuzz/test_fuzzers.py
@@ -32,21 +32,18 @@ def test_fuzz_images(path: str) -> None:
fuzzers.fuzz_image(f.read())
assert True
except (
+ # Known exceptions from Pillow
OSError,
SyntaxError,
MemoryError,
ValueError,
NotImplementedError,
OverflowError,
- ):
- # Known exceptions that are through from Pillow
- assert True
- except (
+ # Known Image.* exceptions
Image.DecompressionBombError,
Image.DecompressionBombWarning,
UnidentifiedImageError,
):
- # Known Image.* exceptions
assert True
finally:
fuzzers.disable_decompressionbomb_error()
From cf438c53eed627e62baff752cb874aad3bcf63d5 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Tue, 14 Jan 2025 21:04:08 +1100
Subject: [PATCH 64/76] Removed UnidentifiedImageError, as it inherits from
OSError
---
Tests/oss-fuzz/test_fuzzers.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py
index 50d1467fc..e42ec90aa 100644
--- a/Tests/oss-fuzz/test_fuzzers.py
+++ b/Tests/oss-fuzz/test_fuzzers.py
@@ -7,7 +7,7 @@ import fuzzers
import packaging
import pytest
-from PIL import Image, UnidentifiedImageError, features
+from PIL import Image, features
from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"):
@@ -42,7 +42,6 @@ def test_fuzz_images(path: str) -> None:
# Known Image.* exceptions
Image.DecompressionBombError,
Image.DecompressionBombWarning,
- UnidentifiedImageError,
):
assert True
finally:
From e70c821436763e09813f0f53c76aaa3bb4fa76f8 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 16 Jan 2025 20:57:49 +1100
Subject: [PATCH 65/76] Removed miniconda CPPFLAGS
---
.ci/build.sh | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.ci/build.sh b/.ci/build.sh
index e678f68ec..ae10cb671 100755
--- a/.ci/build.sh
+++ b/.ci/build.sh
@@ -3,8 +3,5 @@
set -e
python3 -m coverage erase
-if [ $(uname) == "Darwin" ]; then
- export CPPFLAGS="-I/usr/local/miniconda/include";
-fi
make clean
make install-coverage
From 536aee5bbde0fb432c1b22d2d0eed7bd35dbfca2 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Thu, 16 Jan 2025 22:12:53 +1100
Subject: [PATCH 66/76] Test Numpy on amd64
---
.github/workflows/wheels-test.ps1 | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1
index f593c7228..a1edc14ef 100644
--- a/.github/workflows/wheels-test.ps1
+++ b/.github/workflows/wheels-test.ps1
@@ -11,6 +11,9 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
$env:path += ";$pillow\winbuild\build\bin\"
& "$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
+if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
+ & python -m pip install numpy
+}
cd $pillow
& python -VV
if (!$?) { exit $LASTEXITCODE }
From c67ed4678bf16be8630561ce0eb49eb0a2d3be40 Mon Sep 17 00:00:00 2001
From: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Date: Thu, 16 Jan 2025 23:48:44 +1100
Subject: [PATCH 67/76] Moved strings inside debug statement
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
---
src/PIL/TiffImagePlugin.py | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index a246994ef..c871342fc 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -2202,11 +2202,15 @@ class AppendingTiffWriter(io.BytesIO):
if tag in self.Tags:
cur_pos = self.f.tell()
- tagname = TiffTags.lookup(tag).name
- typname = TYPES.get(field_type, "unknown")
- msg = f"fixIFD: {tagname} ({tag}) - type: {typname} ({field_type})"
- msg += f"- type size: {field_size} - count: {count}"
- logger.debug(msg)
+ 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:
self._fixOffsets(count, field_size)
From a04e76a84ff7122de52ad381e1d44dc556040c36 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Fri, 17 Jan 2025 11:51:21 +1100
Subject: [PATCH 68/76] Use arm64 Linux runners
---
.github/workflows/wheels.yml | 71 +++++++-----------------------------
1 file changed, 13 insertions(+), 58 deletions(-)
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index fd89f7585..0402f1b54 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -42,62 +42,7 @@ env:
FORCE_COLOR: 1
jobs:
- build-1-QEMU-emulated-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:
+ build-native-wheels:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
@@ -132,6 +77,14 @@ jobs:
cibw_arch: x86_64
build: "*manylinux*"
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:
- uses: actions/checkout@v4
with:
@@ -153,6 +106,8 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_ENABLE: cpython-prerelease cpython-freethreading
+ CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }}
+ CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_SKIP: pp39-*
@@ -275,7 +230,7 @@ jobs:
scientific-python-nightly-wheels-publish:
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
name: Upload wheels to scientific-python-nightly-wheels
steps:
@@ -292,7 +247,7 @@ jobs:
pypi-publish:
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
name: Upload release to PyPI
environment:
From 176c5b3749fe4642186dceb4c4253e4cd3e60e03 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Fri, 17 Jan 2025 11:51:42 +1100
Subject: [PATCH 69/76] Added pypy to CIBW_ENABLE
---
.github/workflows/wheels.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 0402f1b54..db8e4d58b 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -105,7 +105,7 @@ jobs:
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
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 }}
@@ -184,7 +184,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
- CIBW_ENABLE: cpython-prerelease cpython-freethreading
+ CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
From be8e55d28d3525b05769aee5f36b945bd6e01f77 Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Fri, 17 Jan 2025 18:34:23 +1100
Subject: [PATCH 70/76] Added deprecation warning
---
Tests/test_image.py | 5 ++++
docs/deprecations.rst | 10 +++++++
docs/releasenotes/11.2.0.rst | 58 ++++++++++++++++++++++++++++++++++++
docs/releasenotes/index.rst | 1 +
src/PIL/Image.py | 6 ++++
src/PIL/ImageFile.py | 3 +-
src/PIL/_deprecate.py | 2 ++
7 files changed, 84 insertions(+), 1 deletion(-)
create mode 100644 docs/releasenotes/11.2.0.rst
diff --git a/Tests/test_image.py b/Tests/test_image.py
index fe43cea40..108013463 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -989,6 +989,11 @@ class TestImage:
else:
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)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size)
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 80966ca36..634cee689 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -183,6 +183,16 @@ ExifTags.IFD.Makernote
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``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
----------------
diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst
new file mode 100644
index 000000000..025b64660
--- /dev/null
+++ b/docs/releasenotes/11.2.0.rst
@@ -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
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index bd8e5536f..be9f623d0 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 11.2.0
11.1.0
11.0.0
10.4.0
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 0648161be..e512e6fc7 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1553,6 +1553,12 @@ class Image:
self._exif._loaded = False
self.getexif()
+ def get_child_images(self) -> list[ImageFile.ImageFile]:
+ from . import ImageFile
+
+ deprecate("Image.Image.get_child_images", 13)
+ return ImageFile.ImageFile.get_child_images(self) # type: ignore[arg-type]
+
def getim(self) -> CapsuleType:
"""
Returns a capsule that points to the internal image memory.
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 3476e48ff..93fb47874 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -180,8 +180,8 @@ class ImageFile(Image.Image):
ifds.append((ifd1, exif._info.next))
offset = None
- assert self.fp is not 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
@@ -210,6 +210,7 @@ class ImageFile(Image.Image):
child_images.append(im)
if offset is not None:
+ assert self.fp is not None
self.fp.seek(offset)
return child_images
diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py
index 83952b397..9f9d8bbc9 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -47,6 +47,8 @@ def deprecate(
raise RuntimeError(msg)
elif when == 12:
removed = "Pillow 12 (2025-10-15)"
+ elif when == 13:
+ removed = "Pillow 13 (2026-10-15)"
else:
msg = f"Unknown removal version: {when}. Update {__name__}?"
raise ValueError(msg)
From 6a0ac411e26b2b3436f3790e6830dfe18a914ffc Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Fri, 17 Jan 2025 18:57:12 +1100
Subject: [PATCH 71/76] Added mozjpeg documentation
---
docs/reference/features.rst | 1 +
docs/releasenotes/11.2.0.rst | 58 ++++++++++++++++++++++++++++++++++++
docs/releasenotes/index.rst | 1 +
3 files changed, 60 insertions(+)
create mode 100644 docs/releasenotes/11.2.0.rst
diff --git a/docs/reference/features.rst b/docs/reference/features.rst
index 427c0f606..e5fdca240 100644
--- a/docs/reference/features.rst
+++ b/docs/reference/features.rst
@@ -54,6 +54,7 @@ Feature version numbers are available only where stated.
Support for the following features can be checked:
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
+* ``mozjpeg``: (compile time) Whether Pillow was compiled against the mozjpeg version of libjpeg. Compile-time version number is available.
* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available.
* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst
new file mode 100644
index 000000000..f9eff3c07
--- /dev/null
+++ b/docs/releasenotes/11.2.0.rst
@@ -0,0 +1,58 @@
+11.2.0
+------
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+:cve:`YYYY-XXXXX`: TODO
+^^^^^^^^^^^^^^^^^^^^^^^
+
+TODO
+
+Backwards Incompatible Changes
+==============================
+
+TODO
+^^^^
+
+Deprecations
+============
+
+TODO
+^^^^
+
+TODO
+
+API Changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API Additions
+=============
+
+Check for mozjpeg
+^^^^^^^^^^^^^^^^^
+
+You can check if Pillow has been built against the mozjpeg version of the
+libjpeg library, and what version of mozjpeg is being used::
+
+ from PIL import features
+ features.check_feature("mozjpeg") # True or False
+ features.version_feature("mozjpeg") # "4.1.1" for example, or None
+
+Other Changes
+=============
+
+TODO
+^^^^
+
+TODO
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index bd8e5536f..be9f623d0 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 11.2.0
11.1.0
11.0.0
10.4.0
From 30c4ad484c1e784ea316e53083381e823b26a2f0 Mon Sep 17 00:00:00 2001
From: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Date: Sat, 18 Jan 2025 07:48:15 +1100
Subject: [PATCH 72/76] Updated capitalization
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
---
docs/reference/features.rst | 2 +-
docs/releasenotes/11.2.0.rst | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/reference/features.rst b/docs/reference/features.rst
index e5fdca240..0e173fe87 100644
--- a/docs/reference/features.rst
+++ b/docs/reference/features.rst
@@ -54,7 +54,7 @@ Feature version numbers are available only where stated.
Support for the following features can be checked:
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
-* ``mozjpeg``: (compile time) Whether Pillow was compiled against the mozjpeg version of libjpeg. Compile-time version number is available.
+* ``mozjpeg``: (compile time) Whether Pillow was compiled against the MozJPEG version of libjpeg. Compile-time version number is available.
* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available.
* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst
index f9eff3c07..f1e15377e 100644
--- a/docs/releasenotes/11.2.0.rst
+++ b/docs/releasenotes/11.2.0.rst
@@ -42,7 +42,7 @@ API Additions
Check for mozjpeg
^^^^^^^^^^^^^^^^^
-You can check if Pillow has been built against the mozjpeg version of the
+You can check if Pillow has been built against the MozJPEG version of the
libjpeg library, and what version of mozjpeg is being used::
from PIL import features
From 284297755a8d982f327c08f391192a6a57937d2b Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 18 Jan 2025 07:55:49 +1100
Subject: [PATCH 73/76] Updated capitalization
---
docs/releasenotes/11.2.0.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst
index 725de5092..df28d05af 100644
--- a/docs/releasenotes/11.2.0.rst
+++ b/docs/releasenotes/11.2.0.rst
@@ -44,11 +44,11 @@ TODO
API Additions
=============
-Check for mozjpeg
+Check for MozJPEG
^^^^^^^^^^^^^^^^^
You can check if Pillow has been built against the MozJPEG version of the
-libjpeg library, and what version of mozjpeg is being used::
+libjpeg library, and what version of MozJPEG is being used::
from PIL import features
features.check_feature("mozjpeg") # True or False
From ba606622b4441b87ec74d420a25f0c60882004eb Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 18 Jan 2025 13:53:39 +1100
Subject: [PATCH 74/76] Updated Ubuntu arm to 24.04 with arm64 runner
---
.github/workflows/test-docker.yml | 9 +++++----
docs/installation/platform-support.rst | 6 ++----
2 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index bebb9cda2..0d9033413 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -29,13 +29,13 @@ concurrency:
jobs:
build:
- runs-on: ubuntu-latest
+ runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
+ os: ["ubuntu-latest"]
docker: [
# Run slower jobs first to give them a headstart and reduce waiting time
- ubuntu-22.04-jammy-arm64v8,
ubuntu-24.04-noble-ppc64le,
ubuntu-24.04-noble-s390x,
# Then run the remainder
@@ -55,12 +55,13 @@ jobs:
]
dockerTag: [main]
include:
- - docker: "ubuntu-22.04-jammy-arm64v8"
- qemu-arch: "aarch64"
- docker: "ubuntu-24.04-noble-ppc64le"
qemu-arch: "ppc64le"
- docker: "ubuntu-24.04-noble-s390x"
qemu-arch: "s390x"
+ - docker: "ubuntu-24.04-noble-arm64v8"
+ os: "ubuntu-24.04-arm"
+ dockerTag: main
name: ${{ matrix.docker }}
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index 814d6a9cf..9eafad3c4 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -44,11 +44,9 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
-| +----------------------------+---------------------+
-| | 3.10 | arm64v8 |
+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
-| | | s390x |
+| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, |
+| | | ppc64le, s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2019 | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+
From 4ff18e03b8a703bbc15a9d19ce253c19f1820b5c Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 18 Jan 2025 20:57:04 +1100
Subject: [PATCH 75/76] Moved file pointer handling into ImageFile close
---
src/PIL/Image.py | 9 ---------
src/PIL/ImageFile.py | 23 +++++++++++++++++++++++
2 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index e512e6fc7..a0c9ff8eb 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -619,8 +619,6 @@ class Image:
def close(self) -> None:
"""
- Closes the file pointer, if possible.
-
This operation will destroy the image core and release its memory.
The image data will be unusable afterward.
@@ -629,13 +627,6 @@ class Image:
:py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
more information.
"""
- if hasattr(self, "fp"):
- try:
- self._close_fp()
- self.fp = None
- except Exception as msg:
- logger.debug("Error closing: %s", msg)
-
if getattr(self, "map", None):
self.map: mmap.mmap | None = None
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 93fb47874..d716e3b5c 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -31,6 +31,7 @@ from __future__ import annotations
import abc
import io
import itertools
+import logging
import os
import struct
import sys
@@ -43,6 +44,8 @@ from ._util import is_path
if TYPE_CHECKING:
from ._typing import StrOrBytesPath
+logger = logging.getLogger(__name__)
+
MAXBLOCK = 65536
SAFEBLOCK = 1024 * 1024
@@ -163,6 +166,26 @@ class ImageFile(Image.Image):
def _open(self) -> None:
pass
+ def close(self) -> None:
+ """
+ Closes the file pointer, if possible.
+
+ This operation will destroy the image core and release its memory.
+ The image data will be unusable afterward.
+
+ This function is required to close images that have multiple frames or
+ have not had their file read and closed by the
+ :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
+ more information.
+ """
+ try:
+ self._close_fp()
+ self.fp = None
+ except Exception as msg:
+ logger.debug("Error closing: %s", msg)
+
+ super().close()
+
def get_child_images(self) -> list[ImageFile]:
child_images = []
exif = self.getexif()
From c78d23d5471dc24b20f0eb387442e63ab0c63f9b Mon Sep 17 00:00:00 2001
From: Andrew Murray
Date: Sat, 18 Jan 2025 21:22:44 +1100
Subject: [PATCH 76/76] Moved _close_fp into ImageFile
---
src/PIL/Image.py | 12 +++---------
src/PIL/ImageFile.py | 10 +++++++++-
2 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index a0c9ff8eb..99b1b9ab3 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -603,16 +603,10 @@ class Image:
def __enter__(self):
return self
- def _close_fp(self):
- if getattr(self, "_fp", False):
- if self._fp != self.fp:
- self._fp.close()
- self._fp = DeferredError(ValueError("Operation on closed image"))
- if self.fp:
- self.fp.close()
-
def __exit__(self, *args):
- if hasattr(self, "fp"):
+ from . import ImageFile
+
+ if isinstance(self, ImageFile.ImageFile):
if getattr(self, "_exclusive_fp", False):
self._close_fp()
self.fp = None
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index d716e3b5c..c3901d488 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -39,7 +39,7 @@ from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
from . import ExifTags, Image
from ._deprecate import deprecate
-from ._util import is_path
+from ._util import DeferredError, is_path
if TYPE_CHECKING:
from ._typing import StrOrBytesPath
@@ -166,6 +166,14 @@ class ImageFile(Image.Image):
def _open(self) -> None:
pass
+ def _close_fp(self):
+ if getattr(self, "_fp", False):
+ if self._fp != self.fp:
+ self._fp.close()
+ self._fp = DeferredError(ValueError("Operation on closed image"))
+ if self.fp:
+ self.fp.close()
+
def close(self) -> None:
"""
Closes the file pointer, if possible.