From 0beb2228f9adb387a87c6b944cda2e427f5f264a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 12:44:25 +1100 Subject: [PATCH 001/140] Include JpegImageFile layers in state --- Tests/test_pickle.py | 11 +++++++++++ src/PIL/Image.py | 2 +- src/PIL/JpegImagePlugin.py | 7 +++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index d250ba369..c4f8de013 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -74,6 +74,17 @@ def test_pickle_image( helper_pickle_file(tmp_path, protocol, test_file, test_mode) +def test_pickle_jpeg() -> None: + # Arrange + with Image.open("Tests/images/hopper.jpg") as image: + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) + + # Assert + assert len(unpickled_image.layer) == 3 + assert unpickled_image.layers == 3 + + def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = str(tmp_path / "temp.pkl") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c..ec5ce6cb1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -763,7 +763,7 @@ class Image: def __setstate__(self, state: list[Any]) -> None: Image.__init__(self) - info, mode, size, palette, data = state + info, mode, size, palette, data = state[:5] self.info = info self._mode = mode self._size = size diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e..7dec75218 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -395,6 +395,13 @@ class JpegImageFile(ImageFile.ImageFile): return getattr(self, "_" + name) raise AttributeError(name) + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.layers, self.layer] + + def __setstate__(self, state: list[Any]) -> None: + super().__setstate__(state) + self.layers, self.layer = state[5:] + def load_read(self, read_bytes: int) -> bytes: """ internal: read more image data From 98f975dbbe6ae0ef521522cdea6e00fdc81449c0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 18:56:23 +1100 Subject: [PATCH 002/140] Do not save XMP from info --- Tests/test_file_jpeg.py | 9 +++++++-- src/PIL/JpegImagePlugin.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cde951395..1b889eb40 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -998,8 +998,13 @@ class TestFileJpeg: with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"XMP test" - im.info["xmp"] = b"1" * 65504 - im.save(f) + # Check that XMP is not saved from image info + reloaded.save(f) + + with Image.open(f) as reloaded: + assert "xmp" not in reloaded.info + + im.save(f, xmp=b"1" * 65504) with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"1" * 65504 diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e..6937f2650 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -751,7 +751,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - xmp = info.get("xmp", im.info.get("xmp")) + xmp = info.get("xmp") if xmp: overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len From 203ca12626b22d185b59199b60ceb55081f6c3b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 19:09:22 +1100 Subject: [PATCH 003/140] Allow encoderinfo to be set for appended images --- Tests/test_file_mpo.py | 12 ++++++++++++ src/PIL/Image.py | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index e0f42a266..39aaa1613 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -293,3 +293,15 @@ def test_save_all() -> None: # Test that a single frame image will not be saved as an MPO jpg = roundtrip(im, save_all=True) assert "mp" not in jpg.info + + +def test_save_xmp() -> None: + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im2.encoderinfo = {"xmp": b"Second frame"} + im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) + + assert im_reloaded.info["xmp"] == b"First frame" + + im_reloaded.seek(1) + assert im_reloaded.info["xmp"] == b"Second frame" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c..586da7d3d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2565,7 +2565,7 @@ class Image: self._ensure_mutable() save_all = params.pop("save_all", False) - self.encoderinfo = params + self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} self.encoderconfig: tuple[Any, ...] = () preinit() @@ -2612,6 +2612,11 @@ class Image: except PermissionError: pass raise + finally: + try: + del self.encoderinfo + except AttributeError: + pass if open_fp: fp.close() From 4b9399f8bfb08d4ed1a1113ed6fd4b6137f7c2aa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Oct 2024 22:00:45 +1100 Subject: [PATCH 004/140] Use register_handler --- Tests/test_file_bufrstub.py | 2 +- Tests/test_file_gribstub.py | 2 +- Tests/test_file_hdf5stub.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 77ee5b0ea..fc8920317 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None: im.save(temp_file) assert handler.saved - BufrStubImagePlugin._handler = None + BufrStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index aba473d24..02e464ff1 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None: im.save(temp_file) assert handler.saved - GribStubImagePlugin._handler = None + GribStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 8275bd0d8..024be9e80 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -85,4 +85,4 @@ def test_handler(tmp_path: Path) -> None: im.save(temp_file) assert handler.saved - Hdf5StubImagePlugin._handler = None + Hdf5StubImagePlugin.register_handler(None) 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 005/140] 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 GitHub Actions build status (Test Docker) - AppVeyor CI build status (Windows) GitHub Actions build status (Wheels) 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 006/140] 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 37679c8673e44f31be060ff7bc6772c7f178bee7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 4 Nov 2024 20:55:00 +1100 Subject: [PATCH 007/140] Pass IFDs to libtiff as TIFF_LONG8 --- src/PIL/TiffImagePlugin.py | 4 +++- src/encode.c | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a..c95b65b8e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1915,7 +1915,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if not getattr(Image.core, "libtiff_support_custom_tags", False): continue - if tag in ifd.tagtype: + if tag in TiffTags.TAGS_V2_GROUPS: + types[tag] = TiffTags.LONG8 + elif tag in ifd.tagtype: types[tag] = ifd.tagtype[tag] elif not (isinstance(value, (int, float, str, bytes))): continue diff --git a/src/encode.c b/src/encode.c index 1a4cd489d..09a86a715 100644 --- a/src/encode.c +++ b/src/encode.c @@ -736,7 +736,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { } if (tag_type) { int type_int = PyLong_AsLong(tag_type); - if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { + if (type_int >= TIFF_BYTE && type_int <= TIFF_LONG8) { type = (TIFFDataType)type_int; } } @@ -929,7 +929,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value) ); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField( @@ -959,6 +959,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) ); + } else if (type == TIFF_LONG8) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + ); } else { TRACE( ("Unhandled type for key %d : %s \n", From b6413cd58818c6518ede6638a391bac15d812db0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 07:16:49 +1100 Subject: [PATCH 008/140] Cast to uint64_t --- src/encode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encode.c b/src/encode.c index 09a86a715..d369a1b45 100644 --- a/src/encode.c +++ b/src/encode.c @@ -961,7 +961,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ); } else if (type == TIFF_LONG8) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + &encoder->state, (ttag_t)key_int, (uint64_t)PyLong_AsLongLong(value) ); } else { TRACE( From 3cdaee45f50a1a4c020c382b516299a2bb531ec7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 20:15:23 +1100 Subject: [PATCH 009/140] Raise UnidentifiedImageError when opening TIFF without dimensions --- Tests/test_imagefile.py | 13 +++++++++++++ src/PIL/TiffImagePlugin.py | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 1ee684926..8bef90ce4 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -93,6 +93,19 @@ class TestImageFile: assert p.image is not None assert (48, 48) == p.image.size + @pytest.mark.filterwarnings("ignore:Corrupt EXIF data") + def test_incremental_tiff(self) -> None: + with ImageFile.Parser() as p: + with open("Tests/images/hopper.tif", "rb") as f: + p.feed(f.read(1024)) + + # Check that insufficient data was given in the first feed + assert not p.image + + p.feed(f.read()) + assert p.image is not None + assert (128, 128) == p.image.size + @skip_unless_feature("webp") def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a..d4dcab286 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1433,8 +1433,12 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size - xsize = self.tag_v2.get(IMAGEWIDTH) - ysize = self.tag_v2.get(IMAGELENGTH) + try: + xsize = self.tag_v2[IMAGEWIDTH] + ysize = self.tag_v2[IMAGELENGTH] + except KeyError as e: + msg = "Missing dimensions" + raise TypeError(msg) from e if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) 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 010/140] 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 011/140] 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 6fa775e324a27dac54ff8a9249553e06e1928051 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Nov 2024 22:46:24 +1100 Subject: [PATCH 012/140] Platform guessing affects more than just Linux --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e78f87fe9..fea21ee3f 100644 --- a/setup.py +++ b/setup.py @@ -344,7 +344,7 @@ class pil_build_ext(build_ext): for x in ("raqm", "fribidi") ] + [ - ("disable-platform-guessing", None, "Disable platform guessing on Linux"), + ("disable-platform-guessing", None, "Disable platform guessing"), ("debug", None, "Debug logging"), ] + [("add-imaging-libs=", None, "Add libs to _imaging build")] From 48c7eb22c0aeba649892dcb4766600f6dcfb6bdc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:45:52 +1100 Subject: [PATCH 013/140] Added default value for _Tile args --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/FliImagePlugin.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PcdImagePlugin.py | 2 +- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/XbmImagePlugin.py | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index fb1e301c0..35cc623e7 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -454,7 +454,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) - if hasattr(fp, "flush"): fp.flush() - ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0)]) fp.write(b"\n%%%%EndBinary\n") fp.write(b"grestore end\n") diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 666390be9..b534b30ab 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile): framesize = i32(s) self.decodermaxblock = framesize - self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)] + self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)] self.__offset += framesize diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 7f27d54dc..6c07c85e7 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -99,7 +99,7 @@ class _Tile(NamedTuple): codec_name: str extents: tuple[int, int, int, int] | None offset: int - args: tuple[Any, ...] | str | None + args: tuple[Any, ...] | str | None = None # diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index f3460a787..50fb6a2d9 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -72,7 +72,7 @@ class MspImageFile(ImageFile.ImageFile): if s[:4] == b"DanM": self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))] else: - self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)] + self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] class MspDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index e8ea800a4..ac40383f9 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -47,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile): self._mode = "RGB" self._size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)] + self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)] def load_end(self) -> None: if self.tile_post_rotate: diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 010d3f941..01cc868b2 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -32,7 +32,7 @@ class QoiImageFile(ImageFile.ImageFile): self._mode = "RGB" if channels == 3 else "RGBA" self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)] + self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())] class QoiDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index f3d490a84..cf1a8b9f5 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -67,7 +67,7 @@ class XbmImageFile(ImageFile.ImageFile): self._mode = "1" self._size = xsize, ysize - self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end(), None)] + self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end())] def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0)]) fp.write(b"};\n") From 871963b8ddab57fec9050135a5b58bceedce23db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:53:18 +1100 Subject: [PATCH 014/140] Replaced tuple args with mode string where equivalent --- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/DdsImagePlugin.py | 4 +--- src/PIL/FpxImagePlugin.py | 2 +- src/PIL/FtexImagePlugin.py | 2 +- src/PIL/GdImageFile.py | 2 +- src/PIL/ImtImagePlugin.py | 2 +- src/PIL/MspImagePlugin.py | 4 ++-- src/PIL/PixarImagePlugin.py | 4 +--- src/PIL/SpiderImagePlugin.py | 8 ++------ src/PIL/XVThumbImagePlugin.py | 4 +--- src/PIL/XpmImagePlugin.py | 4 +--- 11 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index e5605635e..2d03af9d7 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -273,7 +273,7 @@ class BlpImageFile(ImageFile.ImageFile): raise BLPFormatError(msg) self._mode = "RGBA" if self._blp_alpha_depth else "RGB" - self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)] class _BLPBaseDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 1b6408237..9349e2841 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -560,9 +560,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + struct.pack("<4I", *rgba_mask) # dwRGBABitMask + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) ) - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] - ) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) def _accept(prefix: bytes) -> bool: diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8fef51076..4cfcb067d 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -170,7 +170,7 @@ class FpxImageFile(ImageFile.ImageFile): "raw", (x, y, x1, y1), i32(s, i) + 28, - (self.rawmode,), + self.rawmode, ) ) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index ddb469bc3..0516b760c 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -95,7 +95,7 @@ class FtexImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] elif format == Format.UNCOMPRESSED: - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")] else: msg = f"Invalid texture compression format: {repr(format)}" raise ValueError(msg) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index f1b4969f2..fc4801e9d 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -76,7 +76,7 @@ class GdImageFile(ImageFile.ImageFile): "raw", (0, 0) + self.size, 7 + true_color_offset + 4 + 256 * 4, - ("L", 0, 1), + "L", ) ] diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 594c56513..068cd5c33 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -62,7 +62,7 @@ class ImtImageFile(ImageFile.ImageFile): "raw", (0, 0) + self.size, self.fp.tell() - len(buffer), - (self.mode, 0, 1), + self.mode, ) ] diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 50fb6a2d9..ef6ae87f8 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -70,7 +70,7 @@ class MspImageFile(ImageFile.ImageFile): self._size = i16(s, 4), i16(s, 6) if s[:4] == b"DanM": - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] else: self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(o16(h)) # image body - ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")]) # diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 36f565f1c..5c465bbdc 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -61,9 +61,7 @@ class PixarImageFile(ImageFile.ImageFile): # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)] # diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 075073f9f..d7f457ae7 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -154,9 +154,7 @@ class SpiderImageFile(ImageFile.ImageFile): self.rawmode = "F;32F" self._mode = "F" - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)] self._fp = self.fp # FIXME: hack @property @@ -280,9 +278,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.writelines(hdr) rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] - ) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 5d1f201a4..75333354d 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -74,9 +74,7 @@ class XVThumbImageFile(ImageFile.ImageFile): self.palette = ImagePalette.raw("RGB", PALETTE) self.tile = [ - ImageFile._Tile( - "raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1) - ) + ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode) ] diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 1fc6c0c39..b985aa5dc 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -101,9 +101,7 @@ class XpmImageFile(ImageFile.ImageFile): self._mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), "P")] def load_read(self, read_bytes: int) -> bytes: # From 84f5c7e5ba51afbeda6c4aaf42a31cea9ee8b082 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:52:36 +1100 Subject: [PATCH 015/140] Added default value for _Tile offset --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/XbmImagePlugin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 35cc623e7..36ba15ec5 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -454,7 +454,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) - if hasattr(fp, "flush"): fp.flush() - ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0)]) + ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) fp.write(b"\n%%%%EndBinary\n") fp.write(b"grestore end\n") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 6c07c85e7..bd0e92eb6 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -98,7 +98,7 @@ def _tilesort(t: _Tile) -> int: class _Tile(NamedTuple): codec_name: str extents: tuple[int, int, int, int] | None - offset: int + offset: int = 0 args: tuple[Any, ...] | str | None = None diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index cf1a8b9f5..943a04470 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size)]) fp.write(b"};\n") From 185a03f1a25ba56ae11d655c04645e0a50710dae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Nov 2024 12:05:06 +1100 Subject: [PATCH 016/140] Do not create new image when exif_transpose() is used in place --- src/PIL/ImageOps.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 44aad0c3c..bb29cc0d3 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -698,10 +698,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image 8: Image.Transpose.ROTATE_90, }.get(orientation) if method is not None: - transposed_image = image.transpose(method) if in_place: - image.im = transposed_image.im - image._size = transposed_image._size + image.im = image.im.transpose(method) + image._size = image.im.size + else: + transposed_image = image.transpose(method) exif_image = image if in_place else transposed_image exif = exif_image.getexif() From 7dcf4d8ab319348239d4f8695c6d3366bc257cbf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Nov 2024 16:43:21 +1100 Subject: [PATCH 017/140] 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 522505b714085ca94a2cab2a71e77a4fc5df7a3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Dec 2024 17:52:34 +1100 Subject: [PATCH 018/140] Support saving CMYK JPEG2000 images --- Tests/test_file_jpeg2k.py | 12 ++++++++++++ src/libImaging/Jpeg2KEncode.c | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index fbf72ae05..8bb290bf3 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -325,6 +325,18 @@ def test_cmyk() -> None: assert im.getpixel((0, 0)) == (185, 134, 0, 0) +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@skip_unless_feature_version("jpg_2000", "2.5.3") +def test_cmyk_save() -> None: + with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2: + assert jp2.mode == "CMYK" + + im = roundtrip(jp2) + assert_image_equal(im, jp2) + + @pytest.mark.parametrize("ext", (".j2k", ".jp2")) def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index d30ccde60..34d1a2294 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -330,6 +330,13 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { components = 4; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgba; +#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2) + } else if (strcmp(im->mode, "CMYK") == 0) { + components = 4; + color_space = OPJ_CLRSPC_CMYK; + pack = j2k_pack_rgba; +#endif } else { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; From fb3d80e390b8ed3111a4658755b035f5e20eb5e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Dec 2024 00:41:27 +1100 Subject: [PATCH 019/140] Fixed connecting discontiguous corners --- .../imagedraw/discontiguous_corners_polygon.png | Bin 486 -> 533 bytes Tests/test_imagedraw.py | 3 +++ src/libImaging/Draw.c | 9 ++++----- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png index 509c42b26e0cbc5e01853915b083d367c1587579..1b58889c8f3ae45243a7509c907f1928534bcbde 100644 GIT binary patch delta 507 zcmV$=WdTX_c%swU!U485w>wyG`7FR7NL{=~F7gqE=@exLJl^>&(VxOp^oSf8ia zhL}fVLnf5pHi+D7J^>rS>@}Z&jbnD1M`NRzjpotVIm~MFXn*Xdtc%T~u_cKYm`7u4 zb5Aml#`f$rpm{%7q?w2WMdmZ9Jmv$tYM1zlSN+e25z{{|T< xlUzDe$vZP-&cnLR`Fl4@D(PjuQYa0yf8_cULF8KV4%i50t$7D*9J9(i8XL_lG>^viVP>00V}E-xGt8s0{h7DSqp_oy z7v|B}vCOaL^=6`p{ zx-yW@%sX@;oL`>6ss&}tQP{jy7s{L0??ngmnl7|5FWZZ5 z<^^5oX`ZzgZGX-0ccHoYoxSL9e%Xan%un{>AoFjtaGZJeq=YAkoRJ&<-`JdWJ^yZh zPW{aTo}5yhZ}4mL8Pdz|GqL&VQ?P{M?R+?oa?YPd*|&W-o5uFwQh3{kbINRgm&E+= z{-We$^*vu@&WF;P59Bw=Z25$j-}aE!{@K0+IYFQ86GhJXGa>d@T{hc8HjiI*d2A1< zc9ZEp%#(a~rkpp^WX{df`g}hP-I2X&ex`d$_dGxT#Oc95ub{>N0000 None: def test_discontiguous_corners_polygon() -> None: img, draw = create_base_image_draw((84, 68)) draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) + draw.polygon( + ((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), BLACK + ) draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) draw.polygon( ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index f1c8ffcff..ea6f8805e 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -501,7 +501,8 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if (current->dx != 0 && roundf(xx[j - 1]) == xx[j - 1]) { + } else if (current->dx != 0 && j % 2 == 1 && + roundf(xx[j - 1]) == xx[j - 1]) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; @@ -510,10 +511,8 @@ polygon_generic( continue; } // Check if the two edges join to make a corner - if (((ymin == current->ymin && ymin == other_edge->ymin) || - (ymin == current->ymax && ymin == other_edge->ymax)) && - xx[j - 1] == (ymin - other_edge->y0) * other_edge->dx + - other_edge->x0) { + if (xx[j - 1] == + (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) { // Determine points from the edges on the next row // Or if this is the last row, check the previous row int offset = ymin == ymax ? -1 : 1; From dd410e4b32c6b7e412304f30c27b8a226c24ebd3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Dec 2024 10:51:45 +1100 Subject: [PATCH 020/140] Added reading of J2K comments --- Tests/test_file_jpeg2k.py | 5 +++-- src/PIL/Jpeg2KImagePlugin.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index fbf72ae05..34176d3ce 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -424,8 +424,9 @@ def test_pclr() -> None: def test_comment() -> None: - with Image.open("Tests/images/comment.jp2") as im: - assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" + for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"): + with Image.open(path) as im: + assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" # Test an image that is truncated partway through a codestream with open("Tests/images/comment.jp2", "rb") as fp: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index b6ebd562b..67828358d 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -252,6 +252,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" self._size, self._mode = _parse_codestream(self.fp) + self._parse_comment() else: sig = sig + self.fp.read(8) @@ -262,6 +263,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile): if dpi is not None: self.info["dpi"] = dpi if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + self.fp.seek(length - 2, os.SEEK_CUR) self._parse_comment() else: msg = "not a JPEG 2000 file" @@ -296,10 +300,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile): ] def _parse_comment(self) -> None: - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - self.fp.seek(length - 2, os.SEEK_CUR) - while True: marker = self.fp.read(2) if not marker: From 62b7cb62f4ee339677170b0d0fa5943e490ddab7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Dec 2024 19:06:23 +1100 Subject: [PATCH 021/140] Fixed indentation --- src/_imagingcms.c | 54 +++++++++++++++++++++++------------------------ src/display.c | 14 ++++++------ 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 1823bcf03..1805ebde1 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -346,10 +346,10 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { return -1; } - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - // transform color channels only - for (i = 0; i < im->ysize; i++) { + // transform color channels only + for (i = 0; i < im->ysize; i++) { cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); } @@ -362,9 +362,9 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { // enough available on all platforms, so we polyfill it here for now. pyCMScopyAux(hTransform, imOut, im); - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - return 0; + return 0; } static cmsHTRANSFORM @@ -378,17 +378,17 @@ _buildTransform( ) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - /* create the transform */ - hTransform = cmsCreateTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - iRenderingIntent, - cmsFLAGS - ); + /* create the transform */ + hTransform = cmsCreateTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + iRenderingIntent, + cmsFLAGS + ); Py_END_ALLOW_THREADS; @@ -412,19 +412,19 @@ _buildProofTransform( ) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - /* create the transform */ - hTransform = cmsCreateProofingTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - hProofProfile, - iRenderingIntent, - iProofIntent, - cmsFLAGS - ); + /* create the transform */ + hTransform = cmsCreateProofingTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + hProofProfile, + iRenderingIntent, + iProofIntent, + cmsFLAGS + ); Py_END_ALLOW_THREADS; diff --git a/src/display.c b/src/display.c index b4e2e3899..eed75975d 100644 --- a/src/display.c +++ b/src/display.c @@ -690,24 +690,26 @@ PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { SetWindowLongPtr(wnd, 0, (LONG_PTR)callback); SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR)PyThreadState_Get()); - Py_BEGIN_ALLOW_THREADS ShowWindow(wnd, SW_SHOWNORMAL); + Py_BEGIN_ALLOW_THREADS; + ShowWindow(wnd, SW_SHOWNORMAL); SetForegroundWindow(wnd); /* to make sure it's visible */ - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - return Py_BuildValue(F_HANDLE, wnd); + return Py_BuildValue(F_HANDLE, wnd); } PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { MSG msg; - Py_BEGIN_ALLOW_THREADS while (mainloop && GetMessage(&msg, NULL, 0, 0)) { + Py_BEGIN_ALLOW_THREADS; + while (mainloop && GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - Py_INCREF(Py_None); + Py_INCREF(Py_None); return Py_None; } From 622722f295ab137ea6bf54ede8efb8f376d1194b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Dec 2024 20:04:27 +1100 Subject: [PATCH 022/140] Corrected loadImageSeries type hint --- src/PIL/SpiderImagePlugin.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 075073f9f..c83d02885 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -211,26 +211,27 @@ class SpiderImageFile(ImageFile.ImageFile): # given a list of filenames, return a list of images -def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: +def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None: """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: return None - imglist = [] + byte_imgs = [] for img in filelist: if not os.path.exists(img): print(f"unable to find {img}") continue try: with Image.open(img) as im: - im = im.convert2byte() + assert isinstance(im, SpiderImageFile) + byte_im = im.convert2byte() except Exception: if not isSpiderImage(img): print(f"{img} is not a Spider image file") continue - im.info["filename"] = img - imglist.append(im) - return imglist + byte_im.info["filename"] = img + byte_imgs.append(byte_im) + return byte_imgs # -------------------------------------------------------------------- From aef3aa2ab35fdf847aa13017c5b4b315562a1f77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:26:07 +1100 Subject: [PATCH 023/140] Pass file handle to ContainerIO --- Tests/test_file_container.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 237045acc..597ab5083 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -4,8 +4,6 @@ import pytest from PIL import ContainerIO, Image -from .helper import hopper - TEST_FILE = "Tests/images/dummy.container" @@ -15,15 +13,15 @@ def test_sanity() -> None: def test_isatty() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.isatty() is False def test_seekable() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.seekable() is True From 0148684c2412cc69b9606e7e2b6d5d581030e41c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:29:47 +1100 Subject: [PATCH 024/140] Use monkeypatch --- Tests/test_image_thumbnail.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 01bd4b1d7..aa625f229 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -104,20 +104,20 @@ def test_transposed() -> None: assert im.size == (590, 88) -def test_load_first_unless_jpeg() -> None: +def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: - draft = im.draft + original_draft = im.draft def im_draft( mode: str, size: tuple[int, int] ) -> tuple[str, tuple[int, int, float, float]] | None: - result = draft(mode, size) + result = original_draft(mode, size) assert result is not None return result - im.draft = im_draft + monkeypatch.setattr(im, "draft", im_draft) im.thumbnail((64, 64)) From 89f1498796be0053bc40ef757680c9f7bc49e7ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:38:47 +1100 Subject: [PATCH 025/140] Updated argument types to match Image draft --- Tests/test_image_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index aa625f229..1181f6fca 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -110,7 +110,7 @@ def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: original_draft = im.draft def im_draft( - mode: str, size: tuple[int, int] + mode: str | None, size: tuple[int, int] | None ) -> tuple[str, tuple[int, int, float, float]] | None: result = original_draft(mode, size) assert result is not None From 0220b025c5f56ebe4c2f1678e24195672d4d4373 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 12:33:59 +1100 Subject: [PATCH 026/140] Updated documentation for #7947 and #8592 --- docs/handbook/image-file-formats.rst | 8 +++++--- docs/releasenotes/11.1.0.rst | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bf3087f6f..364e1802a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -573,9 +573,11 @@ Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, ``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to ``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``, -``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports -JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files -(``.jp2`` or ``.jpx`` files). +``RGBA``, and ``YCbCr`` images with subsampled components. Pillow 10.4.0 and +later can read ``CMYK`` images with OpenJPEG 2.5.1 and later, and Pillow 11.1.0 +and later can write ``CMYK`` images with OpenJPEG 2.5.3 and later. Pillow +supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 +files (``.jp2`` or ``.jpx`` files). When loading, if you set the ``mode`` on the image prior to the :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58..7fd622beb 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -52,6 +52,11 @@ zlib library, and what version of zlib-ng is being used:: Other Changes ============= +Saving JPEG 2000 CMYK images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files. + zlib-ng in wheels ^^^^^^^^^^^^^^^^^ From ad747f3fd8024145ddc8dbc5f3f95d7e396b3351 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 12:38:50 +1100 Subject: [PATCH 027/140] Added release notes --- docs/releasenotes/11.1.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58..cccf2323d 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -52,6 +52,12 @@ zlib library, and what version of zlib-ng is being used:: Other Changes ============= +Reading JPEG 2000 comments +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG 2000 image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images. + zlib-ng in wheels ^^^^^^^^^^^^^^^^^ From cfe8379d905bb92a1407d1809987fb74163b5024 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 13:02:23 +1100 Subject: [PATCH 028/140] Added release notes for #8483 --- docs/releasenotes/11.1.0.rst | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58..90a0492bd 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -31,10 +31,26 @@ TODO API Changes =========== -TODO -^^^^ +Writing XMP bytes to JPEG and MPO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Pillow 11.0.0 added writing XMP data to JPEG and MPO images:: + + im.info["xmp"] = b"test" + im.save("out.jpg") + +However, this meant that XMP data was automatically kept from an opened image, +which is inconsistent with the rest of Pillow's behaviour. This functionality +has been removed. To write XMP data, the ``xmp`` argument can still be used for +JPEG files:: + + im.save("out.jpg", xmp=b"test") + +To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now +be used:: + + second_im.encoderinfo = {"xmp": b"test"} + im.save("out.mpo", save_all=True, append_images=[second_im]) API Additions ============= From 23083f28abbf0a79bc44c2e7b755663c04368e14 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 14:02:19 +1100 Subject: [PATCH 029/140] Use monkeypatch --- Tests/test_file_png.py | 8 ++------ Tests/test_file_ppm.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ffafc3c58..974e1e75f 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -772,22 +772,18 @@ class TestFilePng: im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer: bool) -> None: - old_stdout = sys.stdout + def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: class MyStdOut: buffer = BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_PNG_FILE) as im: im.save(sys.stdout, "PNG") - # Reset stdout - sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fb08d613a..ee51a5e5a 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -367,22 +367,18 @@ def test_mimetypes(tmp_path: Path) -> None: @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer: bool) -> None: - old_stdout = sys.stdout +def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: class MyStdOut: buffer = BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_FILE) as im: im.save(sys.stdout, "PPM") - # Reset stdout - sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: From f10e9f42d3e434c34a16df514a5435381b21aefb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 14:29:29 +1100 Subject: [PATCH 030/140] Do not use temporary file in grabclipboard() on macOS --- src/PIL/ImageGrab.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e27ca7e50..fe27bfaeb 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -104,28 +104,17 @@ def grab( def grabclipboard() -> Image.Image | list[str] | None: if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - commands = [ - 'set theFile to (open for access POSIX file "' - + filepath - + '" with write permission)', - "try", - " write (the clipboard as «class PNGf») to theFile", - "end try", - "close access theFile", - ] - script = ["osascript"] - for command in commands: - script += ["-e", command] - subprocess.call(script) + p = subprocess.run( + ["osascript", "-e", "get the clipboard as «class PNGf»"], + capture_output=True, + ) + if p.returncode != 0: + return None - im = None - if os.stat(filepath).st_size != 0: - im = Image.open(filepath) - im.load() - os.unlink(filepath) - return im + import binascii + + data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3])) + return Image.open(data) elif sys.platform == "win32": fmt, data = Image.core.grabclipboard_win32() if fmt == "file": # CF_HDROP From 05c981ffd74f6b4de932a1a14f44dc6a3058bb75 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 15:41:19 +1100 Subject: [PATCH 031/140] Removed buffer_size variable --- src/libImaging/Jpeg2KDecode.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index fc927d2f0..4f185b529 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -640,7 +640,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { opj_dparameters_t params; OPJ_COLOR_SPACE color_space; j2k_unpacker_t unpack = NULL; - size_t buffer_size = 0, tile_bytes = 0; + size_t tile_bytes = 0; unsigned n, tile_height, tile_width; int subsampling; int total_component_width = 0; @@ -870,7 +870,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { tile_info.data_size = tile_bytes; } - if (buffer_size < tile_info.data_size) { + if (tile_info.data_size > 0) { /* malloc check ok, overflow and tile size sanity check above */ UINT8 *new = realloc(state->buffer, tile_info.data_size); if (!new) { @@ -883,7 +883,6 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { to valgrind errors. */ memset(new, 0, tile_info.data_size); state->buffer = new; - buffer_size = tile_info.data_size; } if (!opj_decode_tile_data( From 8945875c6ce48fee62d17cea1414938e24808a9f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 21:28:44 +1100 Subject: [PATCH 032/140] Populate DPI from JFIF cm density --- Tests/images/jfif_unit_cm.jpg | Bin 0 -> 391 bytes Tests/test_file_jpeg.py | 4 ++++ src/PIL/JpegImagePlugin.py | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 Tests/images/jfif_unit_cm.jpg diff --git a/Tests/images/jfif_unit_cm.jpg b/Tests/images/jfif_unit_cm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..78b50e60a236bf320a29c433eae2c9afeb759fe9 GIT binary patch literal 391 zcmex=5D>Bm<7<_#hv=|r|IDm3|AQ=W8AeI7R zhP2G0;u4@jeu%>2)a3lU6o&t|8Jrn7*x1UW@aW9W>!`f7NBe`P@aKBkX1<0(2-3z zFp*uUP{gQl;zAB(r;P_igD!qhF-|IK;^Yz&myncFRa4i{)G{$OGqmaka3 zYSZQ|TeofBv2)j None: else: if jfif_unit == 1: self.info["dpi"] = jfif_density + elif jfif_unit == 2: # cm + # 1 dpcm = 2.54 dpi + self.info["dpi"] = tuple(d * 2.54 for d in jfif_density) self.info["jfif_unit"] = jfif_unit self.info["jfif_density"] = jfif_density elif marker == 0xFFE1 and s[:6] == b"Exif\0\0": From 9bebecf36d66b19ac7ba0241ef9eb7febdcaf866 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 22:18:02 +1100 Subject: [PATCH 033/140] Use versionadded --- docs/handbook/image-file-formats.rst | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 364e1802a..2ea49282e 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -572,12 +572,19 @@ JPEG 2000 Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, ``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to ``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. -Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``, -``RGBA``, and ``YCbCr`` images with subsampled components. Pillow 10.4.0 and -later can read ``CMYK`` images with OpenJPEG 2.5.1 and later, and Pillow 11.1.0 -and later can write ``CMYK`` images with OpenJPEG 2.5.3 and later. Pillow -supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 -files (``.jp2`` or ``.jpx`` files). + +.. versionadded:: 8.3.0 + Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with + subsampled components. + +.. versionadded:: 10.4.0 + Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later. + +.. versionadded:: 11.1.0 + Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later. + +Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed +JPEG 2000 files (``.jp2`` or ``.jpx`` files). When loading, if you set the ``mode`` on the image prior to the :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to From 9368a86397a41817f671c3c0bce7b8745bc5e218 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Dec 2024 07:43:47 +1100 Subject: [PATCH 034/140] Keep new IFDs when converting EXIF to bytes --- Tests/test_image.py | 4 ++++ src/PIL/Image.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index c8df474f4..092bc07f6 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -793,6 +793,10 @@ class TestImage: ifd[36864] = b"0220" assert exif.get_ifd(0x8769) == {36864: b"0220"} + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"} + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 90374d804..dff3d063b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -4023,6 +4023,9 @@ class Exif(_ExifBase): head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) + for tag, ifd_dict in self._ifds.items(): + if tag not in self: + ifd[tag] = ifd_dict for tag, value in self.items(): if tag in [ ExifTags.IFD.Exif, From ea962bf1d8dab61d526f885eccb34863ea85228f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Dec 2024 16:59:32 +1100 Subject: [PATCH 035/140] Added RGBX;16N to RGB unpacker --- src/libImaging/Unpack.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index c23d5d889..e9203fe4d 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1695,6 +1695,7 @@ static struct { #ifdef WORDS_BIGENDIAN {"RGB", "RGB;16N", 48, unpackRGB16B}, + {"RGB", "RGBX;16N", 64, unpackRGBA16B}, {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, @@ -1708,6 +1709,7 @@ static struct { {"RGBA", "A;16N", 16, band316B}, #else {"RGB", "RGB;16N", 48, unpackRGB16L}, + {"RGB", "RGBX;16N", 64, unpackRGBA16L}, {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, {"RGBX", "RGBX;16N", 64, unpackRGBA16L}, From 8d28514e409bf3ecbeb3721d8ccb508c09f2b975 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:16:42 +0200 Subject: [PATCH 036/140] Add zizmor to pre-commit and fix potential cache-poisoning in wheels workflow --- .github/workflows/wheels.yml | 2 -- .pre-commit-config.yaml | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c5e55aa62..3b22ee98a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -263,8 +263,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.x" - cache: pip - cache-dependency-path: "Makefile" - run: make sdist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f91260c72..b76f92ec0 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.1 + rev: v0.8.4 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.4 + rev: v19.1.5 hooks: - id: clang-format types: [c] @@ -56,6 +56,11 @@ repos: - id: check-readthedocs - id: check-renovate + - repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v0.10.0 + hooks: + - id: zizmor + - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v1.0.0 hooks: From 167ed55d8b43de26c5ce01c239ce848062e5e995 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Dec 2024 19:37:38 +1100 Subject: [PATCH 037/140] 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 038/140] 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 039/140] 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 040/140] 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 041/140] 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 042/140] 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 043/140] 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 044/140] 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 045/140] 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 046/140] 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 047/140] 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 048/140] 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 049/140] 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 050/140] 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 051/140] 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 052/140] 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 053/140] 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 054/140] 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 055/140] 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 056/140] 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 057/140] 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 058/140] 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 059/140] 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 060/140] 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 061/140] 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 062/140] 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 063/140] 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 064/140] 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 065/140] 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 066/140] 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 067/140] 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 068/140] 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 069/140] 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 070/140] [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 071/140] 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 072/140] 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 073/140] 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 074/140] 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 075/140] 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 076/140] 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 077/140] 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 078/140] 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 079/140] 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 080/140] 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 081/140] 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 082/140] 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 083/140] 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 084/140] 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 085/140] 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 086/140] 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 087/140] 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 088/140] 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 089/140] 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 090/140] 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 091/140] 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 092/140] 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 093/140] 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 094/140] 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 095/140] 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 096/140] 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 097/140] 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 098/140] 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 099/140] 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 100/140] 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 101/140] 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 102/140] 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 103/140] 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 104/140] 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 105/140] 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 106/140] 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 107/140] 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. From 8d9279dd7329cc8dd330f483df2867fd51ca8dcf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Jan 2025 21:58:04 +1100 Subject: [PATCH 108/140] Only use outside border of stroke in text() --- Tests/test_imagedraw.py | 22 ++++++++++++++++++++++ Tests/test_imagefont.py | 14 ++++++++++++++ src/PIL/ImageDraw.py | 1 + src/PIL/ImageFont.py | 1 + src/PIL/_imagingft.pyi | 1 + src/_imagingft.c | 7 +++++-- 6 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 5fc1c2766..28d7ed725 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1396,6 +1396,28 @@ def test_stroke_descender() -> None: assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) +@skip_unless_feature("freetype2") +def test_stroke_inside_gap() -> None: + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((12, 12), "i", "#f00", font, stroke_width=20) + + # Assert + for y in range(im.height): + glyph = "" + for x in range(im.width): + if im.getpixel((x, y)) == (0, 0, 0): + if glyph == "started": + glyph = "ended" + else: + assert glyph != "ended", "Gap inside stroked glyph" + glyph = "started" + + @skip_unless_feature("freetype2") def test_split_word() -> None: # Arrange diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6a0a940b9..f110cc1d0 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -461,6 +461,20 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) +def test_stroke_mask() -> None: + # Arrange + text = "i" + + # Act + font = ImageFont.truetype(FONT_PATH, 128) + mask = font.getmask(text, stroke_width=2) + + # Assert + assert mask.getpixel((34, 5)) == 255 + assert mask.getpixel((38, 5)) == 0 + assert mask.getpixel((42, 5)) == 255 + + def test_load_when_image_not_found() -> None: with tempfile.NamedTemporaryFile(delete=False) as tmp: pass diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d8e4c0c60..d0f6c5b7d 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -643,6 +643,7 @@ class ImageDraw: features=features, language=language, stroke_width=stroke_width, + stroke_filled=True, anchor=anchor, ink=ink, start=start, diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index d8c265560..a4986aa8c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -644,6 +644,7 @@ class FreeTypeFont: features, language, stroke_width, + kwargs.get("stroke_filled", False), anchor, ink, start[0], diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 9cc9822f5..813294747 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -28,6 +28,7 @@ class Font: features: list[str] | None, lang: str | None, stroke_width: float, + stroke_filled: bool, anchor: str | None, foreground_ink_long: int, x_start: float, diff --git a/src/_imagingft.c b/src/_imagingft.c index 3a65007a5..4281e0b5e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -834,6 +834,7 @@ font_render(FontObject *self, PyObject *args) { int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ float stroke_width = 0; + int stroke_filled = 0; PY_LONG_LONG foreground_ink_long = 0; unsigned int foreground_ink; const char *mode = NULL; @@ -853,7 +854,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzfzLffO:render", + "OO|zzOzfpzLffO:render", &string, &fill, &mode, @@ -861,6 +862,7 @@ font_render(FontObject *self, PyObject *args) { &features, &lang, &stroke_width, + &stroke_filled, &anchor, &foreground_ink_long, &x_start, @@ -1005,7 +1007,8 @@ font_render(FontObject *self, PyObject *args) { if (stroker != NULL) { error = FT_Get_Glyph(glyph_slot, &glyph); if (!error) { - error = FT_Glyph_Stroke(&glyph, stroker, 1); + error = stroke_filled ? FT_Glyph_StrokeBorder(&glyph, stroker, 0, 1) + : FT_Glyph_Stroke(&glyph, stroker, 1); } if (!error) { FT_Vector origin = {0, 0}; From 0318304f9ae63fe81e513bdb32c1497539c66176 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Jan 2025 17:27:25 +1100 Subject: [PATCH 109/140] Do not draw normal text onto stroke text if they are the same color --- src/PIL/ImageDraw.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d0f6c5b7d..81f8fbce0 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -693,7 +693,8 @@ class ImageDraw: draw_text(stroke_ink, stroke_width) # Draw normal text - draw_text(ink, 0) + if ink != stroke_ink: + draw_text(ink) else: # Only draw normal text draw_text(ink) From 427244877bb243c29fd07543f577ee6036daff4f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Jan 2025 15:09:12 +1100 Subject: [PATCH 110/140] Support saving cICP chunk --- src/PIL/PngImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4b97992a3..f56555160 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1382,7 +1382,7 @@ def _save( b"\0", # 12: interlace flag ) - chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] + chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) if icc: From 8a90975c14e8176791f431cabbcf14029c1b36a2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Jan 2025 23:14:34 +1100 Subject: [PATCH 111/140] Seek relative to current position --- src/PIL/BufrStubImagePlugin.py | 5 ++--- src/PIL/GribStubImagePlugin.py | 5 ++--- src/PIL/Hdf5StubImagePlugin.py | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 0ee2f653b..50c41c482 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from typing import IO from . import Image, ImageFile @@ -40,13 +41,11 @@ class BufrStubImageFile(ImageFile.StubImageFile): format_description = "BUFR" def _open(self) -> None: - offset = self.fp.tell() - if not _accept(self.fp.read(4)): msg = "Not a BUFR file" raise SyntaxError(msg) - self.fp.seek(offset) + self.fp.seek(-4, os.SEEK_CUR) # make something up self._mode = "F" diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index e9aa084b2..eb1b1483b 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from typing import IO from . import Image, ImageFile @@ -40,13 +41,11 @@ class GribStubImageFile(ImageFile.StubImageFile): format_description = "GRIB" def _open(self) -> None: - offset = self.fp.tell() - if not _accept(self.fp.read(8)): msg = "Not a GRIB file" raise SyntaxError(msg) - self.fp.seek(offset) + self.fp.seek(-8, os.SEEK_CUR) # make something up self._mode = "F" diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index cc9e73deb..ddc218508 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -10,6 +10,7 @@ # from __future__ import annotations +import os from typing import IO from . import Image, ImageFile @@ -40,13 +41,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile): format_description = "HDF5" def _open(self) -> None: - offset = self.fp.tell() - if not _accept(self.fp.read(8)): msg = "Not an HDF file" raise SyntaxError(msg) - self.fp.seek(offset) + self.fp.seek(-8, os.SEEK_CUR) # make something up self._mode = "F" From e31441fc41ff54217317b61db395dfc9b5a0dc79 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Jan 2025 19:51:07 +1100 Subject: [PATCH 112/140] Use Ubuntu 22.04 for 24.04 ppc64le and s390x --- .github/workflows/test-docker.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 0d9033413..da5e191da 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -35,10 +35,6 @@ jobs: matrix: os: ["ubuntu-latest"] docker: [ - # Run slower jobs first to give them a headstart and reduce waiting time - ubuntu-24.04-noble-ppc64le, - ubuntu-24.04-noble-s390x, - # Then run the remainder alpine, amazon-2-amd64, amazon-2023-amd64, @@ -56,9 +52,13 @@ jobs: dockerTag: [main] include: - docker: "ubuntu-24.04-noble-ppc64le" + os: "ubuntu-22.04" qemu-arch: "ppc64le" + dockerTag: main - docker: "ubuntu-24.04-noble-s390x" + os: "ubuntu-22.04" qemu-arch: "s390x" + dockerTag: main - docker: "ubuntu-24.04-noble-arm64v8" os: "ubuntu-24.04-arm" dockerTag: main From 9d4232101fb84da0d7dbf2622b140ba125f65f76 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Jan 2025 07:05:26 +1100 Subject: [PATCH 113/140] Updated libimagequant to 4.3.4 --- depends/install_imagequant.sh | 2 +- docs/installation/building-from-source.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 8d62d5ac7..88756f8f9 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -2,7 +2,7 @@ # install libimagequant archive_name=libimagequant -archive_version=4.3.3 +archive_version=4.3.4 archive=$archive_name-$archive_version diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 03359de31..46a4c1245 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -64,7 +64,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.3.3** + * Pillow has been tested with libimagequant **2.6-4.3.4** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From f52dbe749b00e97b1c81f0dbc3ef398468d65369 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Jan 2025 14:08:29 +1100 Subject: [PATCH 114/140] Updated libpng to 1.6.46 --- .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 410255b7e..e01ad064a 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.45 +LIBPNG_VERSION=1.6.46 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 b9695d1d8..8818c7402 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.45", + "LIBPNG": "1.6.46", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", From 16a8e2bde4b4f9616eef58a447f878e664c2486a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Jan 2025 08:48:12 +1100 Subject: [PATCH 115/140] Updated xz to 5.6.4 --- .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 410255b7e..4ab0f1b30 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -42,7 +42,7 @@ HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.45 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 -XZ_VERSION=5.6.3 +XZ_VERSION=5.6.4 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 ZLIB_NG_VERSION=2.2.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b9695d1d8..1c20fad44 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,7 +120,7 @@ V = { "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", - "XZ": "5.6.3", + "XZ": "5.6.4", "ZLIBNG": "2.2.3", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 569b785371aa717a004adb0166feb565bbb01b7b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 25 Jan 2025 18:04:52 +1100 Subject: [PATCH 116/140] Updated harfbuzz to 10.2.0 (#8688) Co-authored-by: Andrew Murray --- .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 b1b5bcf94..dffb36085 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,7 +38,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=10.1.0 +HARFBUZZ_VERSION=10.2.0 LIBPNG_VERSION=1.6.46 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d18facab4..54b5d983f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "10.1.0", + "HARFBUZZ": "10.2.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBPNG": "1.6.46", From e7ae4aaad04483be775b4dda9bb8803ba63e5669 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Wed, 18 Sep 2024 11:42:44 +0200 Subject: [PATCH 117/140] Use Py_RETURN_NONE macro when possible --- src/_imaging.c | 93 ++++++++++++++++------------------------------ src/_imagingcms.c | 90 +++++++++++++++----------------------------- src/_imagingft.c | 6 +-- src/_imagingmath.c | 6 +-- src/_imagingtk.c | 3 +- src/decode.c | 6 +-- src/display.c | 18 +++------ src/encode.c | 6 +-- src/outline.c | 15 +++----- src/path.c | 6 +-- 10 files changed, 83 insertions(+), 166 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 00772d012..2fd2deffb 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -473,8 +473,7 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { } /* unknown type */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char * @@ -965,8 +964,7 @@ _convert2(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1214,8 +1212,7 @@ _getpixel(ImagingObject *self, PyObject *args) { } if (self->access == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return getpixel(self->image, self->access, x, y); @@ -1417,8 +1414,7 @@ _paste(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1691,8 +1687,7 @@ _putdata(ImagingObject *self, PyObject *args) { Py_XDECREF(seq); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1752,8 +1747,7 @@ _putpalette(ImagingObject *self, PyObject *args) { self->image->palette->size = palettesize * 8 / bits; unpack(self->image->palette->palette, palette, self->image->palette->size); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1777,8 +1771,7 @@ _putpalettealpha(ImagingObject *self, PyObject *args) { strcpy(self->image->palette->mode, "RGBA"); self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1805,8 +1798,7 @@ _putpalettealphas(ImagingObject *self, PyObject *args) { self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1842,8 +1834,7 @@ _putpixel(ImagingObject *self, PyObject *args) { self->access->put_pixel(im, x, y, ink); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2010,8 +2001,7 @@ im_setmode(ImagingObject *self, PyObject *args) { } self->access = ImagingAccessNew(im); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2074,8 +2064,7 @@ _transform(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2202,8 +2191,7 @@ _getbbox(ImagingObject *self, PyObject *args) { } if (!ImagingGetBBox(self->image, bbox, alpha_only)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return Py_BuildValue("iiii", bbox[0], bbox[1], bbox[2], bbox[3]); @@ -2283,8 +2271,7 @@ _getextrema(ImagingObject *self) { } } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2347,8 +2334,7 @@ _fillband(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2363,8 +2349,7 @@ _putband(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2950,8 +2935,7 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -2988,8 +2972,7 @@ _draw_bitmap(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3045,8 +3028,7 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3100,8 +3082,7 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3164,8 +3145,7 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { free(xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3196,8 +3176,7 @@ _draw_points(ImagingDrawObject *self, PyObject *args) { free(xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* from outline.c */ @@ -3225,8 +3204,7 @@ _draw_outline(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3282,8 +3260,7 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3334,8 +3311,7 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { free(ixy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -3389,8 +3365,7 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef _draw_methods[] = { @@ -3595,8 +3570,7 @@ _save_ppm(ImagingObject *self, PyObject *args) { return NULL; } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ @@ -3984,8 +3958,7 @@ _reset_stats(PyObject *self, PyObject *args) { arena->stats_freed_blocks = 0; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4045,8 +4018,7 @@ _set_alignment(PyObject *self, PyObject *args) { ImagingDefaultArena.alignment = alignment; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4070,8 +4042,7 @@ _set_block_size(PyObject *self, PyObject *args) { ImagingDefaultArena.block_size = block_size; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4099,8 +4070,7 @@ _set_blocks_max(PyObject *self, PyObject *args) { return ImagingError_MemoryError(); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -4115,8 +4085,7 @@ _clear_cache(PyObject *self, PyObject *args) { ImagingMemoryClearCache(&ImagingDefaultArena, i); MUTEX_UNLOCK(&ImagingDefaultArena.mutex); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 1805ebde1..14cf2acd2 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -654,8 +654,7 @@ cms_get_display_profile_win32(PyObject *self, PyObject *args) { return PyUnicode_FromStringAndSize(filename, filename_size - 1); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } #endif @@ -672,20 +671,17 @@ _profile_read_mlu(CmsProfileObject *self, cmsTagSignature info) { wchar_t *buf; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } mlu = cmsReadTag(self->profile, info); if (!mlu) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } len = cmsMLUgetWide(mlu, lc, cc, NULL, 0); if (len == 0) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } buf = malloc(len); @@ -723,14 +719,12 @@ _profile_read_signature(CmsProfileObject *self, cmsTagSignature info) { unsigned int *sig; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } sig = (unsigned int *)cmsReadTag(self->profile, info); if (!sig) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_int_as_string(*sig); @@ -780,14 +774,12 @@ _profile_read_ciexyz(CmsProfileObject *self, cmsTagSignature info, int multi) { cmsCIEXYZ *XYZ; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); if (!XYZ) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } if (multi) { return _xyz3_py(XYZ); @@ -801,14 +793,12 @@ _profile_read_ciexyy_triple(CmsProfileObject *self, cmsTagSignature info) { cmsCIExyYTRIPLE *triple; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } triple = (cmsCIExyYTRIPLE *)cmsReadTag(self->profile, info); if (!triple) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* Note: lcms does all the heavy lifting and error checking (nr of @@ -835,21 +825,18 @@ _profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { PyObject *result; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } ncl = (cmsNAMEDCOLORLIST *)cmsReadTag(self->profile, info); if (ncl == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } n = cmsNamedColorCount(ncl); result = PyList_New(n); if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } for (i = 0; i < n; i++) { @@ -858,8 +845,7 @@ _profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { str = PyUnicode_FromString(name); if (str == NULL) { Py_DECREF(result); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } PyList_SET_ITEM(result, i, str); } @@ -926,8 +912,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { result = PyDict_New(); if (result == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } n = cmsGetSupportedIntents(INTENTS, intent_ids, intent_descs); @@ -957,8 +942,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { Py_XDECREF(id); Py_XDECREF(entry); Py_XDECREF(result); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } PyDict_SetItem(result, id, entry); Py_DECREF(id); @@ -1042,8 +1026,7 @@ cms_profile_getattr_creation_date(CmsProfileObject *self, void *closure) { result = cmsGetHeaderCreationDateTime(self->profile, &ct); if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return PyDateTime_FromDateAndTime( @@ -1141,8 +1124,7 @@ cms_profile_getattr_saturation_rendering_intent_gamut( static PyObject * cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); } @@ -1150,8 +1132,7 @@ cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { static PyObject * cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); } @@ -1159,8 +1140,7 @@ cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { static PyObject * cms_profile_getattr_blue_colorant(CmsProfileObject *self, void *closure) { if (!cmsIsMatrixShaper(self->profile)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); } @@ -1176,21 +1156,18 @@ cms_profile_getattr_media_white_point_temperature( cmsBool result; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); if (XYZ == NULL || XYZ->X == 0) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } cmsXYZ2xyY(&xyY, XYZ); result = cmsTempFromWhitePoint(&tempK, &xyY); if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return PyFloat_FromDouble(tempK); } @@ -1229,8 +1206,7 @@ cms_profile_getattr_red_primary(CmsProfileObject *self, void *closure) { result = _calculate_rgb_primaries(self, &primaries); } if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _xyz_py(&primaries.Red); @@ -1245,8 +1221,7 @@ cms_profile_getattr_green_primary(CmsProfileObject *self, void *closure) { result = _calculate_rgb_primaries(self, &primaries); } if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _xyz_py(&primaries.Green); @@ -1261,8 +1236,7 @@ cms_profile_getattr_blue_primary(CmsProfileObject *self, void *closure) { result = _calculate_rgb_primaries(self, &primaries); } if (!result) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return _xyz_py(&primaries.Blue); @@ -1321,14 +1295,12 @@ cms_profile_getattr_icc_measurement_condition(CmsProfileObject *self, void *clos const char *geo; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } mc = (cmsICCMeasurementConditions *)cmsReadTag(self->profile, info); if (!mc) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } if (mc->Geometry == 1) { @@ -1362,14 +1334,12 @@ cms_profile_getattr_icc_viewing_condition(CmsProfileObject *self, void *closure) cmsTagSignature info = cmsSigViewingConditionsTag; if (!cmsIsTag(self->profile, info)) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } vc = (cmsICCViewingConditions *)cmsReadTag(self->profile, info); if (!vc) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } return Py_BuildValue( diff --git a/src/_imagingft.c b/src/_imagingft.c index 3a65007a5..5113cca77 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1371,8 +1371,7 @@ font_setvarname(FontObject *self, PyObject *args) { return geterror(error); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -1426,8 +1425,7 @@ font_setvaraxes(FontObject *self, PyObject *args) { return geterror(error); } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } #endif diff --git a/src/_imagingmath.c b/src/_imagingmath.c index dbe636707..75b3716b5 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -192,8 +192,7 @@ _unop(PyObject *self, PyObject *args) { unop(out, im1); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -226,8 +225,7 @@ _binop(PyObject *self, PyObject *args) { binop(out, im1, im2); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyMethodDef _functions[] = { diff --git a/src/_imagingtk.c b/src/_imagingtk.c index c70d044bb..c44482651 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -37,8 +37,7 @@ _tkinit(PyObject *self, PyObject *args) { /* This will bomb if interp is invalid... */ TkImaging_Init(interp); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyMethodDef functions[] = { diff --git a/src/decode.c b/src/decode.c index 51d0aced2..1f2c22491 100644 --- a/src/decode.c +++ b/src/decode.c @@ -213,8 +213,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { Py_XDECREF(decoder->lock); decoder->lock = op; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -231,8 +230,7 @@ _setfd(ImagingDecoderObject *decoder, PyObject *args) { Py_XINCREF(fd); state->fd = fd; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * diff --git a/src/display.c b/src/display.c index eed75975d..36ab3b237 100644 --- a/src/display.c +++ b/src/display.c @@ -85,8 +85,7 @@ _expose(ImagingDisplayObject *display, PyObject *args) { ImagingExposeDIB(display->dib, hdc); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -112,8 +111,7 @@ _draw(ImagingDisplayObject *display, PyObject *args) { ImagingDrawDIB(display->dib, hdc, dst, src); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } extern Imaging @@ -143,8 +141,7 @@ _paste(ImagingDisplayObject *display, PyObject *args) { ImagingPasteDIB(display->dib, im, xy); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -190,8 +187,7 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) { ReleaseDC(window, dc); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -211,8 +207,7 @@ _frombytes(ImagingDisplayObject *display, PyObject *args) { memcpy(display->dib->bits, buffer.buf, buffer.len); PyBuffer_Release(&buffer); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -709,8 +704,7 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { } Py_END_ALLOW_THREADS; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /* -------------------------------------------------------------------- */ diff --git a/src/encode.c b/src/encode.c index d369a1b45..0bf5e63c5 100644 --- a/src/encode.c +++ b/src/encode.c @@ -278,8 +278,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { Py_XDECREF(encoder->lock); encoder->lock = op; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -296,8 +295,7 @@ _setfd(ImagingEncoderObject *encoder, PyObject *args) { Py_XINCREF(fd); state->fd = fd; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * diff --git a/src/outline.c b/src/outline.c index 27cc255cf..4aa6bd59e 100644 --- a/src/outline.c +++ b/src/outline.c @@ -89,8 +89,7 @@ _outline_move(OutlineObject *self, PyObject *args) { ImagingOutlineMove(self->outline, x0, y0); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -102,8 +101,7 @@ _outline_line(OutlineObject *self, PyObject *args) { ImagingOutlineLine(self->outline, x1, y1); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -115,8 +113,7 @@ _outline_curve(OutlineObject *self, PyObject *args) { ImagingOutlineCurve(self->outline, x1, y1, x2, y2, x3, y3); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -127,8 +124,7 @@ _outline_close(OutlineObject *self, PyObject *args) { ImagingOutlineClose(self->outline); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject * @@ -140,8 +136,7 @@ _outline_transform(OutlineObject *self, PyObject *args) { ImagingOutlineTransform(self->outline, a); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef _outline_methods[] = { diff --git a/src/path.c b/src/path.c index 067f42f62..b508df2ac 100644 --- a/src/path.c +++ b/src/path.c @@ -415,8 +415,7 @@ path_map(PyPathObject *self, PyObject *args) { } self->mapping = 0; - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static int @@ -528,8 +527,7 @@ path_transform(PyPathObject *self, PyObject *args) { } } - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static struct PyMethodDef methods[] = { From e19a1496c21ee5cca0fcbfcd0f1d97b9d8aa6bcc Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:17:51 +1100 Subject: [PATCH 118/140] Use monkeypatch (#8707) Co-authored-by: Andrew Murray --- Tests/check_png_dos.py | 21 ++++--- Tests/test_decompression_bomb.py | 19 +++---- Tests/test_file_fli.py | 27 ++++----- Tests/test_file_gif.py | 96 ++++++++++++++++---------------- Tests/test_file_ico.py | 25 ++++----- Tests/test_file_jpeg.py | 12 ++-- Tests/test_file_jpeg2k.py | 13 ++--- Tests/test_file_libtiff.py | 23 ++++---- Tests/test_file_png.py | 64 +++++++++------------ Tests/test_file_tiff.py | 5 +- Tests/test_file_webp.py | 7 +-- Tests/test_imagefile.py | 20 +++---- Tests/test_map.py | 14 ++--- 13 files changed, 153 insertions(+), 193 deletions(-) diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 63d6657bc..1bfb94ab7 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -3,26 +3,25 @@ from __future__ import annotations import zlib from io import BytesIO +import pytest + from PIL import Image, ImageFile, PngImagePlugin TEST_FILE = "Tests/images/png_decompression_dos.png" -def test_ignore_dos_text() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = True +def test_ignore_dos_text(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) - try: - im = Image.open(TEST_FILE) + with Image.open(TEST_FILE) as im: im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False - assert isinstance(im, PngImagePlugin.PngImageFile) - for s in im.text.values(): - assert len(s) < 1024 * 1024, "Text chunk larger than 1M" + assert isinstance(im, PngImagePlugin.PngImageFile) + for s in im.text.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" - for s in im.info.values(): - assert len(s) < 1024 * 1024, "Text chunk larger than 1M" + for s in im.info.values(): + assert len(s) < 1024 * 1024, "Text chunk larger than 1M" def test_dos_text() -> None: diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index c140156f9..98d833736 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -12,19 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS class TestDecompressionBomb: - def teardown_method(self) -> None: - Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_no_warning_small_file(self) -> None: # Implicit assert: no warning. # A warning would cause a failure. with Image.open(TEST_FILE): pass - def test_no_warning_no_limit(self) -> None: + def test_no_warning_no_limit(self, monkeypatch: pytest.MonkeyPatch) -> None: # Arrange # Turn limit off - Image.MAX_IMAGE_PIXELS = None + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) assert Image.MAX_IMAGE_PIXELS is None # Act / Assert @@ -33,18 +30,18 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass - def test_warning(self) -> None: + def test_warning(self, monkeypatch: pytest.MonkeyPatch) -> None: # Set limit to trigger warning on the test file - Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 128 * 128 - 1) assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 with pytest.warns(Image.DecompressionBombWarning): with Image.open(TEST_FILE): pass - def test_exception(self) -> None: + def test_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 64 * 128 - 1) assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 with pytest.raises(Image.DecompressionBombError): @@ -66,9 +63,9 @@ class TestDecompressionBomb: with pytest.raises(Image.DecompressionBombError): im.seek(1) - def test_exception_gif_zero_width(self) -> None: + def test_exception_gif_zero_width(self, monkeypatch: pytest.MonkeyPatch) -> None: # Set limit to trigger exception on the test file - Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 4 * 64 * 128) assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 with pytest.raises(Image.DecompressionBombError): diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 0a7740cc8..876561a88 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -35,22 +35,19 @@ def test_sanity() -> None: assert im.is_animated -def test_prefix_chunk() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - with Image.open(animated_test_file_with_prefix_chunk) as im: - assert im.mode == "P" - assert im.size == (320, 200) - assert im.format == "FLI" - assert im.info["duration"] == 171 - assert im.is_animated +def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + with Image.open(animated_test_file_with_prefix_chunk) as im: + assert im.mode == "P" + assert im.size == (320, 200) + assert im.format == "FLI" + assert im.info["duration"] == 171 + assert im.is_animated - palette = im.getpalette() - assert palette[3:6] == [255, 255, 255] - assert palette[381:384] == [204, 204, 12] - assert palette[765:] == [252, 0, 0] - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + palette = im.getpalette() + assert palette[3:6] == [255, 255, 255] + assert palette[381:384] == [204, 204, 12] + assert palette[765:] == [252, 0, 0] @pytest.mark.skipif(is_pypy(), reason="Requires CPython") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 5d46b157d..61a9475c7 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -109,7 +109,7 @@ def test_palette_not_needed_for_second_frame() -> None: assert_image_similar(im, hopper("L").convert("RGB"), 8) -def test_strategy() -> None: +def test_strategy(monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/iss634.gif") as im: expected_rgb_always = im.convert("RGB") @@ -119,35 +119,36 @@ def test_strategy() -> None: im.seek(1) expected_different = im.convert("RGB") - try: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS - with Image.open("Tests/images/iss634.gif") as im: - assert im.mode == "RGB" - assert_image_equal(im, expected_rgb_always) + monkeypatch.setattr( + GifImagePlugin, "LOADING_STRATEGY", GifImagePlugin.LoadingStrategy.RGB_ALWAYS + ) + with Image.open("Tests/images/iss634.gif") as im: + assert im.mode == "RGB" + assert_image_equal(im, expected_rgb_always) - with Image.open("Tests/images/chi.gif") as im: - assert im.mode == "RGBA" - assert_image_equal(im, expected_rgb_always_rgba) + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "RGBA" + assert_image_equal(im, expected_rgb_always_rgba) - GifImagePlugin.LOADING_STRATEGY = ( - GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY - ) - # Stay in P mode with only a global palette - with Image.open("Tests/images/chi.gif") as im: - assert im.mode == "P" + monkeypatch.setattr( + GifImagePlugin, + "LOADING_STRATEGY", + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY, + ) + # Stay in P mode with only a global palette + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "P" - im.seek(1) - assert im.mode == "P" - assert_image_equal(im.convert("RGB"), expected_different) + im.seek(1) + assert im.mode == "P" + assert_image_equal(im.convert("RGB"), expected_different) - # Change to RGB mode when a frame has an individual palette - with Image.open("Tests/images/iss634.gif") as im: - assert im.mode == "P" + # Change to RGB mode when a frame has an individual palette + with Image.open("Tests/images/iss634.gif") as im: + assert im.mode == "P" - im.seek(1) - assert im.mode == "RGB" - finally: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + im.seek(1) + assert im.mode == "RGB" def test_optimize() -> None: @@ -555,17 +556,15 @@ def test_dispose_background_transparency() -> None: def test_transparent_dispose( loading_strategy: GifImagePlugin.LoadingStrategy, expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], + monkeypatch: pytest.MonkeyPatch, ) -> None: - GifImagePlugin.LOADING_STRATEGY = loading_strategy - try: - with Image.open("Tests/images/transparent_dispose.gif") as img: - for frame in range(3): - img.seek(frame) - for x in range(3): - color = img.getpixel((x, 0)) - assert color == expected_colors[frame][x] - finally: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) + with Image.open("Tests/images/transparent_dispose.gif") as img: + for frame in range(3): + img.seek(frame) + for x in range(3): + color = img.getpixel((x, 0)) + assert color == expected_colors[frame][x] def test_dispose_previous() -> None: @@ -1398,24 +1397,23 @@ def test_lzw_bits() -> None: ), ) def test_extents( - test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy + test_file: str, + loading_strategy: GifImagePlugin.LoadingStrategy, + monkeypatch: pytest.MonkeyPatch, ) -> None: - GifImagePlugin.LOADING_STRATEGY = loading_strategy - try: - with Image.open("Tests/images/" + test_file) as im: - assert im.size == (100, 100) + monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) + with Image.open("Tests/images/" + test_file) as im: + assert im.size == (100, 100) - # Check that n_frames does not change the size - assert im.n_frames == 2 - assert im.size == (100, 100) + # Check that n_frames does not change the size + assert im.n_frames == 2 + assert im.size == (100, 100) - im.seek(1) - assert im.size == (150, 150) + im.seek(1) + assert im.size == (150, 150) - im.load() - assert im.im.size == (150, 150) - finally: - GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + im.load() + assert im.im.size == (150, 150) def test_missing_background() -> None: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index e81aae669..e240faf1e 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -243,26 +243,23 @@ def test_draw_reloaded(tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") -def test_truncated_mask() -> None: +def test_truncated_mask(monkeypatch: pytest.MonkeyPatch) -> None: # 1 bpp with open("Tests/images/hopper_mask.ico", "rb") as fp: data = fp.read() - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) data = data[:-3] - try: - with Image.open(io.BytesIO(data)) as im: - assert im.mode == "1" + with Image.open(io.BytesIO(data)) as im: + assert im.mode == "1" - # 32 bpp - output = io.BytesIO() - expected = hopper("RGBA") - expected.save(output, "ico", bitmap_format="bmp") + # 32 bpp + output = io.BytesIO() + expected = hopper("RGBA") + expected.save(output, "ico", bitmap_format="bmp") - data = output.getvalue()[:-1] + data = output.getvalue()[:-1] - with Image.open(io.BytesIO(data)) as im: - assert im.mode == "RGB" - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + with Image.open(io.BytesIO(data)) as im: + assert im.mode == "RGB" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 4be9e16a7..772ecc2bc 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -530,12 +530,13 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_truncated_jpeg_should_read_all_the_data(self) -> None: + def test_truncated_jpeg_should_read_all_the_data( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: filename = "Tests/images/truncated_jpeg.jpg" - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) with Image.open(filename) as im: im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False assert im.getbbox() is not None def test_truncated_jpeg_throws_oserror(self) -> None: @@ -1024,7 +1025,7 @@ class TestFileJpeg: im.save(f, xmp=b"1" * 65505) @pytest.mark.timeout(timeout=1) - def test_eof(self) -> None: + def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): @@ -1039,9 +1040,8 @@ class TestFileJpeg: im.tile = [ ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), ] - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False def test_separate_tables(self) -> None: im = hopper() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 711e988df..589240191 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -181,14 +181,11 @@ def test_load_dpi() -> None: assert "dpi" not in im.info -def test_restricted_icc_profile() -> None: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - # JPEG2000 image with a restricted ICC profile and a known colorspace - with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im: - assert im.mode == "RGB" - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False +def test_restricted_icc_profile(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + # JPEG2000 image with a restricted ICC profile and a known colorspace + with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im: + assert im.mode == "RGB" @pytest.mark.skipif( diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 18dd11182..a5715db1b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1156,23 +1156,22 @@ class TestFileLibTiff(LibTiffTestCase): assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None: + def test_save_single_strip( + self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") if not argument: - TiffImagePlugin.STRIP_SIZE = 2**18 - try: - arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} - if argument: - arguments["strip_size"] = 2**18 - im.save(out, "TIFF", **arguments) + monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18) + arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} + if argument: + arguments["strip_size"] = 2**18 + im.save(out, "TIFF", **arguments) - with Image.open(out) as im: - assert isinstance(im, TiffImagePlugin.TiffImageFile) - assert len(im.tag_v2[STRIPOFFSETS]) == 1 - finally: - TiffImagePlugin.STRIP_SIZE = 65536 + with Image.open(out) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + assert len(im.tag_v2[STRIPOFFSETS]) == 1 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index d87883279..efd2e5cd9 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -363,7 +363,7 @@ class TestFilePng: with pytest.raises((OSError, SyntaxError)): im.verify() - def test_verify_ignores_crc_error(self) -> None: + def test_verify_ignores_crc_error(self, monkeypatch: pytest.MonkeyPatch) -> None: # check ignores crc errors in ancillary chunks chunk_data = chunk(b"tEXt", b"spam") @@ -373,24 +373,20 @@ class TestFilePng: with pytest.raises(SyntaxError): PngImagePlugin.PngImageFile(BytesIO(image_data)) - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im = load(image_data) - assert im is not None - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + im = load(image_data) + assert im is not None - def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None: + def test_verify_not_ignores_crc_error_in_required_chunk( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: # check does not ignore crc errors in required chunks image_data = MAGIC + IHDR[:-1] + b"q" + TAIL - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - with pytest.raises(SyntaxError): - PngImagePlugin.PngImageFile(BytesIO(image_data)) - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + with pytest.raises(SyntaxError): + PngImagePlugin.PngImageFile(BytesIO(image_data)) def test_roundtrip_dpi(self) -> None: # Check dpi roundtripping @@ -600,7 +596,7 @@ class TestFilePng: (b"prIV", b"VALUE3", True), ] - def test_textual_chunks_after_idat(self) -> None: + def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/hopper.png") as im: assert "comment" in im.text for k, v in { @@ -614,18 +610,17 @@ class TestFilePng: with pytest.raises(OSError): assert isinstance(im.text, dict) + # Raises an EOFError in load_end + with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} + # Raises a UnicodeDecodeError in load_end with Image.open("Tests/images/truncated_image.png") as im: # The file is truncated with pytest.raises(OSError): im.text - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) assert isinstance(im.text, dict) - ImageFile.LOAD_TRUNCATED_IMAGES = False - - # Raises an EOFError in load_end - with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: - assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} def test_unknown_compression_method(self) -> None: with pytest.raises(SyntaxError, match="Unknown compression method"): @@ -651,15 +646,16 @@ class TestFilePng: @pytest.mark.parametrize( "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") ) - def test_truncated_chunks(self, cid: bytes) -> None: + def test_truncated_chunks( + self, cid: bytes, monkeypatch: pytest.MonkeyPatch + ) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): png.call(cid, 0, 0) - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) png.call(cid, 0, 0) - ImageFile.LOAD_TRUNCATED_IMAGES = False @pytest.mark.parametrize("save_all", (True, False)) def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: @@ -789,17 +785,14 @@ class TestFilePng: with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) - def test_truncated_end_chunk(self) -> None: + def test_truncated_end_chunk(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/truncated_end_chunk.png") as im: with pytest.raises(OSError): im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - with Image.open("Tests/images/truncated_end_chunk.png") as im: - assert_image_equal_tofile(im, "Tests/images/hopper.png") - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + with Image.open("Tests/images/truncated_end_chunk.png") as im: + assert_image_equal_tofile(im, "Tests/images/hopper.png") @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @@ -808,11 +801,11 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2 * 1024 # max increase in K iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs - def test_leak_load(self) -> None: + def test_leak_load(self, monkeypatch: pytest.MonkeyPatch) -> None: with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) with Image.open(DATA) as im: im.load() @@ -820,7 +813,4 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): with Image.open(DATA) as im: im.load() - try: - self._test_leak(core) - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + self._test_leak(core) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 757d3f96a..67f808b60 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -939,11 +939,10 @@ class TestFileTiff: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") - def test_timeout(self) -> None: + def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: - ImageFile.LOAD_TRUNCATED_IMAGES = True + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) im.load() - ImageFile.LOAD_TRUNCATED_IMAGES = False @pytest.mark.parametrize( "test_file", diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index ad5aa9ed6..abe888241 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -28,9 +28,9 @@ except ImportError: class TestUnsupportedWebp: - def test_unsupported(self) -> None: + def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: if HAVE_WEBP: - WebPImagePlugin.SUPPORTED = False + monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False) file_path = "Tests/images/hopper.webp" with pytest.warns(UserWarning): @@ -38,9 +38,6 @@ class TestUnsupportedWebp: with Image.open(file_path): pass - if HAVE_WEBP: - WebPImagePlugin.SUPPORTED = True - @skip_unless_feature("webp") class TestFileWebp: diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 8bef90ce4..b05d29dae 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -191,13 +191,10 @@ class TestImageFile: im.load() @skip_unless_feature("zlib") - def test_truncated_without_errors(self) -> None: + def test_truncated_without_errors(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/truncated_image.png") as im: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + im.load() @skip_unless_feature("zlib") def test_broken_datastream_with_errors(self) -> None: @@ -206,13 +203,12 @@ class TestImageFile: im.load() @skip_unless_feature("zlib") - def test_broken_datastream_without_errors(self) -> None: + def test_broken_datastream_without_errors( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: - ImageFile.LOAD_TRUNCATED_IMAGES = True - try: - im.load() - finally: - ImageFile.LOAD_TRUNCATED_IMAGES = False + monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) + im.load() class MockPyDecoder(ImageFile.PyDecoder): diff --git a/Tests/test_map.py b/Tests/test_map.py index 93140f6e5..1278ba3a6 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -7,36 +7,30 @@ import pytest from PIL import Image -def test_overflow() -> None: +def test_overflow(monkeypatch: pytest.MonkeyPatch) -> None: # There is the potential to overflow comparisons in map.c # if there are > SIZE_MAX bytes in the image or if # the file encodes an offset that makes # (offset + size(bytes)) > SIZE_MAX # Note that this image triggers the decompression bomb warning: - max_pixels = Image.MAX_IMAGE_PIXELS - Image.MAX_IMAGE_PIXELS = None + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) # This image hits the offset test. with Image.open("Tests/images/l2rgb_read.bmp") as im: with pytest.raises((ValueError, MemoryError, OSError)): im.load() - Image.MAX_IMAGE_PIXELS = max_pixels - -def test_tobytes() -> None: +def test_tobytes(monkeypatch: pytest.MonkeyPatch) -> None: # Note that this image triggers the decompression bomb warning: - max_pixels = Image.MAX_IMAGE_PIXELS - Image.MAX_IMAGE_PIXELS = None + monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) # Previously raised an access violation on Windows with Image.open("Tests/images/l2rgb_read.bmp") as im: with pytest.raises((ValueError, MemoryError, OSError)): im.tobytes() - Image.MAX_IMAGE_PIXELS = max_pixels - @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_ysize() -> None: From a9d05a1e5122f1bab86d9c4d1b34a9cd40093d51 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 28 Jan 2025 07:59:44 +1100 Subject: [PATCH 119/140] Fixed unclosed file warnings (#8705) Co-authored-by: Andrew Murray --- Tests/test_file_libtiff.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index a5715db1b..033294710 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1103,13 +1103,15 @@ class TestFileLibTiff(LibTiffTestCase): ) def test_buffering(self, test_file: str) -> None: # load exif first - with Image.open(open(test_file, "rb", buffering=1048576)) as im: - exif = dict(im.getexif()) + with open(test_file, "rb", buffering=1048576) as f: + with Image.open(f) as im: + exif = dict(im.getexif()) # load image before exif - with Image.open(open(test_file, "rb", buffering=1048576)) as im2: - im2.load() - exif_after_load = dict(im2.getexif()) + with open(test_file, "rb", buffering=1048576) as f: + with Image.open(f) as im2: + im2.load() + exif_after_load = dict(im2.getexif()) assert exif == exif_after_load From 849768df7afbe02094fea2cda31f71e9267265d6 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 17 Sep 2024 15:49:20 +0200 Subject: [PATCH 120/140] Remove unused declaration --- src/libImaging/Imaging.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 31052c68a..0c2d3fc2e 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -609,10 +609,6 @@ ImagingLibTiffDecode( extern int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); #endif -#ifdef HAVE_LIBMPEG -extern int -ImagingMpegDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); -#endif extern int ImagingMspDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int From f598c0323392f12f0bd6d0b26e3c7de106c0b11d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jan 2025 17:33:55 +1100 Subject: [PATCH 121/140] Removed unused file --- src/libImaging/Except.c | 72 ----------------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 src/libImaging/Except.c diff --git a/src/libImaging/Except.c b/src/libImaging/Except.c deleted file mode 100644 index f42ff9aec..000000000 --- a/src/libImaging/Except.c +++ /dev/null @@ -1,72 +0,0 @@ -/* - * The Python Imaging Library - * $Id$ - * - * default exception handling - * - * This module is usually overridden by application code (e.g. - * _imaging.c for PIL's standard Python bindings). If you get - * linking errors, remove this file from your project/library. - * - * history: - * 1995-06-15 fl Created - * 1998-12-29 fl Minor tweaks - * 2003-09-13 fl Added ImagingEnter/LeaveSection() - * - * Copyright (c) 1997-2003 by Secret Labs AB. - * Copyright (c) 1995-2003 by Fredrik Lundh. - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -/* exception state */ - -void * -ImagingError_OSError(void) { - fprintf(stderr, "*** exception: file access error\n"); - return NULL; -} - -void * -ImagingError_MemoryError(void) { - fprintf(stderr, "*** exception: out of memory\n"); - return NULL; -} - -void * -ImagingError_ModeError(void) { - return ImagingError_ValueError("bad image mode"); -} - -void * -ImagingError_Mismatch(void) { - return ImagingError_ValueError("images don't match"); -} - -void * -ImagingError_ValueError(const char *message) { - if (!message) { - message = "exception: bad argument to function"; - } - fprintf(stderr, "*** %s\n", message); - return NULL; -} - -void -ImagingError_Clear(void) { - /* nop */; -} - -/* thread state */ - -void -ImagingSectionEnter(ImagingSectionCookie *cookie) { - /* pass */ -} - -void -ImagingSectionLeave(ImagingSectionCookie *cookie) { - /* pass */ -} From 9a4f39588dc082f6a6fbab3354b2a55fc588c195 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jan 2025 18:58:53 +1100 Subject: [PATCH 122/140] Use embedded color for text length in multiline_text --- src/PIL/ImageDraw.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d8e4c0c60..dd691eeec 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -742,7 +742,12 @@ class ImageDraw: line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: line_width = self.textlength( - line, font, direction=direction, features=features, language=language + line, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, ) widths.append(line_width) max_width = max(max_width, line_width) From 7093de46a7629956b77fba1ce1bfaf4ebb9c194d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jan 2025 19:42:25 +1100 Subject: [PATCH 123/140] Moved common multiline code into _prepare_multiline_text --- src/PIL/ImageDraw.py | 263 +++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 136 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index dd691eeec..d8b5180de 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -557,21 +557,6 @@ class ImageDraw: return split_character in text - def _multiline_split(self, text: AnyStr) -> list[AnyStr]: - return text.split("\n" if isinstance(text, str) else b"\n") - - def _multiline_spacing( - self, - font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, - spacing: float, - stroke_width: float, - ) -> float: - return ( - self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] - + stroke_width - + spacing - ) - def text( self, xy: tuple[float, float], @@ -697,6 +682,101 @@ class ImageDraw: # Only draw normal text draw_text(ink) + def _prepare_multiline_text( + self, + xy: tuple[float, float], + text: AnyStr, + font: ( + ImageFont.ImageFont + | ImageFont.FreeTypeFont + | ImageFont.TransposedFont + | None + ), + anchor: str | None, + spacing: float, + align: str, + direction: str | None, + features: list[str] | None, + language: str | None, + stroke_width: float, + embedded_color: bool, + font_size: float | None, + ) -> tuple[ + ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, + str, + list[tuple[tuple[float, float], AnyStr]], + ]: + if direction == "ttb": + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) + + if anchor is None: + anchor = "la" + elif len(anchor) != 2: + msg = "anchor must be a 2 character string" + raise ValueError(msg) + elif anchor[1] in "tb": + msg = "anchor not supported for multiline text" + raise ValueError(msg) + + if font is None: + font = self._getfont(font_size) + + widths = [] + max_width: float = 0 + lines = text.split("\n" if isinstance(text, str) else b"\n") + line_spacing = ( + self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + + stroke_width + + spacing + ) + + for line in lines: + line_width = self.textlength( + line, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + widths.append(line_width) + max_width = max(max_width, line_width) + + top = xy[1] + if anchor[1] == "m": + top -= (len(lines) - 1) * line_spacing / 2.0 + elif anchor[1] == "d": + top -= (len(lines) - 1) * line_spacing + + parts = [] + for idx, line in enumerate(lines): + left = xy[0] + width_difference = max_width - widths[idx] + + # first align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + + # then align by align parameter + if align == "left": + pass + elif align == "center": + left += width_difference / 2.0 + elif align == "right": + left += width_difference + else: + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) + + parts.append(((left, top), line)) + + top += line_spacing + + return font, anchor, parts + def multiline_text( self, xy: tuple[float, float], @@ -720,67 +800,24 @@ class ImageDraw: *, font_size: float | None = None, ) -> None: - if direction == "ttb": - msg = "ttb direction is unsupported for multiline text" - raise ValueError(msg) - - if anchor is None: - anchor = "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - widths = [] - max_width: float = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - for line in lines: - line_width = self.textlength( - line, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - top = xy[1] - if anchor[1] == "m": - top -= (len(lines) - 1) * line_spacing / 2.0 - elif anchor[1] == "d": - top -= (len(lines) - 1) * line_spacing - - for idx, line in enumerate(lines): - left = xy[0] - width_difference = max_width - widths[idx] - - # first align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - - # then align by align parameter - if align == "left": - pass - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center" or "right"' - raise ValueError(msg) + font, anchor, lines = self._prepare_multiline_text( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + font_size, + ) + for xy, line in lines: self.text( - (left, top), + xy, line, fill, font, @@ -792,7 +829,6 @@ class ImageDraw: stroke_fill=stroke_fill, embedded_color=embedded_color, ) - top += line_spacing def textlength( self, @@ -894,69 +930,26 @@ class ImageDraw: *, font_size: float | None = None, ) -> tuple[float, float, float, float]: - if direction == "ttb": - msg = "ttb direction is unsupported for multiline text" - raise ValueError(msg) - - if anchor is None: - anchor = "la" - elif len(anchor) != 2: - msg = "anchor must be a 2 character string" - raise ValueError(msg) - elif anchor[1] in "tb": - msg = "anchor not supported for multiline text" - raise ValueError(msg) - - if font is None: - font = self._getfont(font_size) - - widths = [] - max_width: float = 0 - lines = self._multiline_split(text) - line_spacing = self._multiline_spacing(font, spacing, stroke_width) - for line in lines: - line_width = self.textlength( - line, - font, - direction=direction, - features=features, - language=language, - embedded_color=embedded_color, - ) - widths.append(line_width) - max_width = max(max_width, line_width) - - top = xy[1] - if anchor[1] == "m": - top -= (len(lines) - 1) * line_spacing / 2.0 - elif anchor[1] == "d": - top -= (len(lines) - 1) * line_spacing + font, anchor, lines = self._prepare_multiline_text( + xy, + text, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + embedded_color, + font_size, + ) bbox: tuple[float, float, float, float] | None = None - for idx, line in enumerate(lines): - left = xy[0] - width_difference = max_width - widths[idx] - - # first align left by anchor - if anchor[0] == "m": - left -= width_difference / 2.0 - elif anchor[0] == "r": - left -= width_difference - - # then align by align parameter - if align == "left": - pass - elif align == "center": - left += width_difference / 2.0 - elif align == "right": - left += width_difference - else: - msg = 'align must be "left", "center" or "right"' - raise ValueError(msg) - + for xy, line in lines: bbox_line = self.textbbox( - (left, top), + xy, line, font, anchor, @@ -976,8 +969,6 @@ class ImageDraw: max(bbox[3], bbox_line[3]), ) - top += line_spacing - if bbox is None: return xy[0], xy[1], xy[0], xy[1] return bbox From 10eaff8ac7548ff50cefc003b27e2ba1a46ed71b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jan 2025 20:12:45 +1100 Subject: [PATCH 124/140] Added "justify" align for multiline text --- Tests/images/multiline_text_justify.png | Bin 0 -> 3244 bytes Tests/test_imagefont.py | 3 ++- docs/reference/ImageDraw.rst | 20 ++++++++++++-------- src/PIL/ImageDraw.py | 24 +++++++++++++++++++++--- 4 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 Tests/images/multiline_text_justify.png diff --git a/Tests/images/multiline_text_justify.png b/Tests/images/multiline_text_justify.png new file mode 100644 index 0000000000000000000000000000000000000000..32eed34cd219df9a59b7056c0ed17fa3879adea9 GIT binary patch literal 3244 zcmb_e`#;nBA6F?WN@eDfDD`zGk!#kDvE(-98qIwn<~q5|rBiVzq2(;M;nb2em*YCu z7KL1vM$9FI<~ogR=HBnL`T7eL-pN1@)#)qt}h96@N!gmjVFJ<>~Q^+ut5F5)EPlg<(<=sE-o%kl-qG}&1AAq&r|h@ zGNJ{`2DZ!XgZ&f{6=e<$DJUpZ6)rC?7b$fwM-~?qF#=DDiG_TrH%5Ifvdr4AcL*f& z!(jYxFB414)bU{I*z%$KUhn=m+G{_-JUu-xYrIU>X4r5zALK7L4x~R*O$lCA*psH7@7rT5RcxmL?Szk zqf?zIWu>Lx);4>kYhB|vR=j*K>ge!#bF`x-z)V}hljAblE6g=i6r$3G{NM}mM2I6l>I>n?3Ij=oGyFI-#rdXUO|r z)Ye{GrcS;In>n=I?pxhJp{Ob=KX6%Vm+-(~FrXwjp3pbPnP_QhZZ@)InS9;MAIQHP z^^{0FDk@s-p<`r(c6Wc{)0zI)kf9#IJ=>)Y>tbms%=(9-;@q=jS?^qfd}Kb78pUzP zUxY$;fed_ne7;B=Yn_fl7TGOS(DoL0;grF~xXmAD91XGvvWW){9LUoRnzosnXOP)X zZ{ECVU|_&K{BCb|+tSjqp+fpz51+Ko=T}^>Y}P zZ2-|MlMC)+eG#=thA((c)(1^}3Krq{r#_?+tVK*jPsu@h18fZ%G|u z85g(HmLz;rYlJ~IyWrlOBCM^)C_IP)JEBm=70T-BMa*HWZe)*NLCnkA+QDFIMn*;+ zeU?8F8#2m28zHKrqqFbA>3bjeq~Ot-E>BH;nPbWCyfh_sgTKs=E5Kks7Y8X~Vq#ax zs1k>lFJ9P!Z|`iZ{^k@fM0xef(?cgIMgV7+b*-qsqoc#tc^|rs^zoux+)C?E*$or7 zgQ16UhI*HDbSh!+OOhhb%0YXoy~@8B`tS92M<*oQ6r<&0V)8D8wmTbPnEb~dTzoth zTWXevoBN0YvqJTwjV5;alNAFCdwbgj^FOf1wncZ*|x+XcAdQ%9JlhdmCeTb5-{MUp|a``Yz~q5q8$OR$G8l08x0TgSIXS}!|Es(&jM>2SYmSbCKKiH-90RoJa>Y|L#adlU*55|vd`Qu3q{ zdRS6YQoZtB%~^l`Y4mevjq5|SaJ+u~!{@FqD^OwnB0CJXvv$tRR1G3$5dNa6Dc0M2 zdbBn$EiLVr4Ba~@+yy>fF5`K2l2ScF zkG~`uZkh<60{(U8vVVkqzi`=gtjW`IYdQ^W@N`ab&T! zmX?;Xau-&23|r?&aeB}`&0wIYV^A^*-5ed(nSNSXS((S|(qQLFQ8ajy^V}1I{BuuobMaGcN$=mk2OI#f zp4Wo`0Lm(_50$$ujSpkAHs>E2StsKX$HDPIC0npYA=A;DfEnMB$*Z^EgYVd&3giPNF z6yTnz?F$j>JYM|PLNS15=0XI!tiq-GkG-ADWk8Cirlw@w2*nG6cfd8Rt*s=I%vPAM z?`(}9Ex~M+t~dGO<;%LdIzyRsplf8te3&$R?IOn`o+sXN$|HK}U}N;d=kq^nB;@35 zP!9I?VlmLSGiS{N&(u6VxBs~JhZFliN57@~OE%sH#Z0Q|pVHKW+JT9Ad8g`+60@@G z!Hymta`o1s!RyT4FOtf~L2$PoHh?Y_Rn>Uv>Kp4Wq0Fuqwn}?AH&ehRB|hDM+&Kf? z_9P{ z^^5`nk$AJ#wX?JHvY}xec4T^0ZM!Gu%7pxrZ@HoEW>>l&jcRi7Q0QCxVq}a_dsn@F zZ}uumI|e4Bz<=fN+9P2TD5h)K(9TZW7x?OhREZ1gdxRf<4F>S=@s5s;R#sLR%nP&z zEu3Wmhqu+F#gmE&-*S6XGJ*)-rXSaM6?(8(EFC?)Nn9WFuB-%ycC&Y*Eaz`I8c#D$ zkq?GI>S}6A-I{de<>eI=Ue?#wH#f`7B~p*eCK8E6_N042?bgcFy8+$xFwYITPEN^K zQ0jTPB+bjon|C!-RQeYpWL=amee7)CfNA+&3s7KphCoH+x}fIf=EXLJ>-9HS08OI| z^YuY~Pb=-++^kXHYX9nyZ0yPK1e|p}{7DA9AYV*YwtQg0R2~%>S!d>SB#jal_T-|8 zowgT@be^w7>~3p=1DfEzxwp-+v9>){byA%YqLPj6$|6SN2+cP1gB?) zg%N)RFo&W4!GGV;;i?A7AZBJ}4t(FaZ2ebx`qo!HW}@o(O{H&ADs(zM`Pk_IfB#n$ zN-Tg_=rk-^CM}inNUAM&cm17Xc;BZ_j~!G_%gA6VfXT@6sano0=e0gMEfEN492@xk zf1$YBD|L0cX;*PSzL|3}91Wx-oTaoAb+(t2Dc=gj6p?@5|NT}|iXLy@{hm>oFgV)|3OCopCL9&#jU8gPyR zAXOJEmQ9^ZxU8i$9jf2h*qAP3&~`=n`A?672$ZtDUmOtkDt{1?I5tgSO>YFx>_U5ohMe#?_L2taQEDfuL&S@ouzj{9S+hY=P0fnT z;I6MDk;py3+z<$aFhosLI?YPq=zk`F|Ht=jDIZooCBA+rAiU~VJaYunv>fhv@1HSP BV(I_@ literal 0 HcmV?d00001 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6a0a940b9..3ccbf9b7d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -254,7 +254,8 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: @pytest.mark.parametrize( - "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) + "align, ext", + (("left", ""), ("center", "_center"), ("right", "_right"), ("justify", "_justify")), ) def test_render_multiline_text_align( font: ImageFont.FreeTypeFont, align: str, ext: str diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 3e9aa73f8..602a8f3e3 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -387,8 +387,9 @@ Methods the number of pixels between lines. :param align: If the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, - ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. + ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -455,8 +456,9 @@ Methods of Pillow, but implemented only in version 8.0.0. :param spacing: The number of pixels between lines. - :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -599,8 +601,9 @@ Methods the number of pixels between lines. :param align: If the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, - ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. + ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -650,8 +653,9 @@ Methods vertical text. See :ref:`text-anchors` for details. This parameter is ignored for non-TrueType fonts. :param spacing: The number of pixels between lines. - :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. - Use the ``anchor`` parameter to specify the alignment to ``xy``. + :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines + the relative alignment of lines. Use the ``anchor`` parameter to + specify the alignment to ``xy``. :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index d8b5180de..da7098789 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -761,17 +761,35 @@ class ImageDraw: left -= width_difference # then align by align parameter - if align == "left": + if align in ("left", "justify"): pass elif align == "center": left += width_difference / 2.0 elif align == "right": left += width_difference else: - msg = 'align must be "left", "center" or "right"' + msg = 'align must be "left", "center", "right" or "justify"' raise ValueError(msg) - parts.append(((left, top), line)) + if align == "justify" and width_difference != 0: + words = line.split(" " if isinstance(text, str) else b" ") + word_widths = [ + self.textlength( + word, + font, + direction=direction, + features=features, + language=language, + embedded_color=embedded_color, + ) + for word in words + ] + width_difference = max_width - sum(word_widths) + for i, word in enumerate(words): + parts.append(((left, top), word)) + left += word_widths[i] + width_difference / (len(words) - 1) + else: + parts.append(((left, top), line)) top += line_spacing From 1e115987afbc92aef02b489ed8fea1875821d174 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Jan 2025 19:09:03 +1100 Subject: [PATCH 125/140] Do not install libimagequant --- .github/workflows/test-mingw.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bb6d7dc37..045926482 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,7 +60,6 @@ jobs: mingw-w64-x86_64-gcc \ mingw-w64-x86_64-ghostscript \ mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libraqm \ mingw-w64-x86_64-libtiff \ From 9a58456c9b6a06518f3ce653ce02ec6e25512121 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 1 Feb 2025 00:44:26 +1100 Subject: [PATCH 126/140] Added versionadded for justify Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageDraw.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 602a8f3e3..a2e64a22a 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -390,6 +390,8 @@ Methods ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. + + .. versionadded:: 11.2.0 ``justify`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. From ec72d20d23e1dc2b06792535a6db7df778a8ad94 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Feb 2025 00:47:21 +1100 Subject: [PATCH 127/140] Added release notes --- docs/reference/ImageDraw.rst | 10 ++++++++-- docs/releasenotes/11.2.0.rst | 12 ++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index a2e64a22a..b2f1bdc93 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -390,8 +390,8 @@ Methods ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - - .. versionadded:: 11.2.0 ``justify`` + + .. versionadded:: 11.2.0 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -461,6 +461,8 @@ Methods :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. + + .. versionadded:: 11.2.0 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -606,6 +608,8 @@ Methods ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. + + .. versionadded:: 11.2.0 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. @@ -658,6 +662,8 @@ Methods :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. + + .. versionadded:: 11.2.0 ``"justify"`` :param direction: Direction of the text. It can be ``"rtl"`` (right to left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). Requires libraqm. diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index df28d05af..7e0008e66 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -44,6 +44,18 @@ TODO API Additions ============= +"justify" multiline text alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to "left", "center" and "right", multiline text can also be aligned using +"justify":: + + from PIL import Image, ImageDraw + im = Image.new("RGB", (50, 25)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") + draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") + Check for MozJPEG ^^^^^^^^^^^^^^^^^ From 5bbbc462403067c42dec782c11920aeecf9206a0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Feb 2025 01:13:04 +1100 Subject: [PATCH 128/140] Fixed exceptions when closing AppendingTiffWriter --- Tests/test_file_tiff.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 67f808b60..af4bae5dc 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -780,15 +780,17 @@ class TestFileTiff: 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.seek(-4, os.SEEK_CUR) a.writeLong(2**32 - 1) - assert b.getvalue() == data + b"\xff\xff\xff\xff" + assert b.getvalue() == data[:-4] + 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.seek(-2, os.SEEK_CUR) a.rewriteLastShortToLong(2**32 - 1) - assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff" + assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff" def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. From fca48db866870dea024e4f627059b17571940349 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 1 Feb 2025 10:02:42 +1100 Subject: [PATCH 129/140] Added quote marks Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/11.2.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index 7e0008e66..5929de3b1 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -44,11 +44,11 @@ TODO API Additions ============= -"justify" multiline text alignment -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``"justify"`` multiline text alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In addition to "left", "center" and "right", multiline text can also be aligned using -"justify":: +In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be aligned using +``"justify"``:: from PIL import Image, ImageDraw im = Image.new("RGB", (50, 25)) From 69c95725179c2d7cbba1104ed5c99d2e9092f43d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Feb 2025 10:54:18 +1100 Subject: [PATCH 130/140] Added ImageDraw link --- 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 5929de3b1..f7e644cf3 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -47,8 +47,8 @@ API Additions ``"justify"`` multiline text alignment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be aligned using -``"justify"``:: +In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be +aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: from PIL import Image, ImageDraw im = Image.new("RGB", (50, 25)) From 347a3865bf809918edaa4391978394dec47c80e3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Feb 2025 12:21:15 +1100 Subject: [PATCH 131/140] Revert "Ignore brew dependencies for libraqm on macOS 13" This reverts commit dfd53564ff6a3fc7d35a5884bc0ef03939bcec0a. --- .github/workflows/macos-install.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 2301a3a7e..6aa59a4ac 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -10,15 +10,11 @@ brew install \ ghostscript \ jpeg-turbo \ libimagequant \ + libraqm \ libtiff \ little-cms2 \ openjpeg \ webp -if [[ "$ImageOS" == "macos13" ]]; then - brew install --ignore-dependencies libraqm -else - brew install libraqm -fi export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" python3 -m pip install coverage From ce1996d8040bd2bab17a16ceb678eed3325194eb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Feb 2025 19:32:59 +1100 Subject: [PATCH 132/140] Use getpixel() instead of load() --- Tests/test_file_gif.py | 13 ++++++------- Tests/test_image.py | 4 +--- Tests/test_image_convert.py | 4 +--- Tests/test_image_quantize.py | 4 +--- Tests/test_imageops.py | 8 ++------ Tests/test_numpy.py | 4 +--- 6 files changed, 12 insertions(+), 25 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 61a9475c7..46215db1f 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -86,12 +86,12 @@ def test_invalid_file() -> None: def test_l_mode_transparency() -> None: with Image.open("Tests/images/no_palette_with_transparency.gif") as im: assert im.mode == "L" - assert im.load()[0, 0] == 128 + assert im.getpixel((0, 0)) == 128 assert im.info["transparency"] == 255 im.seek(1) assert im.mode == "L" - assert im.load()[0, 0] == 128 + assert im.getpixel((0, 0)) == 128 def test_l_mode_after_rgb() -> None: @@ -311,7 +311,7 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() - original_color = im.convert("RGB").load()[0, 0] + original_color = im.convert("RGB").getpixel((0, 0)) im.seek(1) assert im.mode == mode @@ -319,10 +319,10 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: im = im.convert("RGB") # Check a color only from the old palette - assert im.load()[0, 0] == original_color + assert im.getpixel((0, 0)) == original_color # Check a color from the new palette - assert im.load()[24, 24] not in first_frame_colors + assert im.getpixel((24, 24)) not in first_frame_colors def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: @@ -488,8 +488,7 @@ def test_eoferror() -> None: def test_first_frame_transparency() -> None: with Image.open("Tests/images/first_frame_transparency.gif") as im: - px = im.load() - assert px[0, 0] == im.info["transparency"] + assert im.getpixel((0, 0)) == im.info["transparency"] def test_dispose_none() -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 9a2e3c465..e060eb06a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -578,9 +578,7 @@ class TestImage: def test_one_item_tuple(self) -> None: for mode in ("I", "F", "L"): im = Image.new(mode, (100, 100), (5,)) - px = im.load() - assert px is not None - assert px[0, 0] == 5 + assert im.getpixel((0, 0)) == 5 def test_linear_gradient_wrong_mode(self) -> None: # Arrange diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6a925975e..1e66e84df 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -222,9 +222,7 @@ def test_l_macro_rounding(convert_mode: str) -> None: im.palette.getcolor((0, 1, 2)) converted_im = im.convert(convert_mode) - px = converted_im.load() - assert px is not None - converted_color = px[0, 0] + converted_color = converted_im.getpixel((0, 0)) if convert_mode == "LA": assert isinstance(converted_color, tuple) converted_color = converted_color[0] diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 7c564d967..0ca7ad86e 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -148,10 +148,8 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) converted = im.quantize(method=method) - converted_px = converted.load() - assert converted_px is not None assert converted.palette is not None - assert converted_px[0, 0] == converted.palette.colors[color] + assert converted.getpixel((0, 0)) == converted.palette.colors[color] def test_small_palette() -> None: diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 7262f29e6..3621aa50f 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -165,14 +165,10 @@ def test_pad() -> None: def test_pad_round() -> None: im = Image.new("1", (1, 1), 1) new_im = ImageOps.pad(im, (4, 1)) - px = new_im.load() - assert px is not None - assert px[2, 0] == 1 + assert new_im.getpixel((2, 0)) == 1 new_im = ImageOps.pad(im, (1, 4)) - px = new_im.load() - assert px is not None - assert px[0, 2] == 1 + assert new_im.getpixel((0, 2)) == 1 @pytest.mark.parametrize("mode", ("P", "PA")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 79cd14b66..c4ad19d23 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -141,9 +141,7 @@ def test_save_tiff_uint16() -> None: a.shape = TEST_IMAGE_SIZE img = Image.fromarray(a) - img_px = img.load() - assert img_px is not None - assert img_px[0, 0] == pixel_value + assert img.getpixel((0, 0)) == pixel_value @pytest.mark.parametrize( From 90d25060743dfc118816378e3f614040b13f9596 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:35:25 +0000 Subject: [PATCH 133/140] [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.6 → v0.9.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.6...v0.9.4) - [github.com/psf/black-pre-commit-mirror: 24.10.0 → 25.1.0](https://github.com/psf/black-pre-commit-mirror/compare/24.10.0...25.1.0) - [github.com/PyCQA/bandit: 1.8.0 → 1.8.2](https://github.com/PyCQA/bandit/compare/1.8.0...1.8.2) - [github.com/pre-commit/mirrors-clang-format: v19.1.6 → v19.1.7](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.6...v19.1.7) - [github.com/python-jsonschema/check-jsonschema: 0.30.0 → 0.31.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.30.0...0.31.1) - [github.com/woodruffw/zizmor-pre-commit: v1.0.0 → v1.3.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.0.0...v1.3.0) - [github.com/tox-dev/tox-ini-fmt: 1.4.1 → 1.5.0](https://github.com/tox-dev/tox-ini-fmt/compare/1.4.1...1.5.0) --- .pre-commit-config.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20fa7d04f..a8c8cee15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.6 + rev: v0.9.4 hooks: - id: ruff args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.10.0 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.8.0 + rev: 1.8.2 hooks: - id: bandit args: [--severity-level=high] @@ -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.6 + rev: v19.1.7 hooks: - id: clang-format types: [c] @@ -50,14 +50,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.1 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.0.0 + rev: v1.3.0 hooks: - id: zizmor @@ -78,7 +78,7 @@ repos: additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.4.1 + rev: 1.5.0 hooks: - id: tox-ini-fmt From 955d678ca201fd530027d90626f91aad07c64f0e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:35:58 +0000 Subject: [PATCH 134/140] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 2 +- Tests/test_file_libtiff.py | 2 +- Tests/test_file_pdf.py | 4 ++-- Tests/test_file_ppm.py | 4 ++-- Tests/test_file_tiff.py | 10 ++++----- Tests/test_file_wmf.py | 2 +- Tests/test_imagedraw.py | 2 +- Tests/test_imagefont.py | 6 +++--- Tests/test_imagefontctl.py | 42 +++++++++++++++++++------------------- Tests/test_imagepalette.py | 2 +- Tests/test_imagepath.py | 2 +- Tests/test_pdfparser.py | 18 ++++++++-------- src/PIL/ImImagePlugin.py | 4 ++-- src/PIL/Image.py | 4 ++-- src/PIL/ImtImagePlugin.py | 2 +- src/PIL/JpegImagePlugin.py | 12 +++++------ src/PIL/MpoImagePlugin.py | 4 ++-- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/PdfParser.py | 36 ++++++++++++++++---------------- src/PIL/PngImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 16 +++++++-------- src/PIL/_tkinter_finder.py | 3 +-- 23 files changed, 91 insertions(+), 92 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 772ecc2bc..91bf3cf74 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -934,7 +934,7 @@ class TestFileJpeg: def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None: size = 4097 - buffer = BytesIO(b"\xFF" * size) # Many xFF bytes + buffer = BytesIO(b"\xff" * size) # Many xFF bytes max_pos = 0 orig_read = buffer.read diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 033294710..369c2db1b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -309,7 +309,7 @@ class TestFileLibTiff(LibTiffTestCase): } def check_tags( - tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str], ) -> None: im = hopper() diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 1d5001b1a..815686a52 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -264,7 +264,7 @@ def test_pdf_append(tmp_path: Path) -> None: # append some info pdf.info.Title = "abc" pdf.info.Author = "def" - pdf.info.Subject = "ghi\uABCD" + pdf.info.Subject = "ghi\uabcd" pdf.info.Keywords = "qw)e\\r(ty" pdf.info.Creator = "hopper()" pdf.start_writing() @@ -292,7 +292,7 @@ def test_pdf_append(tmp_path: Path) -> None: assert pdf.info.Title == "abc" assert pdf.info.Producer == "PdfParser" assert pdf.info.Keywords == "qw)e\\r(ty" - assert pdf.info.Subject == "ghi\uABCD" + assert pdf.info.Subject == "ghi\uabcd" assert b"CreationDate" in pdf.info assert b"ModDate" in pdf.info check_pdf_pages_consistency(pdf) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index ee51a5e5a..bb59767f0 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -49,7 +49,7 @@ def test_sanity() -> None: (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)), # P6 with maxval < 255 ( - b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11", + b"P6 3 1 17 \x00\x01\x02\x08\x09\x0a\x0f\x10\x11", "RGB", ( (0, 15, 30), @@ -60,7 +60,7 @@ def test_sanity() -> None: # P6 with maxval > 255 ( b"P6 3 1 257 \x00\x00\x00\x01\x00\x02" - b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF", + b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xff\xff", "RGB", ( (0, 1, 2), diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index af4bae5dc..fe8f69848 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,14 +759,14 @@ class TestFileTiff: with pytest.raises(RuntimeError): a.fixOffsets(1) - 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: 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") + 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 @@ -777,7 +777,7 @@ class TestFileTiff: 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" + 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.seek(-4, os.SEEK_CUR) @@ -785,7 +785,7 @@ class TestFileTiff: assert b.getvalue() == data[:-4] + 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" + 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.seek(-2, os.SEEK_CUR) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 2f1f8cdbc..bc14ed9d7 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -71,7 +71,7 @@ def test_load_float_dpi() -> None: with open("Tests/images/drawing.emf", "rb") as fp: data = fp.read() - b = BytesIO(data[:8] + b"\x06\xFA" + data[10:]) + b = BytesIO(data[:8] + b"\x06\xfa" + data[10:]) with Image.open(b) as im: assert im.info["dpi"][0] == 2540 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 28d7ed725..d127175eb 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -812,7 +812,7 @@ def test_rounded_rectangle( tuple[int, int, int, int] | tuple[list[int]] | tuple[tuple[int, int], tuple[int, int]] - ) + ), ) -> None: # Arrange im = Image.new("RGB", (200, 200)) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f110cc1d0..4b41d8336 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -557,7 +557,7 @@ def test_render_empty(font: ImageFont.FreeTypeFont) -> None: def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: # issue #3777 - text = "A\u278A\U0001F12B" + text = "A\u278a\U0001f12b" target = "Tests/images/unicode_extended.png" ttf = ImageFont.truetype( @@ -1026,7 +1026,7 @@ def test_sbix(layout_engine: ImageFont.Layout) -> None: im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) - d.text((50, 50), "\uE901", font=font, embedded_color=True) + d.text((50, 50), "\ue901", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) except OSError as e: # pragma: no cover @@ -1043,7 +1043,7 @@ def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) - d.text((50, 50), "\uE901", (100, 0, 0), font=font) + d.text((50, 50), "\ue901", (100, 0, 0), font=font) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) except OSError as e: # pragma: no cover diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 24c7b871a..c85eb499c 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -229,7 +229,7 @@ def test_getlength( @pytest.mark.parametrize("direction", ("ltr", "ttb")) @pytest.mark.parametrize( "text", - ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), + ("i" + ("\u030c" * 15) + "i", "i" + "\u032c" * 15 + "i", "\u035cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) def test_getlength_combine(mode: str, direction: str, text: str) -> None: @@ -272,27 +272,27 @@ def test_anchor_ttb(anchor: str) -> None: combine_tests = ( # extends above (e.g. issue #4553) - ("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08), - ("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08), - ("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08), - ("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08), - ("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3), - ("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3), + ("caron", "a\u030c\u030c\u030c\u030c\u030cb", None, None, 0.08), + ("caron_la", "a\u030c\u030c\u030c\u030c\u030cb", "la", None, 0.08), + ("caron_lt", "a\u030c\u030c\u030c\u030c\u030cb", "lt", None, 0.08), + ("caron_ls", "a\u030c\u030c\u030c\u030c\u030cb", "ls", None, 0.08), + ("caron_ttb", "ca" + ("\u030c" * 15) + "b", None, "ttb", 0.3), + ("caron_ttb_lt", "ca" + ("\u030c" * 15) + "b", "lt", "ttb", 0.3), # extends below - ("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02), - ("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02), - ("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02), - ("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02), - ("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03), - ("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03), + ("caron_below", "a\u032c\u032c\u032c\u032c\u032cb", None, None, 0.02), + ("caron_below_ld", "a\u032c\u032c\u032c\u032c\u032cb", "ld", None, 0.02), + ("caron_below_lb", "a\u032c\u032c\u032c\u032c\u032cb", "lb", None, 0.02), + ("caron_below_ls", "a\u032c\u032c\u032c\u032c\u032cb", "ls", None, 0.02), + ("caron_below_ttb", "a" + ("\u032c" * 15) + "b", None, "ttb", 0.03), + ("caron_below_ttb_lb", "a" + ("\u032c" * 15) + "b", "lb", "ttb", 0.03), # extends to the right (e.g. issue #3745) - ("double_breve_below", "a\u035Ci", None, None, 0.02), - ("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02), - ("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02), - ("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02), - ("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02), - ("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02), - ("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02), + ("double_breve_below", "a\u035ci", None, None, 0.02), + ("double_breve_below_ma", "a\u035ci", "ma", None, 0.02), + ("double_breve_below_ra", "a\u035ci", "ra", None, 0.02), + ("double_breve_below_ttb", "a\u035cb", None, "ttb", 0.02), + ("double_breve_below_ttb_rt", "a\u035cb", "rt", "ttb", 0.02), + ("double_breve_below_ttb_mt", "a\u035cb", "mt", "ttb", 0.02), + ("double_breve_below_ttb_st", "a\u035cb", "st", "ttb", 0.02), # extends to the left (fail=0.064) ("overline", "i\u0305", None, None, 0.02), ("overline_la", "i\u0305", "la", None, 0.02), @@ -346,7 +346,7 @@ def test_combine_multiline(anchor: str, align: str) -> None: path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word + text = "i\u0305\u035c\ntext" # i with overline and double breve, and a word im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 6cf0079dd..6d0e6f36f 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -189,7 +189,7 @@ def test_2bit_palette(tmp_path: Path) -> None: rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 img = Image.frombytes("P", (6, 1), rgb) - img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB + img.putpalette(b"\xff\x00\x00\x00\xff\x00\x00\x00\xff") # RGB img.save(outfile, format="PNG") assert_image_equal_tofile(img, outfile) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 76bdf1e5f..1b1ee6bac 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -79,7 +79,7 @@ def test_path_constructors( ), ) def test_invalid_path_constructors( - coords: tuple[str, str] | Sequence[Sequence[int]] + coords: tuple[str, str] | Sequence[Sequence[int]], ) -> None: # Act with pytest.raises(ValueError) as e: diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index f6b12cb20..d85fb1212 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -20,10 +20,10 @@ from PIL.PdfParser import ( def test_text_encode_decode() -> None: - assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c" - assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc" + assert encode_text("abc") == b"\xfe\xff\x00a\x00b\x00c" + assert decode_text(b"\xfe\xff\x00a\x00b\x00c") == "abc" assert decode_text(b"abc") == "abc" - assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" + assert decode_text(b"\x1b a \x1c") == "\u02d9 a \u02dd" def test_indirect_refs() -> None: @@ -45,8 +45,8 @@ def test_parsing() -> None: assert PdfParser.get_value(b"false%", 0) == (False, 5) assert PdfParser.get_value(b"null<", 0) == (None, 4) assert PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0) == (123, 15) - assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1F\xA3", 8) - assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1F\xA0", 17) + assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1f\xa3", 8) + assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1f\xa0", 17) assert PdfParser.get_value(b"(asd)", 0) == (b"asd", 5) assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13) assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14) @@ -56,9 +56,9 @@ def test_parsing() -> None: assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12) assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12) assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7) - assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6) - assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5) - assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6) + assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2b", 6) + assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2b", 5) + assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2ba", 6) assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7) assert PdfParser.get_value(b" 123 (", 0) == (123, 4) assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0 @@ -118,7 +118,7 @@ def test_pdf_repr() -> None: assert pdf_repr(None) == b"null" assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" - assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" + assert pdf_repr(PdfBinary(b"\x90\x1f\xa0")) == b"<901FA0>" def test_duplicate_xref_entry() -> None: diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index b4215a0b1..2a26d0b29 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -145,7 +145,7 @@ class ImImageFile(ImageFile.ImageFile): if s == b"\r": continue - if not s or s == b"\0" or s == b"\x1A": + if not s or s == b"\0" or s == b"\x1a": break # FIXME: this may read whole file if not a text file @@ -209,7 +209,7 @@ class ImImageFile(ImageFile.ImageFile): self._mode = self.info[MODE] # Skip forward to start of image data - while s and s[:1] != b"\x1A": + while s and s[:1] != b"\x1a": s = self.fp.read(1) if not s: msg = "File truncated" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 99b1b9ab3..e723b6a2e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -514,7 +514,7 @@ class ImagePointTransform: def _getscaleoffset( - expr: Callable[[ImagePointTransform], ImagePointTransform | float] + expr: Callable[[ImagePointTransform], ImagePointTransform | float], ) -> tuple[float, float]: a = expr(ImagePointTransform(1, 0)) return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) @@ -3884,7 +3884,7 @@ class Exif(_ExifBase): return self._fixup_dict(dict(info)) def _get_head(self) -> bytes: - version = b"\x2B" if self.bigtiff else b"\x2A" + version = b"\x2b" if self.bigtiff else b"\x2a" if self.endian == "<": head = b"II" + version + b"\x00" + o32le(8) else: diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 068cd5c33..c4eccee34 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -55,7 +55,7 @@ class ImtImageFile(ImageFile.ImageFile): if not s: break - if s == b"\x0C": + if s == b"\x0c": # image data begins self.tile = [ ImageFile._Tile( diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 457690aac..19639f634 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -325,7 +325,7 @@ MARKER = { def _accept(prefix: bytes) -> bool: # Magic number was taken from https://en.wikipedia.org/wiki/JPEG - return prefix[:3] == b"\xFF\xD8\xFF" + return prefix[:3] == b"\xff\xd8\xff" ## @@ -342,7 +342,7 @@ class JpegImageFile(ImageFile.ImageFile): if not _accept(s): msg = "not a JPEG file" raise SyntaxError(msg) - s = b"\xFF" + s = b"\xff" # Create attributes self.bits = self.layers = 0 @@ -417,7 +417,7 @@ class JpegImageFile(ImageFile.ImageFile): # Premature EOF. # Pretend file is finished adding EOI marker self._ended = True - return b"\xFF\xD9" + return b"\xff\xd9" return s @@ -712,7 +712,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def validate_qtables( qtables: ( str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None - ) + ), ) -> list[list[int]] | None: if qtables is None: return qtables @@ -769,7 +769,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: msg = "XMP data is too long" raise ValueError(msg) size = o16(2 + overhead_len + len(xmp)) - extra += b"\xFF\xE1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp + extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp icc_profile = info.get("icc_profile") if icc_profile: @@ -783,7 +783,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: for marker in markers: size = o16(2 + overhead_len + len(marker)) extra += ( - b"\xFF\xE2" + b"\xff\xe2" + size + b"ICC_PROFILE\0" + o8(i) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 71f89a09a..e08f80b6b 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -51,7 +51,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if not offsets: # APP2 marker im_frame.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 + b"\xff\xe2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 ) exif = im_frame.encoderinfo.get("exif") if isinstance(exif, Image.Exif): @@ -84,7 +84,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ifd[0xB002] = mpentries fp.seek(mpf_offset) - fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8)) + fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8)) fp.seek(0, os.SEEK_END) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 32436cea3..299405ae0 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + o16(dpi[0]) + o16(dpi[1]) + b"\0" * 24 - + b"\xFF" * 24 + + b"\xff" * 24 + b"\0" + o8(planes) + o16(stride) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 7cb2d241b..41b38ebbf 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -19,14 +19,14 @@ def encode_text(s: str) -> bytes: PDFDocEncoding = { 0x16: "\u0017", - 0x18: "\u02D8", - 0x19: "\u02C7", - 0x1A: "\u02C6", - 0x1B: "\u02D9", - 0x1C: "\u02DD", - 0x1D: "\u02DB", - 0x1E: "\u02DA", - 0x1F: "\u02DC", + 0x18: "\u02d8", + 0x19: "\u02c7", + 0x1A: "\u02c6", + 0x1B: "\u02d9", + 0x1C: "\u02dd", + 0x1D: "\u02db", + 0x1E: "\u02da", + 0x1F: "\u02dc", 0x80: "\u2022", 0x81: "\u2020", 0x82: "\u2021", @@ -36,29 +36,29 @@ PDFDocEncoding = { 0x86: "\u0192", 0x87: "\u2044", 0x88: "\u2039", - 0x89: "\u203A", + 0x89: "\u203a", 0x8A: "\u2212", 0x8B: "\u2030", - 0x8C: "\u201E", - 0x8D: "\u201C", - 0x8E: "\u201D", + 0x8C: "\u201e", + 0x8D: "\u201c", + 0x8E: "\u201d", 0x8F: "\u2018", 0x90: "\u2019", - 0x91: "\u201A", + 0x91: "\u201a", 0x92: "\u2122", - 0x93: "\uFB01", - 0x94: "\uFB02", + 0x93: "\ufb01", + 0x94: "\ufb02", 0x95: "\u0141", 0x96: "\u0152", 0x97: "\u0160", 0x98: "\u0178", - 0x99: "\u017D", + 0x99: "\u017d", 0x9A: "\u0131", 0x9B: "\u0142", 0x9C: "\u0153", 0x9D: "\u0161", - 0x9E: "\u017E", - 0xA0: "\u20AC", + 0x9E: "\u017e", + 0xA0: "\u20ac", } diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index f56555160..5ea87686d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1433,7 +1433,7 @@ def _save( chunk(fp, b"tRNS", transparency[:alpha_bytes]) else: transparency = max(0, min(255, transparency)) - alpha = b"\xFF" * transparency + b"\0" + alpha = b"\xff" * transparency + b"\0" chunk(fp, b"tRNS", alpha[:alpha_bytes]) elif im.mode in ("1", "L", "I", "I;16"): transparency = max(0, min(65535, transparency)) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 4e779df17..fb228f572 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -230,7 +230,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): msg = b"Invalid token for this mode: %s" % bytes([token]) raise ValueError(msg) data = (data + tokens)[:total_bytes] - invert = bytes.maketrans(b"01", b"\xFF\x00") + invert = bytes.maketrans(b"01", b"\xff\x00") return data.translate(invert) def _decode_blocks(self, maxval: int) -> bytearray: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f49c09822..f557d104b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -275,12 +275,12 @@ OPEN_INFO = { MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) PREFIXES = [ - b"MM\x00\x2A", # Valid TIFF header with big-endian byte order - b"II\x2A\x00", # Valid TIFF header with little-endian byte order - b"MM\x2A\x00", # Invalid TIFF header, assume big-endian - b"II\x00\x2A", # Invalid TIFF header, assume little-endian - b"MM\x00\x2B", # BigTIFF with big-endian byte order - b"II\x2B\x00", # BigTIFF with little-endian byte order + b"MM\x00\x2a", # Valid TIFF header with big-endian byte order + b"II\x2a\x00", # Valid TIFF header with little-endian byte order + b"MM\x2a\x00", # Invalid TIFF header, assume big-endian + b"II\x00\x2a", # Invalid TIFF header, assume little-endian + b"MM\x00\x2b", # BigTIFF with big-endian byte order + b"II\x2b\x00", # BigTIFF with little-endian byte order ] if not getattr(Image.core, "libtiff_support_custom_tags", True): @@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __init__( self, - ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00", + ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00", prefix: bytes | None = None, group: int | None = None, ) -> None: @@ -2047,7 +2047,7 @@ class AppendingTiffWriter(io.BytesIO): self.offsetOfNewPage = 0 self.IIMM = iimm = self.f.read(4) - self._bigtiff = b"\x2B" in iimm + self._bigtiff = b"\x2b" in iimm if not iimm: # empty file - first page self.isFirst = True diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index beddfb062..9c0143003 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,5 +1,4 @@ -""" Find compiled module linking to Tcl / Tk libraries -""" +"""Find compiled module linking to Tcl / Tk libraries""" from __future__ import annotations From 00790e925dc007a67eb166c69ec87e48678e28b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 4 Feb 2025 06:49:46 +1100 Subject: [PATCH 135/140] Updated comment --- 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 91bf3cf74..a2481c336 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -934,7 +934,7 @@ class TestFileJpeg: def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None: size = 4097 - buffer = BytesIO(b"\xff" * size) # Many xFF bytes + buffer = BytesIO(b"\xff" * size) # Many xff bytes max_pos = 0 orig_read = buffer.read From a7d7a1080ed2e507613201a9f837b40227247ff7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Feb 2025 18:42:35 +1100 Subject: [PATCH 136/140] Removed redundant argument parsing --- src/_imaging.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 2fd2deffb..9ce4b34aa 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1013,10 +1013,6 @@ _convert_transparent(ImagingObject *self, PyObject *args) { static PyObject * _copy(ImagingObject *self, PyObject *args) { - if (!PyArg_ParseTuple(args, "")) { - return NULL; - } - return PyImagingNew(ImagingCopy(self->image)); } From b19506a4993b9003809c711b50fd0e82cba1bbd9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Feb 2025 19:12:50 +1100 Subject: [PATCH 137/140] Simplify Python code by passing tuples to C --- Tests/test_color_lut.py | 20 +++++++++----------- src/PIL/ImageFilter.py | 4 +--- src/PIL/ImageFont.py | 3 +-- src/PIL/JpegImagePlugin.py | 3 +-- src/PIL/WebPImagePlugin.py | 3 +-- src/PIL/_imagingft.pyi | 3 +-- src/_imaging.c | 2 +- src/_imagingft.c | 2 +- src/_webp.c | 2 +- src/encode.c | 2 +- 10 files changed, 18 insertions(+), 26 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index baa899df5..26945ae1a 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -19,7 +19,7 @@ except ImportError: class TestColorLut3DCoreAPI: def generate_identity_table( self, channels: int, size: int | tuple[int, int, int] - ) -> tuple[int, int, int, int, list[float]]: + ) -> tuple[int, tuple[int, int, int], list[float]]: if isinstance(size, tuple): size_1d, size_2d, size_3d = size else: @@ -39,9 +39,7 @@ class TestColorLut3DCoreAPI: ] return ( channels, - size_1d, - size_2d, - size_3d, + (size_1d, size_2d, size_3d), [item for sublist in table for item in sublist], ) @@ -89,21 +87,21 @@ class TestColorLut3DCoreAPI: with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7 + "RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 7 ) with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9 + "RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 9 ) with pytest.raises(TypeError): im.im.color_lut_3d( - "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8 + "RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, "0"] * 8 ) with pytest.raises(TypeError): - im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) + im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), 16) @pytest.mark.parametrize( "lut_mode, table_channels, table_size", @@ -264,7 +262,7 @@ class TestColorLut3DCoreAPI: assert_image_equal( Image.merge('RGB', im.split()[::-1]), im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, [ + 3, (2, 2, 2), [ 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, @@ -286,7 +284,7 @@ class TestColorLut3DCoreAPI: # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, + 3, (2, 2, 2), [ -1, -1, -1, 2, -1, -1, -1, 2, -1, 2, 2, -1, @@ -307,7 +305,7 @@ class TestColorLut3DCoreAPI: # fmt: off transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, - 3, 2, 2, 2, + 3, (2, 2, 2), [ -3, -3, -3, 5, -3, -3, -3, 5, -3, 5, 5, -3, diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index b350e56f4..1c8b29b11 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -598,8 +598,6 @@ class Color3DLUT(MultibandFilter): self.mode or image.mode, Image.Resampling.BILINEAR, self.channels, - self.size[0], - self.size[1], - self.size[2], + self.size, self.table, ) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a4986aa8c..c8f05fbb7 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -647,8 +647,7 @@ class FreeTypeFont: kwargs.get("stroke_filled", False), anchor, ink, - start[0], - start[1], + start, ) def font_variant( diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 19639f634..a1c9c443a 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -816,8 +816,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: optimize, info.get("keep_rgb", False), info.get("streamtype", 0), - dpi[0], - dpi[1], + dpi, subsampling, info.get("restart_marker_blocks", 0), info.get("restart_marker_rows", 0), diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index c7f855527..066fe551f 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -223,8 +223,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Setup the WebP animation encoder enc = _webp.WebPAnimEncoder( - im.size[0], - im.size[1], + im.size, background, loop, minimize_size, diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 813294747..1cb1429d6 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -31,8 +31,7 @@ class Font: stroke_filled: bool, anchor: str | None, foreground_ink_long: int, - x_start: float, - y_start: float, + start: tuple[float, float], /, ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... def getsize( diff --git a/src/_imaging.c b/src/_imaging.c index 2fd2deffb..975c700dc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -866,7 +866,7 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "siiiiiO:color_lut_3d", + "sii(iii)O:color_lut_3d", &mode, &filter, &table_channels, diff --git a/src/_imagingft.c b/src/_imagingft.c index c202a8059..2aa425e32 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -854,7 +854,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzfpzLffO:render", + "OO|zzOzfpzL(ff)O:render", &string, &fill, &mode, diff --git a/src/_webp.c b/src/_webp.c index dfda7048d..308f031e0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -164,7 +164,7 @@ _anim_encoder_new(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "iiIiiiiii", + "(ii)Iiiiiii", &width, &height, &bgcolor, diff --git a/src/encode.c b/src/encode.c index 0bf5e63c5..74dd4a3fd 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1097,7 +1097,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnpnnnnnnOz#y#y#", + "ss|nnnnpn(nn)nnnOz#y#y#", &mode, &rawmode, &quality, From a37702dd8a02aff7a16d8d0a4d3b94279737be34 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Feb 2025 18:36:14 +1100 Subject: [PATCH 138/140] Removed unused format character --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 2aa425e32..a668ac411 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -854,7 +854,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzfpzL(ff)O:render", + "OO|zzOzfpzL(ff):render", &string, &fill, &mode, From 7924b6a11f37902c8a1a080741384bf79fbd8905 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Feb 2025 20:20:57 +1100 Subject: [PATCH 139/140] Use member names to initialize modules --- src/_imaging.c | 7 +++---- src/_imagingcms.c | 7 +++---- src/_imagingft.c | 7 +++---- src/_imagingmath.c | 7 +++---- src/_imagingmorph.c | 8 ++++---- src/_imagingtk.c | 7 +++---- src/_webp.c | 7 +++---- 7 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 2fd2deffb..cd9bde273 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4439,10 +4439,9 @@ PyInit__imaging(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imaging", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + .m_name = "_imaging", + .m_size = -1, + .m_methods = functions, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 14cf2acd2..6037e8bc4 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1520,10 +1520,9 @@ PyInit__imagingcms(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingcms", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - pyCMSdll_methods, /* m_methods */ + .m_name = "_imagingcms", + .m_size = -1, + .m_methods = pyCMSdll_methods, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingft.c b/src/_imagingft.c index c202a8059..ab3bc8dba 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1630,10 +1630,9 @@ PyInit__imagingft(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingft", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - _functions, /* m_methods */ + .m_name = "_imagingft", + .m_size = -1, + .m_methods = _functions, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 75b3716b5..4b9bf08ba 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -308,10 +308,9 @@ PyInit__imagingmath(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingmath", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - _functions, /* m_methods */ + .m_name = "_imagingmath", + .m_size = -1, + .m_methods = _functions, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index b763e3a6f..a20888294 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -252,10 +252,10 @@ PyInit__imagingmorph(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingmorph", /* m_name */ - "A module for doing image morphology", /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + .m_name = "_imagingmorph", + .m_doc = "A module for doing image morphology", + .m_size = -1, + .m_methods = functions, }; m = PyModule_Create(&module_def); diff --git a/src/_imagingtk.c b/src/_imagingtk.c index c44482651..4e06fe9b8 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -50,10 +50,9 @@ PyMODINIT_FUNC PyInit__imagingtk(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_imagingtk", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - functions, /* m_methods */ + .m_name = "_imagingtk", + .m_size = -1, + .m_methods = functions, }; PyObject *m; m = PyModule_Create(&module_def); diff --git a/src/_webp.c b/src/_webp.c index dfda7048d..ded9f8ca2 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -835,10 +835,9 @@ PyInit__webp(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, - "_webp", /* m_name */ - NULL, /* m_doc */ - -1, /* m_size */ - webpMethods, /* m_methods */ + .m_name = "_webp", + .m_size = -1, + .m_methods = webpMethods, }; m = PyModule_Create(&module_def); From 41861e8e9ffb968945ff6acba422d31f0d69220b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Feb 2025 19:26:49 +1100 Subject: [PATCH 140/140] Updated AffineTransform docstring to mention it uses the inverse matrix --- src/PIL/ImageTransform.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index a3d8f441a..fb144ff38 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -48,9 +48,9 @@ class AffineTransform(Transform): Define an affine image transform. This function takes a 6-tuple (a, b, c, d, e, f) which contain the first - two rows from an affine transform matrix. For each pixel (x, y) in the - output image, the new value is taken from a position (a x + b y + c, - d x + e y + f) in the input image, rounded to nearest pixel. + two rows from the inverse of an affine transform matrix. For each pixel + (x, y) in the output image, the new value is taken from a position (a x + + b y + c, d x + e y + f) in the input image, rounded to nearest pixel. This function can be used to scale, translate, rotate, and shear the original image. @@ -58,7 +58,7 @@ class AffineTransform(Transform): See :py:meth:`.Image.transform` :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows - from an affine transform matrix. + from the inverse of an affine transform matrix. """ method = Image.Transform.AFFINE