From a60610c93f1a7fb2738156470d613793e5fa7a89 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 13 Oct 2024 07:42:17 +1100 Subject: [PATCH 001/628] Added type hints --- src/PIL/ImageMath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 15464947d..484797f91 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -173,10 +173,10 @@ class _Operand: return self.apply("rshift", self, other) # logical - def __eq__(self, other): + def __eq__(self, other: _Operand | float) -> _Operand: # type: ignore[override] return self.apply("eq", self, other) - def __ne__(self, other): + def __ne__(self, other: _Operand | float) -> _Operand: # type: ignore[override] return self.apply("ne", self, other) def __lt__(self, other: _Operand | float) -> _Operand: From f2cc87b1f0627dfce07ab2867e29ba07e5055f33 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 13 Oct 2024 11:19:41 +1100 Subject: [PATCH 002/628] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0a1703668..fcc8c324f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Update licence to MIT-CMU #8460 + [hugovk] + - Conditionally define ImageCms type hint to avoid requiring core #8197 [radarhere] From 3911afbc97f5b3195609800f471794f6b3e349dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Oct 2024 16:56:48 +1100 Subject: [PATCH 003/628] Removed Fedora 39 --- .github/workflows/test-docker.yml | 1 - docs/installation/platform-support.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 21e1275e7..880fe3eea 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -46,7 +46,6 @@ jobs: centos-stream-9-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-39-amd64, fedora-40-amd64, gentoo, ubuntu-22.04-jammy-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 00dec41d1..21ebd1ad5 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -29,8 +29,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 39 | 3.12 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 40 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | From 204aae6682fc936f5350b3fe70335776f81480a7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 15 Oct 2024 08:55:00 +0300 Subject: [PATCH 004/628] 11.0.0 version bump --- CHANGES.rst | 2 +- src/PIL/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fcc8c324f..8a20fe16a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,7 @@ Changelog (Pillow) ================== -11.0.0 (unreleased) +11.0.0 (2024-10-15) ------------------- - Update licence to MIT-CMU #8460 diff --git a/src/PIL/_version.py b/src/PIL/_version.py index c4a72ad7e..963d8c906 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.0.0.dev0" +__version__ = "11.0.0" From a433f24f0bf5a9df98bbfe3c076a060131e92854 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 15 Oct 2024 22:14:26 +1100 Subject: [PATCH 005/628] tile is already an empty list --- src/PIL/GifImagePlugin.py | 1 - src/PIL/WebPImagePlugin.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 57c291792..a7c4f8b2c 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -103,7 +103,6 @@ class GifImageFile(ImageFile.ImageFile): self.info["version"] = s[:6] self._size = i16(s, 6), i16(s, 8) - self.tile = [] flags = s[10] bits = (flags & 7) + 1 diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 64188f28c..c7f855527 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -60,7 +60,6 @@ class WebPImageFile(ImageFile.ImageFile): self.is_animated = self.n_frames > 1 self._mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode - self.tile = [] # Attempt to read ICC / EXIF / XMP chunks from file icc_profile = self._decoder.get_chunk("ICCP") From 455e6da119ac01ffc3aeb68486cef47e1f3f8dcb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:30:25 +0300 Subject: [PATCH 006/628] 11.1.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 963d8c906..0807f949c 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.0.0" +__version__ = "11.1.0.dev0" From 8393972c3540cf7bba737b0687d79c5d8ebca3ef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Oct 2024 09:56:33 +1100 Subject: [PATCH 007/628] Skip PyPy3.9 wheels --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 837e5ea91..34452fa56 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -152,6 +152,7 @@ jobs: CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_PRERELEASE_PYTHONS: True + CIBW_SKIP: pp39-* MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - uses: actions/upload-artifact@v4 @@ -224,6 +225,7 @@ jobs: CIBW_CACHE_PATH: "C:\\cibw" CIBW_FREE_THREADED_SUPPORT: True CIBW_PRERELEASE_PYTHONS: True + CIBW_SKIP: pp39-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow From 7c194b54ef3e7a04c5b669353622687075b5ebb4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Oct 2024 21:37:14 +1100 Subject: [PATCH 008/628] Fixed disabling a feature --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 60707083f..def341784 100644 --- a/setup.py +++ b/setup.py @@ -389,7 +389,7 @@ class pil_build_ext(build_ext): pass for x in self.feature: if getattr(self, f"disable_{x}"): - setattr(self.feature, x, False) + self.feature.set(x, False) self.feature.required.discard(x) _dbg("Disabling %s", x) if getattr(self, f"enable_{x}"): From 11c654c187ffbede40c54e6344ddec49f495d4a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Oct 2024 23:07:35 +1100 Subject: [PATCH 009/628] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8a20fe16a..ff126c611 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +11.1.0 (unreleased) +------------------- + +- Fixed disabling a feature during install #8469 + [radarhere] + 11.0.0 (2024-10-15) ------------------- From c252b708ee7f90dbc3b6a86056f390b7b4712f52 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:41:38 +0000 Subject: [PATCH 010/628] Update dependency mypy to v1.12.0 --- .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 dcb3996e2..852444d37 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.11.2 +mypy==1.12.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 5ff20273d90c1be49a9662cf6d4c53c9bd53aadb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 10:46:26 +1100 Subject: [PATCH 011/628] Updated type hints --- src/PIL/TiffImagePlugin.py | 46 +++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ff5a6f9e9..198bd422e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -685,22 +685,33 @@ class ImageFileDirectory_v2(_IFDv2Base): else: self.tagtype[tag] = TiffTags.UNDEFINED if all(isinstance(v, IFDRational) for v in values): - self.tagtype[tag] = ( - TiffTags.RATIONAL - if all(v >= 0 for v in values) - else TiffTags.SIGNED_RATIONAL - ) - elif all(isinstance(v, int) for v in values): - if all(0 <= v < 2**16 for v in values): - self.tagtype[tag] = TiffTags.SHORT - elif all(-(2**15) < v < 2**15 for v in values): - self.tagtype[tag] = TiffTags.SIGNED_SHORT + for v in values: + assert isinstance(v, IFDRational) + if v < 0: + self.tagtype[tag] = TiffTags.SIGNED_RATIONAL + break else: - self.tagtype[tag] = ( - TiffTags.LONG - if all(v >= 0 for v in values) - else TiffTags.SIGNED_LONG - ) + self.tagtype[tag] = TiffTags.RATIONAL + elif all(isinstance(v, int) for v in values): + short = True + signed_short = True + long = True + for v in values: + assert isinstance(v, int) + if short and not (0 <= v < 2**16): + short = False + if signed_short and not (-(2**15) < v < 2**15): + signed_short = False + if long and v < 0: + long = False + if short: + self.tagtype[tag] = TiffTags.SHORT + elif signed_short: + self.tagtype[tag] = TiffTags.SIGNED_SHORT + elif long: + self.tagtype[tag] = TiffTags.LONG + else: + self.tagtype[tag] = TiffTags.SIGNED_LONG elif all(isinstance(v, float) for v in values): self.tagtype[tag] = TiffTags.DOUBLE elif all(isinstance(v, str) for v in values): @@ -718,7 +729,10 @@ class ImageFileDirectory_v2(_IFDv2Base): is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) if not is_ifd: - values = tuple(info.cvt_enum(value) for value in values) + values = tuple( + info.cvt_enum(value) if isinstance(value, str) else value + for value in values + ) dest = self._tags_v1 if legacy_api else self._tags_v2 From 4611a246618b1b3e09efc829e36205184376dc75 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 10:58:48 +1100 Subject: [PATCH 012/628] Fix IFDRational with a zero denominator --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 198bd422e..6bf39b75a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -294,7 +294,7 @@ def _accept(prefix: bytes) -> bool: def _limit_rational( val: float | Fraction | IFDRational, max_val: int ) -> tuple[IntegralLike, IntegralLike]: - inv = abs(float(val)) > 1 + inv = abs(val) > 1 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d From 0beb2228f9adb387a87c6b944cda2e427f5f264a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 12:44:25 +1100 Subject: [PATCH 013/628] 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 d59b169ed257ca14d86c90dbf3384b8b3999fc5c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 23:10:48 +1100 Subject: [PATCH 014/628] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ff126c611..ae3841569 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Fix IFDRational with a zero denominator #8474 + [radarhere] + - Fixed disabling a feature during install #8469 [radarhere] From a337138f92d88d56488e5150d7ce8a3e0f0a91f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 08:32:14 +1100 Subject: [PATCH 015/628] Updated type hint --- Tests/test_image_resize.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 8548fb5da..57fcf9a34 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -10,7 +10,7 @@ from pathlib import Path import pytest -from PIL import Image +from PIL import Image, ImageFile from .helper import ( assert_image_equal, @@ -179,7 +179,7 @@ class TestImagingCoreResize: @pytest.fixture -def gradients_image() -> Generator[Image.Image, None, None]: +def gradients_image() -> Generator[ImageFile.ImageFile, None, None]: with Image.open("Tests/images/radial_gradients.png") as im: im.load() try: @@ -189,7 +189,7 @@ def gradients_image() -> Generator[Image.Image, None, None]: class TestReducingGapResize: - def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: + def test_reducing_gap_values(self, gradients_image: ImageFile.ImageFile) -> None: ref = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, reducing_gap=None ) @@ -210,7 +210,7 @@ class TestReducingGapResize: ) def test_reducing_gap_1( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -230,7 +230,7 @@ class TestReducingGapResize: ) def test_reducing_gap_2( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -250,7 +250,7 @@ class TestReducingGapResize: ) def test_reducing_gap_3( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: @@ -266,7 +266,9 @@ class TestReducingGapResize: @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) def test_reducing_gap_8( - self, gradients_image: Image.Image, box: tuple[float, float, float, float] + self, + gradients_image: ImageFile.ImageFile, + box: tuple[float, float, float, float], ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( @@ -281,7 +283,7 @@ class TestReducingGapResize: ) def test_box_filter( self, - gradients_image: Image.Image, + gradients_image: ImageFile.ImageFile, box: tuple[float, float, float, float], epsilon: float, ) -> None: From 98f975dbbe6ae0ef521522cdea6e00fdc81449c0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 18:56:23 +1100 Subject: [PATCH 016/628] 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 017/628] 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 5b065970753d464c0d283f9e0c2f4a0971800ed1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 19:29:22 +1100 Subject: [PATCH 018/628] Use fixture to re-open image for each test --- Tests/test_file_jpeg2k.py | 57 +++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 26b085601..79f53e211 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -2,6 +2,7 @@ from __future__ import annotations import os import re +from collections.abc import Generator from io import BytesIO from pathlib import Path from typing import Any @@ -29,8 +30,16 @@ EXTRA_DIR = "Tests/images/jpeg2000" pytestmark = skip_unless_feature("jpg_2000") -test_card = Image.open("Tests/images/test-card.png") -test_card.load() + +@pytest.fixture +def test_card() -> Generator[ImageFile.ImageFile, None, None]: + with Image.open("Tests/images/test-card.png") as im: + im.load() + try: + yield im + finally: + im.close() + # OpenJPEG 2.0.0 outputs this debugging message sometimes; we should # ignore it---it doesn't represent a test failure. @@ -74,7 +83,7 @@ def test_invalid_file() -> None: Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio() -> None: +def test_bytesio(test_card: ImageFile.ImageFile) -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) with Image.open(data) as im: @@ -86,7 +95,7 @@ def test_bytesio() -> None: # PIL (they were made using Adobe Photoshop) -def test_lossless(tmp_path: Path) -> None: +def test_lossless(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() outfile = str(tmp_path / "temp_test-card.png") @@ -94,54 +103,56 @@ def test_lossless(tmp_path: Path) -> None: assert_image_similar(im, test_card, 1.0e-3) -def test_lossy_tiled() -> None: +def test_lossy_tiled(test_card: ImageFile.ImageFile) -> None: assert_image_similar_tofile( test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 ) -def test_lossless_rt() -> None: +def test_lossless_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card) assert_image_equal(im, test_card) -def test_lossy_rt() -> None: +def test_lossy_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_tiled_rt() -> None: +def test_tiled_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, tile_size=(128, 128)) assert_image_equal(im, test_card) -def test_tiled_offset_rt() -> None: +def test_tiled_offset_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) assert_image_equal(im, test_card) -def test_tiled_offset_too_small() -> None: +def test_tiled_offset_too_small(test_card: ImageFile.ImageFile) -> None: with pytest.raises(ValueError): roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt() -> None: +def test_irreversible_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, irreversible=True, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_prog_qual_rt() -> None: +def test_prog_qual_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") assert_image_similar(im, test_card, 2.0) -def test_prog_res_rt() -> None: +def test_prog_res_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, num_resolutions=8, progression="RLCP") assert_image_equal(im, test_card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions: int) -> None: +def test_default_num_resolutions( + test_card: ImageFile.ImageFile, num_resolutions: int +) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -205,7 +216,7 @@ def test_header_errors() -> None: pass -def test_layers_type(tmp_path: Path) -> None: +def test_layers_type(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) @@ -215,7 +226,7 @@ def test_layers_type(tmp_path: Path) -> None: test_card.save(outfile, quality_layers=quality_layers_str) -def test_layers() -> None: +def test_layers(test_card: ImageFile.ImageFile) -> None: out = BytesIO() test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) @@ -245,7 +256,13 @@ def test_layers() -> None: (None, {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: +def test_no_jp2( + test_card: ImageFile.ImageFile, + name: str, + args: dict[str, bool], + offset: int, + data: bytes, +) -> None: out = BytesIO() if name: out.name = name @@ -254,7 +271,7 @@ def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> N assert out.read(2) == data -def test_mct() -> None: +def test_mct(test_card: ImageFile.ImageFile) -> None: # Three component for val in (0, 1): out = BytesIO() @@ -419,7 +436,7 @@ def test_comment() -> None: pass -def test_save_comment() -> None: +def test_save_comment(test_card: ImageFile.ImageFile) -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) @@ -457,7 +474,7 @@ def test_crashes(test_file: str) -> None: @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker() -> None: +def test_plt_marker(test_card: ImageFile.ImageFile) -> None: # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) From 55579084cd57461517dfe77d7804dfa24219a9f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Oct 2024 20:40:13 +1100 Subject: [PATCH 019/628] Corrected EMF DPI --- Tests/test_file_wmf.py | 7 +++++++ src/PIL/WmfImagePlugin.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 79e707263..424640d7b 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path from typing import IO @@ -61,6 +62,12 @@ def test_load_float_dpi() -> None: with Image.open("Tests/images/drawing.emf") as im: assert im.info["dpi"] == 1423.7668161434979 + with open("Tests/images/drawing.emf", "rb") as fp: + data = fp.read() + b = BytesIO(data[:8] + b"\x06\xFA" + data[10:]) + with Image.open(b) as im: + assert im.info["dpi"][0] == 2540 + def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 68f8a74f5..cad6c98d5 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -128,7 +128,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame - xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) + xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0]) ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) self.info["wmf_bbox"] = x0, y0, x1, y1 From beb32bbb1f9bb713a840421e3e9f5191d5f51944 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 00:18:00 +0000 Subject: [PATCH 020/628] Update dependency mypy to v1.12.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 852444d37..047307bb5 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.12.0 +mypy==1.12.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 22c05e232c67343b193b9fdeca9e7b683d470bc6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Oct 2024 17:56:02 +1100 Subject: [PATCH 021/628] Update license to MIT-CMU --- LICENSE | 2 +- docs/about.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 7990a6e57..8837c290c 100644 --- a/LICENSE +++ b/LICENSE @@ -7,7 +7,7 @@ Pillow is the friendly PIL fork. It is Copyright © 2010-2024 by Jeffrey A. Clark and contributors -Like PIL, Pillow is licensed under the open source HPND License: +Like PIL, Pillow is licensed under the open source MIT-CMU License: By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply diff --git a/docs/about.rst b/docs/about.rst index 98cdd8e5a..c51ddebd0 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -18,7 +18,7 @@ The fork author's goal is to foster and support active development of PIL throug License ------- -Like PIL, Pillow is `licensed under the open source HPND License `_ +Like PIL, Pillow is `licensed under the open source MIT-CMU License `_ Why a fork? ----------- From 6a55f2df0346e3d97515ee46e4dbb966886fdc94 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 02:36:15 +0000 Subject: [PATCH 022/628] Update dependency mypy to v1.13.0 --- .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 047307bb5..c84a3533b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.12.1 +mypy==1.13.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 8af2d7640ead3ddeba9fdc598dc8e72d580dfdfb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Oct 2024 23:26:13 +1100 Subject: [PATCH 023/628] Pass palette mode to putpalette --- Tests/test_file_gif.py | 6 ++++-- src/PIL/GifImagePlugin.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 16c8466f3..4eb8c0d02 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -4,6 +4,7 @@ import warnings from collections.abc import Generator from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -1431,7 +1432,8 @@ def test_saving_rgba(tmp_path: Path) -> None: assert reloaded_rgba.load()[0, 0][3] == 0 -def test_optimizing_p_rgba(tmp_path: Path) -> None: +@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) +def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: out = str(tmp_path / "temp.gif") im1 = Image.new("P", (100, 100)) @@ -1443,7 +1445,7 @@ def test_optimizing_p_rgba(tmp_path: Path) -> None: im2 = Image.new("P", (100, 100)) im2.putpalette(data, "RGBA") - im1.save(out, save_all=True, append_images=[im2]) + im1.save(out, save_all=True, append_images=[im2], **params) with Image.open(out) as reloaded: assert reloaded.n_frames == 2 diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index a7c4f8b2c..47022d584 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -695,8 +695,9 @@ def _write_multiple_frames( ) background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) - assert im_frames[0].im.palette is not None - background_im.putpalette(im_frames[0].im.palette) + first_palette = im_frames[0].im.palette + assert first_palette is not None + background_im.putpalette(first_palette, first_palette.mode) bbox = _getbbox(background_im, im_frame)[1] elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: From c8e301c47456114f245261fccd316b4fd8964f3e Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 24 Oct 2024 15:52:59 +0200 Subject: [PATCH 024/628] Fix SEGFAULT from calling FT_New_Face/FT_Done_Face in multiple threads --- src/_imagingft.c | 12 +- src/thirdparty/pythoncapi_compat.h | 341 ++++++++++++++++++++++++++++- 2 files changed, 351 insertions(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 7f2464124..6fed821bc 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -81,7 +81,10 @@ struct { /* -------------------------------------------------------------------- */ /* font objects */ - static FT_Library library; +static FT_Library library; +#ifdef Py_GIL_DISABLED +PyMutex ft_library_mutex; +#endif typedef struct { PyObject_HEAD FT_Face face; @@ -187,7 +190,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { if (filename && font_bytes_size <= 0) { self->font_bytes = NULL; + MUTEX_LOCK(&ft_library_mutex); error = FT_New_Face(library, filename, index, &self->face); + MUTEX_UNLOCK(&ft_library_mutex); } else { /* need to have allocated storage for font_bytes for the life of the object.*/ /* Don't free this before FT_Done_Face */ @@ -197,6 +202,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); + MUTEX_LOCK(&ft_library_mutex); error = FT_New_Memory_Face( library, (FT_Byte *)self->font_bytes, @@ -204,6 +210,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { index, &self->face ); + MUTEX_UNLOCK(&ft_library_mutex); } } @@ -1433,7 +1440,9 @@ font_setvaraxes(FontObject *self, PyObject *args) { static void font_dealloc(FontObject *self) { if (self->face) { + MUTEX_LOCK(&ft_library_mutex); FT_Done_Face(self->face); + MUTEX_UNLOCK(&ft_library_mutex); } if (self->font_bytes) { PyMem_Free(self->font_bytes); @@ -1639,6 +1648,7 @@ PyInit__imagingft(void) { } #ifdef Py_GIL_DISABLED + ft_library_mutex = (PyMutex) {0}; PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif diff --git a/src/thirdparty/pythoncapi_compat.h b/src/thirdparty/pythoncapi_compat.h index 51e8c0de7..ca23d5ffa 100644 --- a/src/thirdparty/pythoncapi_compat.h +++ b/src/thirdparty/pythoncapi_compat.h @@ -7,7 +7,10 @@ // https://github.com/python/pythoncapi_compat // // Latest version: -// https://raw.githubusercontent.com/python/pythoncapi_compat/master/pythoncapi_compat.h +// https://raw.githubusercontent.com/python/pythoncapi-compat/main/pythoncapi_compat.h +// +// This file was vendored from the following commit: +// https://github.com/python/pythoncapi-compat/commit/0041177c4f348c8952b4c8980b2c90856e61c7c7 // // SPDX-License-Identifier: 0BSD @@ -45,6 +48,13 @@ extern "C" { # define _PyObject_CAST(op) _Py_CAST(PyObject*, op) #endif +#ifndef Py_BUILD_ASSERT +# define Py_BUILD_ASSERT(cond) \ + do { \ + (void)sizeof(char [1 - 2 * !(cond)]); \ + } while(0) +#endif + // bpo-42262 added Py_NewRef() to Python 3.10.0a3 #if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_NewRef) @@ -1338,6 +1348,166 @@ PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, } #endif +#if PY_VERSION_HEX < 0x030D00B3 +# define Py_BEGIN_CRITICAL_SECTION(op) { +# define Py_END_CRITICAL_SECTION() } +# define Py_BEGIN_CRITICAL_SECTION2(a, b) { +# define Py_END_CRITICAL_SECTION2() } +#endif + +#if PY_VERSION_HEX < 0x030E0000 && PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) +typedef struct PyUnicodeWriter PyUnicodeWriter; + +static inline void PyUnicodeWriter_Discard(PyUnicodeWriter *writer) +{ + _PyUnicodeWriter_Dealloc((_PyUnicodeWriter*)writer); + PyMem_Free(writer); +} + +static inline PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length) +{ + if (length < 0) { + PyErr_SetString(PyExc_ValueError, + "length must be positive"); + return NULL; + } + + const size_t size = sizeof(_PyUnicodeWriter); + PyUnicodeWriter *pub_writer = (PyUnicodeWriter *)PyMem_Malloc(size); + if (pub_writer == _Py_NULL) { + PyErr_NoMemory(); + return _Py_NULL; + } + _PyUnicodeWriter *writer = (_PyUnicodeWriter *)pub_writer; + + _PyUnicodeWriter_Init(writer); + if (_PyUnicodeWriter_Prepare(writer, length, 127) < 0) { + PyUnicodeWriter_Discard(pub_writer); + return NULL; + } + writer->overallocate = 1; + return pub_writer; +} + +static inline PyObject* PyUnicodeWriter_Finish(PyUnicodeWriter *writer) +{ + PyObject *str = _PyUnicodeWriter_Finish((_PyUnicodeWriter*)writer); + assert(((_PyUnicodeWriter*)writer)->buffer == NULL); + PyMem_Free(writer); + return str; +} + +static inline int +PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch) +{ + if (ch > 0x10ffff) { + PyErr_SetString(PyExc_ValueError, + "character must be in range(0x110000)"); + return -1; + } + + return _PyUnicodeWriter_WriteChar((_PyUnicodeWriter*)writer, ch); +} + +static inline int +PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Str(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Repr(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer, + const char *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)strlen(str); + } + + PyObject *str_obj = PyUnicode_FromStringAndSize(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *writer, + const wchar_t *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)wcslen(str); + } + + PyObject *str_obj = PyUnicode_FromWideChar(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str, + Py_ssize_t start, Py_ssize_t end) +{ + if (!PyUnicode_Check(str)) { + PyErr_Format(PyExc_TypeError, "expect str, not %T", str); + return -1; + } + if (start < 0 || start > end) { + PyErr_Format(PyExc_ValueError, "invalid start argument"); + return -1; + } + if (end > PyUnicode_GET_LENGTH(str)) { + PyErr_Format(PyExc_ValueError, "invalid end argument"); + return -1; + } + + return _PyUnicodeWriter_WriteSubstring((_PyUnicodeWriter*)writer, str, + start, end); +} + +static inline int +PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...) +{ + va_list vargs; + va_start(vargs, format); + PyObject *str = PyUnicode_FromFormatV(format, vargs); + va_end(vargs); + if (str == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} +#endif // PY_VERSION_HEX < 0x030E0000 // gh-116560 added PyLong_GetSign() to Python 3.14.0a0 #if PY_VERSION_HEX < 0x030E00A0 @@ -1354,6 +1524,175 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign) #endif +// gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) +{ + if (!PyUnicode_Check(str1)) { + PyErr_Format(PyExc_TypeError, "first argument must be str, not %s", + Py_TYPE(str1)->tp_name); + return -1; + } + if (!PyUnicode_Check(str2)) { + PyErr_Format(PyExc_TypeError, "second argument must be str, not %s", + Py_TYPE(str2)->tp_name); + return -1; + } + +#if PY_VERSION_HEX >= 0x030d0000 && !defined(PYPY_VERSION) + PyAPI_FUNC(int) _PyUnicode_Equal(PyObject *str1, PyObject *str2); + + return _PyUnicode_Equal(str1, str2); +#elif PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#elif PY_VERSION_HEX >= 0x03090000 && defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#else + return (PyUnicode_Compare(str1, str2) == 0); +#endif +} +#endif + + +// gh-121645 added PyBytes_Join() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) +{ + return _PyBytes_Join(sep, iterable); +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) +{ +#if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) + PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void *src, Py_ssize_t len); + + return _Py_HashBytes(ptr, len); +#else + Py_hash_t hash; + PyObject *bytes = PyBytes_FromStringAndSize((const char*)ptr, len); + if (bytes == NULL) { + return -1; + } + hash = PyObject_Hash(bytes); + Py_DECREF(bytes); + return hash; +#endif +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyIter_NextItem(PyObject *iter, PyObject **item) +{ + iternextfunc tp_iternext; + + assert(iter != NULL); + assert(item != NULL); + + tp_iternext = Py_TYPE(iter)->tp_iternext; + if (tp_iternext == NULL) { + *item = NULL; + PyErr_Format(PyExc_TypeError, "expected an iterator, got '%s'", + Py_TYPE(iter)->tp_name); + return -1; + } + + if ((*item = tp_iternext(iter))) { + return 1; + } + if (!PyErr_Occurred()) { + return 0; + } + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyErr_Clear(); + return 0; + } + return -1; +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyLong_FromInt32(int32_t value) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + return PyLong_FromLong(value); +} + +static inline PyObject* PyLong_FromInt64(int64_t value) +{ + Py_BUILD_ASSERT(sizeof(long long) >= 8); + return PyLong_FromLongLong(value); +} + +static inline PyObject* PyLong_FromUInt32(uint32_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long) >= 4); + return PyLong_FromUnsignedLong(value); +} + +static inline PyObject* PyLong_FromUInt64(uint64_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long long) >= 8); + return PyLong_FromUnsignedLongLong(value); +} + +static inline int PyLong_AsInt32(PyObject *obj, int32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(int) == 4); + int value = PyLong_AsInt(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int32_t)value; + return 0; +} + +static inline int PyLong_AsInt64(PyObject *obj, int64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + long long value = PyLong_AsLongLong(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int64_t)value; + return 0; +} + +static inline int PyLong_AsUInt32(PyObject *obj, uint32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + unsigned long value = PyLong_AsUnsignedLong(obj); + if (value == (unsigned long)-1 && PyErr_Occurred()) { + return -1; + } +#if SIZEOF_LONG > 4 + if ((unsigned long)UINT32_MAX < value) { + PyErr_SetString(PyExc_OverflowError, + "Python int too large to convert to C uint32_t"); + return -1; + } +#endif + *pvalue = (uint32_t)value; + return 0; +} + +static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + unsigned long long value = PyLong_AsUnsignedLongLong(obj); + if (value == (unsigned long long)-1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (uint64_t)value; + return 0; +} +#endif + + #ifdef __cplusplus } #endif From 7999da38a7ff5aa359aa6ae3a7f88b835c7f7f06 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:07:43 +0000 Subject: [PATCH 025/628] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_imagingft.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 6fed821bc..884b8f96c 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -81,7 +81,7 @@ struct { /* -------------------------------------------------------------------- */ /* font objects */ -static FT_Library library; + static FT_Library library; #ifdef Py_GIL_DISABLED PyMutex ft_library_mutex; #endif @@ -1648,7 +1648,7 @@ PyInit__imagingft(void) { } #ifdef Py_GIL_DISABLED - ft_library_mutex = (PyMutex) {0}; + ft_library_mutex = (PyMutex){0}; PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif From 0fe55d611505fefaaa0d4b7b3a87b803714762de Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 09:23:13 +0800 Subject: [PATCH 026/628] Isolate macOS build from Homebrew. --- .github/workflows/wheels-dependencies.sh | 96 ++++++++++++++++++------ .github/workflows/wheels-test.sh | 15 ++-- .gitignore | 1 + pyproject.toml | 7 ++ setup.py | 94 +++++++++++++---------- wheels/multibuild | 2 +- 6 files changed, 140 insertions(+), 75 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97c1adf09..35b04c6d6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -23,6 +23,8 @@ OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 +RAQM_VERSION=0.7.1 +FRIBIDI_VERSION=1.0.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.2.2 else @@ -38,7 +40,23 @@ BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +function build_pkg_config { + if [ -e pkg-config-stamp ]; then return; fi + # This essentially duplicates the Homebrew recipe: + # https://github.com/Homebrew/homebrew-core/blob/master/Formula/p/pkg-config.rb + ORIGINAL_CFLAGS=$CFLAGS + CFLAGS="$CFLAGS -Wno-int-conversion" + build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ + --disable-debug --disable-host-tool --with-internal-glib \ + --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ + --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include + CFLAGS=$ORIGINAL_CFLAGS + export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config + touch pkg-config-stamp +} + function build_brotli { + if [ -e brotli-stamp ]; then return; fi local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ @@ -48,25 +66,25 @@ function build_brotli { cp /usr/local/lib64/libbrotli* /usr/local/lib cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig fi + touch brotli-stamp } function build_harfbuzz { - python3 -m pip install meson ninja + if [ -e harfbuzz-stamp ]; then return; fi + python -m pip install meson ninja local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) + && meson setup build --prefix=$BUILD_PREFIX --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ && meson install) if [[ "$MB_ML_LIBC" == "manylinux" ]]; then cp /usr/local/lib64/libharfbuzz* /usr/local/lib fi + touch harfbuzz-stamp } function build { - if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then - sudo chown -R runner /usr/local - fi build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel @@ -77,17 +95,23 @@ function build { if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig - fi else sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib build_libjpeg_turbo - build_tiff + if [ -n "$IS_MACOS" ]; then + # Custom tiff build to include jpeg; by default, configure won't include + # headers/libs in the custom macOS prefix + build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ + --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib + else + build_tiff + fi + build_libpng build_lcms2 build_openjpeg @@ -113,33 +137,55 @@ function build { fi build_harfbuzz + + if [ -n "$IS_MACOS" ]; then + build_simple fribidi $FRIBIDI_VERSION https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION tar.xz --enable-shared + build_simple raqm $RAQM_VERSION https://github.com/Host_Oman/libraqm/releases/download/v$RAQM_VERSION tar.gz --enable-shared + fi } +# Perform all dependency builds in the build subfolder. +mkdir -p build +pushd build > /dev/null + # Any stuff that you need to do before you start building the wheels # Runs in the root directory of this repository. -curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip -untar pillow-depends-main.zip +if [[ ! -d pillow-depends-main ]]; then + if [[ ! -f pillow-depends-main.zip ]]; then + echo "Download pillow dependency sources..." + curl -fSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip + fi + untar pillow-depends-main.zip +fi if [[ -n "$IS_MACOS" ]]; then - # libtiff and libxcb cause a conflict with building libtiff and libxcb - # libxau and libxdmcp cause an issue on macOS < 11 - # remove cairo to fix building harfbuzz on arm64 - # remove lcms2 and libpng to fix building openjpeg on arm64 - # remove jpeg-turbo to avoid inclusion on arm64 - # remove webp and zstd to avoid inclusion on x86_64 - # curl from brew requires zstd, use system curl - brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - brew remove --ignore-dependencies jpeg-turbo - else - brew remove --ignore-dependencies webp - fi + # Build and install into the `deps` folder. + BUILD_PREFIX=$(pwd)/deps - brew install pkg-config + # Homebrew (or similar packaging environments) install can contain some of + # the libraries that we're going to build. However, they may be compiled + # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, + # and they may bring in other dependencies that we don't want. The same will + # be true of any other locations on the path. To avoid conflicts, strip the + # path down to the bare mimimum (which, on macOS, won't include any + # development dependencies). + export PATH="$BUILD_PREFIX/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" + export CMAKE_PREFIX_PATH=$BUILD_PREFIX + + # Link the brew command into our isolated build directory. + mkdir -p "$BUILD_PREFIX/bin" + mkdir -p "$BUILD_PREFIX/lib" + + # Ensure pkg-confg and cmake are available + build_pkg_config + python -m pip install cmake fi wrap_wheel_builder build +# Return to the project root to finish the build +popd > /dev/null + # Append licenses for filename in wheels/dependency_licenses/*; do echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index b30b1725f..05f5162c7 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,16 +1,13 @@ #!/bin/bash set -e -if [[ "$OSTYPE" == "darwin"* ]]; then - brew install fribidi - export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" - if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then - sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib +# For Unix, Ensure fibidi is installed by the system. +if [[ "$OSTYPE" != "darwin"* ]]; then + if [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then + apk add curl fribidi + else + yum install -y fribidi fi -elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then - apk add curl fribidi -else - yum install -y fribidi fi python3 -m pip install numpy diff --git a/.gitignore b/.gitignore index 1dd6c9175..9b5773257 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ lib64/ parts/ sdist/ var/ +wheelhouse/ *.egg-info/ .installed.cfg *.egg diff --git a/pyproject.toml b/pyproject.toml index c55be7693..53546f0ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,10 +93,17 @@ version = { attr = "PIL.__version__" } [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 + config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" +# Add an explicit dependencies prefix for macOS, and don't request vendored libraries. +macos.config-settings = "raqm=enable imagequant=disable dependencies-prefix=./build/deps" + test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" +[tool.cibuildwheel.macos.environment] +PATH = "$(pwd)/build/deps/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" + [tool.black] exclude = "wheels/multibuild" diff --git a/setup.py b/setup.py index 60707083f..cb1da7c73 100644 --- a/setup.py +++ b/setup.py @@ -344,6 +344,11 @@ class pil_build_ext(build_ext): for x in ("raqm", "fribidi") ] + [ + ( + "dependencies-prefix", + None, + "The prefix where build dependencies are located.", + ), ("disable-platform-guessing", None, "Disable platform guessing on Linux"), ("debug", None, "Debug logging"), ] @@ -355,6 +360,7 @@ class pil_build_ext(build_ext): return True if value in configuration.get(option, []) else None def initialize_options(self) -> None: + self.dependencies_prefix = configuration.get("dependencies-prefix", []) self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) @@ -564,48 +570,56 @@ class pil_build_ext(build_ext): ) elif sys.platform == "darwin": - # attempt to make sure we pick freetype2 over other versions - _add_directory(include_dirs, "/sw/include/freetype2") - _add_directory(include_dirs, "/sw/lib/freetype2/include") - # fink installation directories - _add_directory(library_dirs, "/sw/lib") - _add_directory(include_dirs, "/sw/include") - # darwin ports installation directories - _add_directory(library_dirs, "/opt/local/lib") - _add_directory(include_dirs, "/opt/local/include") - - # if Homebrew is installed, use its lib and include directories - try: - prefix = ( - subprocess.check_output(["brew", "--prefix"]) - .strip() - .decode("latin1") - ) - except Exception: - # Homebrew not installed - prefix = None - - ft_prefix = None - - if prefix: - # add Homebrew's include and lib directories - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) - _add_directory( - include_dirs, os.path.join(prefix, "opt", "zlib", "include") - ) - ft_prefix = os.path.join(prefix, "opt", "freetype") - - if ft_prefix and os.path.isdir(ft_prefix): - # freetype might not be linked into Homebrew's prefix - _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) - _add_directory(include_dirs, os.path.join(ft_prefix, "include")) + if self.dependencies_prefix: + # Use the explicitly provided prefixes for dependencies. + for prefix in self.dependencies_prefix: + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) else: - # fall back to freetype from XQuartz if - # Homebrew's freetype is missing - _add_directory(library_dirs, "/usr/X11/lib") - _add_directory(include_dirs, "/usr/X11/include") + # Guess the dependency locations based on homebrew/fink/macports + # attempt to make sure we pick freetype2 over other versions + _add_directory(include_dirs, "/sw/include/freetype2") + _add_directory(include_dirs, "/sw/lib/freetype2/include") + # fink installation directories + _add_directory(library_dirs, "/sw/lib") + _add_directory(include_dirs, "/sw/include") + # darwin ports installation directories + _add_directory(library_dirs, "/opt/local/lib") + _add_directory(include_dirs, "/opt/local/include") + # if Homebrew is installed, use its lib and include directories + try: + prefix = ( + subprocess.check_output(["brew", "--prefix"]) + .strip() + .decode("latin1") + ) + except Exception: + # Homebrew not installed + prefix = None + + ft_prefix = None + + if prefix: + # add Homebrew's include and lib directories + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) + ft_prefix = os.path.join(prefix, "opt", "freetype") + + if ft_prefix and os.path.isdir(ft_prefix): + # freetype might not be linked into Homebrew's prefix + _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) + _add_directory(include_dirs, os.path.join(ft_prefix, "include")) + else: + # fall back to freetype from XQuartz if + # Homebrew's freetype is missing + _add_directory(library_dirs, "/usr/X11/lib") + _add_directory(include_dirs, "/usr/X11/include") + + # Add the macOS SDK path. sdk_path = self.get_macos_sdk_path() if sdk_path: _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) diff --git a/wheels/multibuild b/wheels/multibuild index 452dd2d17..8a9333e30 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 452dd2d1705f6b2375369a6570c415beb3163f70 +Subproject commit 8a9333e307fa3f24db2089a66f25e38a8272c261 From fc35fcc5ea844cd0184af6e999d0f35835a4254a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 10:55:30 +0800 Subject: [PATCH 027/628] Cleanups and typos identified by code review. --- .github/workflows/wheels-dependencies.sh | 8 ++++---- .github/workflows/wheels-test.sh | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 35b04c6d6..2c0835e7e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -23,7 +23,7 @@ OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -RAQM_VERSION=0.7.1 +RAQM_VERSION=0.10.2 FRIBIDI_VERSION=1.0.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.2.2 @@ -71,7 +71,7 @@ function build_brotli { function build_harfbuzz { if [ -e harfbuzz-stamp ]; then return; fi - python -m pip install meson ninja + python3 -m pip install meson ninja local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ @@ -176,9 +176,9 @@ if [[ -n "$IS_MACOS" ]]; then mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" - # Ensure pkg-confg and cmake are available + # Ensure pkg-config and cmake are available build_pkg_config - python -m pip install cmake + python3 -m pip install cmake fi wrap_wheel_builder build diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 05f5162c7..3a8fc87ba 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -# For Unix, Ensure fibidi is installed by the system. +# For Unix, ensure fribidi is installed by the system. if [[ "$OSTYPE" != "darwin"* ]]; then if [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi From 00809a25516e1396e3e3ee38a760733efffbd1a9 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 13:42:46 +0800 Subject: [PATCH 028/628] Tweaks to ensure isolation from Homebrew on x86_64. --- .github/workflows/wheels-dependencies.sh | 34 +++++++++++++++++------- pyproject.toml | 2 +- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2c0835e7e..dbabd4aef 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,6 +4,9 @@ if [ -z "$IS_MACOS" ]; then export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} export MB_ML_VER=${AUDITWHEEL_POLICY:9} + + # Build and install into the `build/deps` folder. + BUILD_PREFIX=$(pwd)/build/deps fi export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh @@ -84,6 +87,18 @@ function build_harfbuzz { touch harfbuzz-stamp } +function build_raqm { + if [ -e raqm-stamp ]; then return; fi + python3 -m pip install meson ninja + + local out_dir=$(fetch_unpack https://github.com/HOST-Oman/libraqm/releases/download/v$RAQM_VERSION/raqm-$RAQM_VERSION.tar.xz raqm-$RAQM_VERSION.tar.xz) + (cd $out_dir \ + && meson setup build --prefix=$BUILD_PREFIX) + (cd $out_dir/build \ + && meson install) + touch raqm-stamp +} + function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then @@ -105,9 +120,12 @@ function build { build_libjpeg_turbo if [ -n "$IS_MACOS" ]; then # Custom tiff build to include jpeg; by default, configure won't include - # headers/libs in the custom macOS prefix + # headers/libs in the custom macOS prefix. Explicitly disable webp and + # zstd, because on x86_64 macs, it will pick up the Homebrew versions of + # webp and zstd from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ - --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib + --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ + --disable-webp --disable-zstd else build_tiff fi @@ -140,7 +158,7 @@ function build { if [ -n "$IS_MACOS" ]; then build_simple fribidi $FRIBIDI_VERSION https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION tar.xz --enable-shared - build_simple raqm $RAQM_VERSION https://github.com/Host_Oman/libraqm/releases/download/v$RAQM_VERSION tar.gz --enable-shared + build_raqm fi } @@ -159,9 +177,6 @@ if [[ ! -d pillow-depends-main ]]; then fi if [[ -n "$IS_MACOS" ]]; then - # Build and install into the `deps` folder. - BUILD_PREFIX=$(pwd)/deps - # Homebrew (or similar packaging environments) install can contain some of # the libraries that we're going to build. However, they may be compiled # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, @@ -169,16 +184,17 @@ if [[ -n "$IS_MACOS" ]]; then # be true of any other locations on the path. To avoid conflicts, strip the # path down to the bare mimimum (which, on macOS, won't include any # development dependencies). - export PATH="$BUILD_PREFIX/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" + export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" export CMAKE_PREFIX_PATH=$BUILD_PREFIX # Link the brew command into our isolated build directory. mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" - # Ensure pkg-config and cmake are available + # Ensure pkg-config is available build_pkg_config - python3 -m pip install cmake + # Ensure cmake is available + python3 -m pip cmake fi wrap_wheel_builder build diff --git a/pyproject.toml b/pyproject.toml index 53546f0ab..3839cf840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,7 @@ test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" [tool.cibuildwheel.macos.environment] -PATH = "$(pwd)/build/deps/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" +PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" [tool.black] exclude = "wheels/multibuild" From 06dbfedb0fbfcf38f6d6298ac4979e82fb653956 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 13:51:37 +0800 Subject: [PATCH 029/628] Bump the multibuild version to fix jpeg-turbo issue. --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 8a9333e30..22e8ad0c6 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 8a9333e307fa3f24db2089a66f25e38a8272c261 +Subproject commit 22e8ad0c69acdb0c94abab74da6e09f2810d50d5 From 5a8373e527bbea93cb9c3be87c206683f488ad8e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 14:47:51 +0800 Subject: [PATCH 030/628] Correct a dumb pip invocation error. --- .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 dbabd4aef..0990943d3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -194,7 +194,7 @@ if [[ -n "$IS_MACOS" ]]; then # Ensure pkg-config is available build_pkg_config # Ensure cmake is available - python3 -m pip cmake + python3 -m pip install cmake fi wrap_wheel_builder build From 140a06e0fda60b7474dbdfde53b2ad2f1d344603 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 16:04:56 +0800 Subject: [PATCH 031/628] Explicitly disable libdeflate on libtiff. --- .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 0990943d3..c6f57200f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -125,7 +125,7 @@ function build { # webp and zstd from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ - --disable-webp --disable-zstd + --disable-webp --disable-zstd --disable-libdeflate else build_tiff fi From 0961d3d22bc966f19f68ce01db6d07373dfbe6bc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 16:10:29 +0800 Subject: [PATCH 032/628] Possible fix for linux build failures. --- .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 c6f57200f..4d58bee90 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -113,7 +113,7 @@ function build { build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else - sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc + sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib From c46946f3a4c89a52637cc8e0c0f67d8e85d18a16 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Oct 2024 19:13:39 +1100 Subject: [PATCH 033/628] Added filename placeholder in URL --- winbuild/build_prepare.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a21fbef91..b106eb7de 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -131,7 +131,7 @@ V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") DEPS: dict[str, dict[str, Any]] = { "libjpeg": { "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" - f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", + "FILENAME/download", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], @@ -161,7 +161,7 @@ DEPS: dict[str, dict[str, Any]] = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": f"https://zlib.net/zlib{V['ZLIB_DOTLESS']}.zip", + "url": "https://zlib.net/FILENAME", "filename": f"zlib{V['ZLIB_DOTLESS']}.zip", "dir": f"zlib-{V['ZLIB']}", "license": "README", @@ -175,7 +175,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"*.lib"], }, "xz": { - "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz", + "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", "filename": f"xz-{V['XZ']}.tar.gz", "dir": f"xz-{V['XZ']}", "license": "COPYING", @@ -188,7 +188,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"lzma.lib"], }, "libwebp": { - "url": f"http://downloads.webmproject.org/releases/webp/libwebp-{V['LIBWEBP']}.tar.gz", + "url": "http://downloads.webmproject.org/releases/webp/FILENAME", "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", "dir": f"libwebp-{V['LIBWEBP']}", "license": "COPYING", @@ -210,7 +210,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], }, "libtiff": { - "url": f"https://download.osgeo.org/libtiff/tiff-{V['TIFF']}.tar.gz", + "url": "https://download.osgeo.org/libtiff/FILENAME", "filename": f"tiff-{V['TIFF']}.tar.gz", "dir": f"tiff-{V['TIFF']}", "license": "LICENSE.md", @@ -268,7 +268,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": ["*.lib"], }, "freetype": { - "url": f"https://download.savannah.gnu.org/releases/freetype/freetype-{V['FREETYPE']}.tar.gz", + "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", "filename": f"freetype-{V['FREETYPE']}.tar.gz", "dir": f"freetype-{V['FREETYPE']}", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], @@ -303,7 +303,7 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], }, "lcms2": { - "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/lcms2-{V['LCMS2']}.tar.gz/download", # noqa: E501 + "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", "filename": f"lcms2-{V['LCMS2']}.tar.gz", "dir": f"lcms2-{V['LCMS2']}", "license": "LICENSE", @@ -497,7 +497,7 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: except RuntimeError as exc: # Otherwise try upstream print(exc) - download_dep(url, file) + download_dep(url.replace("FILENAME", filename), file) print("Extracting " + filename) sources_dir_abs = os.path.abspath(sources_dir) From fa7678987e034599344304dbfac5d533c30f7820 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Oct 2024 19:56:36 +1100 Subject: [PATCH 034/628] Simplified code --- winbuild/build_prepare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b106eb7de..c8332d11c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -130,8 +130,7 @@ V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation DEPS: dict[str, dict[str, Any]] = { "libjpeg": { - "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" - "FILENAME/download", + "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], From 43c34fc0a5c22b1c00152a246fd39fe02925043d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 17:20:34 +0800 Subject: [PATCH 035/628] Copy manylinux lib64 files from the correct built prefix. --- .github/workflows/wheels-dependencies.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4d58bee90..9dae28717 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -66,8 +66,8 @@ function build_brotli { && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libbrotli* /usr/local/lib - cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig + cp $BUILD_PREFIX/lib64/libbrotli* $BUILD_PREFIX/lib + cp $BUILD_PREFIX/lib64/pkgconfig/libbrotli* $BUILD_PREFIX/lib/pkgconfig fi touch brotli-stamp } @@ -82,7 +82,7 @@ function build_harfbuzz { (cd $out_dir/build \ && meson install) if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libharfbuzz* /usr/local/lib + cp $BUILD_PREFIX/lib64/libharfbuzz* $BUILD_PREFIX/lib fi touch harfbuzz-stamp } @@ -113,7 +113,7 @@ function build { build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else - sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc + sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib @@ -133,8 +133,8 @@ function build { build_libpng build_lcms2 build_openjpeg - if [ -f /usr/local/lib64/libopenjp2.so ]; then - cp /usr/local/lib64/libopenjp2.so /usr/local/lib + if [ -f $BUILD_PREFIX/lib64/libopenjp2.so ]; then + cp $BUILD_PREFIX/lib64/libopenjp2.so $BUILD_PREFIX/lib fi ORIGINAL_CFLAGS=$CFLAGS From 4b9399f8bfb08d4ed1a1113ed6fd4b6137f7c2aa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Oct 2024 22:00:45 +1100 Subject: [PATCH 036/628] 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 bb3515d649cda179037161ee3bea264a4d289b32 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Fri, 25 Oct 2024 17:32:29 +0200 Subject: [PATCH 037/628] Make PyMutex static and get rid of initialization --- src/_imagingft.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 884b8f96c..d38279f3e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -83,7 +83,7 @@ struct { static FT_Library library; #ifdef Py_GIL_DISABLED -PyMutex ft_library_mutex; +static PyMutex ft_library_mutex; #endif typedef struct { @@ -1648,7 +1648,6 @@ PyInit__imagingft(void) { } #ifdef Py_GIL_DISABLED - ft_library_mutex = (PyMutex){0}; PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif From 3e0849bfb56f6fa8c73ff1bde044551ed72ac2d3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 25 Oct 2024 20:35:17 +0200 Subject: [PATCH 038/628] winbuild: Replace zlib with zlib-ng built with CMake --- winbuild/build_prepare.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a21fbef91..2065e41a9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,11 +120,10 @@ V = { "OPENJPEG": "2.5.2", "TIFF": "4.6.0", "XZ": "5.6.3", - "ZLIB": "1.3.1", + "ZLIBNG": "2.2.2", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) -V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation @@ -161,18 +160,22 @@ DEPS: dict[str, dict[str, Any]] = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": f"https://zlib.net/zlib{V['ZLIB_DOTLESS']}.zip", - "filename": f"zlib{V['ZLIB_DOTLESS']}.zip", - "dir": f"zlib-{V['ZLIB']}", - "license": "README", - "license_pattern": "Copyright notice:\n\n(.+)$", + "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.zip", + "filename": f"zlib-ng-{V['ZLIBNG']}.zip", + "dir": f"zlib-ng-{V['ZLIBNG']}", + "license": "LICENSE.md", + "patch": { + r"CMakeLists.txt": { + "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlib)", # noqa: E501 + }, + }, "build": [ - cmd_nmake(r"win32\Makefile.msc", "clean"), - cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), - cmd_copy("zlib.lib", "z.lib"), + *cmds_cmake( + "zlib", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON" + ), ], "headers": [r"z*.h"], - "libs": [r"*.lib"], + "libs": [r"zlib.lib"], }, "xz": { "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz", From 7885066e5fc6ae56db0bef18eb2c188a6aa9a181 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 25 Oct 2024 20:37:21 +0200 Subject: [PATCH 039/628] PIL.features: Add a compile-time zlib-ng feature flag and version number --- Tests/check_wheel.py | 3 +++ docs/reference/features.rst | 1 + src/PIL/features.py | 7 ++++++- src/_imaging.c | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f5..374e48e8a 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -34,10 +34,13 @@ def test_wheel_features() -> None: "fribidi", "harfbuzz", "libjpeg_turbo", + "zlib_ng", "xcb", } if sys.platform == "win32": expected_features.remove("xcb") + else: + expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features diff --git a/docs/reference/features.rst b/docs/reference/features.rst index fcff96735..427c0f606 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. +* ``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. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. diff --git a/src/PIL/features.py b/src/PIL/features.py index 75d59e01c..3645e3def 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"), + "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), "xcb": ("PIL._imaging", "HAVE_XCB", None), } @@ -308,7 +309,11 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: # this check is also in src/_imagingcms.c:setup_module() version_static = tuple(int(x) for x in v.split(".")) < (2, 7) t = "compiled for" if version_static else "loaded" - if name == "raqm": + if name == "zlib": + zlib_ng_version = version_feature("zlib_ng") + if zlib_ng_version is not None: + v += ", compiled for zlib-ng " + zlib_ng_version + elif name == "raqm": for f in ("fribidi", "harfbuzz"): v2 = version_feature(f) if v2 is not None: diff --git a/src/_imaging.c b/src/_imaging.c index 2db4486b2..5d6d97bed 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4397,6 +4397,20 @@ setup_module(PyObject *m) { } #endif + PyObject *have_zlibng; +#ifdef ZLIBNG_VERSION + have_zlibng = Py_True; + { + PyObject *v = PyUnicode_FromString(ZLIBNG_VERSION); + PyDict_SetItemString(d, "zlib_ng_version", v ? v : Py_None); + Py_XDECREF(v); + } +#else + have_zlibng = Py_False; +#endif + Py_INCREF(have_zlibng); + PyModule_AddObject(m, "HAVE_ZLIBNG", have_zlibng); + #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); From a43e5bb7354006766a67a353aa21d9a0198cdcab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 14:26:47 +1100 Subject: [PATCH 040/628] brew remove libdeflate --- .github/workflows/wheels-dependencies.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97c1adf09..7970d4d15 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -121,6 +121,7 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de untar pillow-depends-main.zip if [[ -n "$IS_MACOS" ]]; then + # libdeflate may cause a minimum target error when repairing the wheel # libtiff and libxcb cause a conflict with building libtiff and libxcb # libxau and libxdmcp cause an issue on macOS < 11 # remove cairo to fix building harfbuzz on arm64 @@ -132,7 +133,7 @@ if [[ -n "$IS_MACOS" ]]; then if [[ "$CIBW_ARCHS" == "arm64" ]]; then brew remove --ignore-dependencies jpeg-turbo else - brew remove --ignore-dependencies webp + brew remove --ignore-dependencies libdeflate webp fi brew install pkg-config From e1f4b5a68fa783126f5a81ccba5fac4526f6ab00 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 15:10:41 +1100 Subject: [PATCH 041/628] Move MPO into "Fully supported formats" --- docs/handbook/image-file-formats.rst | 48 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8183473e4..bf3087f6f 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -692,6 +692,30 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: you fail to do this, you will get errors about not being able to load the ``_imaging`` DLL). +MPO +^^^ + +Pillow reads and writes Multi Picture Object (MPO) files. When first opened, it loads +the primary image. The :py:meth:`~PIL.Image.Image.seek` and +:py:meth:`~PIL.Image.Image.tell` methods may be used to read other pictures from the +file. The pictures are zero-indexed and random access is supported. + +.. _mpo-saving: + +Saving +~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +option will also be available. + +**append_images** + A list of images to append as additional pictures. Each of the + images in the list can be single or multiframe images. + + .. versionadded:: 9.3.0 + MSP ^^^ @@ -1435,30 +1459,6 @@ Note that there may be an embedded gamma of 2.2 in MIC files. To enable MIC support, you must install :pypi:`olefile`. -MPO -^^^ - -Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary -image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` -methods may be used to read other pictures from the file. The pictures are -zero-indexed and random access is supported. - -.. _mpo-saving: - -Saving -~~~~~~ - -When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default -only the first frame of a multiframe image will be saved. If the ``save_all`` -argument is present and true, then all frames will be saved, and the following -option will also be available. - -**append_images** - A list of images to append as additional pictures. Each of the - images in the list can be single or multiframe images. - - .. versionadded:: 9.3.0 - PCD ^^^ From 413bbb31c959a3e2a6929d4a12861a79d82f8e80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 16:15:46 +1100 Subject: [PATCH 042/628] Fixed catching warnings --- Tests/test_bmp_reference.py | 2 ++ Tests/test_file_dcx.py | 4 ++++ Tests/test_file_fli.py | 4 ++++ Tests/test_file_gif.py | 4 ++++ Tests/test_file_icns.py | 2 ++ Tests/test_file_im.py | 4 ++++ Tests/test_file_jpeg.py | 2 ++ Tests/test_file_mpo.py | 4 ++++ Tests/test_file_png.py | 2 ++ Tests/test_file_psd.py | 4 ++++ Tests/test_file_spider.py | 4 ++++ Tests/test_file_tar.py | 4 ++++ Tests/test_file_tiff.py | 4 ++++ Tests/test_file_webp.py | 2 ++ Tests/test_image.py | 2 ++ Tests/test_imageqt.py | 2 ++ Tests/test_numpy.py | 2 ++ 17 files changed, 52 insertions(+) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 7f8487921..82cab39c6 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -22,6 +22,8 @@ def test_bad() -> None: for f in get_files("b"): # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + try: with Image.open(f) as im: im.load() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 65337cad9..5deacd878 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -36,6 +36,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_FILE) im.load() im.close() @@ -43,6 +45,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index f86fb8d09..0a7740cc8 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -65,6 +65,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(static_test_file) im.load() im.close() @@ -81,6 +83,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(static_test_file) as im: im.load() diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 16c8466f3..248347d5b 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -46,6 +46,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_GIF) im.load() im.close() @@ -67,6 +69,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_GIF) as im: im.load() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 16f6b3651..141b88dfa 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -21,6 +21,8 @@ def test_sanity() -> None: with Image.open(TEST_FILE) as im: # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + im.load() assert im.mode == "RGBA" diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 036965bf5..1d3fa485f 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -41,6 +41,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_IM) im.load() im.close() @@ -48,6 +50,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_IM) as im: im.load() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cde951395..2c66652e5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -850,6 +850,8 @@ class TestFileJpeg: out = str(tmp_path / "out.jpg") with warnings.catch_warnings(): + warnings.simplefilter("error") + im.save(out, exif=exif) with Image.open(out) as reloaded: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index e0f42a266..949583185 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -48,6 +48,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(test_files[0]) im.load() im.close() @@ -63,6 +65,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(test_files[0]) as im: im.load() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0abf9866f..ffafc3c58 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -338,6 +338,8 @@ class TestFilePng: with Image.open(TEST_PNG_FILE) as im: # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + im.verify() with Image.open(TEST_PNG_FILE) as im: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index e6c79e40b..5f22001f3 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -35,6 +35,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(test_file) im.load() im.close() @@ -42,6 +44,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(test_file) as im: im.load() diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 66c88e9d8..4cafda865 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -34,6 +34,8 @@ def test_unclosed_file() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_FILE) im.load() im.close() @@ -41,6 +43,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 6217ebedd..49220a8b6 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -37,11 +37,15 @@ def test_unclosed_file() -> None: def test_close() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() def test_contextmanager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index af766022b..6f51d4651 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -72,6 +72,8 @@ class TestFileTiff: def test_closed_file(self) -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open("Tests/images/multipage.tiff") im.load() im.close() @@ -88,6 +90,8 @@ class TestFileTiff: def test_context_manager(self) -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open("Tests/images/multipage.tiff") as im: im.load() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 79f6bb4e0..ad5aa9ed6 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -191,6 +191,8 @@ class TestFileWebp: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: with warnings.catch_warnings(): + warnings.simplefilter("error") + image.save(tmp_path / "temp.webp") def test_file_pointer_could_be_reused(self) -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 9b65041f4..c8df474f4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -737,6 +737,8 @@ class TestImage: # Act/Assert with Image.open(test_file) as im: with warnings.catch_warnings(): + warnings.simplefilter("error") + im.save(temp_file) def test_no_new_file_on_error(self, tmp_path: Path) -> None: diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 22cd674ce..2d7ca0ae0 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -52,4 +52,6 @@ def test_image(mode: str) -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 040472d69..79cd14b66 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -264,4 +264,6 @@ def test_no_resource_warning_for_numpy_array() -> None: with Image.open(test_file) as im: # Act/Assert with warnings.catch_warnings(): + warnings.simplefilter("error") + array(im) From ce4059171cc5696e7c7d8c5dc74990d5e874bf58 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 18:41:05 +1100 Subject: [PATCH 043/628] Skip failing records when rendering --- Tests/test_file_wmf.py | 12 +++++++++++- src/display.c | 13 +++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 79e707263..d730a049a 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path from typing import IO @@ -7,7 +8,7 @@ import pytest from PIL import Image, ImageFile, WmfImagePlugin -from .helper import assert_image_similar_tofile, hopper +from .helper import assert_image_equal_tofile, assert_image_similar_tofile, hopper def test_load_raw() -> None: @@ -34,6 +35,15 @@ def test_load() -> None: assert im.load()[0, 0] == (255, 255, 255) +def test_render() -> None: + with open("Tests/images/drawing.emf", "rb") as fp: + data = fp.read() + b = BytesIO(data[:808] + b"\x00" + data[809:]) + with Image.open(b) as im: + if hasattr(Image.core, "drawwmf"): + assert_image_equal_tofile(im, "Tests/images/drawing.emf") + + def test_register_handler(tmp_path: Path) -> None: class TestHandler(ImageFile.StubHandler): methodCalled = False diff --git a/src/display.c b/src/display.c index b4e2e3899..03b9316c3 100644 --- a/src/display.c +++ b/src/display.c @@ -716,6 +716,14 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { #define GET32(p, o) ((DWORD *)(p + o))[0] +BOOL +enhMetaFileProc( + HDC hdc, HANDLETABLE FAR *lpht, CONST ENHMETARECORD *lpmr, int nHandles, LPARAM data +) { + PlayEnhMetaFileRecord(hdc, lpht, lpmr, nHandles); + return TRUE; +} + PyObject * PyImaging_DrawWmf(PyObject *self, PyObject *args) { HBITMAP bitmap; @@ -796,10 +804,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { /* FIXME: make background transparent? configurable? */ FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); - if (!PlayEnhMetaFile(dc, meta, &rect)) { - PyErr_SetString(PyExc_OSError, "cannot render metafile"); - goto error; - } + EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect); /* step 4: extract bits from bitmap */ From f92599aa9394d9b1f59a503a796bc0fb6ddb8cda Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 19:05:16 +1100 Subject: [PATCH 044/628] Renamed fixture --- Tests/test_file_jpeg2k.py | 102 +++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 79f53e211..fbf72ae05 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -32,7 +32,7 @@ pytestmark = skip_unless_feature("jpg_2000") @pytest.fixture -def test_card() -> Generator[ImageFile.ImageFile, None, None]: +def card() -> Generator[ImageFile.ImageFile, None, None]: with Image.open("Tests/images/test-card.png") as im: im.load() try: @@ -83,78 +83,76 @@ def test_invalid_file() -> None: Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio(test_card: ImageFile.ImageFile) -> None: +def test_bytesio(card: ImageFile.ImageFile) -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) with Image.open(data) as im: im.load() - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) # These two test pre-written JPEG 2000 files that were not written with # PIL (they were made using Adobe Photoshop) -def test_lossless(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: +def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() outfile = str(tmp_path / "temp_test-card.png") im.save(outfile) - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) -def test_lossy_tiled(test_card: ImageFile.ImageFile) -> None: - assert_image_similar_tofile( - test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 - ) +def test_lossy_tiled(card: ImageFile.ImageFile) -> None: + assert_image_similar_tofile(card, "Tests/images/test-card-lossy-tiled.jp2", 2.0) -def test_lossless_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card) - assert_image_equal(im, test_card) +def test_lossless_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card) + assert_image_equal(im, card) -def test_lossy_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) +def test_lossy_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, quality_layers=[20]) + assert_image_similar(im, card, 2.0) -def test_tiled_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, tile_size=(128, 128)) - assert_image_equal(im, test_card) +def test_tiled_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, tile_size=(128, 128)) + assert_image_equal(im, card) -def test_tiled_offset_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) - assert_image_equal(im, test_card) +def test_tiled_offset_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) + assert_image_equal(im, card) -def test_tiled_offset_too_small(test_card: ImageFile.ImageFile) -> None: +def test_tiled_offset_too_small(card: ImageFile.ImageFile) -> None: with pytest.raises(ValueError): - roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) + roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, irreversible=True, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) +def test_irreversible_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, irreversible=True, quality_layers=[20]) + assert_image_similar(im, card, 2.0) -def test_prog_qual_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") - assert_image_similar(im, test_card, 2.0) +def test_prog_qual_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, quality_layers=[60, 40, 20], progression="LRCP") + assert_image_similar(im, card, 2.0) -def test_prog_res_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, num_resolutions=8, progression="RLCP") - assert_image_equal(im, test_card) +def test_prog_res_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, num_resolutions=8, progression="RLCP") + assert_image_equal(im, card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) def test_default_num_resolutions( - test_card: ImageFile.ImageFile, num_resolutions: int + card: ImageFile.ImageFile, num_resolutions: int ) -> None: d = 1 << (num_resolutions - 1) - im = test_card.resize((d - 1, d - 1)) + im = card.resize((d - 1, d - 1)) with pytest.raises(OSError): roundtrip(im, num_resolutions=num_resolutions) reloaded = roundtrip(im) @@ -216,31 +214,31 @@ def test_header_errors() -> None: pass -def test_layers_type(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: +def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: - test_card.save(outfile, quality_layers=quality_layers) + card.save(outfile, quality_layers=quality_layers) for quality_layers_str in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers_str) + card.save(outfile, quality_layers=quality_layers_str) -def test_layers(test_card: ImageFile.ImageFile) -> None: +def test_layers(card: ImageFile.ImageFile) -> None: out = BytesIO() - test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") + card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) with Image.open(out) as im: im.layers = 1 im.load() - assert_image_similar(im, test_card, 13) + assert_image_similar(im, card, 13) out.seek(0) with Image.open(out) as im: im.layers = 3 im.load() - assert_image_similar(im, test_card, 0.4) + assert_image_similar(im, card, 0.4) @pytest.mark.parametrize( @@ -257,7 +255,7 @@ def test_layers(test_card: ImageFile.ImageFile) -> None: ), ) def test_no_jp2( - test_card: ImageFile.ImageFile, + card: ImageFile.ImageFile, name: str, args: dict[str, bool], offset: int, @@ -266,20 +264,20 @@ def test_no_jp2( out = BytesIO() if name: out.name = name - test_card.save(out, "JPEG2000", **args) + card.save(out, "JPEG2000", **args) out.seek(offset) assert out.read(2) == data -def test_mct(test_card: ImageFile.ImageFile) -> None: +def test_mct(card: ImageFile.ImageFile) -> None: # Three component for val in (0, 1): out = BytesIO() - test_card.save(out, "JPEG2000", mct=val, no_jp2=True) + card.save(out, "JPEG2000", mct=val, no_jp2=True) assert out.getvalue()[59] == val with Image.open(out) as im: - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) # Single component should have MCT disabled for val in (0, 1): @@ -436,22 +434,22 @@ def test_comment() -> None: pass -def test_save_comment(test_card: ImageFile.ImageFile) -> None: +def test_save_comment(card: ImageFile.ImageFile) -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() - test_card.save(out, "JPEG2000", comment=comment) + card.save(out, "JPEG2000", comment=comment) with Image.open(out) as im: assert im.info["comment"] == b"Created by Pillow" out = BytesIO() long_comment = b" " * 65531 - test_card.save(out, "JPEG2000", comment=long_comment) + card.save(out, "JPEG2000", comment=long_comment) with Image.open(out) as im: assert im.info["comment"] == long_comment with pytest.raises(ValueError): - test_card.save(out, "JPEG2000", comment=long_comment + b" ") + card.save(out, "JPEG2000", comment=long_comment + b" ") @pytest.mark.parametrize( @@ -474,10 +472,10 @@ def test_crashes(test_file: str) -> None: @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker(test_card: ImageFile.ImageFile) -> None: +def test_plt_marker(card: ImageFile.ImageFile) -> None: # Search the start of the codesteam for PLT out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, plt=True) + card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: marker = out.read(2) From ed910a68d637661ccb4c4a7c1245769176fec93d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 11:42:35 +1100 Subject: [PATCH 045/628] Only replace version suffix if zlib-ng is present --- Tests/test_features.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index ed7929973..f8f7f6eec 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -36,10 +36,11 @@ def test_version() -> None: else: assert function(name) == version if name != "PIL": - if name == "zlib" and version is not None: - version = re.sub(".zlib-ng$", "", version) - elif name == "libtiff" and version is not None: - version = re.sub("t$", "", version) + if version is not None: + if name == "zlib" and features.check_feature("zlib_ng"): + version = re.sub(".zlib-ng$", "", version) + elif name == "libtiff": + version = re.sub("t$", "", version) assert version is None or re.search(r"\d+(\.\d+)*$", version) for module in features.modules: From 29cdbce39e1d3860486319b46ea829e0c3566279 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 21:13:01 +1100 Subject: [PATCH 046/628] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ae3841569..87945bc84 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Corrected EMF DPI #8485 + [radarhere] + - Fix IFDRational with a zero denominator #8474 [radarhere] From b4ba4665410c70ad8976b092ee9b1ad89625c0ed Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 27 Oct 2024 07:03:35 +1100 Subject: [PATCH 047/628] Do not skip failing records on 32-bit --- Tests/test_file_wmf.py | 2 ++ src/display.c | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 2adf38d48..e60a5b64e 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from io import BytesIO from pathlib import Path from typing import IO @@ -35,6 +36,7 @@ def test_load() -> None: assert im.load()[0, 0] == (255, 255, 255) +@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_render() -> None: with open("Tests/images/drawing.emf", "rb") as fp: data = fp.read() diff --git a/src/display.c b/src/display.c index 03b9316c3..fe5801fc0 100644 --- a/src/display.c +++ b/src/display.c @@ -716,12 +716,12 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { #define GET32(p, o) ((DWORD *)(p + o))[0] -BOOL +int enhMetaFileProc( - HDC hdc, HANDLETABLE FAR *lpht, CONST ENHMETARECORD *lpmr, int nHandles, LPARAM data + HDC hdc, HANDLETABLE *lpht, const ENHMETARECORD *lpmr, int nHandles, LPARAM data ) { PlayEnhMetaFileRecord(hdc, lpht, lpmr, nHandles); - return TRUE; + return 1; } PyObject * @@ -804,7 +804,14 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { /* FIXME: make background transparent? configurable? */ FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); +#ifdef _WIN64 EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect); +#else + if (!PlayEnhMetaFile(dc, meta, &rect)) { + PyErr_SetString(PyExc_OSError, "cannot render metafile"); + goto error; + } +#endif /* step 4: extract bits from bitmap */ From 73600eea94bfb695c52f7746c3523b5ce332d567 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2024 00:34:24 +1100 Subject: [PATCH 048/628] Detach PyQt6 QPixmap instance before returning --- .github/workflows/test-windows.yml | 6 ++++++ src/PIL/ImageQt.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c8842e37b..f6d0aeb1d 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -80,6 +80,12 @@ jobs: pytest-cov pytest-timeout + - name: Install CPython dependencies + if: "!contains(matrix.python-version, 'pypy')" + run: > + python3 -m pip install + PyQt6 + - name: Install dependencies id: install run: | diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index a3d647138..2cc40f855 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -213,4 +213,7 @@ def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: qimage = toqimage(im) - return getattr(QPixmap, "fromImage")(qimage) + pixmap = getattr(QPixmap, "fromImage")(qimage) + if qt_version == "6": + pixmap.detach() + return pixmap From e5706a590b16fcfc2b826de19addc2e33d3c3508 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2024 09:04:06 +1100 Subject: [PATCH 049/628] Upgraded multibuild to remove openjpeg lib64 copy --- .github/workflows/wheels-dependencies.sh | 3 --- wheels/multibuild | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 7970d4d15..3a80a7e74 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -91,9 +91,6 @@ function build { build_libpng build_lcms2 build_openjpeg - if [ -f /usr/local/lib64/libopenjp2.so ]; then - cp /usr/local/lib64/libopenjp2.so /usr/local/lib - fi ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" diff --git a/wheels/multibuild b/wheels/multibuild index 452dd2d17..9a9d1275f 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 452dd2d1705f6b2375369a6570c415beb3163f70 +Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 From 08554684b3f382f3d729db5e6cd1fe94126dab66 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 07:46:59 +0800 Subject: [PATCH 050/628] Revert fribidi/raqm changes for macOS builds. --- .github/workflows/wheels-dependencies.sh | 5 ----- .github/workflows/wheels-test.sh | 21 ++++++++++++++++----- pyproject.toml | 4 ++-- setup.py | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 9dae28717..220842371 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -155,11 +155,6 @@ function build { fi build_harfbuzz - - if [ -n "$IS_MACOS" ]; then - build_simple fribidi $FRIBIDI_VERSION https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION tar.xz --enable-shared - build_raqm - fi } # Perform all dependency builds in the build subfolder. diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 3a8fc87ba..e671c8d45 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,13 +1,24 @@ #!/bin/bash set -e -# For Unix, ensure fribidi is installed by the system. -if [[ "$OSTYPE" != "darwin"* ]]; then - if [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then - apk add curl fribidi +# Ensure fribidi is installed by the system. +if [[ "$OSTYPE" == "darwin"* ]]; then + # If Homebrew is on the path during the build, it may leak into the wheels. + # However, we need a *do* need Homebrew to provide a copy of fribidi for + # testing purposes so that we can verify the fribidi shim works as expected. + if [[ "$(uname -m)" == "x86_64" ]]; then + HOMEBREW_HOME=/usr/local/homebrew else - yum install -y fribidi + HOMEBREW_HOME=/opt/homebrew fi + $HOMEBREW_HOME/bin/brew install fribidi + + # Add the Homebrew lib folder so that vendored libraries can be found. + export DYLD_LIBRARY_PATH=$HOMEBREW_HOME/lib +elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then + apk add curl fribidi +else + yum install -y fribidi fi python3 -m pip install numpy diff --git a/pyproject.toml b/pyproject.toml index 3839cf840..da8548dcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,8 +95,8 @@ before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" -# Add an explicit dependencies prefix for macOS, and don't request vendored libraries. -macos.config-settings = "raqm=enable imagequant=disable dependencies-prefix=./build/deps" +# Add an explicit dependencies prefix for macOS. +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index 16c25cbda..38d793b14 100644 --- a/setup.py +++ b/setup.py @@ -454,7 +454,7 @@ class pil_build_ext(build_ext): def get_macos_sdk_path(self) -> str | None: try: sdk_path = ( - subprocess.check_output(["xcrun", "--show-sdk-path"]) + subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", "macosx"]) .strip() .decode("latin1") ) From 8308bf361e7d4bac39b1710e4c3c0725c7ad07ac Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 07:47:30 +0800 Subject: [PATCH 051/628] Bump multibuild to include more cmake changes. --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 22e8ad0c6..9a9d1275f 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 22e8ad0c69acdb0c94abab74da6e09f2810d50d5 +Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 From c74a5bdd85c3314fbdbbe2a72d17adad85fae5f0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:02:05 +0800 Subject: [PATCH 052/628] Correct paths used for Linux build. --- .github/workflows/wheels-dependencies.sh | 31 ++++-------------------- pyproject.toml | 4 +++ setup.py | 3 +-- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 220842371..0eb15d553 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -26,8 +26,6 @@ OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -RAQM_VERSION=0.10.2 -FRIBIDI_VERSION=1.0.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.2.2 else @@ -63,12 +61,8 @@ function build_brotli { local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ - && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp $BUILD_PREFIX/lib64/libbrotli* $BUILD_PREFIX/lib - cp $BUILD_PREFIX/lib64/pkgconfig/libbrotli* $BUILD_PREFIX/lib/pkgconfig - fi touch brotli-stamp } @@ -78,27 +72,12 @@ function build_harfbuzz { local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX --buildtype=release -Dfreetype=enabled -Dglib=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ && meson install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp $BUILD_PREFIX/lib64/libharfbuzz* $BUILD_PREFIX/lib - fi touch harfbuzz-stamp } -function build_raqm { - if [ -e raqm-stamp ]; then return; fi - python3 -m pip install meson ninja - - local out_dir=$(fetch_unpack https://github.com/HOST-Oman/libraqm/releases/download/v$RAQM_VERSION/raqm-$RAQM_VERSION.tar.xz raqm-$RAQM_VERSION.tar.xz) - (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX) - (cd $out_dir/build \ - && meson install) - touch raqm-stamp -} - function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then @@ -133,9 +112,6 @@ function build { build_libpng build_lcms2 build_openjpeg - if [ -f $BUILD_PREFIX/lib64/libopenjp2.so ]; then - cp $BUILD_PREFIX/lib64/libopenjp2.so $BUILD_PREFIX/lib - fi ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" @@ -190,6 +166,9 @@ if [[ -n "$IS_MACOS" ]]; then build_pkg_config # Ensure cmake is available python3 -m pip install cmake +else + # Ensure that any built libraries are on the linker path. + export LD_LIBRARY_PATH=$BUILD_PREFIX/lib fi wrap_wheel_builder build diff --git a/pyproject.toml b/pyproject.toml index da8548dcb..af3d04757 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,10 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +[tool.cibuildwheel.linux.environment] +LD_LIBRARY_PATH = "$(pwd)/build/deps/lib" +PKG_CONFIG_PATH = "$(pwd)/build/deps/lib/pkgconfig:$(pwd)/build/deps/share/pkgconfig" + [tool.black] exclude = "wheels/multibuild" diff --git a/setup.py b/setup.py index 38d793b14..d7e305fc0 100644 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ _LIB_IMAGING = ( "codec_fd", ) -DEBUG = False +DEBUG = True class DependencyException(Exception): @@ -479,7 +479,6 @@ class pil_build_ext(build_ext): pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): pkg_config = _pkg_config - # # add configured kits for root_name, lib_name in { From 72d81e24a81e6e765f21e5794218f080e7718ec7 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:25:47 +0800 Subject: [PATCH 053/628] Simplify Linux config by correcting a logic error in macOS config. --- .github/workflows/wheels-dependencies.sh | 7 ++----- pyproject.toml | 4 ---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0eb15d553..2e915364a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,8 +4,8 @@ if [ -z "$IS_MACOS" ]; then export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} export MB_ML_VER=${AUDITWHEEL_POLICY:9} - - # Build and install into the `build/deps` folder. +else + # Build and install macOS builds into the `build/deps` folder. BUILD_PREFIX=$(pwd)/build/deps fi export PLAT=$CIBW_ARCHS @@ -166,9 +166,6 @@ if [[ -n "$IS_MACOS" ]]; then build_pkg_config # Ensure cmake is available python3 -m pip install cmake -else - # Ensure that any built libraries are on the linker path. - export LD_LIBRARY_PATH=$BUILD_PREFIX/lib fi wrap_wheel_builder build diff --git a/pyproject.toml b/pyproject.toml index af3d04757..da8548dcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,10 +104,6 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" -[tool.cibuildwheel.linux.environment] -LD_LIBRARY_PATH = "$(pwd)/build/deps/lib" -PKG_CONFIG_PATH = "$(pwd)/build/deps/lib/pkgconfig:$(pwd)/build/deps/share/pkgconfig" - [tool.black] exclude = "wheels/multibuild" From ec214e4d7caa471c087e0d690b20d75b6dd929fc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:39:58 +0800 Subject: [PATCH 054/628] Can't check IS_MACOS until common_utils is invoked. --- .github/workflows/wheels-dependencies.sh | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2e915364a..9ce851f0f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,13 +1,5 @@ #!/bin/bash # Define custom utilities -# Test for macOS with [ -n "$IS_MACOS" ] -if [ -z "$IS_MACOS" ]; then - export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} - export MB_ML_VER=${AUDITWHEEL_POLICY:9} -else - # Build and install macOS builds into the `build/deps` folder. - BUILD_PREFIX=$(pwd)/build/deps -fi export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh @@ -15,6 +7,16 @@ if [ -z "$IS_MACOS" ]; then source wheels/multibuild/manylinux_utils.sh fi +# Test for macOS with [ -n "$IS_MACOS" ] +echo "IS MACOS: $IS_MACOS" +if [ -z "$IS_MACOS" ]; then + export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + export MB_ML_VER=${AUDITWHEEL_POLICY:9} +elseac + # Build and install macOS builds into the `build/deps` folder. + BUILD_PREFIX=$(pwd)/build/deps +fi + ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds From d1a4f8029b2d89f19e4652c96e3b1909b90aa5df Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:54:15 +0800 Subject: [PATCH 055/628] Don't use multibuild variables before invoking multibuild. --- .github/workflows/wheels-dependencies.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 9ce851f0f..fd13930db 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,4 +1,14 @@ #!/bin/bash + +# Setup that needs to be done before multibuild utils are invoked +if [[ "$(uname -s)" == "Darwin" ]]; then + # Build and install macOS builds into the `build/deps` folder. + BUILD_PREFIX=$(pwd)/build/deps +else + export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + export MB_ML_VER=${AUDITWHEEL_POLICY:9} +fi + # Define custom utilities export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh @@ -7,16 +17,6 @@ if [ -z "$IS_MACOS" ]; then source wheels/multibuild/manylinux_utils.sh fi -# Test for macOS with [ -n "$IS_MACOS" ] -echo "IS MACOS: $IS_MACOS" -if [ -z "$IS_MACOS" ]; then - export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} - export MB_ML_VER=${AUDITWHEEL_POLICY:9} -elseac - # Build and install macOS builds into the `build/deps` folder. - BUILD_PREFIX=$(pwd)/build/deps -fi - ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds From 6d137043f712d6790eba835a8ec727e0a4fbb603 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 10:01:46 +0800 Subject: [PATCH 056/628] Remove stray debug. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d7e305fc0..38d793b14 100644 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ _LIB_IMAGING = ( "codec_fd", ) -DEBUG = True +DEBUG = False class DependencyException(Exception): @@ -479,6 +479,7 @@ class pil_build_ext(build_ext): pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): pkg_config = _pkg_config + # # add configured kits for root_name, lib_name in { From 2d1d801ec032bfb760bede88da6dcaab5ff3c75e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2024 22:15:56 +1100 Subject: [PATCH 057/628] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 87945bc84..9d45e2214 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Detach PyQt6 QPixmap instance before returning #8509 + [radarhere] + - Corrected EMF DPI #8485 [radarhere] From 624848ff97e438e5ae7bd239254edceaa2751af1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Oct 2024 22:21:53 +1100 Subject: [PATCH 058/628] Do not repeatedly save to the same path --- Tests/test_file_jpeg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 2c66652e5..c41a61f4b 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -543,10 +543,10 @@ class TestFileJpeg: ) def test_qtables(self, tmp_path: Path) -> None: def _n_qtables_helper(n: int, test_file: str) -> None: + b = BytesIO() with Image.open(test_file) as im: - f = str(tmp_path / "temp.jpg") - im.save(f, qtables=[[n] * 64] * n) - with Image.open(f) as im: + im.save(b, "JPEG", qtables=[[n] * 64] * n) + with Image.open(b) as im: assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization From 80cf74030d533a5ebc8e921ae8094316123b6837 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:13:01 +1100 Subject: [PATCH 059/628] Removed fixture Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- 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 c41a61f4b..347a162a5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -541,7 +541,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_qtables(self, tmp_path: Path) -> None: + def test_qtables(self) -> None: def _n_qtables_helper(n: int, test_file: str) -> None: b = BytesIO() with Image.open(test_file) as im: From c6912f81ff20211c2638be64bc198024aa9a5b40 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 30 Oct 2024 05:43:23 +0800 Subject: [PATCH 060/628] Corrected typo in code comment. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index e671c8d45..9bcf42458 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -4,7 +4,7 @@ set -e # Ensure fribidi is installed by the system. if [[ "$OSTYPE" == "darwin"* ]]; then # If Homebrew is on the path during the build, it may leak into the wheels. - # However, we need a *do* need Homebrew to provide a copy of fribidi for + # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then HOMEBREW_HOME=/usr/local/homebrew From 67c2e04f706f9f7f8efa6bd6437632ab8e546cae Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 30 Oct 2024 08:37:19 +0200 Subject: [PATCH 061/628] Add trove-classifiers>=2024.10.12 to 'tests' extra and use for Windows CI --- .github/workflows/test-windows.yml | 13 +------------ pyproject.toml | 1 + 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f6d0aeb1d..4ac6f8769 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -69,17 +69,6 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: Install Python dependencies - run: > - python3 -m pip install - coverage>=7.4.2 - defusedxml - olefile - pyroma - pytest - pytest-cov - pytest-timeout - - name: Install CPython dependencies if: "!contains(matrix.python-version, 'pypy')" run: > @@ -184,7 +173,7 @@ jobs: - name: Build Pillow run: | $FLAGS="-C raqm=vendor -C fribidi=vendor" - cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." + cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]" & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh diff --git a/pyproject.toml b/pyproject.toml index c55be7693..45d5c2692 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ optional-dependencies.tests = [ "pytest", "pytest-cov", "pytest-timeout", + "trove-classifiers>=2024.10.12", ] optional-dependencies.typing = [ "typing-extensions; python_version<'3.10'", From 0bf15f0f2a856907bf925fc797a107a33069b734 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:24:37 +0200 Subject: [PATCH 062/628] Upgrade pip --- .github/workflows/test-windows.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 4ac6f8769..728182b1e 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -71,9 +71,9 @@ jobs: - name: Install CPython dependencies if: "!contains(matrix.python-version, 'pypy')" - run: > - python3 -m pip install - PyQt6 + run: | + python3 -m pip install --upgrade pip + python3 -m pip install PyQt6 - name: Install dependencies id: install From 01270b5859800355b12fa4403c7684d9bf72c6f5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 31 Oct 2024 06:27:54 +0800 Subject: [PATCH 063/628] Use the intended entry point for the x86_64 brew binary. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 9bcf42458..e2ef33711 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -7,7 +7,7 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then - HOMEBREW_HOME=/usr/local/homebrew + HOMEBREW_HOME=/usr/local else HOMEBREW_HOME=/opt/homebrew fi From 51e36230660667df38c68379314b652b34da47e5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 31 Oct 2024 09:50:38 +0800 Subject: [PATCH 064/628] Revert x86_64 homebrew location change (with explanation). --- .github/workflows/wheels-test.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index e2ef33711..b697b740e 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -7,7 +7,10 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then - HOMEBREW_HOME=/usr/local + # Use the "installed" location, rather than /usr/local, for two reasons: + # firstly, Homebrew allows libraries to be *installed*, but not linked; + # and secondly, we don't want any *other* leakage from /usr/local. + HOMEBREW_HOME=/usr/local/Homebrew else HOMEBREW_HOME=/opt/homebrew fi From aae90c3c016ee36b838d1ca5d522301d98731261 Mon Sep 17 00:00:00 2001 From: "Pietro F. Fontana" Date: Fri, 1 Nov 2024 15:42:08 +0100 Subject: [PATCH 065/628] Allow linking to shared zlib If compiled as shared library zlib produces `zlib1.dll` and `zdll.lib`. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index def341784..48863c397 100644 --- a/setup.py +++ b/setup.py @@ -690,6 +690,8 @@ class pil_build_ext(build_ext): feature.set("zlib", "z") elif sys.platform == "win32" and _find_library_file(self, "zlib"): feature.set("zlib", "zlib") # alternative name + elif sys.platform == "win32" and _find_library_file(self, "zdll"): + feature.set("zlib", "zdll") # different name if shared if feature.want("jpeg"): _dbg("Looking for jpeg") From 71016f23b4b441536a5c713d3cad53e2870655fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Nov 2024 17:51:01 +1100 Subject: [PATCH 066/628] Added Fedora 41 --- .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 880fe3eea..5bfbe1bdb 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,6 +47,7 @@ jobs: debian-12-bookworm-x86, debian-12-bookworm-amd64, fedora-40-amd64, + fedora-41-amd64, gentoo, ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 21ebd1ad5..da6763736 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,6 +31,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 40 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 41 | 3.13 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 13 Ventura | 3.9 | x86-64 | From 141e8d25462cfaa56e6d707b704c832b950b86bf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 2 Nov 2024 12:08:23 +0200 Subject: [PATCH 067/628] Remove unused 'gcov: true' for codecov-action@v4 --- .github/workflows/test-docker.yml | 1 - .github/workflows/test.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 5bfbe1bdb..101807745 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -102,7 +102,6 @@ jobs: with: flags: GHA_Docker name: ${{ matrix.docker }} - gcov: true token: ${{ secrets.CODECOV_ORG_TOKEN }} success: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6576292b5..29949f4e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -158,7 +158,6 @@ jobs: with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} - gcov: true token: ${{ secrets.CODECOV_ORG_TOKEN }} success: From 6fe7160cb97f13a93735a365ff33b5f388d8067d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sat, 2 Nov 2024 14:03:53 +0100 Subject: [PATCH 068/628] Update Windows 11 Arm64 tested 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 21ebd1ad5..d8671ee14 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -146,7 +146,7 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+----------------------------+------------------+--------------+ | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | +| Windows 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm64 | +----------------------------------+----------------------------+------------------+--------------+ | Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ 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 069/628] 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 070/628] 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 9faf598c890ced5828ba13ac7f5445b4310fff0c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 2 Nov 2024 23:29:56 +0200 Subject: [PATCH 071/628] Fix warning[artipacked]: credential persistence through GitHub Actions artifacts --- .github/workflows/docs.yml | 2 ++ .github/workflows/lint.yml | 2 ++ .github/workflows/test-cygwin.yml | 2 ++ .github/workflows/test-docker.yml | 2 ++ .github/workflows/test-mingw.yml | 2 ++ .github/workflows/test-valgrind.yml | 2 ++ .github/workflows/test-windows.yml | 4 ++++ .github/workflows/test.yml | 2 ++ .github/workflows/wheels.yml | 7 +++++++ 9 files changed, 25 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 92e860cb5..626824f38 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,6 +33,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cc4760288..8e789a734 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: pre-commit cache uses: actions/cache@v4 diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 0aa79e423..656054e89 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -48,6 +48,8 @@ jobs: - name: Checkout Pillow uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install Cygwin uses: cygwin/cygwin-install-action@v4 diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 101807745..03608319a 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -65,6 +65,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index c7a73439c..bfd393db5 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -46,6 +46,8 @@ jobs: steps: - name: Checkout Pillow uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up shell run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 63aec586b..8818b3b23 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -40,6 +40,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f6d0aeb1d..c1ba52719 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -44,16 +44,20 @@ jobs: steps: - name: Checkout Pillow uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout cached dependencies uses: actions/checkout@v4 with: + persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images uses: actions/checkout@v4 with: + persist-credentials: false repository: python-pillow/test-images path: Tests\test-images diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29949f4e0..87acd7ddb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 34452fa56..45f186341 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -61,6 +61,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: true - uses: actions/setup-python@v5 @@ -132,6 +133,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + persist-credentials: false submodules: true - uses: actions/setup-python@v5 @@ -173,10 +175,13 @@ jobs: - cibw_arch: ARM64 steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout extra test images uses: actions/checkout@v4 with: + persist-credentials: false repository: python-pillow/test-images path: Tests\test-images @@ -253,6 +258,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 From d3db931f21bcaa723071c7b4d669eead0fbdabab Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 2 Nov 2024 23:31:21 +0200 Subject: [PATCH 072/628] Fix error[excessive-permissions]: overly broad workflow or job-level permissions --- .github/workflows/stale.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 545c2e364..61ccf58e2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: permissions: - issues: write + contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -15,6 +15,8 @@ concurrency: jobs: stale: if: github.repository_owner == 'python-pillow' + permissions: + issues: write runs-on: ubuntu-latest From 924df0ac5c44815b397f375c7bed7a69ba02b956 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 23:22:57 +0000 Subject: [PATCH 073/628] Migrate config .github/renovate.json --- .github/renovate.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index d1d824335..f48b670ec 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,7 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:recommended" ], "labels": [ "Dependency" @@ -9,9 +9,13 @@ "packageRules": [ { "groupName": "github-actions", - "matchManagers": ["github-actions"], - "separateMajorMinor": "false" + "matchManagers": [ + "github-actions" + ], + "separateMajorMinor": false } ], - "schedule": ["on the 3rd day of the month"] + "schedule": [ + "on the 3rd day of the month" + ] } From 37679c8673e44f31be060ff7bc6772c7f178bee7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 4 Nov 2024 20:55:00 +1100 Subject: [PATCH 074/628] 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 4b7f6a6eb083b3c112f4713cd1fb74bd93ee3e7c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:35:35 +0000 Subject: [PATCH 075/628] [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.6.9 → v0.7.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.7.2) - [github.com/psf/black-pre-commit-mirror: 24.8.0 → 24.10.0](https://github.com/psf/black-pre-commit-mirror/compare/24.8.0...24.10.0) - [github.com/pre-commit/mirrors-clang-format: v19.1.1 → v19.1.3](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.1...v19.1.3) - [github.com/python-jsonschema/check-jsonschema: 0.29.3 → 0.29.4](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.3...0.29.4) - [github.com/tox-dev/pyproject-fmt: 2.2.4 → v2.5.0](https://github.com/tox-dev/pyproject-fmt/compare/2.2.4...v2.5.0) - [github.com/abravalheri/validate-pyproject: v0.20.2 → v0.22](https://github.com/abravalheri/validate-pyproject/compare/v0.20.2...v0.22) --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6254b8941..ddc98fdc3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.7.2 hooks: - id: ruff args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black @@ -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.1 + rev: v19.1.3 hooks: - id: clang-format types: [c] @@ -50,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.3 + rev: 0.29.4 hooks: - id: check-github-workflows - id: check-readthedocs @@ -62,12 +62,12 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.2.4 + rev: v2.5.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.20.2 + rev: v0.22 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] From 5628213ab03b59e2a70087c65cf4f05aa8de8cdf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:17:47 +0200 Subject: [PATCH 076/628] Upgrade pip for all --- .github/workflows/test-windows.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 728182b1e..9fcb8cb94 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -69,10 +69,13 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py + - name: Upgrade pip + run: | + python3 -m pip install --upgrade pip + - name: Install CPython dependencies if: "!contains(matrix.python-version, 'pypy')" run: | - python3 -m pip install --upgrade pip python3 -m pip install PyQt6 - name: Install dependencies From 607acbf95e8ee0a5900d3406324bffc77e02b49a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 07:05:39 +1100 Subject: [PATCH 077/628] Allow window to be supplied for ImageGrab.grab() on Windows --- Tests/test_imagegrab.py | 5 +++++ docs/reference/ImageGrab.rst | 5 +++++ src/PIL/ImageGrab.py | 11 ++++++++- src/display.c | 43 ++++++++++++++++++++++++++++++------ 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 5cd510751..032dac8cc 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -57,6 +57,11 @@ class TestImageGrab: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grab_invalid_handle(self) -> None: + with pytest.raises(OSError): + ImageGrab.grab(window=-1) + def test_grabclipboard(self) -> None: if sys.platform == "darwin": subprocess.call(["screencapture", "-cx"]) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index db2987eb0..6435e1a0c 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -39,6 +39,11 @@ or the clipboard to a PIL image memory. You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. .. versionadded:: 7.1.0 + + :param handle: + HWND, to capture a single window. Windows only. + + .. versionadded:: 11.1.0 :return: An image .. py:function:: grabclipboard() diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e27ca7e50..4dcaaa6c3 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -22,15 +22,20 @@ import shutil import subprocess import sys import tempfile +from typing import TYPE_CHECKING from . import Image +if TYPE_CHECKING: + from . import ImageWin + def grab( bbox: tuple[int, int, int, int] | None = None, include_layered_windows: bool = False, all_screens: bool = False, xdisplay: str | None = None, + window: int | ImageWin.HWND | None = None, ) -> Image.Image: im: Image.Image if xdisplay is None: @@ -51,8 +56,12 @@ def grab( return im_resized return im elif sys.platform == "win32": + if window is not None: + all_screens = -1 offset, size, data = Image.core.grabscreen_win32( - include_layered_windows, all_screens + include_layered_windows, + all_screens, + int(window) if window is not None else 0, ) im = Image.frombytes( "RGB", diff --git a/src/display.c b/src/display.c index b4e2e3899..30b7ada11 100644 --- a/src/display.c +++ b/src/display.c @@ -320,25 +320,36 @@ typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE); PyObject * PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { - int x = 0, y = 0, width, height; - int includeLayeredWindows = 0, all_screens = 0; + int x = 0, y = 0, width = -1, height; + int includeLayeredWindows = 0, screens = 0; HBITMAP bitmap; BITMAPCOREHEADER core; HDC screen, screen_copy; + HWND wnd; DWORD rop; PyObject *buffer; HANDLE dpiAwareness; HMODULE user32; Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; - if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) { + if (!PyArg_ParseTuple( + args, "|ii" F_HANDLE, &includeLayeredWindows, &screens, &wnd + )) { return NULL; } /* step 1: create a memory DC large enough to hold the entire screen */ - screen = CreateDC("DISPLAY", NULL, NULL, NULL); + if (screens == -1) { + screen = GetDC(wnd); + if (screen == NULL) { + PyErr_SetString(PyExc_OSError, "unable to get device context for handle"); + return NULL; + } + } else { + screen = CreateDC("DISPLAY", NULL, NULL, NULL); + } screen_copy = CreateCompatibleDC(screen); // added in Windows 10 (1607) @@ -351,11 +362,17 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); } - if (all_screens) { + if (screens == 1) { x = GetSystemMetrics(SM_XVIRTUALSCREEN); y = GetSystemMetrics(SM_YVIRTUALSCREEN); width = GetSystemMetrics(SM_CXVIRTUALSCREEN); height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + } else if (screens == -1) { + RECT rect; + if (GetClientRect(wnd, &rect)) { + width = rect.right; + height = rect.bottom; + } } else { width = GetDeviceCaps(screen, HORZRES); height = GetDeviceCaps(screen, VERTRES); @@ -367,6 +384,10 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { FreeLibrary(user32); + if (width == -1) { + goto error; + } + bitmap = CreateCompatibleBitmap(screen, width, height); if (!bitmap) { goto error; @@ -412,7 +433,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { DeleteObject(bitmap); DeleteDC(screen_copy); - DeleteDC(screen); + if (screens == -1) { + ReleaseDC(wnd, screen); + } else { + DeleteDC(screen); + } return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); @@ -420,7 +445,11 @@ error: PyErr_SetString(PyExc_OSError, "screen grab failed"); DeleteDC(screen_copy); - DeleteDC(screen); + if (screens == -1) { + ReleaseDC(wnd, screen); + } else { + DeleteDC(screen); + } return NULL; } From b6413cd58818c6518ede6638a391bac15d812db0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 07:16:49 +1100 Subject: [PATCH 078/628] 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 28e5b929f8dcae3312f45e8aeb8d3b03b290036d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 08:40:09 +1100 Subject: [PATCH 079/628] Test 0 --- Tests/test_imagegrab.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 032dac8cc..35aaa7ff0 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -59,8 +59,10 @@ class TestImageGrab: @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grab_invalid_handle(self) -> None: - with pytest.raises(OSError): + with pytest.raises(OSError, match="unable to get device context for handle"): ImageGrab.grab(window=-1) + with pytest.raises(OSError, match="screen grab failed"): + ImageGrab.grab(window=0) def test_grabclipboard(self) -> None: if sys.platform == "darwin": From 2d23a84049eb22f62107eb5f4cbe7c421b074d12 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 17:18:46 +1100 Subject: [PATCH 080/628] Fixed type hint --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index def341784..1a8c03eb3 100644 --- a/setup.py +++ b/setup.py @@ -1001,7 +1001,7 @@ def debug_build() -> bool: return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD -files = ["src/_imaging.c"] +files: list[str | os.PathLike[str]] = ["src/_imaging.c"] for src_file in _IMAGING: files.append("src/" + src_file + ".c") for src_file in _LIB_IMAGING: From 9622266c2a9b81b7b9a9e2e670571f9889bbe4d5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 18:33:25 +1100 Subject: [PATCH 081/628] Use DPI awareness from window --- src/display.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/display.c b/src/display.c index 30b7ada11..b78d0dca1 100644 --- a/src/display.c +++ b/src/display.c @@ -316,6 +316,7 @@ PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) { /* -------------------------------------------------------------------- */ /* Windows screen grabber */ +typedef HANDLE(__stdcall *Func_GetWindowDpiAwarenessContext)(HANDLE); typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE); PyObject * @@ -330,6 +331,7 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { PyObject *buffer; HANDLE dpiAwareness; HMODULE user32; + Func_GetWindowDpiAwarenessContext GetWindowDpiAwarenessContext_function; Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; if (!PyArg_ParseTuple( @@ -358,8 +360,19 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext )GetProcAddress(user32, "SetThreadDpiAwarenessContext"); if (SetThreadDpiAwarenessContext_function != NULL) { - // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) - dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); + if (screens == -1) { + GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext + )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); + DPI_AWARENESS_CONTEXT dpiAwarenessContext = + GetWindowDpiAwarenessContext_function(wnd); + if (dpiAwarenessContext != NULL) { + dpiAwareness = + SetThreadDpiAwarenessContext_function(dpiAwarenessContext); + } + } else { + // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) + dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); + } } if (screens == 1) { From 48db4a1d4d1baec7fdd77474db00029f24c2cef1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 19:40:03 +1100 Subject: [PATCH 082/628] Use test image filename --- docs/reference/ImageFile.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index fdfeb60f9..64abd71d1 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -19,7 +19,7 @@ Example: Parse an image from PIL import ImageFile - fp = open("hopper.pgm", "rb") + fp = open("hopper.ppm", "rb") p = ImageFile.Parser() From 3cdaee45f50a1a4c020c382b516299a2bb531ec7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 20:15:23 +1100 Subject: [PATCH 083/628] 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 a1d862f1d67e621213e0239d3e45838abeab363e Mon Sep 17 00:00:00 2001 From: "Pietro F. Fontana" Date: Tue, 5 Nov 2024 15:39:03 +0100 Subject: [PATCH 084/628] Be more specific in comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 48863c397..68252f3a9 100644 --- a/setup.py +++ b/setup.py @@ -691,7 +691,7 @@ class pil_build_ext(build_ext): elif sys.platform == "win32" and _find_library_file(self, "zlib"): feature.set("zlib", "zlib") # alternative name elif sys.platform == "win32" and _find_library_file(self, "zdll"): - feature.set("zlib", "zdll") # different name if shared + feature.set("zlib", "zdll") # dll import library if feature.want("jpeg"): _dbg("Looking for jpeg") From e82b5398e750c5dd37dfcc2edaf6b947371b53ef Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 6 Nov 2024 14:12:07 +0800 Subject: [PATCH 085/628] Correct handling of vendored fribidi. --- .github/workflows/wheels-dependencies.sh | 46 ++++++++++++++++-------- .github/workflows/wheels-test.sh | 8 +++-- .gitignore | 4 +++ pyproject.toml | 4 +-- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index fd13930db..ff06e99d2 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,16 +1,34 @@ #!/bin/bash # Setup that needs to be done before multibuild utils are invoked +PROJECTDIR=$(pwd) if [[ "$(uname -s)" == "Darwin" ]]; then - # Build and install macOS builds into the `build/deps` folder. - BUILD_PREFIX=$(pwd)/build/deps + # Safety check - macOS builds require that CIBW_ARCHS is set, and that it + # only contains a single value (even though cwbuildwheel) allows multiple + # values in CIBW_ARCHS. + if [[ -z "$CIBW_ARCHS" ]]; then + echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." + exit 1 + fi + if [[ "$CIBW_ARCHS" == *" "* ]]; then + echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS." + exit 1 + fi + + # Build macOS dependencies in `build/darwin` + # Install them into `build/deps/darwin` + WORKDIR=$(pwd)/build/darwin + BUILD_PREFIX=$(pwd)/build/deps/darwin + PLAT=$CIBW_ARCHS else - export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} - export MB_ML_VER=${AUDITWHEEL_POLICY:9} + # Build prefix will default to /usr/local + WORKDIR=$(pwd)/build + PLAT=$CIBW_ARCHS + MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + MB_ML_VER=${AUDITWHEEL_POLICY:9} fi # Define custom utilities -export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh if [ -z "$IS_MACOS" ]; then @@ -117,9 +135,6 @@ function build { ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" - if [[ -n "$IS_MACOS" ]]; then - CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" - fi build_libwebp CFLAGS=$ORIGINAL_CFLAGS @@ -127,7 +142,7 @@ function build { if [ -n "$IS_MACOS" ]; then # Custom freetype build - build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no + build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --without-harfbuzz else build_freetype fi @@ -136,17 +151,18 @@ function build { } # Perform all dependency builds in the build subfolder. -mkdir -p build -pushd build > /dev/null +mkdir -p $WORKDIR +pushd $WORKDIR > /dev/null # Any stuff that you need to do before you start building the wheels # Runs in the root directory of this repository. -if [[ ! -d pillow-depends-main ]]; then - if [[ ! -f pillow-depends-main.zip ]]; then +if [[ ! -d $WORKDIR/pillow-depends-main ]]; then + if [[ ! -f $PROJECTDIR/pillow-depends-main.zip ]]; then echo "Download pillow dependency sources..." - curl -fSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip + curl -fSL -o $PROJECTDIR/pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip fi - untar pillow-depends-main.zip + echo "Unpacking pillow dependency sources..." + untar $PROJECTDIR/pillow-depends-main.zip fi if [[ -n "$IS_MACOS" ]]; then diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index b697b740e..d1db8e8a5 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -16,8 +16,12 @@ if [[ "$OSTYPE" == "darwin"* ]]; then fi $HOMEBREW_HOME/bin/brew install fribidi - # Add the Homebrew lib folder so that vendored libraries can be found. - export DYLD_LIBRARY_PATH=$HOMEBREW_HOME/lib + # Add the lib folder for fribidi so that the vendored library can be found. + # Don't use /opt/homebrew/lib directly - use the lib folder where the + # installed copy of fribidi is cellared. This ensures we don't pick up the + # Homebrew version of any other library that we're dependent on (most notably, + # freetype). + export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_HOME/lib/libfribidi.dylib)) elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi else diff --git a/.gitignore b/.gitignore index 9b5773257..3033c2ea7 100644 --- a/.gitignore +++ b/.gitignore @@ -91,5 +91,9 @@ Tests/images/msp Tests/images/picins Tests/images/sunraster +# Test and dependency downloads +pillow-depends-main.zip +pillow-test-images.zip + # pyinstaller *.spec diff --git a/pyproject.toml b/pyproject.toml index da8548dcb..26fa45817 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,13 +96,13 @@ build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" # Add an explicit dependencies prefix for macOS. -macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps" +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps/darwin" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" [tool.cibuildwheel.macos.environment] -PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" [tool.black] exclude = "wheels/multibuild" From b0345c1c7b7605063064118563e8068fde5f9343 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Nov 2024 22:08:22 +1100 Subject: [PATCH 086/628] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index a0bada7b4..b9d633142 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -75,7 +75,9 @@ 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.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm | +| +----------------------------+------------------+ | +| | 3.8 | 10.4.0 | | +----------------------------------+----------------------------+------------------+--------------+ | macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm | +----------------------------------+----------------------------+------------------+--------------+ From 042f3ff083d98d211c2035dd45e511a63800d06e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 7 Nov 2024 07:52:18 +1100 Subject: [PATCH 087/628] Require coverage>=7.4.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 45d5c2692..bff295bc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ optional-dependencies.mic = [ ] optional-dependencies.tests = [ "check-manifest", - "coverage", + "coverage>=7.4.2", "defusedxml", "markdown2", "olefile", 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 088/628] 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 089/628] 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 7763350f072ca4ebe714352871df3ef8429b40bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 8 Nov 2024 07:30:09 +1100 Subject: [PATCH 090/628] Fallback to PER_MONITOR_AWARE --- src/display.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/display.c b/src/display.c index b78d0dca1..da248443c 100644 --- a/src/display.c +++ b/src/display.c @@ -368,6 +368,8 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { if (dpiAwarenessContext != NULL) { dpiAwareness = SetThreadDpiAwarenessContext_function(dpiAwarenessContext); + } else { + dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); } } else { // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) From a44b3067b0e31d6c3a89ba270db20b1a1a71d226 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 8 Nov 2024 07:45:29 +1100 Subject: [PATCH 091/628] Fallback to PER_MONITOR_AWARE if GetWindowDpiAwarenessContext is not available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/display.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display.c b/src/display.c index da248443c..f28926427 100644 --- a/src/display.c +++ b/src/display.c @@ -365,7 +365,7 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); DPI_AWARENESS_CONTEXT dpiAwarenessContext = GetWindowDpiAwarenessContext_function(wnd); - if (dpiAwarenessContext != NULL) { + if (GetWindowDpiAwarenessContext_function != NULL && dpiAwarenessContext != NULL) { dpiAwareness = SetThreadDpiAwarenessContext_function(dpiAwarenessContext); } else { From 288d77efd6f198d1c01c3eb62c333c6021b521bc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:45:58 +0000 Subject: [PATCH 092/628] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/display.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/display.c b/src/display.c index f28926427..89fec2c28 100644 --- a/src/display.c +++ b/src/display.c @@ -365,7 +365,8 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); DPI_AWARENESS_CONTEXT dpiAwarenessContext = GetWindowDpiAwarenessContext_function(wnd); - if (GetWindowDpiAwarenessContext_function != NULL && dpiAwarenessContext != NULL) { + if (GetWindowDpiAwarenessContext_function != NULL && + dpiAwarenessContext != NULL) { dpiAwareness = SetThreadDpiAwarenessContext_function(dpiAwarenessContext); } else { From 4b8867069b3b49cfabe2a7ef6275a7c0d85406af Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 7 Nov 2024 22:06:28 +0100 Subject: [PATCH 093/628] Fix GetWindowDpiAwarenessContext NULL check --- src/display.c | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/display.c b/src/display.c index 89fec2c28..5babd4f2a 100644 --- a/src/display.c +++ b/src/display.c @@ -329,7 +329,7 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { HWND wnd; DWORD rop; PyObject *buffer; - HANDLE dpiAwareness; + HANDLE dpiAwareness = NULL; HMODULE user32; Func_GetWindowDpiAwarenessContext GetWindowDpiAwarenessContext_function; Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; @@ -359,23 +359,15 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { user32 = LoadLibraryA("User32.dll"); SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext )GetProcAddress(user32, "SetThreadDpiAwarenessContext"); + GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext + )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); if (SetThreadDpiAwarenessContext_function != NULL) { - if (screens == -1) { - GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext - )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); - DPI_AWARENESS_CONTEXT dpiAwarenessContext = + if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) { + dpiAwareness = GetWindowDpiAwarenessContext_function(wnd); - if (GetWindowDpiAwarenessContext_function != NULL && - dpiAwarenessContext != NULL) { - dpiAwareness = - SetThreadDpiAwarenessContext_function(dpiAwarenessContext); - } else { - dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); - } - } else { - // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) - dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); } + // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) + dpiAwareness = SetThreadDpiAwarenessContext_function(dpiAwareness == NULL ? (HANDLE)-3 : dpiAwareness); } if (screens == 1) { From a6c941ac2c5e0b88d827c6d5c753cce3021e9e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 7 Nov 2024 22:22:02 +0100 Subject: [PATCH 094/628] Do not load GetWindowDpiAwarenessContext until its needed Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/display.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/display.c b/src/display.c index 5babd4f2a..353d52396 100644 --- a/src/display.c +++ b/src/display.c @@ -359,9 +359,9 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { user32 = LoadLibraryA("User32.dll"); SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext )GetProcAddress(user32, "SetThreadDpiAwarenessContext"); - GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext - )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); if (SetThreadDpiAwarenessContext_function != NULL) { + GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext + )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) { dpiAwareness = GetWindowDpiAwarenessContext_function(wnd); From acba5c47f8be16869da92efb877f07451612fa04 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 8 Nov 2024 08:24:13 +1100 Subject: [PATCH 095/628] Lint fix --- src/display.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/display.c b/src/display.c index 353d52396..b0693bf71 100644 --- a/src/display.c +++ b/src/display.c @@ -363,11 +363,12 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) { - dpiAwareness = - GetWindowDpiAwarenessContext_function(wnd); + dpiAwareness = GetWindowDpiAwarenessContext_function(wnd); } // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) - dpiAwareness = SetThreadDpiAwarenessContext_function(dpiAwareness == NULL ? (HANDLE)-3 : dpiAwareness); + dpiAwareness = SetThreadDpiAwarenessContext_function( + dpiAwareness == NULL ? (HANDLE)-3 : dpiAwareness + ); } if (screens == 1) { From 4e35852ccc97f3e455f5130f1cd22f2f10292955 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 8 Nov 2024 05:34:35 +0800 Subject: [PATCH 096/628] Correct typo in comment. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .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 ff06e99d2..2dea8c62a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,7 +4,7 @@ PROJECTDIR=$(pwd) if [[ "$(uname -s)" == "Darwin" ]]; then # Safety check - macOS builds require that CIBW_ARCHS is set, and that it - # only contains a single value (even though cwbuildwheel) allows multiple + # only contains a single value (even though cibuildwheel) allows multiple # values in CIBW_ARCHS. if [[ -z "$CIBW_ARCHS" ]]; then echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." From 0a5b0f29e58cf9763b886e3ce300a79b5f4bac63 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 00:18:59 +1100 Subject: [PATCH 097/628] Run gcc problem matcher on Python 3.13 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87acd7ddb..c33309bbf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,7 +116,7 @@ jobs: GHA_PYTHON_VERSION: ${{ matrix.python-version }} - name: Register gcc problem matcher - if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'" run: echo "::add-matcher::.github/problem-matchers/gcc.json" - name: Build From 681a03b1c38b26a36cddda812ead882b4af24eb4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 9 Nov 2024 10:29:29 +0800 Subject: [PATCH 098/628] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2dea8c62a..f4106429d 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,8 +4,8 @@ PROJECTDIR=$(pwd) if [[ "$(uname -s)" == "Darwin" ]]; then # Safety check - macOS builds require that CIBW_ARCHS is set, and that it - # only contains a single value (even though cibuildwheel) allows multiple - # values in CIBW_ARCHS. + # only contains a single value (even though cibuildwheel allows multiple + # values in CIBW_ARCHS). if [[ -z "$CIBW_ARCHS" ]]; then echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." exit 1 @@ -171,7 +171,7 @@ if [[ -n "$IS_MACOS" ]]; then # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, # and they may bring in other dependencies that we don't want. The same will # be true of any other locations on the path. To avoid conflicts, strip the - # path down to the bare mimimum (which, on macOS, won't include any + # path down to the bare minimum (which, on macOS, won't include any # development dependencies). export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" export CMAKE_PREFIX_PATH=$BUILD_PREFIX From 9f09d48f3710409725beef1ca1be66da33855922 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 18:20:52 +1100 Subject: [PATCH 099/628] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9d45e2214..7e8e94ed9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Allow linking to zlib import library on Windows #8519 + [cubanpit, nulano] + - Detach PyQt6 QPixmap instance before returning #8509 [radarhere] From 7674b1a07a1dfdf1c0944aea1fefb95c4f62f5b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 18:43:12 +1100 Subject: [PATCH 100/628] Removed quotes after dropping support for Python 3.8 --- src/PIL/_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 0a7d87cc2..335804b79 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -47,7 +47,7 @@ class SupportsRead(Protocol[_T_co]): def read(self, __length: int = ...) -> _T_co: ... -StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] +StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] __all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] From 2303018ebf751b7d6bc9d0e3d830cdd5f529a5b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 20:06:33 +1100 Subject: [PATCH 101/628] Removed use of os.path.realpath --- src/PIL/Image.py | 6 +++--- src/PIL/ImageFile.py | 2 +- src/PIL/ImageFont.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c..9b76ce8bd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2550,7 +2550,7 @@ class Image: filename: str | bytes = "" open_fp = False if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) + filename = os.fspath(fp) open_fp = True elif fp == sys.stdout: try: @@ -2559,7 +2559,7 @@ class Image: pass if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes - filename = os.path.realpath(os.fspath(fp.name)) + filename = os.fspath(fp.name) # may mutate self! self._ensure_mutable() @@ -3463,7 +3463,7 @@ def open( exclusive_fp = False filename: str | bytes = "" if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) + filename = os.fspath(fp) if filename: fp = builtins.open(filename, "rb") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index d69d84568..7f27d54dc 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -130,7 +130,7 @@ class ImageFile(Image.Image): if is_path(fp): # filename self.fp = open(fp, "rb") - self.filename = os.path.realpath(os.fspath(fp)) + self.filename = os.fspath(fp) self._exclusive_fp = True else: # stream diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b694b817e..d8c265560 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -270,7 +270,7 @@ class FreeTypeFont: ) if is_path(font): - font = os.path.realpath(os.fspath(font)) + font = os.fspath(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: From 5bff2f3b2894ec6923c590d0c37b18177d0634bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 10 Nov 2024 21:32:54 +1100 Subject: [PATCH 102/628] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7e8e94ed9..f6ed5eb82 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Removed use of os.path.realpath #8545 + [radarhere] + - Allow linking to zlib import library on Windows #8519 [cubanpit, nulano] From 378df7a5b22e47a0652619bd2f3218674c579d91 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Nov 2024 22:41:57 +1100 Subject: [PATCH 103/628] Disable platform guessing instead of adding dependencies-prefix --- pyproject.toml | 4 +-- setup.py | 93 ++++++++++++++++++++++---------------------------- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 26fa45817..abb7be645 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,8 +95,8 @@ before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" -# Add an explicit dependencies prefix for macOS. -macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps/darwin" +# Disable platform guessing on macOS +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index e4ce04883..4770c5eb7 100644 --- a/setup.py +++ b/setup.py @@ -344,11 +344,6 @@ class pil_build_ext(build_ext): for x in ("raqm", "fribidi") ] + [ - ( - "dependencies-prefix", - None, - "The prefix where build dependencies are located.", - ), ("disable-platform-guessing", None, "Disable platform guessing on Linux"), ("debug", None, "Debug logging"), ] @@ -360,7 +355,6 @@ class pil_build_ext(build_ext): return True if value in configuration.get(option, []) else None def initialize_options(self) -> None: - self.dependencies_prefix = configuration.get("dependencies-prefix", []) self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) @@ -570,54 +564,47 @@ class pil_build_ext(build_ext): ) elif sys.platform == "darwin": - if self.dependencies_prefix: - # Use the explicitly provided prefixes for dependencies. - for prefix in self.dependencies_prefix: - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) + # attempt to make sure we pick freetype2 over other versions + _add_directory(include_dirs, "/sw/include/freetype2") + _add_directory(include_dirs, "/sw/lib/freetype2/include") + # fink installation directories + _add_directory(library_dirs, "/sw/lib") + _add_directory(include_dirs, "/sw/include") + # darwin ports installation directories + _add_directory(library_dirs, "/opt/local/lib") + _add_directory(include_dirs, "/opt/local/include") + + # if Homebrew is installed, use its lib and include directories + try: + prefix = ( + subprocess.check_output(["brew", "--prefix"]) + .strip() + .decode("latin1") + ) + except Exception: + # Homebrew not installed + prefix = None + + ft_prefix = None + + if prefix: + # add Homebrew's include and lib directories + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) + ft_prefix = os.path.join(prefix, "opt", "freetype") + + if ft_prefix and os.path.isdir(ft_prefix): + # freetype might not be linked into Homebrew's prefix + _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) + _add_directory(include_dirs, os.path.join(ft_prefix, "include")) else: - # Guess the dependency locations based on homebrew/fink/macports - # attempt to make sure we pick freetype2 over other versions - _add_directory(include_dirs, "/sw/include/freetype2") - _add_directory(include_dirs, "/sw/lib/freetype2/include") - # fink installation directories - _add_directory(library_dirs, "/sw/lib") - _add_directory(include_dirs, "/sw/include") - # darwin ports installation directories - _add_directory(library_dirs, "/opt/local/lib") - _add_directory(include_dirs, "/opt/local/include") - - # if Homebrew is installed, use its lib and include directories - try: - prefix = ( - subprocess.check_output(["brew", "--prefix"]) - .strip() - .decode("latin1") - ) - except Exception: - # Homebrew not installed - prefix = None - - ft_prefix = None - - if prefix: - # add Homebrew's include and lib directories - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) - _add_directory( - include_dirs, os.path.join(prefix, "opt", "zlib", "include") - ) - ft_prefix = os.path.join(prefix, "opt", "freetype") - - if ft_prefix and os.path.isdir(ft_prefix): - # freetype might not be linked into Homebrew's prefix - _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) - _add_directory(include_dirs, os.path.join(ft_prefix, "include")) - else: - # fall back to freetype from XQuartz if - # Homebrew's freetype is missing - _add_directory(library_dirs, "/usr/X11/lib") - _add_directory(include_dirs, "/usr/X11/include") + # fall back to freetype from XQuartz if + # Homebrew's freetype is missing + _add_directory(library_dirs, "/usr/X11/lib") + _add_directory(include_dirs, "/usr/X11/include") # Add the macOS SDK path. sdk_path = self.get_macos_sdk_path() From 6fa775e324a27dac54ff8a9249553e06e1928051 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Nov 2024 22:46:24 +1100 Subject: [PATCH 104/628] 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 9dc690427f009f898ef74b8799569346eb0b3fd5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 13 Nov 2024 11:03:26 +0800 Subject: [PATCH 105/628] Correct the lookup of libfribidi on x86 macOS installs. --- .github/workflows/wheels-test.sh | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index d1db8e8a5..ce83a4278 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -7,21 +7,18 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then - # Use the "installed" location, rather than /usr/local, for two reasons: - # firstly, Homebrew allows libraries to be *installed*, but not linked; - # and secondly, we don't want any *other* leakage from /usr/local. - HOMEBREW_HOME=/usr/local/Homebrew + HOMEBREW_PREFIX=/usr/local else - HOMEBREW_HOME=/opt/homebrew + HOMEBREW_PREFIX=/opt/homebrew fi - $HOMEBREW_HOME/bin/brew install fribidi + $HOMEBREW_PREFIX/bin/brew install fribidi # Add the lib folder for fribidi so that the vendored library can be found. - # Don't use /opt/homebrew/lib directly - use the lib folder where the + # Don't use $HOMEWBREW_PREFIX/lib directly - use the lib folder where the # installed copy of fribidi is cellared. This ensures we don't pick up the # Homebrew version of any other library that we're dependent on (most notably, # freetype). - export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_HOME/lib/libfribidi.dylib)) + export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_PREFIX/lib/libfribidi.dylib)) elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi else From 48c7eb22c0aeba649892dcb4766600f6dcfb6bdc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:45:52 +1100 Subject: [PATCH 106/628] 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 107/628] 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 108/628] 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 109/628] 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 54f23345d2fd1894c7cf89475e385fcbb3d3f16d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 16 Nov 2024 09:58:29 +0800 Subject: [PATCH 110/628] More tweaks from code review. --- .github/workflows/wheels-dependencies.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f4106429d..ce2e40103 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -19,14 +19,13 @@ if [[ "$(uname -s)" == "Darwin" ]]; then # Install them into `build/deps/darwin` WORKDIR=$(pwd)/build/darwin BUILD_PREFIX=$(pwd)/build/deps/darwin - PLAT=$CIBW_ARCHS else # Build prefix will default to /usr/local WORKDIR=$(pwd)/build - PLAT=$CIBW_ARCHS MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_VER=${AUDITWHEEL_POLICY:9} fi +PLAT=$CIBW_ARCHS # Define custom utilities source wheels/multibuild/common_utils.sh @@ -119,9 +118,9 @@ function build { build_libjpeg_turbo if [ -n "$IS_MACOS" ]; then # Custom tiff build to include jpeg; by default, configure won't include - # headers/libs in the custom macOS prefix. Explicitly disable webp and - # zstd, because on x86_64 macs, it will pick up the Homebrew versions of - # webp and zstd from /usr/local. + # headers/libs in the custom macOS prefix. Explicitly disable webp, + # libdeflate and zstd, because on x86_64 macs, it will pick up the + # Homebrew versions of those libraries from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ --disable-webp --disable-zstd --disable-libdeflate From 6136a92d061e77e622c974c35aa0605f96eb05e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Nov 2024 09:48:28 +0000 Subject: [PATCH 111/628] Update codecov/codecov-action action to v5 --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 656054e89..b906ada92 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -135,7 +135,7 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: GHA_Cygwin diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 03608319a..cc5f9d4a5 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -100,7 +100,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bfd393db5..a14e7e1ed 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -85,7 +85,7 @@ jobs: .ci/test.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 9b5da137d..d1924eb20 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -215,7 +215,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c33309bbf..1405c64f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,7 +156,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From c70dacff62c07341c4bb9d9c28e26e4e703ab5ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Nov 2024 21:44:00 +1100 Subject: [PATCH 112/628] Updated from deprecated "file" to "files" --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index b906ada92..2076cad7a 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -137,7 +137,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Cygwin name: Cygwin Python 3.${{ matrix.python-minor-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a14e7e1ed..cca25edba 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -87,7 +87,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Windows name: "MSYS2 MinGW" token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index d1924eb20..d905a3925 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -217,7 +217,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Windows name: ${{ runner.os }} Python ${{ matrix.python-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} From 96b898c826982c5b99f2a9387e0d2011a73fe20a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 18 Nov 2024 09:56:45 +0800 Subject: [PATCH 113/628] A couple more cleanups from code review. --- .github/workflows/wheels-dependencies.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index ce2e40103..f86099af1 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -62,8 +62,7 @@ BROTLI_VERSION=1.1.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi - # This essentially duplicates the Homebrew recipe: - # https://github.com/Homebrew/homebrew-core/blob/master/Formula/p/pkg-config.rb + # This essentially duplicates the Homebrew recipe ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ @@ -108,7 +107,6 @@ function build { if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib - build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc @@ -123,7 +121,7 @@ function build { # Homebrew versions of those libraries from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ - --disable-webp --disable-zstd --disable-libdeflate + --disable-webp --disable-libdeflate --disable-zstd else build_tiff fi @@ -134,6 +132,9 @@ function build { ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" + if [[ -n "$IS_MACOS" ]]; then + CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + fi build_libwebp CFLAGS=$ORIGINAL_CFLAGS @@ -141,7 +142,7 @@ function build { if [ -n "$IS_MACOS" ]; then # Custom freetype build - build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --without-harfbuzz + build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no else build_freetype fi @@ -175,7 +176,7 @@ if [[ -n "$IS_MACOS" ]]; then export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" export CMAKE_PREFIX_PATH=$BUILD_PREFIX - # Link the brew command into our isolated build directory. + # Ensure the basic structure of the build prefix directory exists. mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" From e02e4aaf1de9503e6e88a60f292998d174d23034 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 19:31:54 +1100 Subject: [PATCH 114/628] Updated harfbuzz to 10.1.0 --- .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 f86099af1..89226810a 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.2 -HARFBUZZ_VERSION=10.0.1 +HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.4 OPENJPEG_VERSION=2.5.2 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c8332d11c..5606ac7f7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -112,7 +112,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "10.0.1", + "HARFBUZZ": "10.1.0", "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", From c7a9582eab34bc46b74c245aa137424b9cfaee74 Mon Sep 17 00:00:00 2001 From: Tom Flanagan Date: Tue, 19 Nov 2024 20:40:14 -0800 Subject: [PATCH 115/628] Fix file position desync when calling into libtiff --- Tests/test_libtiff_file_position.py | 23 +++++++++++++++++++++++ src/PIL/TiffImagePlugin.py | 17 +++++++++-------- 2 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 Tests/test_libtiff_file_position.py diff --git a/Tests/test_libtiff_file_position.py b/Tests/test_libtiff_file_position.py new file mode 100644 index 000000000..d614ea8ec --- /dev/null +++ b/Tests/test_libtiff_file_position.py @@ -0,0 +1,23 @@ +from __future__ import annotations + + +import pytest + +from PIL import Image + + +@pytest.mark.parametrize('test_file', [ + 'Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif', + 'Tests/images/old-style-jpeg-compression.tif', +]) +def test_libtiff_exif_loading(test_file) -> None: + # loading image before exif + im1 = Image.open(open(test_file, 'rb', buffering=1048576)) + im1.load() + exif1 = dict(im1.getexif()) + + # loading exif before image + im2 = Image.open(open(test_file, 'rb', buffering=1048576)) + exif2 = dict(im2.getexif()) + + assert exif1 == exif2 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a..726286772 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1216,10 +1216,6 @@ class TiffImageFile(ImageFile.ImageFile): def _seek(self, frame: int) -> None: self.fp = self._fp - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() - while len(self._frame_pos) <= frame: if not self.__next: msg = "no more images in TIFF file" @@ -1303,10 +1299,6 @@ class TiffImageFile(ImageFile.ImageFile): if not self.is_animated: self._close_exclusive_fp_after_loading = True - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() - # load IFD data from fp before it is closed exif = self.getexif() for key in TiffTags.TAGS_V2_GROUPS: @@ -1381,8 +1373,17 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("have fileno, calling fileno version of the decoder.") if not close_self_fp: self.fp.seek(0) + # Save and restore the file position, because libtiff will move it + # outside of the python runtime, and that will confuse + # io.BufferedReader and possible others. + # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), + # because the buffer read head already may not equal the actual + # file position, and fp.seek() may just adjust it's internal + # pointer and not actually seek the OS file handle. + pos = os.lseek(fp, 0, os.SEEK_CUR) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") + os.lseek(fp, pos, os.SEEK_SET) else: # we have something else. logger.debug("don't have fileno or getvalue. just reading") From 44cc0bebb8998897f03d77479df750f208d9d00e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 05:52:50 +0000 Subject: [PATCH 116/628] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_libtiff_file_position.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Tests/test_libtiff_file_position.py b/Tests/test_libtiff_file_position.py index d614ea8ec..43ed82c30 100644 --- a/Tests/test_libtiff_file_position.py +++ b/Tests/test_libtiff_file_position.py @@ -1,23 +1,25 @@ from __future__ import annotations - import pytest from PIL import Image -@pytest.mark.parametrize('test_file', [ - 'Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif', - 'Tests/images/old-style-jpeg-compression.tif', -]) +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", + "Tests/images/old-style-jpeg-compression.tif", + ], +) def test_libtiff_exif_loading(test_file) -> None: # loading image before exif - im1 = Image.open(open(test_file, 'rb', buffering=1048576)) + im1 = Image.open(open(test_file, "rb", buffering=1048576)) im1.load() exif1 = dict(im1.getexif()) # loading exif before image - im2 = Image.open(open(test_file, 'rb', buffering=1048576)) + im2 = Image.open(open(test_file, "rb", buffering=1048576)) exif2 = dict(im2.getexif()) assert exif1 == exif2 From cb1653f6272ebc543530ae03cd7cf8577ed13231 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Nov 2024 22:33:23 +1100 Subject: [PATCH 117/628] Updated comment --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 726286772..08bb78915 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1374,7 +1374,7 @@ class TiffImageFile(ImageFile.ImageFile): if not close_self_fp: self.fp.seek(0) # Save and restore the file position, because libtiff will move it - # outside of the python runtime, and that will confuse + # outside of the Python runtime, and that will confuse # io.BufferedReader and possible others. # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), # because the buffer read head already may not equal the actual From 925db4552658ac0d8c037b0ac0b39bd7eb96f8f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Nov 2024 22:40:29 +1100 Subject: [PATCH 118/628] Moved test --- Tests/test_file_libtiff.py | 19 +++++++++++++++++++ Tests/test_libtiff_file_position.py | 25 ------------------------- 2 files changed, 19 insertions(+), 25 deletions(-) delete mode 100644 Tests/test_libtiff_file_position.py diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 62f8719af..9c49b1534 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1098,6 +1098,25 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_similar(base_im, im, 0.7) + @pytest.mark.parametrize( + "test_file", + [ + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", + "Tests/images/old-style-jpeg-compression.tif", + ], + ) + 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()) + + # load image before exif + with Image.open(open(test_file, "rb", buffering=1048576)) as im2: + im2.load() + exif_after_load = dict(im2.getexif()) + + assert exif == exif_after_load + @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") def test_sampleformat_not_corrupted(self) -> None: # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted diff --git a/Tests/test_libtiff_file_position.py b/Tests/test_libtiff_file_position.py deleted file mode 100644 index 43ed82c30..000000000 --- a/Tests/test_libtiff_file_position.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", - "Tests/images/old-style-jpeg-compression.tif", - ], -) -def test_libtiff_exif_loading(test_file) -> None: - # loading image before exif - im1 = Image.open(open(test_file, "rb", buffering=1048576)) - im1.load() - exif1 = dict(im1.getexif()) - - # loading exif before image - im2 = Image.open(open(test_file, "rb", buffering=1048576)) - exif2 = dict(im2.getexif()) - - assert exif1 == exif2 From 82dfbc35b190a818a1cbdbee6524eaf31605b7bd Mon Sep 17 00:00:00 2001 From: Tom Flanagan Date: Wed, 20 Nov 2024 05:21:14 -0800 Subject: [PATCH 119/628] Update src/PIL/TiffImagePlugin.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 726286772..08bb78915 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1374,7 +1374,7 @@ class TiffImageFile(ImageFile.ImageFile): if not close_self_fp: self.fp.seek(0) # Save and restore the file position, because libtiff will move it - # outside of the python runtime, and that will confuse + # outside of the Python runtime, and that will confuse # io.BufferedReader and possible others. # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), # because the buffer read head already may not equal the actual From bfe40b5129e9250243b1d425172c18f7c4e15e10 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Nov 2024 19:22:39 +1100 Subject: [PATCH 120/628] Prevent Cygwin bash from being used by codecov-action --- .github/workflows/test-cygwin.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 2076cad7a..5b0a03946 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -133,6 +133,7 @@ jobs: - name: After success run: | bash.exe .ci/after_success.sh + rm C:\cygwin\bin\bash.EXE - name: Upload coverage uses: codecov/codecov-action@v5 From 69c9a7ffcfd42b0486f02f17119c6d05f1d04969 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Nov 2024 20:03:15 +1100 Subject: [PATCH 121/628] Use zlib-ng on Linux --- .github/workflows/wheels-dependencies.sh | 20 +++++++++++++++++--- Tests/check_wheel.py | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f86099af1..5d23fc036 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -50,10 +50,10 @@ if [[ -n "$IS_MACOS" ]]; then else GIFLIB_VERSION=5.2.1 fi -if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then +if [[ -n "$IS_MACOS" ]]; then ZLIB_VERSION=1.3.1 else - ZLIB_VERSION=1.2.8 + ZLIB_NG_VERSION=2.2.2 fi LIBWEBP_VERSION=1.4.0 BZIP2_VERSION=1.0.8 @@ -74,6 +74,16 @@ function build_pkg_config { touch pkg-config-stamp } +function build_zlib_ng { + if [ -e zlib-stamp ]; then return; fi + fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz + (cd zlib-ng-$ZLIB_NG_VERSION \ + && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ + && make -j4 \ + && make install) + touch zlib-stamp +} + function build_brotli { if [ -e brotli-stamp ]; then return; fi local cmake=$(get_modern_cmake) @@ -101,7 +111,11 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - build_new_zlib + if [ -n "$IS_MACOS" ]; then + build_new_zlib + else + build_zlib_ng + fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 374e48e8a..787d4f743 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -40,7 +40,7 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") - else: + elif sys.platform == "darwin": expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features From 9c3d8fb581cab18abb60117f08358b51b18a44b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2024 11:02:50 +1100 Subject: [PATCH 122/628] Do not install Pyroma, to use only system packages --- .github/workflows/test-mingw.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bfd393db5..fd8faba71 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -68,12 +68,13 @@ 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-python3-setuptools \ + mingw-w64-x86_64-python-pytest \ + mingw-w64-x86_64-python-pytest-cov \ + mingw-w64-x86_64-python-pytest-timeout \ mingw-w64-x86_64-python-pyqt6 - python3 -m ensurepip - python3 -m pip install pyroma pytest pytest-cov pytest-timeout - pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow From 30f03b2bff689af1f395c183cf991bc34f8b0bab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2024 11:05:33 +1100 Subject: [PATCH 123/628] Removed SETUPTOOLS_USE_DISTUTILS="stdlib" --- .github/workflows/test-mingw.yml | 3 +-- docs/installation/building-from-source.rst | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index fd8faba71..93fe8a064 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -69,7 +69,6 @@ jobs: mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools \ mingw-w64-x86_64-python-pytest \ mingw-w64-x86_64-python-pytest-cov \ mingw-w64-x86_64-python-pytest-timeout \ @@ -78,7 +77,7 @@ jobs: pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . + run: CFLAGS="-coverage" python3 -m pip install . - name: Test Pillow run: | diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 4b5175827..19294f72c 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -195,11 +195,6 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libraqm - https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with - MSYS2. To workaround this, before installing Pillow you must run:: - - export SETUPTOOLS_USE_DISTUTILS=stdlib - .. tab:: FreeBSD .. Note:: Only FreeBSD 10 and 11 tested From 16e409a8d28fecbf83cd67409224e65ebb371a93 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 24 Nov 2024 13:46:28 +0200 Subject: [PATCH 124/628] Test 3.13t (free-threaded) from Quansight-Labs/setup-python on Linux/macOS --- .github/workflows/test.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1405c64f9..83a696f5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: ] python-version: [ "pypy3.10", + "3.13t", "3.13", "3.12", "3.11", @@ -52,14 +53,14 @@ jobs: - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.10", PYTHONOPTIMIZE: 2 } # Free-threaded - - { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true } + - { python-version: "3.13t", disable-gil: true } # M1 only available for 3.10+ - { os: "macos-13", python-version: "3.9" } exclude: - { os: "macos-latest", python-version: "3.9" } runs-on: ${{ matrix.os }} - name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v4 @@ -67,8 +68,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - if: "${{ !matrix.disable-gil }}" + uses: Quansight-Labs/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -77,13 +77,6 @@ jobs: ".ci/*.sh" "pyproject.toml" - - name: Set up Python ${{ matrix.python-version }} (free-threaded) - uses: deadsnakes/action@v3.2.0 - if: "${{ matrix.disable-gil }}" - with: - python-version: ${{ matrix.python-version }} - nogil: ${{ matrix.disable-gil }} - - name: Set PYTHON_GIL if: "${{ matrix.disable-gil }}" run: | From 540a0b73a7ebb3ec5571e3ea027c73cbe95b2fc6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 23 Nov 2024 00:20:40 +1100 Subject: [PATCH 125/628] Use installed cmake --- .github/workflows/wheels-dependencies.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f86099af1..ddda43423 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -76,10 +76,9 @@ function build_pkg_config { function build_brotli { if [ -e brotli-stamp ]; then return; fi - local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ - && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) touch brotli-stamp } From 7dcf4d8ab319348239d4f8695c6d3366bc257cbf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Nov 2024 16:43:21 +1100 Subject: [PATCH 126/628] 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 5174c596ca6c78725efdb2cda8ace39f11d8e215 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:06:47 +0200 Subject: [PATCH 127/628] Release drafter: move removals, deprecations, documentation up, and uncategorised changes last --- .github/release-drafter.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 3711d91f0..de0ab4805 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -3,18 +3,19 @@ tag-template: "$NEXT_MINOR_VERSION" change-template: '- $TITLE #$NUMBER [@$AUTHOR]' categories: - - title: "Dependencies" - label: "Dependency" + - title: "Removals" + label: "Removal" - title: "Deprecations" label: "Deprecation" - title: "Documentation" label: "Documentation" - - title: "Removals" - label: "Removal" + - title: "Dependencies" + label: "Dependency" - title: "Testing" label: "Testing" - title: "Type hints" label: "Type hints" + - title: "Other changes" exclude-labels: - "changelog: skip" @@ -23,6 +24,4 @@ template: | https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html - ## Changes - $CHANGES From 02250e81b729ddb32952f96e57bf9d952ed70cf8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Nov 2024 07:58:10 +1100 Subject: [PATCH 128/628] Added link to GitHub releases --- CHANGES.rst | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f6ed5eb82..dfbbd24b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,26 +2,12 @@ Changelog (Pillow) ================== -11.1.0 (unreleased) -------------------- +11.1.0 and newer +---------------- -- Removed use of os.path.realpath #8545 - [radarhere] +See GitHub Releases: -- Allow linking to zlib import library on Windows #8519 - [cubanpit, nulano] - -- Detach PyQt6 QPixmap instance before returning #8509 - [radarhere] - -- Corrected EMF DPI #8485 - [radarhere] - -- Fix IFDRational with a zero denominator #8474 - [radarhere] - -- Fixed disabling a feature during install #8469 - [radarhere] +- https://github.com/python-pillow/Pillow/releases 11.0.0 (2024-10-15) ------------------- From c36a5b669b225fdcb6b4b3a41fc5b5e647a651c1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:29:05 +0200 Subject: [PATCH 129/628] Update/remove references to CHANGES.rst --- .github/CONTRIBUTING.md | 1 - README.md | 2 +- RELEASING.md | 2 -- codecov.yml | 2 +- pyproject.toml | 2 +- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d03fcf0d9..ba2b7d8ed 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,7 +19,6 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Follow PEP 8. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. - 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. ## Reporting Issues diff --git a/README.md b/README.md index 5bbebaccb..057d0acf0 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ The core image library is designed for fast access to data stored in a few basic - [Issues](https://github.com/python-pillow/Pillow/issues) - [Pull requests](https://github.com/python-pillow/Pillow/pulls) - [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) -- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) +- [Changelog](https://github.com/python-pillow/Pillow/releases) - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) ## Report a Vulnerability diff --git a/RELEASING.md b/RELEASING.md index 9e6ec5dd4..ebdbb6406 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,7 +12,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] 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 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`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: ```bash @@ -34,7 +33,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. Released as needed for security, installation or critical bug fixes. * [ ] Make necessary changes in `main` branch. -* [ ] Update `CHANGES.rst`. * [ ] Check out release branch e.g.: ```bash git checkout -t remotes/origin/5.2.x diff --git a/codecov.yml b/codecov.yml index 8646576bb..84920238f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,7 +1,7 @@ # Documentation: https://docs.codecov.com/docs/codecov-yaml codecov: - # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" + # Avoid "Missing base report" due to committing with "[CI skip]" # https://github.com/codecov/support/issues/363 # https://docs.codecov.com/docs/comparing-commits allow_coverage_offsets: true diff --git a/pyproject.toml b/pyproject.toml index c6d95f2ab..1c7b2c4b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ optional-dependencies.typing = [ optional-dependencies.xmp = [ "defusedxml", ] -urls.Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" +urls.Changelog = "https://github.com/python-pillow/Pillow/releases" urls.Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" urls.Homepage = "https://python-pillow.org" From 4986609938c6fd38af6efd8501fe157b0443e5bc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2024 23:44:32 +1100 Subject: [PATCH 130/628] Use zlib-ng on macOS --- .github/workflows/wheels-dependencies.sh | 12 ++---------- Tests/check_wheel.py | 2 -- pyproject.toml | 1 + 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5d23fc036..a2aec07c3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -50,11 +50,7 @@ if [[ -n "$IS_MACOS" ]]; then else GIFLIB_VERSION=5.2.1 fi -if [[ -n "$IS_MACOS" ]]; then - ZLIB_VERSION=1.3.1 -else - ZLIB_NG_VERSION=2.2.2 -fi +ZLIB_NG_VERSION=2.2.2 LIBWEBP_VERSION=1.4.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 @@ -111,11 +107,7 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - if [ -n "$IS_MACOS" ]; then - build_new_zlib - else - build_zlib_ng - fi + build_zlib_ng build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 787d4f743..563be0b74 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -40,7 +40,5 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") - elif sys.platform == "darwin": - expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features diff --git a/pyproject.toml b/pyproject.toml index c6d95f2ab..af09ccbc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib" [tool.black] exclude = "wheels/multibuild" From 10527918e3570f1e6772ac90db42d2288cce7ae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Nov 2024 17:13:07 +1100 Subject: [PATCH 131/628] Use brew formula to install libraqm --- docs/installation/building-from-source.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 19294f72c..ff60120ae 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -148,13 +148,7 @@ Many of Pillow's features require external libraries: The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp - - To install libraqm on macOS use Homebrew to install its dependencies:: - - brew install freetype harfbuzz fribidi - - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + brew install libjpeg libraqm libtiff little-cms2 openjpeg webp .. tab:: Windows From 1fab591f42fa5a9502fade7873481b567c033849 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Nov 2024 21:17:31 +1100 Subject: [PATCH 132/628] Python 3.12 is tested on MinGW --- 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 b9d633142..35f863374 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -55,7 +55,7 @@ These platforms are built and tested for every change. | +----------------------------+---------------------+ | | 3.13 | x86 | | +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86-64 | +| | 3.12 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ From 1d15ce8c494075f521600c21b9efb0d5eaa9aa03 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Nov 2024 23:17:42 +1100 Subject: [PATCH 133/628] Updated build option error messages --- setup.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index fbd23a568..eea21fa34 100644 --- a/setup.py +++ b/setup.py @@ -393,13 +393,14 @@ class pil_build_ext(build_ext): self.feature.required.discard(x) _dbg("Disabling %s", x) if getattr(self, f"enable_{x}"): - msg = f"Conflicting options: --enable-{x} and --disable-{x}" + msg = f"Conflicting options: '-C {x}=enable' and '-C {x}=disable'" raise ValueError(msg) if x == "freetype": - _dbg("--disable-freetype implies --disable-raqm") + _dbg("'-C freetype=disable' implies '-C raqm=disable'") if getattr(self, "enable_raqm"): msg = ( - "Conflicting options: --enable-raqm and --disable-freetype" + "Conflicting options: " + "'-C raqm=enable' and '-C freetype=disable'" ) raise ValueError(msg) setattr(self, "disable_raqm", True) @@ -407,15 +408,17 @@ class pil_build_ext(build_ext): _dbg("Requiring %s", x) self.feature.required.add(x) if x == "raqm": - _dbg("--enable-raqm implies --enable-freetype") + _dbg("'-C raqm=enable' implies '-C freetype=enable'") self.feature.required.add("freetype") for x in ("raqm", "fribidi"): if getattr(self, f"vendor_{x}"): if getattr(self, "disable_raqm"): - msg = f"Conflicting options: --vendor-{x} and --disable-raqm" + msg = f"Conflicting options: '-C {x}=vendor' and '-C raqm=disable'" raise ValueError(msg) if x == "fribidi" and not getattr(self, "vendor_raqm"): - msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm" + msg = ( + f"Conflicting options: '-C {x}=vendor' and not '-C raqm=vendor'" + ) raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) @@ -1047,7 +1050,7 @@ except DependencyException as err: msg = f""" The headers or library files could not be found for {str(err)}, -which was requested by the option flag --enable-{str(err)} +which was requested by the option flag '-C {str(err)}=enable' """ sys.stderr.write(msg) From 513fe2ca2d23c7f1685b089fc6087ab02b2828df Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:15:54 +0200 Subject: [PATCH 134/628] Only use start year in copyright, remove end years --- LICENSE | 2 +- docs/COPYING | 2 +- docs/conf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 8837c290c..10dd42d9e 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2024 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey A. Clark and contributors Like PIL, Pillow is licensed under the open source MIT-CMU License: diff --git a/docs/COPYING b/docs/COPYING index d5ee19f81..17fba5b87 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2024 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey A. Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/conf.py b/docs/conf.py index b81d86c6c..e1e3f1b8f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,7 +55,7 @@ master_doc = "index" project = "Pillow (PIL Fork)" copyright = ( "1995-2011 Fredrik Lundh and contributors, " - "2010-2024 Jeffrey A. Clark and contributors." + "2010 Jeffrey A. Clark and contributors." ) author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" From 0000729f2a79da62ba64332484103ffacf1700a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:34:54 +0000 Subject: [PATCH 135/628] [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.7.2 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.2...v0.8.1) - [github.com/PyCQA/bandit: 1.7.10 → 1.8.0](https://github.com/PyCQA/bandit/compare/1.7.10...1.8.0) - [github.com/pre-commit/mirrors-clang-format: v19.1.3 → v19.1.4](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.3...v19.1.4) - [github.com/python-jsonschema/check-jsonschema: 0.29.4 → 0.30.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.4...0.30.0) - [github.com/abravalheri/validate-pyproject: v0.22 → v0.23](https://github.com/abravalheri/validate-pyproject/compare/v0.22...v0.23) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ddc98fdc3..f91260c72 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.7.2 + rev: v0.8.1 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.10 + rev: 1.8.0 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.3 + rev: v19.1.4 hooks: - id: clang-format types: [c] @@ -50,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.4 + rev: 0.30.0 hooks: - id: check-github-workflows - id: check-readthedocs @@ -67,7 +67,7 @@ repos: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.22 + rev: v0.23 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] From 0ab21dff1ed0b16944a5458ab73dfa3a5c060b88 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:29:38 +0200 Subject: [PATCH 136/628] Fix new Ruff errors --- src/PIL/ImImagePlugin.py | 2 +- src/PIL/Image.py | 21 ++++++--------------- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 13 +++++++------ src/PIL/_typing.py | 2 +- 7 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index f9f47348c..b4215a0b1 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -357,7 +357,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: name = "".join([name[: 92 - len(ext)], ext]) fp.write(f"Name: {name}\r\n".encode("ascii")) - fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii")) + fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii")) fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) if im.mode in ["P", "PA"]: fp.write(b"Lut: 1\r\n") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9b76ce8bd..1e289b6c3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -692,13 +692,10 @@ class Image: ) def __repr__(self) -> str: - return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( - self.__class__.__module__, - self.__class__.__name__, - self.mode, - self.size[0], - self.size[1], - id(self), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]} " + f"at 0x{id(self):X}>" ) def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: @@ -707,14 +704,8 @@ class Image: # Same as __repr__ but without unpredictable id(self), # to keep Jupyter notebook `text/plain` output stable. p.text( - "<%s.%s image mode=%s size=%dx%d>" - % ( - self.__class__.__module__, - self.__class__.__name__, - self.mode, - self.size[0], - self.size[1], - ) + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>" ) def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e..ab6a2f497 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -72,7 +72,7 @@ def APP(self: JpegImageFile, marker: int) -> None: n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) - app = "APP%d" % (marker & 15) + app = f"APP{marker & 15}" self.app[app] = s # compatibility self.applist.append((app, s)) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 8445d5cc7..32436cea3 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -86,7 +86,7 @@ class PcxImageFile(ImageFile.ImageFile): elif bits == 1 and planes in (2, 4): mode = "P" - rawmode = "P;%dL" % planes + rawmode = f"P;{planes}L" self.palette = ImagePalette.raw("RGB", s[16:64]) elif version == 5 and bits == 8 and planes == 1: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4e1227204..4b97992a3 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -523,7 +523,7 @@ class PngStream(ChunkStream): assert self.fp is not None s = ImageFile._safe_read(self.fp, length) - raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) + raw_vals = struct.unpack(f">{len(s) // 4}I", s) self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) return s diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 08bb78915..9ee55347a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -935,9 +935,9 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tagdata[tag] = data self.tagtype[tag] = typ - msg += " - value: " + ( - "" % size if size > 32 else repr(data) - ) + bytes_value = size if size > 32 else repr(data) + msg += f" - value: " + logger.debug(msg) (self.next,) = ( @@ -981,9 +981,10 @@ class ImageFileDirectory_v2(_IFDv2Base): tagname = TiffTags.lookup(tag, self.group).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" - msg += " - value: " + ( - "" % len(data) if len(data) >= 16 else str(values) + bytes_value = len(data) if len(data) >= 16 else str(values) + msg = ( + f"save: {tagname} ({tag}) - type: {typname} ({typ})" + f" - value: " ) logger.debug(msg) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 335804b79..34a9a81e1 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -44,7 +44,7 @@ _T_co = TypeVar("_T_co", covariant=True) class SupportsRead(Protocol[_T_co]): - def read(self, __length: int = ...) -> _T_co: ... + def read(self, length: int = ..., /) -> _T_co: ... StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] From c16ae6fefa6cb939d274c95d1969889dced9dd15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 09:19:26 +1100 Subject: [PATCH 137/628] Do not describe raw data as a table --- src/PIL/TiffImagePlugin.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 9ee55347a..be4a57b74 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -935,8 +935,8 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tagdata[tag] = data self.tagtype[tag] = typ - bytes_value = size if size > 32 else repr(data) - msg += f" - value: " + msg += " - value: " + msg += f"" if size > 32 else repr(data) logger.debug(msg) @@ -981,11 +981,8 @@ class ImageFileDirectory_v2(_IFDv2Base): tagname = TiffTags.lookup(tag, self.group).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - bytes_value = len(data) if len(data) >= 16 else str(values) - msg = ( - f"save: {tagname} ({tag}) - type: {typname} ({typ})" - f" - value: " - ) + msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: " + msg += f"" if len(data) >= 16 else str(values) logger.debug(msg) # count is sum of lengths for string and arbitrary data From 80c562573a4b5723b1094fb9b07a582912e3d62b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 02:28:52 +0000 Subject: [PATCH 138/628] Update dependency cibuildwheel to v2.22.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index c4511439c..833aca23d 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.3 +cibuildwheel==2.22.0 From f4a9304bb358f45a088d3c237ada88703e0e0d8e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 13:35:00 +1100 Subject: [PATCH 139/628] Use CIBW_ENABLE --- .github/workflows/wheels.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 45f186341..c5e55aa62 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -85,7 +85,7 @@ jobs: 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_PRERELEASE_PYTHONS: True + CIBW_ENABLE: cpython-prerelease # Extra options for manylinux. CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} @@ -150,10 +150,9 @@ jobs: env: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} - CIBW_FREE_THREADED_SUPPORT: True + CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} - CIBW_PRERELEASE_PYTHONS: True CIBW_SKIP: pp39-* MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} @@ -228,8 +227,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" - CIBW_FREE_THREADED_SUPPORT: True - CIBW_PRERELEASE_PYTHONS: True + CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_SKIP: pp39-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm From 3ce33a253a1bd76fdde6eb557cf271b3b2f01553 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 20:14:03 +1100 Subject: [PATCH 140/628] self.tile is not None --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 7f27d54dc..9836cd688 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -120,7 +120,7 @@ class ImageFile(Image.Image): self.custom_mimetype: str | None = None self.tile: list[_Tile] = [] - """ A list of tile descriptors, or ``None`` """ + """ A list of tile descriptors """ self.readonly = 1 # until we know better From c5474ed43363e24602c92e5ca7b37899ce37b9a5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 6 Dec 2024 20:26:07 +0200 Subject: [PATCH 141/628] Replace python-pillow.org with python-pillow.github.io --- docs/handbook/tutorial.rst | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 3df8e0d20..f771ae7ae 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -678,7 +678,7 @@ Reading from URL from PIL import Image from urllib.request import urlopen - url = "https://python-pillow.org/assets/images/pillow-logo.png" + url = "https://python-pillow.github.io/assets/images/pillow-logo.png" img = Image.open(urlopen(url)) diff --git a/pyproject.toml b/pyproject.toml index 1c7b2c4b2..aaaba0032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ optional-dependencies.xmp = [ urls.Changelog = "https://github.com/python-pillow/Pillow/releases" urls.Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" -urls.Homepage = "https://python-pillow.org" +urls.Homepage = "https://python-pillow.github.io" urls.Mastodon = "https://fosstodon.org/@pillow" urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Source = "https://github.com/python-pillow/Pillow" From 2215eaf21d8e9096fdd13c34225c67c0893d82db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Dec 2024 06:25:48 +1100 Subject: [PATCH 142/628] Updated openjpeg to 2.5.3 --- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_openjpeg.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index ddda43423..e88a010ef 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -41,7 +41,7 @@ FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=10.0.1 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.4 -OPENJPEG_VERSION=2.5.2 +OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 8c2967bc2..1f8d78193 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,7 +1,7 @@ #!/bin/bash # install openjpeg -archive=openjpeg-2.5.2 +archive=openjpeg-2.5.3 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index ff60120ae..03359de31 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -58,7 +58,7 @@ Many of Pillow's features require external libraries: * **openjpeg** provides JPEG 2000 functionality. * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, - **2.4.0**, **2.5.0** and **2.5.2**. + **2.4.0**, **2.5.0**, **2.5.2** and **2.5.3**. * Pillow does **not** support the earlier **1.5** series which ships with Debian Jessie. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c8332d11c..dc802a3e5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ V = { "LCMS2": "2.16", "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", - "OPENJPEG": "2.5.2", + "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.3", "ZLIB": "1.3.1", From 522505b714085ca94a2cab2a71e77a4fc5df7a3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Dec 2024 17:52:34 +1100 Subject: [PATCH 143/628] 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 9ed96704d3788fa58d9682d293c4190e5da07c2d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Dec 2024 07:28:48 +1100 Subject: [PATCH 144/628] Do not attempt to install cmake if it is already present --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 9a9d1275f..74a9795bc 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 +Subproject commit 74a9795bc64ff786b7e7d33bdec2843cf17e512e From e361930936cd1208beeb292654a3af7548d863b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Dec 2024 18:56:59 +1100 Subject: [PATCH 145/628] Updated libjpeg-turbo to 3.1.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5f8a4cf78..86f51a586 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.44 -JPEGTURBO_VERSION=3.0.4 +JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1efe0dcac..fd104eb23 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ V = { "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "10.1.0", - "JPEGTURBO": "3.0.4", + "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", @@ -155,7 +155,7 @@ DEPS: dict[str, dict[str, Any]] = { cmd_copy("cjpeg-static.exe", "cjpeg.exe"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], - "headers": ["j*.h"], + "headers": ["jconfig.h", r"src\j*.h"], "libs": ["libjpeg.lib"], "bins": ["cjpeg.exe", "djpeg.exe"], }, From 642b44c3159077d0863c29181c8c6fbe60039e5b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Dec 2024 01:25:28 +1100 Subject: [PATCH 146/628] Test libjpeg-turbo on macOS --- .github/workflows/macos-install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index ddb421230..2301a3a7e 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -8,8 +8,8 @@ fi brew install \ freetype \ ghostscript \ + jpeg-turbo \ libimagequant \ - libjpeg \ libtiff \ little-cms2 \ openjpeg \ From 0e5f5fcb297b1e08ec83773562663b8ad9e53aee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Dec 2024 16:33:20 +1100 Subject: [PATCH 147/628] Updated libXau to 1.0.12 --- .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 86f51a586..36036ff26 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -105,7 +105,7 @@ function build { build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc From 62d8ccc015d68a9bb3da555f9800786652de4962 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Dec 2024 12:11:41 +1100 Subject: [PATCH 148/628] Derive dir from filename if root is the same --- winbuild/build_prepare.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5bdb5fafe..2d804d557 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -131,7 +131,6 @@ DEPS: dict[str, dict[str, Any]] = { "libjpeg": { "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", - "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" @@ -161,7 +160,6 @@ DEPS: dict[str, dict[str, Any]] = { "zlib": { "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.zip", "filename": f"zlib-ng-{V['ZLIBNG']}.zip", - "dir": f"zlib-ng-{V['ZLIBNG']}", "license": "LICENSE.md", "patch": { r"CMakeLists.txt": { @@ -179,7 +177,6 @@ DEPS: dict[str, dict[str, Any]] = { "xz": { "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", "filename": f"xz-{V['XZ']}.tar.gz", - "dir": f"xz-{V['XZ']}", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -192,7 +189,6 @@ DEPS: dict[str, dict[str, Any]] = { "libwebp": { "url": "http://downloads.webmproject.org/releases/webp/FILENAME", "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", - "dir": f"libwebp-{V['LIBWEBP']}", "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { @@ -214,7 +210,6 @@ DEPS: dict[str, dict[str, Any]] = { "libtiff": { "url": "https://download.osgeo.org/libtiff/FILENAME", "filename": f"tiff-{V['TIFF']}.tar.gz", - "dir": f"tiff-{V['TIFF']}", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -247,7 +242,6 @@ DEPS: dict[str, dict[str, Any]] = { "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", - "dir": f"lpng{V['LIBPNG_DOTLESS']}", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), @@ -261,7 +255,6 @@ DEPS: dict[str, dict[str, Any]] = { "brotli": { "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz", "filename": f"brotli-{V['BROTLI']}.tar.gz", - "dir": f"brotli-{V['BROTLI']}", "license": "LICENSE", "build": [ *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -272,7 +265,6 @@ DEPS: dict[str, dict[str, Any]] = { "freetype": { "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", "filename": f"freetype-{V['FREETYPE']}.tar.gz", - "dir": f"freetype-{V['FREETYPE']}", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -307,7 +299,6 @@ DEPS: dict[str, dict[str, Any]] = { "lcms2": { "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", "filename": f"lcms2-{V['LCMS2']}.tar.gz", - "dir": f"lcms2-{V['LCMS2']}", "license": "LICENSE", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -333,7 +324,6 @@ DEPS: dict[str, dict[str, Any]] = { "openjpeg": { "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz", "filename": f"openjpeg-{V['OPENJPEG']}.tar.gz", - "dir": f"openjpeg-{V['OPENJPEG']}", "license": "LICENSE", "build": [ *cmds_cmake( @@ -348,7 +338,6 @@ DEPS: dict[str, dict[str, Any]] = { # commit: Merge branch 'master' into msvc (matches 2.17.0 tag) "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", - "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab", "license": "COPYRIGHT", "patch": { "CMakeLists.txt": { @@ -368,7 +357,6 @@ DEPS: dict[str, dict[str, Any]] = { "harfbuzz": { "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", - "dir": f"harfbuzz-{V['HARFBUZZ']}", "license": "COPYING", "build": [ *cmds_cmake( @@ -383,7 +371,6 @@ DEPS: dict[str, dict[str, Any]] = { "fribidi": { "url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip", "filename": f"fribidi-{V['FRIBIDI']}.zip", - "dir": f"fribidi-{V['FRIBIDI']}", "license": "COPYING", "build": [ cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"), @@ -763,6 +750,8 @@ def main() -> None: } for k, v in DEPS.items(): + if "dir" not in v: + v["dir"] = re.sub(r"\.(tar\.gz|zip)", "", v["filename"]) prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) print() From 6373e8bcc470ffa8849a17fa8e90e1146e0c3c6c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Dec 2024 22:58:35 +1100 Subject: [PATCH 149/628] Use .tar.gz zlib to match macOS and Linux --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5bdb5fafe..a68a0b132 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -159,8 +159,8 @@ DEPS: dict[str, dict[str, Any]] = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.zip", - "filename": f"zlib-ng-{V['ZLIBNG']}.zip", + "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.tar.gz", + "filename": f"zlib-ng-{V['ZLIBNG']}.tar.gz", "dir": f"zlib-ng-{V['ZLIBNG']}", "license": "LICENSE.md", "patch": { From 7f6ebfb8c597d8d1a074993f463992b6dac34c41 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Dec 2024 23:40:38 +1100 Subject: [PATCH 150/628] Added release notes for #8500 --- docs/releasenotes/11.1.0.rst | 59 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 60 insertions(+) create mode 100644 docs/releasenotes/11.1.0.rst diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst new file mode 100644 index 000000000..c5d0afd58 --- /dev/null +++ b/docs/releasenotes/11.1.0.rst @@ -0,0 +1,59 @@ +11.1.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Check for zlib-ng +^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the zlib-ng version of the +zlib library, and what version of zlib-ng is being used:: + + from PIL import features + features.check_feature("zlib_ng") # True or False + features.version_feature("zlib_ng") # "2.2.2" for example, or None + +Other Changes +============= + +zlib-ng in wheels +^^^^^^^^^^^^^^^^^ + +Wheels are now built against zlib-ng for improved speed. In tests, saving a PNG +was found to be more than twice as fast at higher compression levels. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 641cda4ef..bd8e5536f 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.1.0 11.0.0 10.4.0 10.3.0 From 5e35ca359c4182af07c0c02e61ebd8d270bb34b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Dec 2024 20:17:36 +1100 Subject: [PATCH 151/628] Updated libjpeg-turbo URL --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a68a0b132..e702c7014 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -129,7 +129,7 @@ V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) # dependencies, listed in order of compilation DEPS: dict[str, dict[str, Any]] = { "libjpeg": { - "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download", + "url": f"https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/{V['JPEGTURBO']}/libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], From 095811452172bc4efd4be172f850c2f564e35cba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Dec 2024 21:03:33 +1100 Subject: [PATCH 152/628] Corrected harfbuzz URL --- .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 de2471c3f..3e2a14462 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -93,7 +93,7 @@ function build_harfbuzz { if [ -e harfbuzz-stamp ]; then return; fi python3 -m pip install meson ninja - local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ From 962bfc6fd5e949cfdf8c0d18877acc2b8f82a78c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Dec 2024 10:39:52 +1100 Subject: [PATCH 153/628] Updated libjpeg-turbo URL --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 74a9795bc..42d761728 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 74a9795bc64ff786b7e7d33bdec2843cf17e512e +Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155 From de8335ba8fe121cf1f1b7b1565f535fb066094bf Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:07:34 +1100 Subject: [PATCH 154/628] Extract tar files with "data" filter in Windows build scripts (#8606) Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- winbuild/build_prepare.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 607f672ba..188872dfc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import re import shutil import struct import subprocess +import sys from typing import Any @@ -507,7 +508,10 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: if sources_dir_abs != member_prefix: msg = "Attempted Path Traversal in Tar File" raise RuntimeError(msg) - tgz.extractall(sources_dir) + if sys.version_info >= (3, 12): + tgz.extractall(sources_dir, filter="data") + else: + tgz.extractall(sources_dir) else: msg = "Unknown archive type: " + filename raise RuntimeError(msg) From cbc55c4621b79176702bf14f188e5a439c7804a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:08:51 +1100 Subject: [PATCH 155/628] Raise ValueError when WMF inch is zero (#8600) Co-authored-by: Andrew Murray --- Tests/test_file_wmf.py | 7 +++++++ src/PIL/WmfImagePlugin.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 424640d7b..2f1f8cdbc 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -35,6 +35,13 @@ def test_load() -> None: assert im.load()[0, 0] == (255, 255, 255) +def test_load_zero_inch() -> None: + b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x00" * 10) + with pytest.raises(ValueError): + with Image.open(b): + pass + + def test_register_handler(tmp_path: Path) -> None: class TestHandler(ImageFile.StubHandler): methodCalled = False diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index cad6c98d5..48e9823e8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -92,6 +92,9 @@ class WmfStubImageFile(ImageFile.StubImageFile): # get units per inch self._inch = word(s, 14) + if self._inch == 0: + msg = "Invalid inch" + raise ValueError(msg) # get bounding box x0 = short(s, 6) From c6f42cb6a59aaaf29acf8831ef5865a688139480 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Dec 2024 14:46:01 +1100 Subject: [PATCH 156/628] Updated libwebp to 1.5.0 --- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3e2a14462..4e0fad79f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -51,7 +51,7 @@ else GIFLIB_VERSION=5.2.1 fi ZLIB_NG_VERSION=2.2.2 -LIBWEBP_VERSION=1.4.0 +LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 diff --git a/depends/install_webp.sh b/depends/install_webp.sh index c47fb35f1..9d2977715 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.4.0 +archive=libwebp-1.5.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 188872dfc..0674a9a15 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ V = { "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBPNG": "1.6.44", - "LIBWEBP": "1.4.0", + "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.3", From 5bd2f489fa7ed1ca4097cf15634d37723919434d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:44:50 +1100 Subject: [PATCH 157/628] Install libjpeg-turbo8-dev (#8610) Co-authored-by: Andrew Murray --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index e85e6bdc5..5c20e7f37 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -21,7 +21,7 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ - ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ + ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ sway wl-clipboard libopenblas-dev fi From cf4110ff06b4d4e0ec43b4a9ca13a28b7d3b11d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 21 Dec 2024 00:55:44 +1100 Subject: [PATCH 158/628] Replace constants with enums (#8611) Co-authored-by: Andrew Murray --- src/PIL/Image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1e289b6c3..440728b08 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1565,7 +1565,7 @@ class Image: 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(513): + if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): assert exif._info is not None ifds.append((ifd1, exif._info.next)) @@ -1577,11 +1577,11 @@ class Image: fp = self.fp if ifd is not None: - thumbnail_offset = ifd.get(513) + 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(514)) + data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount)) fp = io.BytesIO(data) with open(fp) as im: From 3d0f4389499314ca793f6244d13e00cd3ad44de0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:38:05 +0000 Subject: [PATCH 159/628] Update dependency mypy to v1.14.0 --- .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 c84a3533b..cd1b1a1a1 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.13.0 +mypy==1.14.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 08e1f9ebc11285c94a2706dd02146f685b10aec0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Dec 2024 13:04:17 +1100 Subject: [PATCH 160/628] Lint fixes --- Tests/test_color_lut.py | 2 ++ src/PIL/ImageFilter.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 36ab187f2..baa899df5 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -388,10 +388,12 @@ class TestColorLut3DFilter: table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) assert lut.table.shape == (table.size,) table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) assert lut.table.shape == (table.size,) # Check application diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 8b0974b2c..b350e56f4 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -553,7 +553,7 @@ class Color3DLUT(MultibandFilter): ch_out = channels or ch_in size_1d, size_2d, size_3d = self.size - table = [0] * (size_1d * size_2d * size_3d * ch_out) + table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out) idx_in = 0 idx_out = 0 for b in range(size_3d): From b99a00f44f8e34c18b89395eaf67e796ecdf1bf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Dec 2024 07:24:08 +1100 Subject: [PATCH 161/628] Updated MakerNote IFD capitalization --- src/PIL/ExifTags.py | 2 +- src/PIL/Image.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 39b4aa552..231e80f40 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -352,7 +352,7 @@ class Interop(IntEnum): class IFD(IntEnum): Exif = 34665 GPSInfo = 34853 - Makernote = 37500 + MakerNote = 37500 Interop = 40965 IFD1 = -1 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1e289b6c3..fb3f01fc8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3884,7 +3884,7 @@ class Exif(_ExifBase): gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) print(gps_ifd) - Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``, ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: @@ -4047,11 +4047,11 @@ class Exif(_ExifBase): ifd = self._get_ifd_dict(offset, tag) if ifd is not None: self._ifds[tag] = ifd - elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]: if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) tag_data = self._ifds[ExifTags.IFD.Exif][tag] - if tag == ExifTags.IFD.Makernote: + if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 if tag_data[:8] == b"FUJIFILM": @@ -4138,7 +4138,7 @@ class Exif(_ExifBase): ifd = { k: v for (k, v) in ifd.items() - if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) + if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) } return ifd From 5fe80e1c48eff0b74a14377107895c3e2d2f91b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Dec 2024 07:30:59 +1100 Subject: [PATCH 162/628] Use hex values more consistently --- src/PIL/ExifTags.py | 124 ++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 231e80f40..207d4de4e 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -303,38 +303,38 @@ TAGS = { class GPS(IntEnum): - GPSVersionID = 0 - GPSLatitudeRef = 1 - GPSLatitude = 2 - GPSLongitudeRef = 3 - GPSLongitude = 4 - GPSAltitudeRef = 5 - GPSAltitude = 6 - GPSTimeStamp = 7 - GPSSatellites = 8 - GPSStatus = 9 - GPSMeasureMode = 10 - GPSDOP = 11 - GPSSpeedRef = 12 - GPSSpeed = 13 - GPSTrackRef = 14 - GPSTrack = 15 - GPSImgDirectionRef = 16 - GPSImgDirection = 17 - GPSMapDatum = 18 - GPSDestLatitudeRef = 19 - GPSDestLatitude = 20 - GPSDestLongitudeRef = 21 - GPSDestLongitude = 22 - GPSDestBearingRef = 23 - GPSDestBearing = 24 - GPSDestDistanceRef = 25 - GPSDestDistance = 26 - GPSProcessingMethod = 27 - GPSAreaInformation = 28 - GPSDateStamp = 29 - GPSDifferential = 30 - GPSHPositioningError = 31 + GPSVersionID = 0x00 + GPSLatitudeRef = 0x01 + GPSLatitude = 0x02 + GPSLongitudeRef = 0x03 + GPSLongitude = 0x04 + GPSAltitudeRef = 0x05 + GPSAltitude = 0x06 + GPSTimeStamp = 0x07 + GPSSatellites = 0x08 + GPSStatus = 0x09 + GPSMeasureMode = 0x0A + GPSDOP = 0x0B + GPSSpeedRef = 0x0C + GPSSpeed = 0x0D + GPSTrackRef = 0x0E + GPSTrack = 0x0F + GPSImgDirectionRef = 0x10 + GPSImgDirection = 0x11 + GPSMapDatum = 0x12 + GPSDestLatitudeRef = 0x13 + GPSDestLatitude = 0x14 + GPSDestLongitudeRef = 0x15 + GPSDestLongitude = 0x16 + GPSDestBearingRef = 0x17 + GPSDestBearing = 0x18 + GPSDestDistanceRef = 0x19 + GPSDestDistance = 0x1A + GPSProcessingMethod = 0x1B + GPSAreaInformation = 0x1C + GPSDateStamp = 0x1D + GPSDifferential = 0x1E + GPSHPositioningError = 0x1F """Maps EXIF GPS tags to tag names.""" @@ -342,40 +342,40 @@ GPSTAGS = {i.value: i.name for i in GPS} class Interop(IntEnum): - InteropIndex = 1 - InteropVersion = 2 - RelatedImageFileFormat = 4096 - RelatedImageWidth = 4097 - RelatedImageHeight = 4098 + InteropIndex = 0x0001 + InteropVersion = 0x0002 + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageHeight = 0x1002 class IFD(IntEnum): - Exif = 34665 - GPSInfo = 34853 - MakerNote = 37500 - Interop = 40965 + Exif = 0x8769 + GPSInfo = 0x8825 + MakerNote = 0x927C + Interop = 0xA005 IFD1 = -1 class LightSource(IntEnum): - Unknown = 0 - Daylight = 1 - Fluorescent = 2 - Tungsten = 3 - Flash = 4 - Fine = 9 - Cloudy = 10 - Shade = 11 - DaylightFluorescent = 12 - DayWhiteFluorescent = 13 - CoolWhiteFluorescent = 14 - WhiteFluorescent = 15 - StandardLightA = 17 - StandardLightB = 18 - StandardLightC = 19 - D55 = 20 - D65 = 21 - D75 = 22 - D50 = 23 - ISO = 24 - Other = 255 + Unknown = 0x00 + Daylight = 0x01 + Fluorescent = 0x02 + Tungsten = 0x03 + Flash = 0x04 + Fine = 0x09 + Cloudy = 0x0A + Shade = 0x0B + DaylightFluorescent = 0x0C + DayWhiteFluorescent = 0x0D + CoolWhiteFluorescent = 0x0E + WhiteFluorescent = 0x0F + StandardLightA = 0x11 + StandardLightB = 0x12 + StandardLightC = 0x13 + D55 = 0x14 + D65 = 0x15 + D75 = 0x16 + D50 = 0x17 + ISO = 0x18 + Other = 0xFF From 0074c3bf349f0055aaff73fd7864c6a5e785b220 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Dec 2024 11:45:36 +1100 Subject: [PATCH 163/628] Assert that a tuple is returned by getpixel() --- Tests/test_file_dds.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 9a826ebe8..7cc4d79d4 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -331,11 +331,13 @@ def test_dxt5_colorblock_alpha_issue_4142() -> None: with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: px = im.getpixel((0, 0)) + assert isinstance(px, tuple) assert px[0] != 0 assert px[1] != 0 assert px[2] != 0 px = im.getpixel((1, 0)) + assert isinstance(px, tuple) assert px[0] != 0 assert px[1] != 0 assert px[2] != 0 From 5d5543b35caacef1a7f5912caa14b9d257680291 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Dec 2024 11:57:27 +1100 Subject: [PATCH 164/628] Assert that load() does not return None --- Tests/test_file_eps.py | 8 ++++++-- Tests/test_file_gbr.py | 6 +++++- Tests/test_file_gif.py | 22 +++++++++++++++++----- Tests/test_file_icns.py | 8 ++++++-- Tests/test_file_ico.py | 4 +++- Tests/test_file_jpeg2k.py | 1 + Tests/test_file_ppm.py | 1 + Tests/test_file_tga.py | 8 ++++++-- Tests/test_file_wal.py | 8 ++++++-- Tests/test_file_wmf.py | 4 +++- 10 files changed, 54 insertions(+), 16 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 672c04a4d..a0c2f9216 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -95,10 +95,14 @@ def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_load() -> None: with Image.open(FILE1) as im: - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) def test_binary() -> None: diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index be98b08f2..5b59cc07a 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -14,10 +14,14 @@ def test_gbr_file() -> None: def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: + px = im.load() + assert px is not None assert im.load()[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) def test_multiple_load_operations() -> None: diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 5d46b157d..f25d819ea 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -86,12 +86,16 @@ 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 + px = im.load() + assert px is not None + assert px[0, 0] == 128 assert im.info["transparency"] == 255 im.seek(1) assert im.mode == "L" - assert im.load()[0, 0] == 128 + px = im.load() + assert px is not None + assert px[0, 0] == 128 def test_l_mode_after_rgb() -> None: @@ -310,7 +314,9 @@ 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] + px = im.convert("RGB").load() + assert px is not None + original_color = px[0, 0] im.seek(1) assert im.mode == mode @@ -318,10 +324,14 @@ 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 + px = im.load() + assert px is not None + assert px[0, 0] == original_color # Check a color from the new palette - assert im.load()[24, 24] not in first_frame_colors + px = im.load() + assert px is not None + assert px[24, 24] not in first_frame_colors def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: @@ -488,6 +498,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 is not None assert px[0, 0] == im.info["transparency"] @@ -528,6 +539,7 @@ def test_dispose_background_transparency() -> None: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: img.seek(2) px = img.load() + assert px is not None assert px[35, 30][3] == 0 diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 141b88dfa..94f16aeec 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -32,10 +32,14 @@ def test_sanity() -> None: def test_load() -> None: with Image.open(TEST_FILE) as im: - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) def test_save(tmp_path: Path) -> None: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 37770498a..10f3aac9a 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -24,7 +24,9 @@ def test_sanity() -> None: def test_load() -> None: with Image.open(TEST_ICO_FILE) as im: - assert im.load()[0, 0] == (1, 1, 9, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (1, 1, 9, 255) def test_mask() -> None: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index fbf72ae05..b761fcd37 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -63,6 +63,7 @@ def test_sanity() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: px = im.load() + assert px is not None assert px[0, 0] == (0, 0, 0) assert im.mode == "RGB" assert im.size == (640, 480) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fb08d613a..f4acedb30 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -79,6 +79,7 @@ def test_arbitrary_maxval( assert im.mode == mode px = im.load() + assert px is not None assert tuple(px[x, 0] for x in range(3)) == pixels diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index a03a6a6e1..63d1e7615 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -213,10 +213,14 @@ def test_save_orientation(tmp_path: Path) -> None: def test_horizontal_orientations() -> None: # These images have been manually hexedited to have the relevant orientations with Image.open("Tests/images/rgb32rle_top_right.tga") as im: - assert im.load()[90, 90][:3] == (0, 0, 0) + px = im.load() + assert px is not None + assert px[90, 90][:3] == (0, 0, 0) with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: - assert im.load()[90, 90][:3] == (0, 255, 0) + px = im.load() + assert px is not None + assert px[90, 90][:3] == (0, 255, 0) def test_save_rle(tmp_path: Path) -> None: diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index b34975e83..b15d79d61 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -21,7 +21,11 @@ def test_open() -> None: def test_load() -> None: with WalImageFile.open(TEST_FILE) as im: - assert im.load()[0, 0] == 122 + px = im.load() + assert px is not None + assert px[0, 0] == 122 # Test again now that it has already been loaded once - assert im.load()[0, 0] == 122 + px = im.load() + assert px is not None + assert px[0, 0] == 122 diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 2f1f8cdbc..f849453f6 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -32,7 +32,9 @@ def test_load_raw() -> None: def test_load() -> None: with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) def test_load_zero_inch() -> None: From 601a56def1dfdacd76759302595d9d904f2467c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Dec 2024 12:03:13 +1100 Subject: [PATCH 165/628] Assert palette is not None --- Tests/test_file_gif.py | 2 ++ Tests/test_file_jpeg2k.py | 2 ++ Tests/test_file_tga.py | 1 + Tests/test_image.py | 1 + Tests/test_image_convert.py | 1 + Tests/test_image_transform.py | 1 + Tests/test_imagepalette.py | 1 + 7 files changed, 9 insertions(+) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index f25d819ea..9fe5f4fbb 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -313,6 +313,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" + assert im.palette is not None first_frame_colors = im.palette.colors.keys() px = im.convert("RGB").load() assert px is not None @@ -1325,6 +1326,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None: with Image.open(out) as im: # Assert that the frames are correct, and each frame has the same palette assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) + assert im.palette is not None assert im.palette.palette == im.global_palette.palette im.seek(1) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b761fcd37..af6b9b2db 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -413,6 +413,7 @@ def test_subsampling_decode(name: str) -> None: def test_pclr() -> None: with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im: assert im.mode == "P" + assert im.palette is not None assert len(im.palette.colors) == 256 assert im.palette.colors[(255, 255, 255)] == 0 @@ -420,6 +421,7 @@ def test_pclr() -> None: f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" ) as im: assert im.mode == "P" + assert im.palette is not None assert len(im.palette.colors) == 139 assert im.palette.colors[(0, 0, 0, 0)] == 0 diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 63d1e7615..b6396bd64 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -72,6 +72,7 @@ def test_palette_depth_8(tmp_path: Path) -> None: def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: + assert im.palette is not None assert im.palette.mode == "RGBA" assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png") diff --git a/Tests/test_image.py b/Tests/test_image.py index c8df474f4..1d7bd19ca 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -662,6 +662,7 @@ class TestImage: im.putpalette(list(range(256)) * 4, "RGBA") im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) + assert im.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6a925975e..03061ceb1 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -236,6 +236,7 @@ def test_gif_with_rgba_palette_to_p() -> None: with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 im.load() + assert im.palette is not None assert im.palette.mode == "RGB" im_p = im.convert("P") diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 7e83396de..77916929b 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -47,6 +47,7 @@ class TestImageTransform: transformed = im.transform( im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] ) + assert im.palette is not None assert im.palette.palette == transformed.palette.palette def test_extent(self) -> None: diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 6cf0079dd..e2f8308ea 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -17,6 +17,7 @@ def test_sanity() -> None: def test_reload() -> None: with Image.open("Tests/images/hopper.gif") as im: original = im.copy() + assert im.palette is not None im.palette.dirty = 1 assert_image_equal(im.convert("RGB"), original.convert("RGB")) From fb3d80e390b8ed3111a4658755b035f5e20eb5e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Dec 2024 00:41:27 +1100 Subject: [PATCH 166/628] 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 167/628] 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 168/628] 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 169/628] 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 2ea3ea94a117772532e6f04ae7284c5842392af4 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 26 Dec 2024 21:41:51 +0100 Subject: [PATCH 170/628] Skip failing WMF records on 32-bit Windows --- Tests/test_file_wmf.py | 2 -- src/display.c | 9 +-------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 9322bd0c5..d3cad7ca4 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys from io import BytesIO from pathlib import Path from typing import IO @@ -43,7 +42,6 @@ def test_load_zero_inch() -> None: pass -@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_render() -> None: with open("Tests/images/drawing.emf", "rb") as fp: data = fp.read() diff --git a/src/display.c b/src/display.c index fe5801fc0..b5e9c2a3d 100644 --- a/src/display.c +++ b/src/display.c @@ -716,7 +716,7 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { #define GET32(p, o) ((DWORD *)(p + o))[0] -int +static int CALLBACK enhMetaFileProc( HDC hdc, HANDLETABLE *lpht, const ENHMETARECORD *lpmr, int nHandles, LPARAM data ) { @@ -804,14 +804,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { /* FIXME: make background transparent? configurable? */ FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); -#ifdef _WIN64 EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect); -#else - if (!PlayEnhMetaFile(dc, meta, &rect)) { - PyErr_SetString(PyExc_OSError, "cannot render metafile"); - goto error; - } -#endif /* step 4: extract bits from bitmap */ From aef3aa2ab35fdf847aa13017c5b4b315562a1f77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:26:07 +1100 Subject: [PATCH 171/628] 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 172/628] 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 173/628] 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 174/628] 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 175/628] 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 176/628] 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 177/628] 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 178/628] 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 179/628] 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 180/628] 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 181/628] 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 182/628] 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 183/628] 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 184/628] 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 185/628] 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 186/628] 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 187/628] 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 188/628] 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 189/628] 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 190/628] 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 191/628] 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 192/628] 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 193/628] 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 194/628] 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 195/628] 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 196/628] 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 197/628] 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 198/628] 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 199/628] 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 200/628] 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 201/628] 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 202/628] 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 203/628] 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 204/628] 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 205/628] 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 206/628] 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 207/628] 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 208/628] 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 209/628] 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 210/628] 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 211/628] 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 212/628] 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 213/628] 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 214/628] 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 215/628] 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 216/628] 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 217/628] 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 218/628] [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 219/628] 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 220/628] 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 221/628] 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 222/628] 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 223/628] 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 224/628] 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 120ba1c13d482b6f8763ab287ac5811a838f8828 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 8 Jan 2025 14:01:06 +0800 Subject: [PATCH 225/628] Rewrite the install_name of the ZLIB-NG library on macOS. --- .github/workflows/wheels-dependencies.sh | 8 ++++++++ pyproject.toml | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 58621bca1..2eac4d3d7 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -72,6 +72,14 @@ function build_zlib_ng { && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ && make -j4 \ && make install) + + if [ -n "$IS_MACOS" ]; then + # Ensure that on macOS, the library name is an absolute path, not an + # @rpath, so that delocate picks up the right library (and doesn't need + # DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an + # option to control the install_name. + install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib + fi touch zlib-stamp } diff --git a/pyproject.toml b/pyproject.toml index 2c6c7bcd0..aaaba0032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,6 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" -DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib" [tool.black] exclude = "wheels/multibuild" From f281eb9b469320f29006d7454d9c38a974ad65c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Jan 2025 18:27:20 +1100 Subject: [PATCH 226/628] 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 227/628] 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 228/628] 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 229/628] 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 230/628] 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 231/628] 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 232/628] 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 233/628] 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 234/628] 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 235/628] 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 236/628] 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 237/628] 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 238/628] 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 239/628] 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 240/628] 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 241/628] 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 242/628] 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 243/628] 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 244/628] 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 245/628] 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 246/628] 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 247/628] 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 248/628] 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 249/628] 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 250/628] 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 251/628] 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 252/628] 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 253/628] 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 254/628] 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 255/628] 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 256/628] 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 257/628] 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 258/628] 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 259/628] 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 260/628] 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 261/628] 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 262/628] 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 263/628] 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 264/628] 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 265/628] 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 266/628] 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 267/628] 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 268/628] 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 269/628] 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 270/628] 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 271/628] 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 272/628] 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 273/628] 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 274/628] 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 275/628] 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 276/628] 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 277/628] 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 278/628] 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 279/628] 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 280/628] 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 281/628] 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 282/628] [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 283/628] [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 284/628] 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 285/628] 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 286/628] 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 287/628] 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 288/628] 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 289/628] 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 From 1b0095fad45db67c723215e1f9235f839b2637d9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Feb 2025 17:23:41 +1100 Subject: [PATCH 290/628] Pass CFLAGS to build_simple directly --- .github/workflows/wheels-dependencies.sh | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index dffb36085..1dd8d5660 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -54,13 +54,10 @@ BROTLI_VERSION=1.1.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi # This essentially duplicates the Homebrew recipe - ORIGINAL_CFLAGS=$CFLAGS - CFLAGS="$CFLAGS -Wno-int-conversion" - build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ + CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ --disable-debug --disable-host-tool --with-internal-glib \ --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include - CFLAGS=$ORIGINAL_CFLAGS export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config touch pkg-config-stamp } @@ -130,15 +127,13 @@ function build { build_lcms2 build_openjpeg - ORIGINAL_CFLAGS=$CFLAGS - CFLAGS="$CFLAGS -O3 -DNDEBUG" + webp_cflags="-O3 -DNDEBUG" if [[ -n "$IS_MACOS" ]]; then - CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names" fi - build_simple libwebp $LIBWEBP_VERSION \ + CFLAGS="$CFLAGS $webp_cflags" 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 166d0b94d938c97ba06f2718a8772d1f8d88ac60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Feb 2025 21:00:54 +1100 Subject: [PATCH 291/628] Use boolean format argument for irreversible --- src/encode.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/encode.c b/src/encode.c index 74dd4a3fd..2a9fd3805 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1253,7 +1253,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { PyObject *quality_layers = NULL; Py_ssize_t num_resolutions = 0; PyObject *cblk_size = NULL, *precinct_size = NULL; - PyObject *irreversible = NULL; + int irreversible = 0; char *progression = "LRCP"; OPJ_PROG_ORDER prog_order; char *cinema_mode = "no"; @@ -1267,7 +1267,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbnz#p", + "ss|OOOsOnOOpssbbnz#p", &mode, &format, &offset, @@ -1402,7 +1402,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { precinct_size, &context->precinct_width, &context->precinct_height ); - context->irreversible = PyObject_IsTrue(irreversible); + context->irreversible = irreversible; context->progression = prog_order; context->cinema_mode = cine_mode; context->mct = mct; From b59dea60a6a7c83545f83f9e1f723c1a40f3f7cb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Feb 2025 21:07:25 +1100 Subject: [PATCH 292/628] Simplify Python code by receiving tuple from C --- src/PIL/WebPImagePlugin.py | 3 +-- src/_webp.c | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 066fe551f..cbbc24af0 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -46,8 +46,7 @@ class WebPImageFile(ImageFile.ImageFile): self._decoder = _webp.WebPAnimDecoder(self.fp.read()) # Get info from decoder - width, height, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() - self._size = width, height + self._size, loop_count, bgcolor, frame_count, mode = self._decoder.get_info() self.info["loop"] = loop_count bg_a, bg_r, bg_g, bg_b = ( (bgcolor >> 24) & 0xFF, diff --git a/src/_webp.c b/src/_webp.c index 26a5ebbc6..48b1c0a74 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -449,7 +449,7 @@ _anim_decoder_get_info(PyObject *self) { WebPAnimInfo *info = &(decp->info); return Py_BuildValue( - "IIIIIs", + "(II)IIIs", info->canvas_width, info->canvas_height, info->loop_count, From bfa2d64e0e41285d7cbc1016eb98b56b51255575 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 9 Feb 2025 16:02:50 +1100 Subject: [PATCH 293/628] Use member names to initialize PyTypeObjects --- src/_imaging.c | 125 ++++++++-------------------------------------- src/_imagingcms.c | 71 ++++---------------------- src/_imagingft.c | 36 +++---------- src/_webp.c | 70 ++++---------------------- src/decode.c | 36 +++---------- src/display.c | 36 +++---------- src/encode.c | 36 +++---------- src/outline.c | 35 ++----------- src/path.c | 38 +++----------- 9 files changed, 79 insertions(+), 404 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index ee373e964..6482bcc5e 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3769,102 +3769,29 @@ static PySequenceMethods image_as_sequence = { /* type description */ static PyTypeObject Imaging_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingCore", /*tp_name*/ - sizeof(ImagingObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - &image_as_sequence, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingCore", + .tp_basicsize = sizeof(ImagingObject), + .tp_dealloc = (destructor)_dealloc, + .tp_as_sequence = &image_as_sequence, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getsetters, }; static PyTypeObject ImagingFont_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ - sizeof(ImagingFontObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_font_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _font_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingFont", + .tp_basicsize = sizeof(ImagingFontObject), + .tp_dealloc = (destructor)_font_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _font_methods, }; static PyTypeObject ImagingDraw_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingDraw", /*tp_name*/ - sizeof(ImagingDrawObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_draw_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _draw_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDraw", + .tp_basicsize = sizeof(ImagingDrawObject), + .tp_dealloc = (destructor)_draw_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _draw_methods, }; static PyMappingMethods pixel_access_as_mapping = { @@ -3876,20 +3803,10 @@ static PyMappingMethods pixel_access_as_mapping = { /* type description */ static PyTypeObject PixelAccess_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", /*tp_name*/ - sizeof(PixelAccessObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)pixel_access_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - &pixel_access_as_mapping, /*tp_as_mapping*/ - 0 /*tp_hash*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PixelAccess", + .tp_basicsize = sizeof(PixelAccessObject), + .tp_dealloc = (destructor)pixel_access_dealloc, + .tp_as_mapping = &pixel_access_as_mapping, }; /* -------------------------------------------------------------------- */ diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 6037e8bc4..e177feee9 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1410,36 +1410,12 @@ static struct PyGetSetDef cms_profile_getsetters[] = { }; static PyTypeObject CmsProfile_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL.ImageCms.core.CmsProfile", /*tp_name*/ - sizeof(CmsProfileObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)cms_profile_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - cms_profile_methods, /*tp_methods*/ - 0, /*tp_members*/ - cms_profile_getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsProfile", + .tp_basicsize = sizeof(CmsProfileObject), + .tp_dealloc = (destructor)cms_profile_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = cms_profile_methods, + .tp_getset = cms_profile_getsetters, }; static struct PyMethodDef cms_transform_methods[] = { @@ -1447,36 +1423,11 @@ static struct PyMethodDef cms_transform_methods[] = { }; static PyTypeObject CmsTransform_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL.ImageCms.core.CmsTransform", /*tp_name*/ - sizeof(CmsTransformObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)cms_transform_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - cms_transform_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsTransform", + .tp_basicsize = sizeof(CmsTransformObject), + .tp_dealloc = (destructor)cms_transform_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = cms_transform_methods, }; static int diff --git a/src/_imagingft.c b/src/_imagingft.c index 7d754e168..922c3da32 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1518,36 +1518,12 @@ static struct PyGetSetDef font_getsetters[] = { }; static PyTypeObject Font_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "Font", /*tp_name*/ - sizeof(FontObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)font_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - font_methods, /*tp_methods*/ - 0, /*tp_members*/ - font_getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Font", + .tp_basicsize = sizeof(FontObject), + .tp_dealloc = (destructor)font_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = font_methods, + .tp_getset = font_getsetters, }; static PyMethodDef _functions[] = { diff --git a/src/_webp.c b/src/_webp.c index 26a5ebbc6..942f275da 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -530,36 +530,11 @@ static struct PyMethodDef _anim_encoder_methods[] = { // WebPAnimEncoder type definition static PyTypeObject WebPAnimEncoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimEncoder", /*tp_name */ - sizeof(WebPAnimEncoderObject), /*tp_basicsize */ - 0, /*tp_itemsize */ - /* methods */ - (destructor)_anim_encoder_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _anim_encoder_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimEncoder", + .tp_basicsize = sizeof(WebPAnimEncoderObject), + .tp_dealloc = (destructor)_anim_encoder_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _anim_encoder_methods, }; // WebPAnimDecoder methods @@ -573,36 +548,11 @@ static struct PyMethodDef _anim_decoder_methods[] = { // WebPAnimDecoder type definition static PyTypeObject WebPAnimDecoder_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimDecoder", /*tp_name */ - sizeof(WebPAnimDecoderObject), /*tp_basicsize */ - 0, /*tp_itemsize */ - /* methods */ - (destructor)_anim_decoder_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _anim_decoder_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimDecoder", + .tp_basicsize = sizeof(WebPAnimDecoderObject), + .tp_dealloc = (destructor)_anim_decoder_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _anim_decoder_methods, }; /* -------------------------------------------------------------------- */ diff --git a/src/decode.c b/src/decode.c index 1f2c22491..26211a95f 100644 --- a/src/decode.c +++ b/src/decode.c @@ -256,36 +256,12 @@ static struct PyGetSetDef getseters[] = { }; static PyTypeObject ImagingDecoderType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingDecoder", /*tp_name*/ - sizeof(ImagingDecoderObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getseters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDecoder", + .tp_basicsize = sizeof(ImagingDecoderObject), + .tp_dealloc = (destructor)_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getseters, }; /* -------------------------------------------------------------------- */ diff --git a/src/display.c b/src/display.c index 36ab3b237..004c7866b 100644 --- a/src/display.c +++ b/src/display.c @@ -248,36 +248,12 @@ static struct PyGetSetDef getsetters[] = { }; static PyTypeObject ImagingDisplayType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingDisplay", /*tp_name*/ - sizeof(ImagingDisplayObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_delete, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDisplay", + .tp_basicsize = sizeof(ImagingDisplayObject), + .tp_dealloc = (destructor)_delete, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getsetters, }; PyObject * diff --git a/src/encode.c b/src/encode.c index 74dd4a3fd..dd7355811 100644 --- a/src/encode.c +++ b/src/encode.c @@ -323,36 +323,12 @@ static struct PyGetSetDef getseters[] = { }; static PyTypeObject ImagingEncoderType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingEncoder", /*tp_name*/ - sizeof(ImagingEncoderObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getseters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingEncoder", + .tp_basicsize = sizeof(ImagingEncoderObject), + .tp_dealloc = (destructor)_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getseters, }; /* -------------------------------------------------------------------- */ diff --git a/src/outline.c b/src/outline.c index 4aa6bd59e..6eea07c5d 100644 --- a/src/outline.c +++ b/src/outline.c @@ -149,34 +149,9 @@ static struct PyMethodDef _outline_methods[] = { }; static PyTypeObject OutlineType = { - PyVarObject_HEAD_INIT(NULL, 0) "Outline", /*tp_name*/ - sizeof(OutlineObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)_outline_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - _outline_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Outline", + .tp_basicsize = sizeof(OutlineObject), + .tp_dealloc = (destructor)_outline_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _outline_methods, }; diff --git a/src/path.c b/src/path.c index b508df2ac..24820173e 100644 --- a/src/path.c +++ b/src/path.c @@ -598,34 +598,12 @@ static PyMappingMethods path_as_mapping = { }; static PyTypeObject PyPathType = { - PyVarObject_HEAD_INIT(NULL, 0) "Path", /*tp_name*/ - sizeof(PyPathObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)path_dealloc, /*tp_dealloc*/ - 0, /*tp_vectorcall_offset*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_as_async*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - &path_as_sequence, /*tp_as_sequence*/ - &path_as_mapping, /*tp_as_mapping*/ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - getsetters, /*tp_getset*/ + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Path", + .tp_basicsize = sizeof(PyPathObject), + .tp_dealloc = (destructor)path_dealloc, + .tp_as_sequence = &path_as_sequence, + .tp_as_mapping = &path_as_mapping, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = methods, + .tp_getset = getsetters, }; From 422c0f607d04470729768c3204273894c9be9e46 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 9 Feb 2025 16:03:38 +1100 Subject: [PATCH 294/628] Use default tp_flags --- src/_imaging.c | 3 --- src/_imagingcms.c | 2 -- src/_imagingft.c | 1 - src/_webp.c | 2 -- src/decode.c | 1 - src/display.c | 1 - src/encode.c | 1 - src/outline.c | 1 - src/path.c | 1 - 9 files changed, 13 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 6482bcc5e..d5c21fd86 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3773,7 +3773,6 @@ static PyTypeObject Imaging_Type = { .tp_basicsize = sizeof(ImagingObject), .tp_dealloc = (destructor)_dealloc, .tp_as_sequence = &image_as_sequence, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getsetters, }; @@ -3782,7 +3781,6 @@ static PyTypeObject ImagingFont_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingFont", .tp_basicsize = sizeof(ImagingFontObject), .tp_dealloc = (destructor)_font_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _font_methods, }; @@ -3790,7 +3788,6 @@ static PyTypeObject ImagingDraw_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDraw", .tp_basicsize = sizeof(ImagingDrawObject), .tp_dealloc = (destructor)_draw_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _draw_methods, }; diff --git a/src/_imagingcms.c b/src/_imagingcms.c index e177feee9..ea2f70186 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1413,7 +1413,6 @@ static PyTypeObject CmsProfile_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsProfile", .tp_basicsize = sizeof(CmsProfileObject), .tp_dealloc = (destructor)cms_profile_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = cms_profile_methods, .tp_getset = cms_profile_getsetters, }; @@ -1426,7 +1425,6 @@ static PyTypeObject CmsTransform_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "PIL.ImageCms.core.CmsTransform", .tp_basicsize = sizeof(CmsTransformObject), .tp_dealloc = (destructor)cms_transform_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = cms_transform_methods, }; diff --git a/src/_imagingft.c b/src/_imagingft.c index 922c3da32..62dab73e5 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1521,7 +1521,6 @@ static PyTypeObject Font_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Font", .tp_basicsize = sizeof(FontObject), .tp_dealloc = (destructor)font_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = font_methods, .tp_getset = font_getsetters, }; diff --git a/src/_webp.c b/src/_webp.c index 942f275da..c280d9513 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -533,7 +533,6 @@ static PyTypeObject WebPAnimEncoder_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimEncoder", .tp_basicsize = sizeof(WebPAnimEncoderObject), .tp_dealloc = (destructor)_anim_encoder_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _anim_encoder_methods, }; @@ -551,7 +550,6 @@ static PyTypeObject WebPAnimDecoder_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "WebPAnimDecoder", .tp_basicsize = sizeof(WebPAnimDecoderObject), .tp_dealloc = (destructor)_anim_decoder_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _anim_decoder_methods, }; diff --git a/src/decode.c b/src/decode.c index 26211a95f..03db1ce35 100644 --- a/src/decode.c +++ b/src/decode.c @@ -259,7 +259,6 @@ static PyTypeObject ImagingDecoderType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDecoder", .tp_basicsize = sizeof(ImagingDecoderObject), .tp_dealloc = (destructor)_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getseters, }; diff --git a/src/display.c b/src/display.c index 004c7866b..a05387504 100644 --- a/src/display.c +++ b/src/display.c @@ -251,7 +251,6 @@ static PyTypeObject ImagingDisplayType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDisplay", .tp_basicsize = sizeof(ImagingDisplayObject), .tp_dealloc = (destructor)_delete, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getsetters, }; diff --git a/src/encode.c b/src/encode.c index dd7355811..f610d6638 100644 --- a/src/encode.c +++ b/src/encode.c @@ -326,7 +326,6 @@ static PyTypeObject ImagingEncoderType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingEncoder", .tp_basicsize = sizeof(ImagingEncoderObject), .tp_dealloc = (destructor)_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getseters, }; diff --git a/src/outline.c b/src/outline.c index 6eea07c5d..32ab9109c 100644 --- a/src/outline.c +++ b/src/outline.c @@ -152,6 +152,5 @@ static PyTypeObject OutlineType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Outline", .tp_basicsize = sizeof(OutlineObject), .tp_dealloc = (destructor)_outline_dealloc, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = _outline_methods, }; diff --git a/src/path.c b/src/path.c index 24820173e..5affe3a1f 100644 --- a/src/path.c +++ b/src/path.c @@ -603,7 +603,6 @@ static PyTypeObject PyPathType = { .tp_dealloc = (destructor)path_dealloc, .tp_as_sequence = &path_as_sequence, .tp_as_mapping = &path_as_mapping, - .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = methods, .tp_getset = getsetters, }; From c566a81c647834d5789ab0cc7680eb51effcddec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Feb 2025 21:47:37 +1100 Subject: [PATCH 295/628] Updated libimagequant to 4.3.4 --- winbuild/build_prepare.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 54b5d983f..0ea8f0f9f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,6 +116,7 @@ V = { "HARFBUZZ": "10.2.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.16", + "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.46", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", @@ -335,24 +336,15 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"bin\*.lib"], }, "libimagequant": { - # commit: Merge branch 'master' into msvc (matches 2.17.0 tag) - "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", - "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", + "url": "https://github.com/ImageOptim/libimagequant/archive/{V['LIBIMAGEQUANT']}.tar.gz", + "filename": f"libimagequant-{V['LIBIMAGEQUANT']}.tar.gz", "license": "COPYRIGHT", - "patch": { - "CMakeLists.txt": { - "if(OPENMP_FOUND)": "if(false)", - "install": "#install", - # libimagequant does not detect MSVC x86_arm64 cross-compiler correctly - "if(${{CMAKE_SYSTEM_PROCESSOR}} STREQUAL ARM64)": "if({architecture} STREQUAL ARM64)", # noqa: E501 - } - }, "build": [ - *cmds_cmake("imagequant_a"), - cmd_copy("imagequant_a.lib", "imagequant.lib"), + cmd_cd("imagequant-sys"), + "cargo build --release", ], - "headers": [r"*.h"], - "libs": [r"imagequant.lib"], + "headers": ["libimagequant.h"], + "libs": [r"..\target\release\imagequant_sys.lib"], }, "harfbuzz": { "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", From 45d8d8056767988e0ea58a8676a5244d334b37b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Feb 2025 11:36:55 +1100 Subject: [PATCH 296/628] Updated zlib-ng to 2.2.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 dffb36085..edf5ba937 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -45,7 +45,7 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -ZLIB_NG_VERSION=2.2.3 +ZLIB_NG_VERSION=2.2.4 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 54b5d983f..73e3699d7 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.4", - "ZLIBNG": "2.2.3", + "ZLIBNG": "2.2.4", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 8020d423bc42688af8fb83b70d951dbf9daf34ad Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 12 Feb 2025 18:36:14 +1100 Subject: [PATCH 297/628] Use monkeypatch --- Tests/test_file_gif.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 46215db1f..396e09ba9 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1345,7 +1345,7 @@ def test_save_I(tmp_path: Path) -> None: assert_image_equal(reloaded.convert("L"), im.convert("L")) -def test_getdata() -> None: +def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None: # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) @@ -1354,23 +1354,21 @@ def test_getdata() -> None: passed_palette = bytes(255 - i // 3 for i in range(768)) - GifImagePlugin._FORCE_OPTIMIZE = True - try: - h = GifImagePlugin.getheader(im, passed_palette) - d = GifImagePlugin.getdata(im) + monkeypatch.setattr(GifImagePlugin, "_FORCE_OPTIMIZE", True) - import pickle + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) - # Enable to get target values on pre-refactor version - # with open('Tests/images/gif_header_data.pkl', 'wb') as f: - # pickle.dump((h, d), f, 1) - with open("Tests/images/gif_header_data.pkl", "rb") as f: - (h_target, d_target) = pickle.load(f) + import pickle - assert h == h_target - assert d == d_target - finally: - GifImagePlugin._FORCE_OPTIMIZE = False + # Enable to get target values on pre-refactor version + # with open('Tests/images/gif_header_data.pkl', 'wb') as f: + # pickle.dump((h, d), f, 1) + with open("Tests/images/gif_header_data.pkl", "rb") as f: + (h_target, d_target) = pickle.load(f) + + assert h == h_target + assert d == d_target def test_lzw_bits() -> None: From 8f4bfe1fe5c782329314333eac6284f32ff84b7b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 12 Feb 2025 19:12:27 +1100 Subject: [PATCH 298/628] Only crop when saving with disposal method 2 if transparency is present --- Tests/test_file_gif.py | 15 +++++++++++++++ src/PIL/GifImagePlugin.py | 28 +++++++++++++++++----------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 46215db1f..2f0116434 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -762,6 +762,21 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None: assert im.getpixel((0, 0)) == (0, 0, 0, 255) +def test_dispose2_without_transparency(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("P", (100, 100)) + + im2 = Image.new("P", (100, 100), (0, 0, 0)) + im2.putpixel((50, 50), (255, 0, 0)) + + im.save(out, save_all=True, append_images=[im2], disposal=2) + + with Image.open(out) as reloaded: + reloaded.seek(1) + assert reloaded.tile[0].extents == (0, 0, 100, 100) + + def test_transparency_in_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 47022d584..ff7262efc 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -689,16 +689,21 @@ def _write_multiple_frames( im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"] continue if im_frames[-1].encoderinfo.get("disposal") == 2: - if background_im is None: - color = im.encoderinfo.get( - "transparency", im.info.get("transparency", (0, 0, 0)) - ) - background = _get_background(im_frame, color) - background_im = Image.new("P", im_frame.size, background) - first_palette = im_frames[0].im.palette - assert first_palette is not None - background_im.putpalette(first_palette, first_palette.mode) - bbox = _getbbox(background_im, im_frame)[1] + # To appear correctly in viewers using a convention, + # only consider transparency, and not background color + color = im.encoderinfo.get( + "transparency", im.info.get("transparency") + ) + if color is not None: + if background_im is None: + background = _get_background(im_frame, color) + background_im = Image.new("P", im_frame.size, background) + first_palette = im_frames[0].im.palette + assert first_palette is not None + background_im.putpalette(first_palette, first_palette.mode) + bbox = _getbbox(background_im, im_frame)[1] + else: + bbox = (0, 0) + im_frame.size elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: assert im_frame.palette is not None @@ -764,7 +769,8 @@ def _write_multiple_frames( if not palette: frame_data.encoderinfo["include_color_table"] = True - im_frame = im_frame.crop(frame_data.bbox) + if frame_data.bbox != (0, 0) + im_frame.size: + im_frame = im_frame.crop(frame_data.bbox) offset = frame_data.bbox[:2] _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo) return True From ad6c4f82f3d85df5e51d99916fd68f0d8b180244 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Feb 2025 09:27:16 +1100 Subject: [PATCH 299/628] Updated lcms2 to 2.17 --- .github/workflows/wheels-dependencies.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index edf5ba937..155e5fb13 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -44,7 +44,7 @@ JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 TIFF_VERSION=4.6.0 -LCMS2_VERSION=2.16 +LCMS2_VERSION=2.17 ZLIB_NG_VERSION=2.2.4 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 46a4c1245..b400a3436 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -51,7 +51,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.16**. + above uses liblcms2. Tested with **1.19** and **2.7-2.17**. * **libwebp** provides the WebP format. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f942716cb..e3509aee6 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -115,7 +115,7 @@ V = { "FRIBIDI": "1.0.16", "HARFBUZZ": "10.2.0", "JPEGTURBO": "3.1.0", - "LCMS2": "2.16", + "LCMS2": "2.17", "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.46", "LIBWEBP": "1.5.0", From 9f0398ef3239cacf0f0250305f4ccb1db9f6c738 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Feb 2025 21:07:43 +1100 Subject: [PATCH 300/628] Removed unused code --- Tests/helper.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index e7b0db1d6..764935f87 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -9,7 +9,6 @@ import os import shutil import subprocess import sys -import sysconfig import tempfile from collections.abc import Sequence from functools import lru_cache @@ -342,10 +341,6 @@ def is_pypy() -> bool: return hasattr(sys, "pypy_translation_info") -def is_mingw() -> bool: - return sysconfig.get_platform() == "mingw" - - class CachedProperty: def __init__(self, func: Callable[[Any], Any]) -> None: self.func = func From 1c18d29c34789c802e2cfc73d841019bc5f06ca1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 13:26:06 +0200 Subject: [PATCH 301/628] Remove unused bdf_slant and bdf_spacing variables --- src/PIL/BdfFontFile.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index bc1416c74..bfd66aa6a 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -26,17 +26,6 @@ from typing import BinaryIO from . import FontFile, Image -bdf_slant = { - "R": "Roman", - "I": "Italic", - "O": "Oblique", - "RI": "Reverse Italic", - "RO": "Reverse Oblique", - "OT": "Other", -} - -bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} - def bdf_char( f: BinaryIO, From 8261348fff5b5a653767c348a44a01d98238a190 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:27:52 +0200 Subject: [PATCH 302/628] Don't call pip in tox --- tox.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tox.ini b/tox.ini index e79d88500..4065245ee 100644 --- a/tox.ini +++ b/tox.ini @@ -11,12 +11,8 @@ deps = extras = tests commands = - make clean - {envpython} -m pip install . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -allowlist_externals = - make [testenv:lint] skip_install = true From ff960b884188855c4a8afb2a2724674d80b31e04 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:23:59 +0200 Subject: [PATCH 303/628] Remove debug Image._wedge --- Tests/test_file_gif.py | 2 +- Tests/test_format_hsv.py | 28 +++++++++++++--------------- src/PIL/Image.py | 9 --------- src/_imaging.c | 1 - 4 files changed, 14 insertions(+), 26 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 396e09ba9..974aedeb6 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1348,7 +1348,7 @@ def test_save_I(tmp_path: Path) -> None: def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None: # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. - im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) + im = Image.linear_gradient(mode="L").resize((16, 16), Image.Resampling.NEAREST) im.putpalette(ImagePalette.ImagePalette("RGB")) im.info = {"background": 0} diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index c07024a2c..9cbf18566 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -22,28 +22,26 @@ def test_sanity() -> None: Image.new("HSV", (100, 100)) -def wedge() -> Image.Image: - w = Image._wedge() - w90 = w.rotate(90) +def linear_gradient() -> Image.Image: + im = Image.linear_gradient(mode="L") + im90 = im.rotate(90) - (px, h) = w.size + (px, h) = im.size r = Image.new("L", (px * 3, h)) g = r.copy() b = r.copy() - r.paste(w, (0, 0)) - r.paste(w90, (px, 0)) + r.paste(im, (0, 0)) + r.paste(im90, (px, 0)) - g.paste(w90, (0, 0)) - g.paste(w, (2 * px, 0)) + g.paste(im90, (0, 0)) + g.paste(im, (2 * px, 0)) - b.paste(w, (px, 0)) - b.paste(w90, (2 * px, 0)) + b.paste(im, (px, 0)) + b.paste(im90, (2 * px, 0)) - img = Image.merge("RGB", (r, g, b)) - - return img + return Image.merge("RGB", (r, g, b)) def to_xxx_colorsys( @@ -79,8 +77,8 @@ def to_rgb_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") -def test_wedge() -> None: - src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) +def test_linear_gradient() -> None: + src = linear_gradient().resize((3 * 32, 32), Image.Resampling.BILINEAR) im = src.convert("HSV") comparable = to_hsv_colorsys(src) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e723b6a2e..a5243549f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2996,15 +2996,6 @@ class ImageTransformHandler: # -------------------------------------------------------------------- # Factories -# -# Debugging - - -def _wedge() -> Image: - """Create grayscale wedge (for debugging only)""" - - return Image()._new(core.wedge("L")) - def _check_size(size: Any) -> None: """ diff --git a/src/_imaging.c b/src/_imaging.c index ee373e964..daaa56c75 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4256,7 +4256,6 @@ static PyMethodDef functions[] = { {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, - {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ /* Drawing support stuff */ {"font", (PyCFunction)_font_new, METH_VARARGS}, From 028f0d6ea9263928f29e5b62fa67d95870f57144 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:42:28 +0200 Subject: [PATCH 304/628] Remove unused data read --- Tests/test_file_gif.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 974aedeb6..6a295e89a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -22,9 +22,6 @@ from .helper import ( # sample gif stream TEST_GIF = "Tests/images/hopper.gif" -with open(TEST_GIF, "rb") as f: - data = f.read() - def test_sanity() -> None: with Image.open(TEST_GIF) as im: From 126026e5e544ed35a7ea82349f41a1281facccf9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:44:00 +0200 Subject: [PATCH 305/628] Don't shadow builtin open --- Tests/test_file_gif.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 6a295e89a..d50842019 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -34,12 +34,12 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_GIF) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: From 7f414846a3ef1cc5a740a74aa6cdeb49b58a373d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 16 Feb 2025 05:08:22 +1100 Subject: [PATCH 306/628] Don't shadow builtin open --- Tests/test_file_dcx.py | 4 ++-- Tests/test_file_fli.py | 4 ++-- Tests/test_file_im.py | 4 ++-- Tests/test_file_mpo.py | 4 ++-- Tests/test_file_psd.py | 4 ++-- Tests/test_file_spider.py | 4 ++-- Tests/test_file_tiff.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 5deacd878..ab6b9f983 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -26,12 +26,12 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_FILE) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 876561a88..8adbd30f5 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -52,12 +52,12 @@ def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(static_test_file) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 1d3fa485f..d29998801 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -31,12 +31,12 @@ def test_name_limit(tmp_path: Path) -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_IM) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 66fa29177..311085cf7 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -38,12 +38,12 @@ def test_sanity(test_file: str) -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(test_files[0]) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 5f22001f3..1793c269d 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -25,12 +25,12 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(test_file) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 713db848d..cdb7b3e0b 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -24,12 +24,12 @@ def test_sanity() -> None: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open(TEST_FILE) im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file() -> None: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index fe8f69848..a8a407963 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -63,12 +63,12 @@ class TestFileTiff: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self) -> None: - def open() -> None: + def open_test_image() -> None: im = Image.open("Tests/images/multipage.tiff") im.load() with pytest.warns(ResourceWarning): - open() + open_test_image() def test_closed_file(self) -> None: with warnings.catch_warnings(): From 0fbe1860c4f2688a7e18a1b4e525b4b5fb1c5d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sun, 16 Feb 2025 16:32:24 +0100 Subject: [PATCH 307/628] Update `pythoncapi_compat.h` to fix building with PyPy3.11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update `pythoncapi_compat.h` to upstream commit c84545f0e1e21757d4901f75c47333d25a3fcff0, which includes fixes necessary for Pillow to build against PyPy3.11. Otherwise, it fails due to duplicate declarations: ``` In file included from src/encode.c:28: src/thirdparty/pythoncapi_compat.h:295:1: error: static declaration of ‘PyThreadState_GetInterpreter’ follows non-static declaration 295 | PyThreadState_GetInterpreter(PyThreadState *tstate) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ In file included from /usr/include/pypy3.11/Python.h:80, from src/encode.c:26: /usr/include/pypy3.11/pystate.h:35:33: note: previous declaration of ‘PyThreadState_GetInterpreter’ with type ‘PyInterpreterState *(PyThreadState *)’ {aka ‘struct _is *(struct _ts *)’} 35 | PyAPI_FUNC(PyInterpreterState*) PyThreadState_GetInterpreter(PyThreadState *tstate); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` --- src/thirdparty/pythoncapi_compat.h | 521 ++++++++++++++++++++++++++++- 1 file changed, 514 insertions(+), 7 deletions(-) diff --git a/src/thirdparty/pythoncapi_compat.h b/src/thirdparty/pythoncapi_compat.h index ca23d5ffa..04fcf61e0 100644 --- a/src/thirdparty/pythoncapi_compat.h +++ b/src/thirdparty/pythoncapi_compat.h @@ -10,7 +10,7 @@ // https://raw.githubusercontent.com/python/pythoncapi-compat/main/pythoncapi_compat.h // // This file was vendored from the following commit: -// https://github.com/python/pythoncapi-compat/commit/0041177c4f348c8952b4c8980b2c90856e61c7c7 +// https://github.com/python/pythoncapi-compat/commit/c84545f0e1e21757d4901f75c47333d25a3fcff0 // // SPDX-License-Identifier: 0BSD @@ -22,11 +22,15 @@ extern "C" { #endif #include +#include // offsetof() // Python 3.11.0b4 added PyFrame_Back() to Python.h #if PY_VERSION_HEX < 0x030b00B4 && !defined(PYPY_VERSION) # include "frameobject.h" // PyFrameObject, PyFrame_GetBack() #endif +#if PY_VERSION_HEX < 0x030C00A3 +# include // T_SHORT, READONLY +#endif #ifndef _Py_CAST @@ -290,7 +294,7 @@ PyFrame_GetVarString(PyFrameObject *frame, const char *name) // bpo-39947 added PyThreadState_GetInterpreter() to Python 3.9.0a5 -#if PY_VERSION_HEX < 0x030900A5 || defined(PYPY_VERSION) +#if PY_VERSION_HEX < 0x030900A5 || (defined(PYPY_VERSION) && PY_VERSION_HEX < 0x030B0000) static inline PyInterpreterState * PyThreadState_GetInterpreter(PyThreadState *tstate) { @@ -583,7 +587,7 @@ static inline int PyWeakref_GetRef(PyObject *ref, PyObject **pobj) return 0; } *pobj = Py_NewRef(obj); - return (*pobj != NULL); + return 1; } #endif @@ -921,7 +925,7 @@ static inline int PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg) { PyObject **dict = _PyObject_GetDictPtr(obj); - if (*dict == NULL) { + if (dict == NULL || *dict == NULL) { return -1; } Py_VISIT(*dict); @@ -932,7 +936,7 @@ static inline void PyObject_ClearManagedDict(PyObject *obj) { PyObject **dict = _PyObject_GetDictPtr(obj); - if (*dict == NULL) { + if (dict == NULL || *dict == NULL) { return; } Py_CLEAR(*dict); @@ -1207,11 +1211,11 @@ static inline int PyTime_PerfCounter(PyTime_t *result) #endif // gh-111389 added hash constants to Python 3.13.0a5. These constants were -// added first as private macros to Python 3.4.0b1 and PyPy 7.3.9. +// added first as private macros to Python 3.4.0b1 and PyPy 7.3.8. #if (!defined(PyHASH_BITS) \ && ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \ || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \ - && PYPY_VERSION_NUM >= 0x07090000))) + && PYPY_VERSION_NUM >= 0x07030800))) # define PyHASH_BITS _PyHASH_BITS # define PyHASH_MODULUS _PyHASH_MODULUS # define PyHASH_INF _PyHASH_INF @@ -1523,6 +1527,36 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign) } #endif +// gh-126061 added PyLong_IsPositive/Negative/Zero() to Python in 3.14.0a2 +#if PY_VERSION_HEX < 0x030E00A2 +static inline int PyLong_IsPositive(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 1; +} + +static inline int PyLong_IsNegative(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == -1; +} + +static inline int PyLong_IsZero(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 0; +} +#endif + // gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 #if PY_VERSION_HEX < 0x030E00A0 @@ -1693,6 +1727,479 @@ static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) #endif +// gh-102471 added import and export API for integers to 3.14.0a2. +#if PY_VERSION_HEX < 0x030E00A2 && PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +// Helpers to access PyLongObject internals. +static inline void +_PyLong_SetSignAndDigitCount(PyLongObject *op, int sign, Py_ssize_t size) +{ +#if PY_VERSION_HEX >= 0x030C0000 + op->long_value.lv_tag = (uintptr_t)(1 - sign) | ((uintptr_t)(size) << 3); +#elif PY_VERSION_HEX >= 0x030900A4 + Py_SET_SIZE(op, sign * size); +#else + Py_SIZE(op) = sign * size; +#endif +} + +static inline Py_ssize_t +_PyLong_DigitCount(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (Py_ssize_t)(op->long_value.lv_tag >> 3); +#else + return _PyLong_Sign((PyObject*)op) < 0 ? -Py_SIZE(op) : Py_SIZE(op); +#endif +} + +static inline digit* +_PyLong_GetDigits(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (digit*)(op->long_value.ob_digit); +#else + return (digit*)(op->ob_digit); +#endif +} + +typedef struct PyLongLayout { + uint8_t bits_per_digit; + uint8_t digit_size; + int8_t digits_order; + int8_t digit_endianness; +} PyLongLayout; + +typedef struct PyLongExport { + int64_t value; + uint8_t negative; + Py_ssize_t ndigits; + const void *digits; + Py_uintptr_t _reserved; +} PyLongExport; + +typedef struct PyLongWriter PyLongWriter; + +static inline const PyLongLayout* +PyLong_GetNativeLayout(void) +{ + static const PyLongLayout PyLong_LAYOUT = { + PyLong_SHIFT, + sizeof(digit), + -1, // least significant first + PY_LITTLE_ENDIAN ? -1 : 1, + }; + + return &PyLong_LAYOUT; +} + +static inline int +PyLong_Export(PyObject *obj, PyLongExport *export_long) +{ + if (!PyLong_Check(obj)) { + memset(export_long, 0, sizeof(*export_long)); + PyErr_Format(PyExc_TypeError, "expected int, got %s", + Py_TYPE(obj)->tp_name); + return -1; + } + + // Fast-path: try to convert to a int64_t + PyLongObject *self = (PyLongObject*)obj; + int overflow; +#if SIZEOF_LONG == 8 + long value = PyLong_AsLongAndOverflow(obj, &overflow); +#else + // Windows has 32-bit long, so use 64-bit long long instead + long long value = PyLong_AsLongLongAndOverflow(obj, &overflow); +#endif + Py_BUILD_ASSERT(sizeof(value) == sizeof(int64_t)); + // the function cannot fail since obj is a PyLongObject + assert(!(value == -1 && PyErr_Occurred())); + + if (!overflow) { + export_long->value = value; + export_long->negative = 0; + export_long->ndigits = 0; + export_long->digits = 0; + export_long->_reserved = 0; + } + else { + export_long->value = 0; + export_long->negative = _PyLong_Sign(obj) < 0; + export_long->ndigits = _PyLong_DigitCount(self); + if (export_long->ndigits == 0) { + export_long->ndigits = 1; + } + export_long->digits = _PyLong_GetDigits(self); + export_long->_reserved = (Py_uintptr_t)Py_NewRef(obj); + } + return 0; +} + +static inline void +PyLong_FreeExport(PyLongExport *export_long) +{ + PyObject *obj = (PyObject*)export_long->_reserved; + + if (obj) { + export_long->_reserved = 0; + Py_DECREF(obj); + } +} + +static inline PyLongWriter* +PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits) +{ + if (ndigits <= 0) { + PyErr_SetString(PyExc_ValueError, "ndigits must be positive"); + return NULL; + } + assert(digits != NULL); + + PyLongObject *obj = _PyLong_New(ndigits); + if (obj == NULL) { + return NULL; + } + _PyLong_SetSignAndDigitCount(obj, negative?-1:1, ndigits); + + *digits = _PyLong_GetDigits(obj); + return (PyLongWriter*)obj; +} + +static inline void +PyLongWriter_Discard(PyLongWriter *writer) +{ + PyLongObject *obj = (PyLongObject *)writer; + + assert(Py_REFCNT(obj) == 1); + Py_DECREF(obj); +} + +static inline PyObject* +PyLongWriter_Finish(PyLongWriter *writer) +{ + PyObject *obj = (PyObject *)writer; + PyLongObject *self = (PyLongObject*)obj; + Py_ssize_t j = _PyLong_DigitCount(self); + Py_ssize_t i = j; + int sign = _PyLong_Sign(obj); + + assert(Py_REFCNT(obj) == 1); + + // Normalize and get singleton if possible + while (i > 0 && _PyLong_GetDigits(self)[i-1] == 0) { + --i; + } + if (i != j) { + if (i == 0) { + sign = 0; + } + _PyLong_SetSignAndDigitCount(self, sign, i); + } + if (i <= 1) { + long val = sign * (long)(_PyLong_GetDigits(self)[0]); + Py_DECREF(obj); + return PyLong_FromLong(val); + } + + return obj; +} +#endif + + +#if PY_VERSION_HEX < 0x030C00A3 +# define Py_T_SHORT T_SHORT +# define Py_T_INT T_INT +# define Py_T_LONG T_LONG +# define Py_T_FLOAT T_FLOAT +# define Py_T_DOUBLE T_DOUBLE +# define Py_T_STRING T_STRING +# define _Py_T_OBJECT T_OBJECT +# define Py_T_CHAR T_CHAR +# define Py_T_BYTE T_BYTE +# define Py_T_UBYTE T_UBYTE +# define Py_T_USHORT T_USHORT +# define Py_T_UINT T_UINT +# define Py_T_ULONG T_ULONG +# define Py_T_STRING_INPLACE T_STRING_INPLACE +# define Py_T_BOOL T_BOOL +# define Py_T_OBJECT_EX T_OBJECT_EX +# define Py_T_LONGLONG T_LONGLONG +# define Py_T_ULONGLONG T_ULONGLONG +# define Py_T_PYSSIZET T_PYSSIZET + +# if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +# define _Py_T_NONE T_NONE +# endif + +# define Py_READONLY READONLY +# define Py_AUDIT_READ READ_RESTRICTED +# define _Py_WRITE_RESTRICTED PY_WRITE_RESTRICTED +#endif + + +// gh-127350 added Py_fopen() and Py_fclose() to Python 3.14a4 +#if PY_VERSION_HEX < 0x030E00A4 +static inline FILE* Py_fopen(PyObject *path, const char *mode) +{ +#if 0x030400A2 <= PY_VERSION_HEX && !defined(PYPY_VERSION) + PyAPI_FUNC(FILE*) _Py_fopen_obj(PyObject *path, const char *mode); + + return _Py_fopen_obj(path, mode); +#else + FILE *f; + PyObject *bytes; +#if PY_VERSION_HEX >= 0x03000000 + if (!PyUnicode_FSConverter(path, &bytes)) { + return NULL; + } +#else + if (!PyString_Check(path)) { + PyErr_SetString(PyExc_TypeError, "except str"); + return NULL; + } + bytes = Py_NewRef(path); +#endif + const char *path_bytes = PyBytes_AS_STRING(bytes); + + f = fopen(path_bytes, mode); + Py_DECREF(bytes); + + if (f == NULL) { + PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, path); + return NULL; + } + return f; +#endif +} + +static inline int Py_fclose(FILE *file) +{ + return fclose(file); +} +#endif + + +#if 0x03090000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030E0000 && !defined(PYPY_VERSION) +static inline PyObject* +PyConfig_Get(const char *name) +{ + typedef enum { + _PyConfig_MEMBER_INT, + _PyConfig_MEMBER_UINT, + _PyConfig_MEMBER_ULONG, + _PyConfig_MEMBER_BOOL, + _PyConfig_MEMBER_WSTR, + _PyConfig_MEMBER_WSTR_OPT, + _PyConfig_MEMBER_WSTR_LIST, + } PyConfigMemberType; + + typedef struct { + const char *name; + size_t offset; + PyConfigMemberType type; + const char *sys_attr; + } PyConfigSpec; + +#define PYTHONCAPI_COMPAT_SPEC(MEMBER, TYPE, sys_attr) \ + {#MEMBER, offsetof(PyConfig, MEMBER), \ + _PyConfig_MEMBER_##TYPE, sys_attr} + + static const PyConfigSpec config_spec[] = { + PYTHONCAPI_COMPAT_SPEC(argv, WSTR_LIST, "argv"), + PYTHONCAPI_COMPAT_SPEC(base_exec_prefix, WSTR_OPT, "base_exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(base_executable, WSTR_OPT, "_base_executable"), + PYTHONCAPI_COMPAT_SPEC(base_prefix, WSTR_OPT, "base_prefix"), + PYTHONCAPI_COMPAT_SPEC(bytes_warning, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(exec_prefix, WSTR_OPT, "exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(executable, WSTR_OPT, "executable"), + PYTHONCAPI_COMPAT_SPEC(inspect, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(int_max_str_digits, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(interactive, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(module_search_paths, WSTR_LIST, "path"), + PYTHONCAPI_COMPAT_SPEC(optimization_level, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(parser_debug, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(platlibdir, WSTR, "platlibdir"), + PYTHONCAPI_COMPAT_SPEC(prefix, WSTR_OPT, "prefix"), + PYTHONCAPI_COMPAT_SPEC(pycache_prefix, WSTR_OPT, "pycache_prefix"), + PYTHONCAPI_COMPAT_SPEC(quiet, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(stdlib_dir, WSTR_OPT, "_stdlib_dir"), +#endif + PYTHONCAPI_COMPAT_SPEC(use_environment, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(verbose, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(warnoptions, WSTR_LIST, "warnoptions"), + PYTHONCAPI_COMPAT_SPEC(write_bytecode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(xoptions, WSTR_LIST, "_xoptions"), + PYTHONCAPI_COMPAT_SPEC(buffered_stdio, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(check_hash_pycs_mode, WSTR, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(code_debug_ranges, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(configure_c_stdio, BOOL, _Py_NULL), +#if 0x030D0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(cpu_count, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(dev_mode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(dump_refs, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(dump_refs_file, WSTR_OPT, _Py_NULL), +#endif +#ifdef Py_GIL_DISABLED + PYTHONCAPI_COMPAT_SPEC(enable_gil, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(faulthandler, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(hash_seed, ULONG, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(home, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(import_time, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(install_signal_handlers, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(isolated, BOOL, _Py_NULL), +#ifdef MS_WINDOWS + PYTHONCAPI_COMPAT_SPEC(legacy_windows_stdio, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(malloc_stats, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(orig_argv, WSTR_LIST, "orig_argv"), +#endif + PYTHONCAPI_COMPAT_SPEC(parse_argv, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(pathconfig_warnings, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(perf_profiling, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(program_name, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_command, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_filename, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_module, WSTR_OPT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(safe_path, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(show_ref_count, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(site_import, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(skip_source_first_line, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(tracemalloc, UINT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(use_frozen_modules, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(use_hash_seed, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(user_site_directory, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(warn_default_encoding, BOOL, _Py_NULL), +#endif + }; + +#undef PYTHONCAPI_COMPAT_SPEC + + const PyConfigSpec *spec; + int found = 0; + for (size_t i=0; i < sizeof(config_spec) / sizeof(config_spec[0]); i++) { + spec = &config_spec[i]; + if (strcmp(spec->name, name) == 0) { + found = 1; + break; + } + } + if (found) { + if (spec->sys_attr != NULL) { + PyObject *value = PySys_GetObject(spec->sys_attr); + if (value == NULL) { + PyErr_Format(PyExc_RuntimeError, "lost sys.%s", spec->sys_attr); + return NULL; + } + return Py_NewRef(value); + } + + PyAPI_FUNC(const PyConfig*) _Py_GetConfig(void); + + const PyConfig *config = _Py_GetConfig(); + void *member = (char *)config + spec->offset; + switch (spec->type) { + case _PyConfig_MEMBER_INT: + case _PyConfig_MEMBER_UINT: + { + int value = *(int *)member; + return PyLong_FromLong(value); + } + case _PyConfig_MEMBER_BOOL: + { + int value = *(int *)member; + return PyBool_FromLong(value != 0); + } + case _PyConfig_MEMBER_ULONG: + { + unsigned long value = *(unsigned long *)member; + return PyLong_FromUnsignedLong(value); + } + case _PyConfig_MEMBER_WSTR: + case _PyConfig_MEMBER_WSTR_OPT: + { + wchar_t *wstr = *(wchar_t **)member; + if (wstr != NULL) { + return PyUnicode_FromWideChar(wstr, -1); + } + else { + return Py_NewRef(Py_None); + } + } + case _PyConfig_MEMBER_WSTR_LIST: + { + const PyWideStringList *list = (const PyWideStringList *)member; + PyObject *tuple = PyTuple_New(list->length); + if (tuple == NULL) { + return NULL; + } + + for (Py_ssize_t i = 0; i < list->length; i++) { + PyObject *item = PyUnicode_FromWideChar(list->items[i], -1); + if (item == NULL) { + Py_DECREF(tuple); + return NULL; + } + PyTuple_SET_ITEM(tuple, i, item); + } + return tuple; + } + default: + Py_UNREACHABLE(); + } + } + + PyErr_Format(PyExc_ValueError, "unknown config option name: %s", name); + return NULL; +} + +static inline int +PyConfig_GetInt(const char *name, int *value) +{ + PyObject *obj = PyConfig_Get(name); + if (obj == NULL) { + return -1; + } + + if (!PyLong_Check(obj)) { + Py_DECREF(obj); + PyErr_Format(PyExc_TypeError, "config option %s is not an int", name); + return -1; + } + + int as_int = PyLong_AsInt(obj); + Py_DECREF(obj); + if (as_int == -1 && PyErr_Occurred()) { + PyErr_Format(PyExc_OverflowError, + "config option %s value does not fit into a C int", name); + return -1; + } + + *value = as_int; + return 0; +} +#endif // PY_VERSION_HEX > 0x03090000 && !defined(PYPY_VERSION) + + #ifdef __cplusplus } #endif From 216690ff17d03c80860b59f5ba2311383a09525c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:11:54 +0200 Subject: [PATCH 308/628] Add PyPy3.11 to CI --- .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 8faab2ef4..ef49ff332 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.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] architecture: ["x64"] os: ["windows-latest"] include: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3efe0b59..c4ad88be9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,6 +41,7 @@ jobs: "ubuntu-latest", ] python-version: [ + "pypy3.11", "pypy3.10", "3.14", "3.13t", From 9762c9e30eeb6adc6815b25ad619432f707d4632 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Feb 2025 20:20:02 +1100 Subject: [PATCH 309/628] Test unexpected end of tar file --- Tests/test_file_tar.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 49220a8b6..d1a4ca9de 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,6 +1,7 @@ from __future__ import annotations import warnings +from pathlib import Path import pytest @@ -29,6 +30,16 @@ def test_sanity(codec: str, test_path: str, format: str) -> None: assert im.format == format +def test_unexpected_end(tmp_path: Path) -> None: + tmpfile = str(tmp_path / "temp.tar") + with open(tmpfile, "w"): + pass + + with pytest.raises(OSError): + with TarIO.TarIO(tmpfile, "test"): + pass + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: with pytest.warns(ResourceWarning): From 152d982644f4474d14597b4cba878903db400a67 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Feb 2025 20:20:45 +1100 Subject: [PATCH 310/628] Test missing subfile --- Tests/test_file_tar.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index d1a4ca9de..e1a6a55d7 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -40,6 +40,12 @@ def test_unexpected_end(tmp_path: Path) -> None: pass +def test_cannot_find_subfile(tmp_path: Path) -> None: + with pytest.raises(OSError): + with TarIO.TarIO(TEST_TAR_FILE, "test"): + pass + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: with pytest.warns(ResourceWarning): From 15e4c1a72451672d035e9e1525ccac539252e742 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:34:25 +0200 Subject: [PATCH 311/628] Fix ShellCheck --- .ci/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 5c20e7f37..e61752750 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -2,12 +2,12 @@ aptget_update() { - if [ ! -z $1 ]; then + if [ -n "$1" ]; then echo "" echo "Retrying apt-get update..." echo "" fi - output=`sudo apt-get update 2>&1` + output=$(sudo apt-get update 2>&1) echo "$output" if [[ $output == *[WE]:\ * ]]; then return 1 From 017b16b803347bceb19361230c751267fab6e660 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:48:09 +1100 Subject: [PATCH 312/628] Removed argument Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_file_tar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index e1a6a55d7..9fc3edcd7 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -40,7 +40,7 @@ def test_unexpected_end(tmp_path: Path) -> None: pass -def test_cannot_find_subfile(tmp_path: Path) -> None: +def test_cannot_find_subfile() -> None: with pytest.raises(OSError): with TarIO.TarIO(TEST_TAR_FILE, "test"): pass From 19010bb301e559dd6c9b255c9e3134bbde31297f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:49:08 +1100 Subject: [PATCH 313/628] Use match Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_file_tar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 9fc3edcd7..084d0f288 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -35,13 +35,13 @@ def test_unexpected_end(tmp_path: Path) -> None: with open(tmpfile, "w"): pass - with pytest.raises(OSError): + with pytest.raises(OSError, match="unexpected end of tar file"): with TarIO.TarIO(tmpfile, "test"): pass def test_cannot_find_subfile() -> None: - with pytest.raises(OSError): + with pytest.raises(OSError, match="cannot find subfile"): with TarIO.TarIO(TEST_TAR_FILE, "test"): pass From 322e121a92ac6c608ef3d626a19b7d3127ab044f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Feb 2025 07:56:11 +1100 Subject: [PATCH 314/628] Corrected type check --- Tests/test_file_gbr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 5b59cc07a..1b834cd3c 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -16,7 +16,7 @@ def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: px = im.load() assert px is not None - assert im.load()[0, 0] == (0, 0, 0, 0) + assert px[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once px = im.load() From 1e574e6f8bfd5862a5875db38d08a1e83cadb0e2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 19:28:43 +0200 Subject: [PATCH 315/628] Replace slice and comparison with startswith --- Tests/test_file_mpo.py | 4 ++-- Tests/test_file_webp_metadata.py | 2 +- Tests/test_image.py | 4 ++-- docs/example/DdsImagePlugin.py | 2 +- .../writing-your-own-image-plugin.rst | 2 +- src/PIL/BdfFontFile.py | 10 +++++----- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 2 +- src/PIL/BufrStubImagePlugin.py | 2 +- src/PIL/CurImagePlugin.py | 2 +- src/PIL/DdsImagePlugin.py | 2 +- src/PIL/EpsImagePlugin.py | 6 ++++-- src/PIL/FitsImagePlugin.py | 2 +- src/PIL/FpxImagePlugin.py | 2 +- src/PIL/FtexImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 4 ++-- src/PIL/GimpGradientFile.py | 2 +- src/PIL/GimpPaletteFile.py | 2 +- src/PIL/GribStubImagePlugin.py | 2 +- src/PIL/Hdf5StubImagePlugin.py | 2 +- src/PIL/IcnsImagePlugin.py | 8 ++++---- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 4 ++-- src/PIL/Image.py | 2 +- src/PIL/Jpeg2KImagePlugin.py | 5 ++--- src/PIL/JpegImagePlugin.py | 20 +++++++++---------- src/PIL/McIdasImagePlugin.py | 2 +- src/PIL/MicImagePlugin.py | 2 +- src/PIL/MpegImagePlugin.py | 2 +- src/PIL/MspImagePlugin.py | 4 ++-- src/PIL/PaletteFile.py | 2 +- src/PIL/PcdImagePlugin.py | 2 +- src/PIL/PixarImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 2 +- src/PIL/PsdImagePlugin.py | 2 +- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 4 ++-- src/PIL/WebPImagePlugin.py | 2 +- src/PIL/WmfImagePlugin.py | 8 +++----- src/PIL/XVThumbImagePlugin.py | 2 +- src/PIL/XbmImagePlugin.py | 2 +- src/PIL/XpmImagePlugin.py | 4 ++-- 43 files changed, 72 insertions(+), 73 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 311085cf7..ab8f2d5a1 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -77,8 +77,8 @@ def test_app(test_file: str) -> None: with Image.open(test_file) as im: assert im.applist[0][0] == "APP1" assert im.applist[1][0] == "APP2" - assert ( - im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" + assert im.applist[1][1].startswith( + b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" ) assert len(im.applist) == 2 diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index d9a834c75..c68a20d7a 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -40,7 +40,7 @@ def test_read_exif_metadata() -> None: def test_read_exif_metadata_without_prefix() -> None: with Image.open("Tests/images/flower2.webp") as im: # Assert prefix is not present - assert im.info["exif"][:6] != b"Exif\x00\x00" + assert not im.info["exif"].startswith(b"Exif\x00\x00") exif = im.getexif() assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" diff --git a/Tests/test_image.py b/Tests/test_image.py index 4c8aeaa3d..5474f951c 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -74,12 +74,12 @@ class TestImage: def test_sanity(self) -> None: im = Image.new("L", (100, 100)) - assert repr(im)[:45] == " bool: - return prefix[:4] == b"DDS " + return prefix.startswith(b"DDS ") Image.register_open(DdsImageFile.format, DdsImageFile, _accept) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 2e853224d..9e7d14c57 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -54,7 +54,7 @@ true color. def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"SPAM" + return prefix.startswith(b"SPAM") class SpamImageFile(ImageFile.ImageFile): diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index bfd66aa6a..f175e2f4f 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -43,7 +43,7 @@ def bdf_char( s = f.readline() if not s: return None - if s[:9] == b"STARTCHAR": + if s.startswith(b"STARTCHAR"): break id = s[9:].strip().decode("ascii") @@ -51,7 +51,7 @@ def bdf_char( props = {} while True: s = f.readline() - if not s or s[:6] == b"BITMAP": + if not s or s.startswith(b"BITMAP"): break i = s.find(b" ") props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") @@ -60,7 +60,7 @@ def bdf_char( bitmap = bytearray() while True: s = f.readline() - if not s or s[:7] == b"ENDCHAR": + if not s or s.startswith(b"ENDCHAR"): break bitmap += s[:-1] @@ -96,7 +96,7 @@ class BdfFontFile(FontFile.FontFile): super().__init__() s = fp.readline() - if s[:13] != b"STARTFONT 2.1": + if not s.startswith(b"STARTFONT 2.1"): msg = "not a valid BDF file" raise SyntaxError(msg) @@ -105,7 +105,7 @@ class BdfFontFile(FontFile.FontFile): while True: s = fp.readline() - if not s or s[:13] == b"ENDPROPERTIES": + if not s or s.startswith(b"ENDPROPERTIES"): break i = s.find(b" ") props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 8585a8e60..5747c1252 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -246,7 +246,7 @@ class BLPFormatError(NotImplementedError): def _accept(prefix: bytes) -> bool: - return prefix[:4] in (b"BLP1", b"BLP2") + return prefix.startswith((b"BLP1", b"BLP2")) class BlpImageFile(ImageFile.ImageFile): diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index bf8f29577..d60ea591a 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -50,7 +50,7 @@ BIT2MODE = { def _accept(prefix: bytes) -> bool: - return prefix[:2] == b"BM" + return prefix.startswith(b"BM") def _dib_accept(prefix: bytes) -> bool: diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 50c41c482..8c5da14f5 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" + return prefix.startswith((b"BUFR", b"ZCZC")) class BufrStubImageFile(ImageFile.StubImageFile): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index c4be0ceca..b817dbc87 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -26,7 +26,7 @@ from ._binary import i32le as i32 def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\0\0\2\0" + return prefix.startswith(b"\0\0\2\0") ## diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 9349e2841..cdae8dfee 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -564,7 +564,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"DDS " + return prefix.startswith(b"DDS ") Image.register_open(DdsImageFile.format, DdsImageFile, _accept) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 36ba15ec5..5e2ddad99 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -170,7 +170,9 @@ def Ghostscript( def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) + return prefix.startswith(b"%!PS") or ( + len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5 + ) ## @@ -295,7 +297,7 @@ class EpsImageFile(ImageFile.ImageFile): m = field.match(s) if m: k = m.group(1) - if k[:8] == "PS-Adobe": + if k.startswith("PS-Adobe"): self.info["PS-Adobe"] = k[9:] else: self.info[k] = "" diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 6bbd2641a..a3fdc0efe 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -17,7 +17,7 @@ from . import Image, ImageFile def _accept(prefix: bytes) -> bool: - return prefix[:6] == b"SIMPLE" + return prefix.startswith(b"SIMPLE") class FitsImageFile(ImageFile.ImageFile): diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 4cfcb067d..fd992cd9e 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -42,7 +42,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC + return prefix.startswith(olefile.MAGIC) ## diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 0516b760c..26e5bd4a6 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -108,7 +108,7 @@ class FtexImageFile(ImageFile.ImageFile): def _accept(prefix: bytes) -> bool: - return prefix[:4] == MAGIC + return prefix.startswith(MAGIC) Image.register_open(FtexImageFile.format, FtexImageFile, _accept) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index ff7262efc..259e93f09 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -67,7 +67,7 @@ LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST def _accept(prefix: bytes) -> bool: - return prefix[:6] in [b"GIF87a", b"GIF89a"] + return prefix.startswith((b"GIF87a", b"GIF89a")) ## @@ -257,7 +257,7 @@ class GifImageFile(ImageFile.ImageFile): # application extension # info["extension"] = block, self.fp.tell() - if block[:11] == b"NETSCAPE2.0": + if block.startswith(b"NETSCAPE2.0"): block = self.data() if block and len(block) >= 3 and block[0] == 1: self.info["loop"] = i16(block, 1) diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 220eac57e..ec62f8e4e 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -116,7 +116,7 @@ class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" def __init__(self, fp: IO[bytes]) -> None: - if fp.readline()[:13] != b"GIMP Gradient": + if not fp.readline().startswith(b"GIMP Gradient"): msg = "not a GIMP gradient file" raise SyntaxError(msg) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 4cad0ebee..1b7a394c0 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -29,7 +29,7 @@ class GimpPaletteFile: def __init__(self, fp: IO[bytes]) -> None: palette = [o8(i) * 3 for i in range(256)] - if fp.readline()[:12] != b"GIMP Palette": + if not fp.readline().startswith(b"GIMP Palette"): msg = "not a GIMP palette file" raise SyntaxError(msg) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index eb1b1483b..439fc5a3e 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"GRIB" and prefix[7] == 1 + return prefix.startswith(b"GRIB") and prefix[7] == 1 class GribStubImageFile(ImageFile.StubImageFile): diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index ddc218508..76e640f15 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:8] == b"\x89HDF\r\n\x1a\n" + return prefix.startswith(b"\x89HDF\r\n\x1a\n") class HDF5StubImageFile(ImageFile.StubImageFile): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 9757b2b14..a5d5b93ae 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -117,14 +117,14 @@ def read_png_or_jpeg2000( sig = fobj.read(12) im: Image.Image - if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": + if sig.startswith(b"\x89PNG\x0d\x0a\x1a\x0a"): fobj.seek(start) im = PngImagePlugin.PngImageFile(fobj) Image._decompression_bomb_check(im.size) return {"RGBA": im} elif ( - sig[:4] == b"\xff\x4f\xff\x51" - or sig[:4] == b"\x0d\x0a\x87\x0a" + sig.startswith(b"\xff\x4f\xff\x51") + or sig.startswith(b"\x0d\x0a\x87\x0a") or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" ): if not enable_jpeg2k: @@ -387,7 +387,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == MAGIC + return prefix.startswith(MAGIC) Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index e879f1801..55c57f203 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -118,7 +118,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _accept(prefix: bytes) -> bool: - return prefix[:4] == _MAGIC + return prefix.startswith(_MAGIC) class IconHeader(NamedTuple): diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 2a26d0b29..270a29467 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -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 not s.startswith(b"\x1a"): s = self.fp.read(1) if not s: msg = "File truncated" @@ -247,7 +247,7 @@ class ImImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack - if self.rawmode[:2] == "F;": + if self.rawmode.startswith("F;"): # ifunc95 formats try: # use bit decoder (if necessary) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a5243549f..6a2aa3e4c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3998,7 +3998,7 @@ class Exif(_ExifBase): if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 - if tag_data[:8] == b"FUJIFILM": + if tag_data.startswith(b"FUJIFILM"): ifd_offset = i32le(tag_data, 8) ifd_data = tag_data[ifd_offset:] diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 67828358d..e0f4ecae5 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -352,9 +352,8 @@ class Jpeg2KImageFile(ImageFile.ImageFile): def _accept(prefix: bytes) -> bool: - return ( - prefix[:4] == b"\xff\x4f\xff\x51" - or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" + return prefix.startswith( + (b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a") ) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a1c9c443a..3e882403b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -77,7 +77,7 @@ def APP(self: JpegImageFile, marker: int) -> None: self.app[app] = s # compatibility self.applist.append((app, s)) - if marker == 0xFFE0 and s[:4] == b"JFIF": + if marker == 0xFFE0 and s.startswith(b"JFIF"): # extract JFIF information self.info["jfif"] = version = i16(s, 5) # version self.info["jfif_version"] = divmod(version, 256) @@ -95,19 +95,19 @@ def APP(self: JpegImageFile, marker: int) -> None: 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": + elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"): # extract EXIF information if "exif" in self.info: self.info["exif"] += s[6:] else: self.info["exif"] = s self._exif_offset = self.fp.tell() - n + 6 - elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00": + elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"): self.info["xmp"] = s.split(b"\x00", 1)[1] - elif marker == 0xFFE2 and s[:5] == b"FPXR\0": + elif marker == 0xFFE2 and s.startswith(b"FPXR\0"): # extract FlashPix information (incomplete) self.info["flashpix"] = s # FIXME: value will change - elif marker == 0xFFE2 and s[:12] == b"ICC_PROFILE\0": + elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"): # Since an ICC profile can be larger than the maximum size of # a JPEG marker (64K), we need provisions to split it into # multiple markers. The format defined by the ICC specifies @@ -120,7 +120,7 @@ def APP(self: JpegImageFile, marker: int) -> None: # reassemble the profile, rather than assuming that the APP2 # markers appear in the correct sequence. self.icclist.append(s) - elif marker == 0xFFED and s[:14] == b"Photoshop 3.0\x00": + elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"): # parse the image resource block offset = 14 photoshop = self.info.setdefault("photoshop", {}) @@ -153,7 +153,7 @@ def APP(self: JpegImageFile, marker: int) -> None: except struct.error: break # insufficient data - elif marker == 0xFFEE and s[:5] == b"Adobe": + elif marker == 0xFFEE and s.startswith(b"Adobe"): self.info["adobe"] = i16(s, 5) # extract Adobe custom properties try: @@ -162,7 +162,7 @@ def APP(self: JpegImageFile, marker: int) -> None: pass else: self.info["adobe_transform"] = adobe_transform - elif marker == 0xFFE2 and s[:4] == b"MPF\0": + elif marker == 0xFFE2 and s.startswith(b"MPF\0"): # extract MPO information self.info["mp"] = s[4:] # offset is current location minus buffer size @@ -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.startswith(b"\xff\xd8\xff") ## @@ -547,7 +547,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None: return None file_contents = io.BytesIO(data) head = file_contents.read(8) - endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<" + endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<" # process dictionary from . import TiffImagePlugin diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 5dd031be3..b4460a9a5 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -23,7 +23,7 @@ from . import Image, ImageFile def _accept(prefix: bytes) -> bool: - return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" + return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04") ## diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 5f23a34b9..eb10a4c82 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -26,7 +26,7 @@ from . import Image, TiffImagePlugin def _accept(prefix: bytes) -> bool: - return prefix[:8] == olefile.MAGIC + return prefix.startswith(olefile.MAGIC) ## diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index ad4d3e937..5aa00d05b 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -54,7 +54,7 @@ class BitStream: def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\x00\x00\x01\xb3" + return prefix.startswith(b"\x00\x00\x01\xb3") ## diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index ef6ae87f8..277087a86 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -37,7 +37,7 @@ from ._binary import o16le as o16 def _accept(prefix: bytes) -> bool: - return prefix[:4] in [b"DanM", b"LinS"] + return prefix.startswith((b"DanM", b"LinS")) ## @@ -69,7 +69,7 @@ class MspImageFile(ImageFile.ImageFile): self._mode = "1" self._size = i16(s, 4), i16(s, 6) - if s[:4] == b"DanM": + if s.startswith(b"DanM"): self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] else: self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 81652e5ee..2a26e5d4e 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -32,7 +32,7 @@ class PaletteFile: if not s: break - if s[:1] == b"#": + if s.startswith(b"#"): continue if len(s) > 100: msg = "bad palette file" diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index ac40383f9..3aa249988 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -34,7 +34,7 @@ class PcdImageFile(ImageFile.ImageFile): self.fp.seek(2048) s = self.fp.read(2048) - if s[:4] != b"PCD_": + if not s.startswith(b"PCD_"): msg = "not a PCD file" raise SyntaxError(msg) diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 5c465bbdc..d2b6d0a97 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -28,7 +28,7 @@ from ._binary import i16le as i16 def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"\200\350\000\000" + return prefix.startswith(b"\200\350\000\000") ## diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 5ea87686d..4fc6217e1 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -740,7 +740,7 @@ class PngStream(ChunkStream): def _accept(prefix: bytes) -> bool: - return prefix[:8] == _MAGIC + return prefix.startswith(_MAGIC) ## diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index fb228f572..03afa2d2e 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -47,7 +47,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" + return prefix.startswith(b"P") and prefix[1] in b"0123456fy" ## diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 8ff5e3908..c59d302e5 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -47,7 +47,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"8BPS" + return prefix.startswith(b"8BPS") ## diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 01cc868b2..df552243e 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -14,7 +14,7 @@ from ._binary import i32be as i32 def _accept(prefix: bytes) -> bool: - return prefix[:4] == b"qoif" + return prefix.startswith(b"qoif") class QoiImageFile(ImageFile.ImageFile): diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f557d104b..0454038e8 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -288,7 +288,7 @@ if not getattr(Image.core, "libtiff_support_custom_tags", True): def _accept(prefix: bytes) -> bool: - return prefix[:4] in PREFIXES + return prefix.startswith(tuple(PREFIXES)) def _limit_rational( @@ -1280,7 +1280,7 @@ class TiffImageFile(ImageFile.ImageFile): blocks = {} val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: - while val[:4] == b"8BIM": + while val.startswith(b"8BIM"): id = i16(val[4:6]) n = math.ceil((val[6] + 1) / 2) * 2 size = i32(val[6 + n : 10 + n]) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index cbbc24af0..c2dde4431 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -21,7 +21,7 @@ _VP8_MODES_BY_IDENTIFIER = { def _accept(prefix: bytes) -> bool | str: - is_riff_file_format = prefix[:4] == b"RIFF" + is_riff_file_format = prefix.startswith(b"RIFF") is_webp_file = prefix[8:12] == b"WEBP" is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 48e9823e8..04abd52f0 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -68,9 +68,7 @@ if hasattr(Image.core, "drawwmf"): def _accept(prefix: bytes) -> bool: - return ( - prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" - ) + return prefix.startswith((b"\xd7\xcd\xc6\x9a\x00\x00", b"\x01\x00\x00\x00")) ## @@ -87,7 +85,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): # check placable header s = self.fp.read(80) - if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": + if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): # placeable windows metafile # get units per inch @@ -116,7 +114,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): msg = "Unsupported WMF file format" raise SyntaxError(msg) - elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": + elif s.startswith(b"\x01\x00\x00\x00") and s[40:44] == b" EMF": # enhanced metafile # get bounding box diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 75333354d..cde28388f 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -34,7 +34,7 @@ for r in range(8): def _accept(prefix: bytes) -> bool: - return prefix[:6] == _MAGIC + return prefix.startswith(_MAGIC) ## diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 943a04470..1e57aa162 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -38,7 +38,7 @@ xbm_head = re.compile( def _accept(prefix: bytes) -> bool: - return prefix.lstrip()[:7] == b"#define" + return prefix.lstrip().startswith(b"#define") ## diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index b985aa5dc..328f88223 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -25,7 +25,7 @@ xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') def _accept(prefix: bytes) -> bool: - return prefix[:9] == b"/* XPM */" + return prefix.startswith(b"/* XPM */") ## @@ -81,7 +81,7 @@ class XpmImageFile(ImageFile.ImageFile): rgb = s[i + 1] if rgb == b"None": self.info["transparency"] = c - elif rgb[:1] == b"#": + elif rgb.startswith(b"#"): # FIXME: handle colour names (see ImagePalette.py) rgb = int(rgb[1:], 16) palette[c] = ( From 9665eb39726b000bf59c0c5e61793fcbb3c6ecd0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 15 Feb 2025 19:38:03 +0200 Subject: [PATCH 316/628] Replace slice and comparison with endswith --- src/PIL/ImImagePlugin.py | 4 ++-- src/PIL/MicImagePlugin.py | 2 +- src/PIL/XpmImagePlugin.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 270a29467..9f20b30f8 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -155,9 +155,9 @@ class ImImageFile(ImageFile.ImageFile): msg = "not an IM file" raise SyntaxError(msg) - if s[-2:] == b"\r\n": + if s.endswith(b"\r\n"): s = s[:-2] - elif s[-1:] == b"\n": + elif s.endswith(b"\n"): s = s[:-1] try: diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index eb10a4c82..bbddd972e 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -54,7 +54,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self.images = [ path for path in self.ole.listdir() - if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image" + if path[1:] and path[0].endswith(".ACI") and path[1] == "Image" ] # if we didn't find any images, this is probably not diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 328f88223..3c932c41b 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -67,9 +67,9 @@ class XpmImageFile(ImageFile.ImageFile): for _ in range(pal): s = self.fp.readline() - if s[-2:] == b"\r\n": + if s.endswith(b"\r\n"): s = s[:-2] - elif s[-1:] in b"\r\n": + elif s.endswith((b"\r", b"\n")): s = s[:-1] c = s[1] From 4b7e75be2d9305ffa25e113740aa08ff5d4fed74 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Feb 2025 20:47:17 +1100 Subject: [PATCH 317/628] Test errors --- Tests/test_file_sun.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 6cfff8730..ebb069379 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,10 +1,11 @@ from __future__ import annotations +import io import os import pytest -from PIL import Image, SunImagePlugin +from PIL import Image, SunImagePlugin, _binary from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -33,6 +34,44 @@ def test_im1() -> None: assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") +def _sun_header( + depth: int = 0, file_type: int = 0, palette_length: int = 0 +) -> io.BytesIO: + return io.BytesIO( + _binary.o32be(0x59A66A95) + + b"\x00" * 8 + + _binary.o32be(depth) + + b"\x00" * 4 + + _binary.o32be(file_type) + + b"\x00" * 4 + + _binary.o32be(palette_length) + ) + + +def test_unsupported_mode_bit_depth() -> None: + with pytest.raises(SyntaxError, match="Unsupported Mode/Bit Depth"): + with SunImagePlugin.SunImageFile(_sun_header()): + pass + + +def test_unsupported_color_palette_length() -> None: + with pytest.raises(SyntaxError, match="Unsupported Color Palette Length"): + with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1025)): + pass + + +def test_unsupported_palette_type() -> None: + with pytest.raises(SyntaxError, match="Unsupported Palette Type"): + with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1)): + pass + + +def test_unsupported_file_type() -> None: + with pytest.raises(SyntaxError, match="Unsupported Sun Raster file type"): + with SunImagePlugin.SunImageFile(_sun_header(depth=1, file_type=6)): + pass + + @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) From 5d40e6aead98fd7154c889b027c79f3e15c06661 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Feb 2025 20:29:35 +1100 Subject: [PATCH 318/628] Test RGBX raw mode --- Tests/test_file_sun.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index ebb069379..c2f162cf9 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -72,6 +72,22 @@ def test_unsupported_file_type() -> None: pass +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +def test_rgbx() -> None: + with open(os.path.join(EXTRA_DIR, "32bpp.ras"), "rb") as fp: + data = fp.read() + + # Set file type to 3 + data = data[:20] + _binary.o32be(3) + data[24:] + + with Image.open(io.BytesIO(data)) as im: + r, g, b = im.split() + im = Image.merge("RGB", (b, g, r)) + assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png")) + + @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) From b096018fdd5c6c36211023fd372c16a66b7a52f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Feb 2025 22:27:13 +1100 Subject: [PATCH 319/628] Update Sphinx to 8.2 to remove nitpick ignore --- docs/conf.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e1e3f1b8f..bfbcf9151 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import PIL # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "8.1" +needs_sphinx = "8.2" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -121,7 +121,7 @@ nitpicky = True # generating warnings in “nitpicky mode”. Note that type should include the domain name # if present. Example entries would be ('py:func', 'int') or # ('envvar', 'LD_LIBRARY_PATH'). -nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")] +nitpick_ignore = [("py:class", "_CmsProfileCompatible")] # -- Options for HTML output ---------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index aaaba0032..2ffd9faca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dynamic = [ optional-dependencies.docs = [ "furo", "olefile", - "sphinx>=8.1", + "sphinx>=8.2", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph", From 4415b4ad3631a96fb70610ba6672e5c14dcfa174 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Feb 2025 08:47:04 +1100 Subject: [PATCH 320/628] Updated libpng to 1.6.47 --- .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 f0c96d160..0f8eac5bb 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.2.0 -LIBPNG_VERSION=1.6.46 +LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f942716cb..5665abaab 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ V = { "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBIMAGEQUANT": "4.3.4", - "LIBPNG": "1.6.46", + "LIBPNG": "1.6.47", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", From dc94d1d8bba208cf7f24e207a3536ac6eaf22fa9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Feb 2025 18:27:05 +1100 Subject: [PATCH 321/628] Test opening file with plugin directly --- Tests/test_file_mpo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index ab8f2d5a1..6b4f6423b 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -29,12 +29,17 @@ def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile: @pytest.mark.parametrize("test_file", test_files) def test_sanity(test_file: str) -> None: - with Image.open(test_file) as im: + def check(im: ImageFile.ImageFile) -> None: im.load() assert im.mode == "RGB" assert im.size == (640, 480) assert im.format == "MPO" + with Image.open(test_file) as im: + check(im) + with MpoImagePlugin.MpoImageFile(test_file) as im: + check(im) + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: From ae6bb4cac2f666715666a05b46fa43942ef19201 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Feb 2025 23:28:25 +1100 Subject: [PATCH 322/628] Test invalid texture compression format --- Tests/test_file_ftex.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 0c544245a..fdd7b3757 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,5 +1,8 @@ from __future__ import annotations +import io +import struct + import pytest from PIL import FtexImagePlugin, Image @@ -23,3 +26,15 @@ def test_invalid_file() -> None: with pytest.raises(SyntaxError): FtexImagePlugin.FtexImageFile(invalid_file) + + +def test_invalid_texture() -> None: + with open("Tests/images/ftex_dxt1.ftc", "rb") as fp: + data = fp.read() + + # Change texture compression format + data = data[:24] + struct.pack(" Date: Thu, 20 Feb 2025 07:57:10 +1100 Subject: [PATCH 323/628] Only set mode when necessary --- src/PIL/FtexImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 26e5bd4a6..d60e75bb6 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -79,8 +79,6 @@ class FtexImageFile(ImageFile.ImageFile): self._size = struct.unpack("<2i", self.fp.read(8)) mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8)) - self._mode = "RGB" - # Only support single-format files. # I don't know of any multi-format file. assert format_count == 1 @@ -95,6 +93,7 @@ class FtexImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] elif format == Format.UNCOMPRESSED: + self._mode = "RGB" self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")] else: msg = f"Invalid texture compression format: {repr(format)}" From ae7c4920c9fba33b93c8938d07a9c16495dd6d69 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Feb 2025 08:09:44 +1100 Subject: [PATCH 324/628] Test that subsequent compile() calls do not change anything --- Tests/test_fontfile.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index 206499a04..575dada86 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -4,7 +4,20 @@ from pathlib import Path import pytest -from PIL import FontFile +from PIL import FontFile, Image + + +def test_compile() -> None: + font = FontFile.FontFile() + font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0))) + font.compile() + assert font.ysize == 1 + + font.ysize = 2 + font.compile() + + # Assert that compiling again did not change anything + assert font.ysize == 2 def test_save(tmp_path: Path) -> None: From 85f439f575a1ae5e3e11262e9b0a6d838f541aa6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 25 Feb 2025 18:46:22 +1100 Subject: [PATCH 325/628] _seek_check already raises an EOFError --- src/PIL/MicImagePlugin.py | 7 +------ src/PIL/PsdImagePlugin.py | 14 +++++--------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index bbddd972e..9ce38c427 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -73,12 +73,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def seek(self, frame: int) -> None: if not self._seek_check(frame): return - try: - filename = self.images[frame] - except IndexError as e: - msg = "no such frame" - raise EOFError(msg) from e - + filename = self.images[frame] self.fp = self.ole.openstream(filename) TiffImagePlugin.TiffImageFile._open(self) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index c59d302e5..0aada8a06 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -169,15 +169,11 @@ class PsdImageFile(ImageFile.ImageFile): return # seek to given layer (1..max) - try: - _, mode, _, tile = self.layers[layer - 1] - self._mode = mode - self.tile = tile - self.frame = layer - self.fp = self._fp - except IndexError as e: - msg = "no such layer" - raise EOFError(msg) from e + _, mode, _, tile = self.layers[layer - 1] + self._mode = mode + self.tile = tile + self.frame = layer + self.fp = self._fp def tell(self) -> int: # return layer number (0=image, 1..max=layers) From 153fd4801c2344a41260fe8b4b83bd491e51f535 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 27 Feb 2025 22:24:48 +1100 Subject: [PATCH 326/628] Revert "Do not install libimagequant" This reverts commit 1e115987afbc92aef02b489ed8fea1875821d174. --- .github/workflows/test-mingw.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 045926482..bb6d7dc37 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,6 +60,7 @@ 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 3407f765cc81f5be9046fe51c36f7cf1dec6790b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 28 Feb 2025 10:28:48 +1100 Subject: [PATCH 327/628] Document using encoderinfo on subsequent frames from #8483 --- src/PIL/Image.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6a2aa3e4c..d5bfda40e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2475,7 +2475,21 @@ class Image: format to use is determined from the filename extension. If a file object was used instead of a filename, this parameter should always be used. - :param params: Extra parameters to the image writer. + :param params: Extra parameters to the image writer. These can also be + set on the image itself through ``encoderinfo``. This is useful when + saving multiple images:: + + # Saving XMP data to a single image + from PIL import Image + red = Image.new("RGB", (1, 1), "#f00") + red.save("out.mpo", xmp=b"test") + + # Saving XMP data to the second frame of an image + from PIL import Image + black = Image.new("RGB", (1, 1)) + red = Image.new("RGB", (1, 1), "#f00") + red.encoderinfo = {"xmp": b"test"} + black.save("out.mpo", save_all=True, append_images=[red]) :returns: None :exception ValueError: If the output format could not be determined from the file name. Use the format option to solve this. From 5c93145061953d8633397bb79ace396ab1e71eb5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 28 Feb 2025 22:16:52 +1100 Subject: [PATCH 328/628] Allow encoderconfig and encoderinfo to be set for appended TIFF images --- Tests/test_file_tiff.py | 12 ++++++++++++ docs/handbook/image-file-formats.rst | 4 +--- src/PIL/TiffImagePlugin.py | 15 ++++++--------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a8a407963..dff961648 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -661,6 +661,18 @@ class TestFileTiff: assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[278] == 256 + im = hopper() + im2 = Image.new("L", (128, 128)) + im2.encoderinfo = {"tiffinfo": {278: 256}} + im.save(outfile, save_all=True, append_images=[im2]) + + with Image.open(outfile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + assert im.tag_v2[278] == 128 + + im.seek(1) + assert im.tag_v2[278] == 256 + def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" with Image.open(infile) as im: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a915ee4e2..219a070f3 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1162,9 +1162,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum **append_images** A list of images to append as additional frames. Each of the - images in the list can be single or multiframe images. Note however, that for - correct results, all the appended images should have the same - ``encoderinfo`` and ``encoderconfig`` properties. + images in the list can be single or multiframe images. .. versionadded:: 4.2.0 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0454038e8..4e6526be9 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2295,9 +2295,7 @@ class AppendingTiffWriter(io.BytesIO): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - encoderinfo = im.encoderinfo.copy() - encoderconfig = im.encoderconfig - append_images = list(encoderinfo.get("append_images", [])) + append_images = list(im.encoderinfo.get("append_images", [])) if not hasattr(im, "n_frames") and not append_images: return _save(im, fp, filename) @@ -2305,12 +2303,11 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: with AppendingTiffWriter(fp) as tf: for ims in [im] + append_images: - ims.encoderinfo = encoderinfo - ims.encoderconfig = encoderconfig - if not hasattr(ims, "n_frames"): - nfr = 1 - else: - nfr = ims.n_frames + if not hasattr(ims, "encoderinfo"): + ims.encoderinfo = {} + if not hasattr(ims, "encoderconfig"): + ims.encoderconfig = () + nfr = getattr(ims, "n_frames", 1) for idx in range(nfr): ims.seek(idx) From d6b94421d0eff83249adf0c4191d3b3eda2b5b90 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Mar 2025 11:37:49 +1100 Subject: [PATCH 329/628] Updated harfbuzz to 10.4.0 --- .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 f0c96d160..50b7ad488 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.2.0 +HARFBUZZ_VERSION=10.4.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 f942716cb..c21258cb9 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.2.0", + "HARFBUZZ": "10.4.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBIMAGEQUANT": "4.3.4", From ff4f5d4cb68f6cfe9713dd72ad343e505b57b1f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Mar 2025 21:41:30 +1100 Subject: [PATCH 330/628] Test ValueError --- Tests/test_font_bdf.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 136070f9e..8d78019b3 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,5 +1,7 @@ from __future__ import annotations +import io + import pytest from PIL import BdfFontFile, FontFile @@ -8,13 +10,20 @@ filename = "Tests/images/courB08.bdf" def test_sanity() -> None: - with open(filename, "rb") as test_file: - font = BdfFontFile.BdfFontFile(test_file) + with open(filename, "rb") as fp: + font = BdfFontFile.BdfFontFile(fp) assert isinstance(font, FontFile.FontFile) assert len([_f for _f in font.glyph if _f]) == 190 +def test_valueerror() -> None: + with open(filename, "rb") as fp: + data = fp.read() + data = data[:2650] + b"\x00\x00" + data[2652:] + BdfFontFile.BdfFontFile(io.BytesIO(data)) + + def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): From 397f8c752b583cd781fa21fe03149c8781035247 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:50:23 +0000 Subject: [PATCH 331/628] Update dependency cibuildwheel to v2.23.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 833aca23d..2fd3eb6ff 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.22.0 +cibuildwheel==2.23.0 From db4534a8cf35bd7d3a531f5e19e889c7b7c63c30 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:00:26 +0200 Subject: [PATCH 332/628] Build PyPy3.11 wheel for macOS 10.15 x86_64 --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index db8e4d58b..1fe6badae 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -63,7 +63,7 @@ jobs: - name: "macOS 10.15 x86_64" os: macos-13 cibw_arch: x86_64 - build: "pp310*" + build: "pp3*" macosx_deployment_target: "10.15" - name: "macOS arm64" os: macos-latest From c60682af67a364fe185c3b9eea0477980af07cdd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 2 Mar 2025 22:34:58 +1100 Subject: [PATCH 333/628] JPEG comments are from the COM marker --- docs/handbook/image-file-formats.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a915ee4e2..991cadaa2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -454,7 +454,8 @@ The :py:meth:`~PIL.Image.open` method may set the following Raw EXIF data from the image. **comment** - A comment about the image. + A comment about the image, from the COM marker. This is separate from the + UserComment tag that may be stored in the EXIF data. .. versionadded:: 7.1.0 From ebc7a17d86a7789119e1b9c4ea5d186306ed276c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 07:24:13 +1100 Subject: [PATCH 334/628] Removed _show --- src/PIL/ImageTk.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index bf29fdba5..e6a9d8eea 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -28,7 +28,7 @@ from __future__ import annotations import tkinter from io import BytesIO -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from . import Image, ImageFile @@ -263,28 +263,3 @@ def getimage(photo: PhotoImage) -> Image.Image: _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim()) return im - - -def _show(image: Image.Image, title: str | None) -> None: - """Helper for the Image.show method.""" - - class UI(tkinter.Label): - def __init__(self, master: tkinter.Toplevel, im: Image.Image) -> None: - self.image: BitmapImage | PhotoImage - if im.mode == "1": - self.image = BitmapImage(im, foreground="white", master=master) - else: - self.image = PhotoImage(im, master=master) - if TYPE_CHECKING: - image = cast(tkinter._Image, self.image) - else: - image = self.image - super().__init__(master, image=image, bg="black", bd=0) - - if not getattr(tkinter, "_default_root"): - msg = "tkinter not initialized" - raise OSError(msg) - top = tkinter.Toplevel() - if title: - top.title(title) - UI(top, image).pack() From 92cc9bf9027c4767967264a9622f8cde674e3c02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 08:46:20 +1100 Subject: [PATCH 335/628] Support reading grayscale images with 4 channels --- Tests/test_file_jpeg2k.py | 12 ++++++++++++ src/libImaging/Jpeg2KDecode.c | 1 + 2 files changed, 13 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 5748fa5a1..01172fdbb 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -313,6 +313,18 @@ def test_rgba(ext: str) -> None: assert im.mode == "RGBA" +def test_grayscale_four_channels() -> None: + with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp: + data = fp.read() + + # Change color space to OPJ_CLRSPC_GRAY + data = data[:76] + b"\x11" + data[77:] + + with Image.open(BytesIO(data)) as im: + im.load() + assert im.mode == "RGBA" + + @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index 4f185b529..cc6955ca5 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -615,6 +615,7 @@ static const struct j2k_decode_unpacker j2k_unpackers[] = { {"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, {"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, {"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {"RGBA", OPJ_CLRSPC_GRAY, 4, 1, j2ku_srgba_rgba}, {"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, {"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, {"CMYK", OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba}, From 2d97521aa3a7a5f4ab114354e881e05916fae483 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 02:38:52 +0000 Subject: [PATCH 336/628] Update dependency mypy to v1.15.0 --- .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 10e59b885..2e3610478 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.14.1 +mypy==1.15.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From d6272297fc6c8e2e796c264b4229e5d20045aa9c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 14:48:00 +1100 Subject: [PATCH 337/628] Ignore override --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0454038e8..3d36d1abc 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -404,7 +404,7 @@ class IFDRational(Rational): def __repr__(self) -> str: return str(float(self._val)) - def __hash__(self) -> int: + def __hash__(self) -> int: # type: ignore[override] return self._val.__hash__() def __eq__(self, other: object) -> bool: From 4161bb1645fc66c9d587aafc53214f797831fa52 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 19:10:55 +1100 Subject: [PATCH 338/628] Corrected error when XMP is tuple --- Tests/test_imageops.py | 9 +++++++++ src/PIL/ImageOps.py | 14 +++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 3621aa50f..9f2fd5ba2 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -448,6 +448,15 @@ def test_exif_transpose() -> None: assert 0x0112 not in transposed_im.getexif() +def test_exif_transpose_with_xmp_tuple() -> None: + with Image.open("Tests/images/xmp_tags_orientation.png") as im: + assert im.getexif()[0x0112] == 3 + + im.info["xmp"] = (b"test",) + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + + def test_exif_transpose_xml_without_xmp() -> None: with Image.open("Tests/images/xmp_tags_orientation.png") as im: assert im.getexif()[0x0112] == 3 diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index fef1d7328..75dfbee22 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -729,11 +729,15 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image r"([0-9])", ): value = exif_image.info[key] - exif_image.info[key] = ( - re.sub(pattern, "", value) - if isinstance(value, str) - else re.sub(pattern.encode(), b"", value) - ) + if isinstance(value, str): + value = re.sub(pattern, "", value) + elif isinstance(value, tuple): + value = tuple( + re.sub(pattern.encode(), b"", v) for v in value + ) + else: + value = re.sub(pattern.encode(), b"", value) + exif_image.info[key] = value if not in_place: return transposed_image elif not in_place: From 51183c22042303e464d86a26245c272f733f35f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 21:58:29 +1100 Subject: [PATCH 339/628] Fixed loading images --- Tests/test_file_gd.py | 3 +++ src/PIL/GdImageFile.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index d512df284..806532c17 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -4,6 +4,8 @@ import pytest from PIL import GdImageFile, UnidentifiedImageError +from .helper import assert_image_similar_tofile + TEST_GD_FILE = "Tests/images/hopper.gd" @@ -11,6 +13,7 @@ def test_sanity() -> None: with GdImageFile.open(TEST_GD_FILE) as im: assert im.size == (128, 128) assert im.format == "GD" + assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14) def test_bad_mode() -> None: diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index fc4801e9d..891225ce2 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -56,7 +56,7 @@ class GdImageFile(ImageFile.ImageFile): msg = "Not a valid GD 2.x .gd file" raise SyntaxError(msg) - self._mode = "L" # FIXME: "P" + self._mode = "P" self._size = i16(s, 2), i16(s, 4) true_color = s[6] @@ -68,14 +68,14 @@ class GdImageFile(ImageFile.ImageFile): self.info["transparency"] = tindex self.palette = ImagePalette.raw( - "XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4] + "RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4] ) self.tile = [ ImageFile._Tile( "raw", (0, 0) + self.size, - 7 + true_color_offset + 4 + 256 * 4, + 7 + true_color_offset + 6 + 256 * 4, "L", ) ] From a1a467bda2d79baa70775c6cd0d52ddcc1496ee8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Mar 2025 23:55:19 +1100 Subject: [PATCH 340/628] Image.core.outline will no longer raise an AttributeError --- Tests/test_imagedraw.py | 4 ---- src/PIL/ImageDraw.py | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index d127175eb..232cbb16c 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -448,7 +448,6 @@ def test_shape1() -> None: x3, y3 = 95, 5 # Act - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.move(x0, y0) s.curve(x1, y1, x2, y2, x3, y3) @@ -470,7 +469,6 @@ def test_shape2() -> None: x3, y3 = 5, 95 # Act - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.move(x0, y0) s.curve(x1, y1, x2, y2, x3, y3) @@ -489,7 +487,6 @@ def test_transform() -> None: draw = ImageDraw.Draw(im) # Act - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.line(0, 0) s.transform((0, 0, 0, 0, 0, 0)) @@ -1526,7 +1523,6 @@ def test_same_color_outline(bbox: Coords) -> None: x2, y2 = 95, 50 x3, y3 = 95, 5 - assert ImageDraw.Outline is not None s = ImageDraw.Outline() s.move(x0, y0) s.curve(x1, y1, x2, y2, x3, y3) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 742b5f587..c4ebc5931 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -42,11 +42,7 @@ from ._deprecate import deprecate from ._typing import Coords # experimental access to the outline API -Outline: Callable[[], Image.core._Outline] | None -try: - Outline = Image.core.outline -except AttributeError: - Outline = None +Outline: Callable[[], Image.core._Outline] = Image.core.outline if TYPE_CHECKING: from . import ImageDraw2, ImageFont From c1703f53307e668a1eef54e78350f22bb5cbe194 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:15:48 +0000 Subject: [PATCH 341/628] [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.9.4 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.4...v0.9.9) - [github.com/PyCQA/bandit: 1.8.2 → 1.8.3](https://github.com/PyCQA/bandit/compare/1.8.2...1.8.3) - [github.com/python-jsonschema/check-jsonschema: 0.31.1 → 0.31.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.1...0.31.2) - [github.com/woodruffw/zizmor-pre-commit: v1.3.0 → v1.4.1](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.3.0...v1.4.1) - [github.com/tox-dev/pyproject-fmt: v2.5.0 → v2.5.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.5.0...v2.5.1) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8c8cee15..5ff947d41 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.9.4 + rev: v0.9.9 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.8.2 + rev: 1.8.3 hooks: - id: bandit args: [--severity-level=high] @@ -50,14 +50,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.1 + rev: 0.31.2 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.3.0 + rev: v1.4.1 hooks: - id: zizmor @@ -67,7 +67,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.5.0 + rev: v2.5.1 hooks: - id: pyproject-fmt From 5ce8929ed467712025c82311f0cdfa196224a06a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 4 Mar 2025 07:48:12 +1100 Subject: [PATCH 342/628] Updated test name Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_font_bdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 8d78019b3..2ece5457a 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -17,7 +17,7 @@ def test_sanity() -> None: assert len([_f for _f in font.glyph if _f]) == 190 -def test_valueerror() -> None: +def test_zero_width_chars() -> None: with open(filename, "rb") as fp: data = fp.read() data = data[:2650] + b"\x00\x00" + data[2652:] From 1f4beb4a5c5724019ea9b0683432cbc3357d10cc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:53:47 +0200 Subject: [PATCH 343/628] Lint with flake8-pie --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2ffd9faca..780a938a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PGH", # pygrep-hooks + "PIE", # flake8-pie "PT", # flake8-pytest-style "PYI", # flake8-pyi "RUF100", # unused noqa (yesqa) @@ -133,6 +134,7 @@ lint.ignore = [ "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator "E241", # Multiple spaces after ',' + "PIE790", # flake8-pie: unnecessary-placeholder "PT001", # pytest-fixture-incorrect-parentheses-style "PT007", # pytest-parametrize-values-wrong-type "PT011", # pytest-raises-too-broad From e4cac21044d6b7bfe958e9f9d0a4c8d150b444e7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:54:22 +0200 Subject: [PATCH 344/628] Don't use start=0 in range() --- Tests/test_file_gif.py | 2 +- Tests/test_file_webp.py | 2 +- Tests/test_imagedraw.py | 4 ++-- Tests/test_imagepalette.py | 2 +- Tests/test_imagesequence.py | 2 +- Tests/test_pickle.py | 8 ++++---- src/PIL/Image.py | 6 +++--- src/PIL/ImageDraw.py | 4 ++-- src/PIL/ImageOps.py | 10 +++++----- src/PIL/JpegImagePlugin.py | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 2254178d5..d2592da97 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -601,7 +601,7 @@ def test_save_dispose(tmp_path: Path) -> None: Image.new("L", (100, 100), "#111"), Image.new("L", (100, 100), "#222"), ] - for method in range(0, 4): + for method in range(4): im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) with Image.open(out) as img: for _ in range(2): diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index abe888241..6f6074ef2 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -231,7 +231,7 @@ class TestFileWebp: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) - difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) + difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3)) assert difference < 5 def test_duration(self, tmp_path: Path) -> None: diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 232cbb16c..1b4d09f39 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1044,8 +1044,8 @@ def create_base_image_draw( background2: tuple[int, int, int] = GRAY, ) -> tuple[Image.Image, ImageDraw.ImageDraw]: img = Image.new(mode, size, background1) - for x in range(0, size[0]): - for y in range(0, size[1]): + for x in range(size[0]): + for y in range(size[1]): if (x + y) % 2 == 0: img.putpixel((x, y), background2) return img, ImageDraw.Draw(img) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e0b6359b0..782022f51 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -112,7 +112,7 @@ def test_make_linear_lut() -> None: assert isinstance(lut, list) assert len(lut) == 256 # Check values - for i in range(0, len(lut)): + for i in range(len(lut)): assert lut[i] == i diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 9b37435eb..26b287bb4 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -32,7 +32,7 @@ def test_sanity(tmp_path: Path) -> None: def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: i = ImageSequence.Iterator(im) - for index in range(0, im.n_frames): + for index in range(im.n_frames): assert i[index] == next(i) with pytest.raises(IndexError): i[index + 1] diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index c4f8de013..05c41a802 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non ("Tests/images/itxt_chunks.png", None), ], ) -@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) +@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) def test_pickle_image( tmp_path: Path, test_file: str, test_mode: str | None, protocol: int ) -> None: @@ -92,7 +92,7 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: im = im.convert("PA") # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): im._mode = "LA" with open(filename, "wb") as f: pickle.dump(im, f, protocol) @@ -133,7 +133,7 @@ def helper_assert_pickled_font_images( @skip_unless_feature("freetype2") -@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_string(protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -147,7 +147,7 @@ def test_pickle_font_string(protocol: int) -> None: @skip_unless_feature("freetype2") -@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6a2aa3e4c..684c87c4d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1001,7 +1001,7 @@ class Image: elif len(mode) == 3: transparency = tuple( convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) - for i in range(0, len(transparency)) + for i in range(len(transparency)) ) new_im.info["transparency"] = transparency return new_im @@ -4003,7 +4003,7 @@ class Exif(_ExifBase): ifd_data = tag_data[ifd_offset:] makernote = {} - for i in range(0, struct.unpack("H", tag_data[:2])[0]): + for i in range(struct.unpack(">H", tag_data[:2])[0]): ifd_tag, typ, count, data = struct.unpack( ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] ) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c4ebc5931..c2ed9034d 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -1204,7 +1204,7 @@ def _compute_regular_polygon_vertices( degrees = 360 / n_sides # Start with the bottom left polygon vertex current_angle = (270 - 0.5 * degrees) + rotation - for _ in range(0, n_sides): + for _ in range(n_sides): angles.append(current_angle) current_angle += degrees if current_angle > 360: @@ -1227,4 +1227,4 @@ def _color_diff( first = color1 if isinstance(color1, tuple) else (color1,) second = color2 if isinstance(color2, tuple) else (color2,) - return sum(abs(first[i] - second[i]) for i in range(0, len(second))) + return sum(abs(first[i] - second[i]) for i in range(len(second))) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 75dfbee22..da28854b5 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -213,14 +213,14 @@ def colorize( blue = [] # Create the low-end values - for i in range(0, blackpoint): + for i in range(blackpoint): red.append(rgb_black[0]) green.append(rgb_black[1]) blue.append(rgb_black[2]) # Create the mapping (2-color) if rgb_mid is None: - range_map = range(0, whitepoint - blackpoint) + range_map = range(whitepoint - blackpoint) for i in range_map: red.append( @@ -235,8 +235,8 @@ def colorize( # Create the mapping (3-color) else: - range_map1 = range(0, midpoint - blackpoint) - range_map2 = range(0, whitepoint - midpoint) + range_map1 = range(midpoint - blackpoint) + range_map2 = range(whitepoint - midpoint) for i in range_map1: red.append( @@ -256,7 +256,7 @@ def colorize( blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) # Create the high-end values - for i in range(0, 256 - whitepoint): + for i in range(256 - whitepoint): red.append(rgb_white[0]) green.append(rgb_white[1]) blue.append(rgb_white[2]) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 3e882403b..9465d8e2d 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -569,7 +569,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None: mpentries = [] try: rawmpentries = mp[0xB002] - for entrynum in range(0, quant): + for entrynum in range(quant): unpackedentry = struct.unpack_from( f"{endianness}LLLHH", rawmpentries, entrynum * 16 ) From a2b13cc02a68bd8f0bc3a9f84e603930c3a2496f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:56:43 +0200 Subject: [PATCH 345/628] Call startswith/endswith once with a tuple --- src/PIL/IcnsImagePlugin.py | 3 +-- src/PIL/TiffImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index a5d5b93ae..5a88429e5 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -123,8 +123,7 @@ def read_png_or_jpeg2000( Image._decompression_bomb_check(im.size) return {"RGBA": im} elif ( - sig.startswith(b"\xff\x4f\xff\x51") - or sig.startswith(b"\x0d\x0a\x87\x0a") + sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a")) or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" ): if not enable_jpeg2k: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d36d1abc..b8ff47a12 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1584,7 +1584,7 @@ class TiffImageFile(ImageFile.ImageFile): # byte order. elif rawmode == "I;16": rawmode = "I;16N" - elif rawmode.endswith(";16B") or rawmode.endswith(";16L"): + elif rawmode.endswith((";16B", ";16L")): rawmode = rawmode[:-1] + "N" # Offset in the tile tuple is 0, we go from 0,0 to From c0b5d013f6e3313456848f3969231e7ee3ee6031 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 4 Mar 2025 22:19:06 +1100 Subject: [PATCH 346/628] Test bad image size and unknown PCX mode --- Tests/test_file_pcx.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index b3f38c3e5..aa24189f4 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,5 +1,6 @@ from __future__ import annotations +import io from pathlib import Path import pytest @@ -36,6 +37,28 @@ def test_sanity(tmp_path: Path) -> None: im.save(f) +def test_bad_image_size() -> None: + with open("Tests/images/pil184.pcx", "rb") as fp: + data = fp.read() + data = data[:4] + b"\xff\xff" + data[6:] + + b = io.BytesIO(data) + with pytest.raises(SyntaxError, match="bad PCX image size"): + with PcxImagePlugin.PcxImageFile(b): + pass + + +def test_unknown_mode() -> None: + with open("Tests/images/pil184.pcx", "rb") as fp: + data = fp.read() + data = data[:3] + b"\xff" + data[4:] + + b = io.BytesIO(data) + with pytest.raises(OSError, match="unknown PCX mode"): + with Image.open(b): + pass + + def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" From 3607d1ade397fc5a5b41f2a0607a15927e2810fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Mar 2025 00:03:37 +1100 Subject: [PATCH 347/628] Use match argument --- Tests/test_file_libtiff.py | 6 ++---- Tests/test_file_ppm.py | 14 +++++--------- Tests/test_file_tiff.py | 3 +-- Tests/test_file_webp.py | 3 +-- Tests/test_image.py | 3 +-- Tests/test_imagedraw.py | 7 +++---- Tests/test_imagefile.py | 3 +-- Tests/test_imagemorph.py | 26 ++++++++++---------------- Tests/test_imagepath.py | 12 ++---------- 9 files changed, 26 insertions(+), 51 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 369c2db1b..f284c3f2f 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1140,11 +1140,9 @@ class TestFileLibTiff(LibTiffTestCase): def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: - with pytest.raises(OSError) as e: - im.load() - # Assert that the error code is IMAGING_CODEC_MEMORY - assert str(e.value) == "decoder error -9" + with pytest.raises(OSError, match="decoder error -9"): + im.load() @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index d87192ca5..c93a8c73a 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -293,12 +293,10 @@ def test_header_token_too_long(tmp_path: Path) -> None: with open(path, "wb") as f: f.write(b"P6\n 01234567890") - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Token too long in file header: 01234567890"): with Image.open(path): pass - assert str(e.value) == "Token too long in file header: 01234567890" - def test_truncated_file(tmp_path: Path) -> None: # Test EOF in header @@ -306,12 +304,10 @@ def test_truncated_file(tmp_path: Path) -> None: with open(path, "wb") as f: f.write(b"P6") - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Reached EOF while reading header"): with Image.open(path): pass - assert str(e.value) == "Reached EOF while reading header" - # Test EOF for PyDecoder fp = BytesIO(b"P5 3 1 4") with Image.open(fp) as im: @@ -335,12 +331,12 @@ def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) - with pytest.raises(ValueError) as e: + with pytest.raises( + ValueError, match="maxval must be greater than 0 and less than 65536" + ): with Image.open(path): pass - assert str(e.value) == "maxval must be greater than 0 and less than 65536" - def test_neg_ppm() -> None: # Storage.c accepted negative values for xsize, ysize. the diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a8a407963..c1ccf3fe2 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -134,9 +134,8 @@ class TestFileTiff: def test_set_legacy_api(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="Not allowing setting of legacy api"): ifd.legacy_api = False - assert str(e.value) == "Not allowing setting of legacy api" def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index abe888241..d8c4eb589 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -154,9 +154,8 @@ class TestFileWebp: @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path: Path) -> None: im = Image.new("RGB", (15000, 15000)) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="encoding error 6"): im.save(tmp_path / "temp.webp", method=0) - assert str(e.value) == "encoding error 6" @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 5474f951c..d64816b1e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -65,9 +65,8 @@ class TestImage: @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode: str) -> None: - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="unrecognized image mode"): Image.new(mode, (1, 1)) - assert str(e.value) == "unrecognized image mode" def test_exception_inheritance(self) -> None: assert issubclass(UnidentifiedImageError, OSError) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 232cbb16c..1af4455b8 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1626,7 +1626,7 @@ def test_compute_regular_polygon_vertices( 0, ValueError, "bounding_circle should contain 2D coordinates " - "and a radius (e.g. (x, y, r) or ((x, y), r) )", + r"and a radius \(e.g. \(x, y, r\) or \(\(x, y\), r\) \)", ), ( 3, @@ -1640,7 +1640,7 @@ def test_compute_regular_polygon_vertices( ((50, 50, 50), 25), 0, ValueError, - "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", + r"bounding_circle centre should contain 2D coordinates \(e.g. \(x, y\)\)", ), ( 3, @@ -1665,9 +1665,8 @@ def test_compute_regular_polygon_vertices_input_error_handling( expected_error: type[Exception], error_message: str, ) -> None: - with pytest.raises(expected_error) as e: + with pytest.raises(expected_error, match=error_message): ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type] - assert str(e.value) == error_message def test_continuous_horizontal_edges_polygon() -> None: diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index b05d29dae..c60a475a3 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -176,9 +176,8 @@ class TestImageFile: b"0" * ImageFile.SAFEBLOCK ) # only SAFEBLOCK bytes, so that the header is truncated ) - with pytest.raises(OSError) as e: + with pytest.raises(OSError, match="Truncated File Read"): BmpImagePlugin.BmpImageFile(b) - assert str(e.value) == "Truncated File Read" @skip_unless_feature("zlib") def test_truncated_with_errors(self) -> None: diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 6180a7b5d..515e29cea 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -80,15 +80,12 @@ def test_lut(op: str) -> None: def test_no_operator_loaded() -> None: im = Image.new("L", (1, 1)) mop = ImageMorph.MorphOp() - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="No operator loaded"): mop.apply(im) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="No operator loaded"): mop.match(im) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="No operator loaded"): mop.save_lut("") - assert str(e.value) == "No operator loaded" # Test the named patterns @@ -238,15 +235,12 @@ def test_incorrect_mode() -> None: im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Image mode must be L"): mop.apply(im) - assert str(e.value) == "Image mode must be L" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Image mode must be L"): mop.match(im) - assert str(e.value) == "Image mode must be L" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Image mode must be L"): mop.get_on_pixels(im) - assert str(e.value) == "Image mode must be L" def test_add_patterns() -> None: @@ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None: lb.add_patterns(new_patterns) # Act / Assert - with pytest.raises(Exception) as e: + with pytest.raises( + Exception, match='Syntax error in pattern "a pattern with a syntax error"' + ): lb.build_lut() - assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' def test_load_invalid_mrl() -> None: @@ -290,9 +285,8 @@ def test_load_invalid_mrl() -> None: mop = ImageMorph.MorphOp() # Act / Assert - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="Wrong size operator file!"): mop.load_lut(invalid_mrl) - assert str(e.value) == "Wrong size operator file!" def test_roundtrip_mrl(tmp_path: Path) -> None: diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 1b1ee6bac..1ebf12d22 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -81,13 +81,9 @@ def test_path_constructors( def test_invalid_path_constructors( coords: tuple[str, str] | Sequence[Sequence[int]], ) -> None: - # Act - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="incorrect coordinate type"): ImagePath.Path(coords) - # Assert - assert str(e.value) == "incorrect coordinate type" - @pytest.mark.parametrize( "coords", @@ -99,13 +95,9 @@ def test_invalid_path_constructors( ), ) def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: - # Act - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="wrong number of coordinates"): ImagePath.Path(coords) - # Assert - assert str(e.value) == "wrong number of coordinates" - @pytest.mark.parametrize( "coords, expected", From 2309f0fa60bae05881907e374afffc2257376fbc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Mar 2025 21:30:24 +1100 Subject: [PATCH 348/628] Inherit classes with abstractmethod from ABC --- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/Image.py | 4 ++-- src/PIL/ImageFile.py | 2 +- src/PIL/ImageFilter.py | 2 +- src/PIL/ImageShow.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 5747c1252..f7be7746d 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -291,7 +291,7 @@ class BlpImageFile(ImageFile.ImageFile): self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)] -class _BLPBaseDecoder(ImageFile.PyDecoder): +class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder): _pulls_fd = True def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 684c87c4d..c9c9c2e1b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2966,7 +2966,7 @@ class Image: # Abstract handlers. -class ImagePointHandler: +class ImagePointHandler(abc.ABC): """ Used as a mixin by point transforms (for use with :py:meth:`~PIL.Image.Image.point`) @@ -2977,7 +2977,7 @@ class ImagePointHandler: pass -class ImageTransformHandler: +class ImageTransformHandler(abc.ABC): """ Used as a mixin by geometry transforms (for use with :py:meth:`~PIL.Image.Image.transform`) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index c3901d488..4bc70cc76 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -438,7 +438,7 @@ class ImageFile(Image.Image): return self.tell() != frame -class StubHandler: +class StubHandler(abc.ABC): def open(self, im: StubImageFile) -> None: pass diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 1c8b29b11..05829d0c6 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -27,7 +27,7 @@ if TYPE_CHECKING: from ._typing import NumpyArray -class Filter: +class Filter(abc.ABC): @abc.abstractmethod def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore: pass diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index d62893d9c..dd240fb55 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -192,7 +192,7 @@ if sys.platform == "darwin": register(MacViewer) -class UnixViewer(Viewer): +class UnixViewer(abc.ABC, Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} From d186a2a8d60ea1889d3c02c54da9c01076d233e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Mar 2025 21:50:09 +1100 Subject: [PATCH 349/628] Replace NotImplementedError with abstractmethod --- src/PIL/ImageFile.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 4bc70cc76..1bf8a7e5f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -447,7 +447,7 @@ class StubHandler(abc.ABC): pass -class StubImageFile(ImageFile): +class StubImageFile(ImageFile, metaclass=abc.ABCMeta): """ Base class for stub image loaders. @@ -455,9 +455,9 @@ class StubImageFile(ImageFile): certain format, but relies on external code to load the file. """ + @abc.abstractmethod def _open(self) -> None: - msg = "StubImageFile subclass must implement _open" - raise NotImplementedError(msg) + pass def load(self) -> Image.core.PixelAccess | None: loader = self._load() @@ -471,10 +471,10 @@ class StubImageFile(ImageFile): self.__dict__ = image.__dict__ return image.load() + @abc.abstractmethod def _load(self) -> StubHandler | None: """(Hook) Find actual image loader.""" - msg = "StubImageFile subclass must implement _load" - raise NotImplementedError(msg) + pass class Parser: From 5ba72a9b54bd744724e4ec269268c16dd61bb472 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 6 Mar 2025 04:15:55 +1100 Subject: [PATCH 350/628] Merge pull request #8800 from radarhere/path_lists Allow coords to be sequence of lists --- Tests/test_imagedraw.py | 4 +++ Tests/test_imagepath.py | 17 ++------- src/path.c | 78 ++++++++++++++++++++++++++--------------- 3 files changed, 57 insertions(+), 42 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 967bd6738..2767418ea 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -39,6 +39,8 @@ BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X POINTS = ( ((10, 10), (20, 40), (30, 30)), [(10, 10), (20, 40), (30, 30)], + ([10, 10], [20, 40], [30, 30]), + [[10, 10], [20, 40], [30, 30]], (10, 10, 20, 40, 30, 30), [10, 10, 20, 40, 30, 30], ) @@ -46,6 +48,8 @@ POINTS = ( KITE_POINTS = ( ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], + ([10, 50], [70, 10], [90, 50], [70, 90], [10, 50]), + [[10, 50], [70, 10], [90, 50], [70, 90], [10, 50]], ) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 1ebf12d22..ad8acde49 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -68,21 +68,10 @@ def test_path_constructors( assert list(p) == [(0.0, 1.0)] -@pytest.mark.parametrize( - "coords", - ( - ("a", "b"), - ([0, 1],), - [[0, 1]], - ([0.0, 1.0],), - [[0.0, 1.0]], - ), -) -def test_invalid_path_constructors( - coords: tuple[str, str] | Sequence[Sequence[int]], -) -> None: +def test_invalid_path_constructors() -> None: + # Arrange / Act with pytest.raises(ValueError, match="incorrect coordinate type"): - ImagePath.Path(coords) + ImagePath.Path(("a", "b")) @pytest.mark.parametrize( diff --git a/src/path.c b/src/path.c index 5affe3a1f..38300547c 100644 --- a/src/path.c +++ b/src/path.c @@ -109,6 +109,39 @@ path_dealloc(PyPathObject *path) { #define PyPath_Check(op) (Py_TYPE(op) == &PyPathType) +static int +assign_item_to_array(double *xy, Py_ssize_t j, PyObject *op) { + if (PyFloat_Check(op)) { + xy[j++] = PyFloat_AS_DOUBLE(op); + } else if (PyLong_Check(op)) { + xy[j++] = (float)PyLong_AS_LONG(op); + } else if (PyNumber_Check(op)) { + xy[j++] = PyFloat_AsDouble(op); + } else if (PyList_Check(op)) { + for (int k = 0; k < 2; k++) { + PyObject *op1 = PyList_GetItemRef(op, k); + if (op1 == NULL) { + return -1; + } + j = assign_item_to_array(xy, j, op1); + Py_DECREF(op1); + if (j == -1) { + return -1; + } + } + } else { + double x, y; + if (PyArg_ParseTuple(op, "dd", &x, &y)) { + xy[j++] = x; + xy[j++] = y; + } else { + PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); + return -1; + } + } + return j; +} + Py_ssize_t PyPath_Flatten(PyObject *data, double **pxy) { Py_ssize_t i, j, n; @@ -164,48 +197,32 @@ PyPath_Flatten(PyObject *data, double **pxy) { return -1; } -#define assign_item_to_array(op, decref) \ - if (PyFloat_Check(op)) { \ - xy[j++] = PyFloat_AS_DOUBLE(op); \ - } else if (PyLong_Check(op)) { \ - xy[j++] = (float)PyLong_AS_LONG(op); \ - } else if (PyNumber_Check(op)) { \ - xy[j++] = PyFloat_AsDouble(op); \ - } else if (PyArg_ParseTuple(op, "dd", &x, &y)) { \ - xy[j++] = x; \ - xy[j++] = y; \ - } else { \ - PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); \ - if (decref) { \ - Py_DECREF(op); \ - } \ - free(xy); \ - return -1; \ - } \ - if (decref) { \ - Py_DECREF(op); \ - } - /* Copy table to path array */ if (PyList_Check(data)) { for (i = 0; i < n; i++) { - double x, y; PyObject *op = PyList_GetItemRef(data, i); if (op == NULL) { free(xy); return -1; } - assign_item_to_array(op, 1); + j = assign_item_to_array(xy, j, op); + Py_DECREF(op); + if (j == -1) { + free(xy); + return -1; + } } } else if (PyTuple_Check(data)) { for (i = 0; i < n; i++) { - double x, y; PyObject *op = PyTuple_GET_ITEM(data, i); - assign_item_to_array(op, 0); + j = assign_item_to_array(xy, j, op); + if (j == -1) { + free(xy); + return -1; + } } } else { for (i = 0; i < n; i++) { - double x, y; PyObject *op = PySequence_GetItem(data, i); if (!op) { /* treat IndexError as end of sequence */ @@ -217,7 +234,12 @@ PyPath_Flatten(PyObject *data, double **pxy) { return -1; } } - assign_item_to_array(op, 1); + j = assign_item_to_array(xy, j, op); + Py_DECREF(op); + if (j == -1) { + free(xy); + return -1; + } } } From e946c7b14adc7a0aaa9ac883de217a3c0e556a81 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:42:10 +1100 Subject: [PATCH 351/628] Test using _seek to skip frames (#8804) Co-authored-by: Andrew Murray --- Tests/test_file_apng.py | 5 ++++- Tests/test_file_fli.py | 3 +++ Tests/test_file_gif.py | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 9d5154fca..b9a036173 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -34,8 +34,11 @@ def test_apng_basic() -> None: with pytest.raises(EOFError): im.seek(2) - # test rewind support im.seek(0) + with pytest.raises(ValueError, match="cannot seek to frame 2"): + im._seek(2) + + # test rewind support assert im.getpixel((0, 0)) == (255, 0, 0, 255) assert im.getpixel((64, 32)) == (255, 0, 0, 255) im.seek(1) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 8adbd30f5..8a95af62d 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -160,6 +160,9 @@ def test_seek() -> None: assert_image_equal_tofile(im, "Tests/images/a_fli.png") + with pytest.raises(ValueError, match="cannot seek to frame 52"): + im._seek(52) + @pytest.mark.parametrize( "test_file", diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index d2592da97..dbbffc675 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -410,6 +410,10 @@ def test_seek() -> None: except EOFError: assert frame_count == 5 + img.seek(0) + with pytest.raises(ValueError, match="cannot seek to frame 2"): + img._seek(2) + def test_seek_info() -> None: with Image.open("Tests/images/iss634.gif") as im: From 5575c1d072a127e10cf0077ca9dcb7d54ea06c06 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Mar 2025 09:56:00 +1100 Subject: [PATCH 352/628] Test missing frame size --- Tests/test_file_fli.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 8a95af62d..2f39adc69 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,5 +1,6 @@ from __future__ import annotations +import io import warnings import pytest @@ -132,6 +133,15 @@ def test_eoferror() -> None: im.seek(n_frames - 1) +def test_missing_frame_size() -> None: + with open(animated_test_file, "rb") as fp: + data = fp.read() + data = data[:6188] + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(EOFError, match="missing frame size"): + im.seek(1) + + def test_seek_tell() -> None: with Image.open(animated_test_file) as im: layer_number = im.tell() From baa299a6f42d691653f3a6dcfb067be567319c27 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Mar 2025 09:54:46 +1100 Subject: [PATCH 353/628] Moved code outside of context manager --- Tests/test_file_jpeg2k.py | 10 +++++----- Tests/test_file_pcx.py | 4 ++-- Tests/test_font_bdf.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 01172fdbb..e429610ad 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -317,12 +317,12 @@ def test_grayscale_four_channels() -> None: with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp: data = fp.read() - # Change color space to OPJ_CLRSPC_GRAY - data = data[:76] + b"\x11" + data[77:] + # Change color space to OPJ_CLRSPC_GRAY + data = data[:76] + b"\x11" + data[77:] - with Image.open(BytesIO(data)) as im: - im.load() - assert im.mode == "RGBA" + with Image.open(BytesIO(data)) as im: + im.load() + assert im.mode == "RGBA" @pytest.mark.skipif( diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index aa24189f4..21c32268c 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -40,7 +40,7 @@ def test_sanity(tmp_path: Path) -> None: def test_bad_image_size() -> None: with open("Tests/images/pil184.pcx", "rb") as fp: data = fp.read() - data = data[:4] + b"\xff\xff" + data[6:] + data = data[:4] + b"\xff\xff" + data[6:] b = io.BytesIO(data) with pytest.raises(SyntaxError, match="bad PCX image size"): @@ -51,7 +51,7 @@ def test_bad_image_size() -> None: def test_unknown_mode() -> None: with open("Tests/images/pil184.pcx", "rb") as fp: data = fp.read() - data = data[:3] + b"\xff" + data[4:] + data = data[:3] + b"\xff" + data[4:] b = io.BytesIO(data) with pytest.raises(OSError, match="unknown PCX mode"): diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 2ece5457a..b4155c879 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -20,8 +20,8 @@ def test_sanity() -> None: def test_zero_width_chars() -> None: with open(filename, "rb") as fp: data = fp.read() - data = data[:2650] + b"\x00\x00" + data[2652:] - BdfFontFile.BdfFontFile(io.BytesIO(data)) + data = data[:2650] + b"\x00\x00" + data[2652:] + BdfFontFile.BdfFontFile(io.BytesIO(data)) def test_invalid_file() -> None: From a38d4d2583c9ce6e944d44efdb4d5a7106d42ec5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:44:13 +0100 Subject: [PATCH 354/628] Replace deprecated Renovate schedule with cron syntax --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index f48b670ec..91fa0426f 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -16,6 +16,6 @@ } ], "schedule": [ - "on the 3rd day of the month" + "* * 3 * *" ] } From 4b9d9f55cdd567d90ea8d885a50489ffd2ccfe2b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 12 Mar 2025 09:59:25 +1100 Subject: [PATCH 355/628] Updated libtiff to 4.7.0 (#8812) Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 202a8935d..e9c54536e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -43,7 +43,7 @@ LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 -TIFF_VERSION=4.6.0 +TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_NG_VERSION=2.2.4 LIBWEBP_VERSION=1.5.0 diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index b400a3436..2790bc2e6 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -44,7 +44,7 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.7.0** * **libfreetype** provides type related services diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a645722d8..ea3d99394 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,7 +120,7 @@ V = { "LIBPNG": "1.6.47", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", - "TIFF": "4.6.0", + "TIFF": "4.7.0", "XZ": "5.6.4", "ZLIBNG": "2.2.4", } From d97441cb86f1f9187ee303210ccdd34b2f899f6b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:49:59 +1100 Subject: [PATCH 356/628] Install libtiff-dev (#8816) Co-authored-by: Andrew Murray --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index e61752750..62677005e 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -20,7 +20,7 @@ fi set -e if [[ $(uname) != CYGWIN* ]]; then - sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ + sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ sway wl-clipboard libopenblas-dev From 5efcaa460372e91d9d18f2f812f590728903927a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:50:28 +1100 Subject: [PATCH 357/628] Updated Ghostscript to 10.5.0 (#8814) Co-authored-by: Andrew Murray --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ef49ff332..a780c7835 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -94,8 +94,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.4.0 --no-progress - echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.5.0 --no-progress + echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From 7b725a8fc4e97f150793c8be529c9ae0c80dd64f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 13:04:26 +1100 Subject: [PATCH 358/628] DXT3 images are read in RGBA mode --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a1d93821f..b0e20fa84 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -68,7 +68,7 @@ by DirectX. DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode. .. versionadded:: 3.4.0 - DXT3 images can be read in ``RGB`` mode and DX10 images can be read in + DXT3 images can be read in ``RGBA`` mode and DX10 images can be read in ``RGB`` and ``RGBA`` mode. .. versionadded:: 6.0.0 From db30ef74232af617b331e2995cc25ac039def106 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 15 Mar 2025 19:49:03 +0000 Subject: [PATCH 359/628] Update dependency cibuildwheel to v2.23.1 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 2fd3eb6ff..f2109ed61 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.23.0 +cibuildwheel==2.23.1 From 9953256bbf324458b4a7c4e5d070e5e1c4add0dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 16 Mar 2025 22:12:14 +1100 Subject: [PATCH 360/628] Revert "Use Ubuntu 22.04 for 24.04 ppc64le and s390x" This reverts commit e31441fc41ff54217317b61db395dfc9b5a0dc79. --- .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 da5e191da..0d9033413 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -35,6 +35,10 @@ 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, @@ -52,13 +56,9 @@ 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 7767e83e6c40c01f0d61df480153bf9cf6d12a06 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 16 Mar 2025 22:24:13 +1100 Subject: [PATCH 361/628] Use action to setup qemu --- .github/workflows/test-docker.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 0d9033413..25aef55fb 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -75,8 +75,9 @@ jobs: - name: Set up QEMU if: "matrix.qemu-arch" - run: | - docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }} + uses: docker/setup-qemu-action@v3 + with: + platforms: ${{ matrix.qemu-arch }} - name: Docker pull run: | From e1cd9ad5ac17b3923f8842cb0696c94065688f64 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Mar 2025 20:45:49 +1100 Subject: [PATCH 362/628] Use maxsplit --- src/PIL/GimpPaletteFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 1b7a394c0..74f870ca7 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -45,7 +45,7 @@ class GimpPaletteFile: msg = "bad palette file" raise SyntaxError(msg) - v = tuple(map(int, s.split()[:3])) + v = tuple(map(int, s.split(maxsplit=3)[:3])) if len(v) != 3: msg = "bad palette entry" raise ValueError(msg) From 1f6fd3b994f9a810246fa28a863cb849eba4586c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Mar 2025 20:49:37 +1100 Subject: [PATCH 363/628] Only convert to int if there are enough items --- src/PIL/GimpPaletteFile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 74f870ca7..f1b3844be 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -45,12 +45,12 @@ class GimpPaletteFile: msg = "bad palette file" raise SyntaxError(msg) - v = tuple(map(int, s.split(maxsplit=3)[:3])) - if len(v) != 3: + v = s.split(maxsplit=3) + if len(v) < 3: msg = "bad palette entry" raise ValueError(msg) - palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) + palette[i] = o8(int(v[0])) + o8(int(v[1])) + o8(int(v[2])) self.palette = b"".join(palette) From 6e597a1ca742ea0fbfaa75f3e02a129d6faa3643 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Mar 2025 22:08:59 +1100 Subject: [PATCH 364/628] Do not force palette length to be 256 --- Tests/test_file_gimppalette.py | 1 + src/PIL/GimpPaletteFile.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index e8d5f1705..b362fc8ff 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -32,3 +32,4 @@ def test_get_palette() -> None: # Assert assert mode == "RGB" + assert len(palette) / 3 == 11 diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index f1b3844be..b289ecb52 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -27,12 +27,11 @@ class GimpPaletteFile: rawmode = "RGB" def __init__(self, fp: IO[bytes]) -> None: - palette = [o8(i) * 3 for i in range(256)] - if not fp.readline().startswith(b"GIMP Palette"): msg = "not a GIMP palette file" raise SyntaxError(msg) + palette: list[int] = [] for i in range(256): s = fp.readline() if not s: @@ -40,6 +39,7 @@ class GimpPaletteFile: # skip fields and comment lines if re.match(rb"\w+:|#", s): + palette.append(o8(i) * 3) continue if len(s) > 100: msg = "bad palette file" @@ -50,7 +50,7 @@ class GimpPaletteFile: msg = "bad palette entry" raise ValueError(msg) - palette[i] = o8(int(v[0])) + o8(int(v[1])) + o8(int(v[2])) + palette.append(o8(int(v[0])) + o8(int(v[1])) + o8(int(v[2]))) self.palette = b"".join(palette) From ca0c940cb1b932c67bf166dc600bd1eb5b264bde Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Mar 2025 22:09:35 +1100 Subject: [PATCH 365/628] Do not add palette entries when reading other lines --- Tests/test_file_gimppalette.py | 2 +- src/PIL/GimpPaletteFile.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index b362fc8ff..ff9cc91c5 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -32,4 +32,4 @@ def test_get_palette() -> None: # Assert assert mode == "RGB" - assert len(palette) / 3 == 11 + assert len(palette) / 3 == 8 diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index b289ecb52..bbbe2781a 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -32,14 +32,13 @@ class GimpPaletteFile: raise SyntaxError(msg) palette: list[int] = [] - for i in range(256): + for _ in range(256): s = fp.readline() if not s: break # skip fields and comment lines if re.match(rb"\w+:|#", s): - palette.append(o8(i) * 3) continue if len(s) > 100: msg = "bad palette file" From 669a288beb222e61b6e9107940f755517e0ac2ff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Mar 2025 22:33:43 +1100 Subject: [PATCH 366/628] Convert all entries to bytes at once --- src/PIL/GimpPaletteFile.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index bbbe2781a..0f079f457 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -18,8 +18,6 @@ from __future__ import annotations import re from typing import IO -from ._binary import o8 - class GimpPaletteFile: """File handler for GIMP's palette format.""" @@ -49,9 +47,9 @@ class GimpPaletteFile: msg = "bad palette entry" raise ValueError(msg) - palette.append(o8(int(v[0])) + o8(int(v[1])) + o8(int(v[2]))) + palette += (int(v[i]) for i in range(3)) - self.palette = b"".join(palette) + self.palette = bytes(palette) def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode From 6c7917d7a6031ae22e1d9eaccc2e536123ea25c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Mar 2025 07:54:47 +1100 Subject: [PATCH 367/628] Revert to zlib on macOS < 10.15 --- .github/workflows/wheels-dependencies.sh | 7 ++++++- Tests/check_wheel.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index e9c54536e..e0c18d6c2 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -45,6 +45,7 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 +ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.4 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 @@ -106,7 +107,11 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - build_zlib_ng + if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then + build_new_zlib + else + build_zlib_ng + fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 563be0b74..8ba40ba3f 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,9 +1,12 @@ from __future__ import annotations +import platform import sys from PIL import features +from .helper import is_pypy + def test_wheel_modules() -> None: expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} @@ -40,5 +43,7 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") + elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm": + expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features From 3dbd0e57bae0dbe2a3f2c006fb767417a2c5419e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Mar 2025 10:14:38 +1100 Subject: [PATCH 368/628] Added DXT1 encoding --- Tests/test_file_dds.py | 31 ++++++- setup.py | 1 + src/PIL/DdsImagePlugin.py | 61 ++++++++----- src/_imaging.c | 3 + src/encode.c | 18 ++++ src/libImaging/BcnEncode.c | 173 +++++++++++++++++++++++++++++++++++++ src/libImaging/Imaging.h | 2 + 7 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 src/libImaging/BcnEncode.c diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 7cc4d79d4..7a6099ce7 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -9,7 +9,12 @@ import pytest from PIL import DdsImagePlugin, Image -from .helper import assert_image_equal, assert_image_equal_tofile, hopper +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + hopper, +) TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" @@ -389,5 +394,25 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None: assert im.mode == mode im.save(out) - with Image.open(out) as reloaded: - assert_image_equal(im, reloaded) + assert_image_equal_tofile(im, out) + + +def test_save_dxt1(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DXT1) as im: + im.convert("RGB").save(out, pixel_format="DXT1") + assert_image_similar_tofile(im, out, 1.84) + + im_alpha = im.copy() + im_alpha.putpixel((0, 0), (0, 0, 0, 0)) + im_alpha.save(out, pixel_format="DXT1") + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) + + im_l = im.convert("L") + im_l.save(out, pixel_format="DXT1") + assert_image_similar_tofile(im_l.convert("RGBA"), out, 9.25) + + im_alpha.convert("LA").save(out, pixel_format="DXT1") + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) diff --git a/setup.py b/setup.py index a85731db9..9fac993b1 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ _LIB_IMAGING = ( "Reduce", "Bands", "BcnDecode", + "BcnEncode", "BitDecode", "Blend", "Chops", diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index cdae8dfee..718f376e8 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -518,30 +518,43 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: msg = f"cannot write mode {im.mode} as DDS" raise OSError(msg) - alpha = im.mode[-1] == "A" - if im.mode[0] == "L": - pixel_flags = DDPF.LUMINANCE - rawmode = im.mode - if alpha: - rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] - else: - rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] - else: - pixel_flags = DDPF.RGB - rawmode = im.mode[::-1] - rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] - - if alpha: - r, g, b, a = im.split() - im = Image.merge("RGBA", (a, r, g, b)) - if alpha: - pixel_flags |= DDPF.ALPHAPIXELS - rgba_mask.append(0xFF000000 if alpha else 0) - - flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT + flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT bitcount = len(im.getbands()) * 8 - pitch = (im.width * bitcount + 7) // 8 + raw = im.encoderinfo.get("pixel_format") != "DXT1" + if raw: + codec_name = "raw" + flags |= DDSD.PITCH + pitch = (im.width * bitcount + 7) // 8 + alpha = im.mode[-1] == "A" + if im.mode[0] == "L": + pixel_flags = DDPF.LUMINANCE + rawmode = im.mode + if alpha: + rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] + else: + rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] + else: + pixel_flags = DDPF.RGB + rawmode = im.mode[::-1] + rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] + + if alpha: + r, g, b, a = im.split() + im = Image.merge("RGBA", (a, r, g, b)) + if alpha: + pixel_flags |= DDPF.ALPHAPIXELS + rgba_mask.append(0xFF000000 if alpha else 0) + + fourcc = 0 + else: + codec_name = "bcn" + flags |= DDSD.LINEARSIZE + pitch = (im.width + 3) * 4 + rawmode = None + rgba_mask = [0, 0, 0, 0] + pixel_flags = DDPF.FOURCC + fourcc = D3DFMT.DXT1 fp.write( o32(DDS_MAGIC) + struct.pack( @@ -556,11 +569,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ) + struct.pack("11I", *((0,) * 11)) # reserved # pfsize, pfflags, fourcc, bitcount - + struct.pack("<4I", 32, pixel_flags, 0, bitcount) + + struct.pack("<4I", 32, pixel_flags, fourcc, bitcount) + 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)]) + ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, rawmode)]) def _accept(prefix: bytes) -> bool: diff --git a/src/_imaging.c b/src/_imaging.c index fa38dcc05..330a7eef4 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4041,6 +4041,8 @@ PyImaging_ZipDecoderNew(PyObject *self, PyObject *args); /* Encoders (in encode.c) */ extern PyObject * +PyImaging_BcnEncoderNew(PyObject *self, PyObject *args); +extern PyObject * PyImaging_EpsEncoderNew(PyObject *self, PyObject *args); extern PyObject * PyImaging_GifEncoderNew(PyObject *self, PyObject *args); @@ -4109,6 +4111,7 @@ static PyMethodDef functions[] = { /* Codecs */ {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, + {"bcn_encoder", (PyCFunction)PyImaging_BcnEncoderNew, METH_VARARGS}, {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, diff --git a/src/encode.c b/src/encode.c index 13d5cdaf7..d27783f0c 100644 --- a/src/encode.c +++ b/src/encode.c @@ -350,6 +350,24 @@ get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode) return 0; } +/* -------------------------------------------------------------------- */ +/* BCN */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) { + return NULL; + } + + encoder->encode = ImagingBcnEncode; + + return (PyObject *)encoder; +} + /* -------------------------------------------------------------------- */ /* EPS */ /* -------------------------------------------------------------------- */ diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c new file mode 100644 index 000000000..42a46358c --- /dev/null +++ b/src/libImaging/BcnEncode.c @@ -0,0 +1,173 @@ +/* + * The Python Imaging Library + * + * encoder for DXT1-compressed data + * + * Format documentation: + * https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + * + */ + +#include "Imaging.h" + +#include "Bcn.h" + +typedef struct { + UINT8 color[3]; +} rgb; + +typedef struct { + UINT8 color[3]; + int alpha; +} rgba; + +static rgb +decode_565(UINT16 x) { + rgb item; + int r, g, b; + r = (x & 0xf800) >> 8; + r |= r >> 5; + item.color[0] = r; + g = (x & 0x7e0) >> 3; + g |= g >> 6; + item.color[1] = g; + b = (x & 0x1f) << 3; + b |= b >> 5; + item.color[2] = b; + return item; +} + +static UINT16 +encode_565(rgba item) { + UINT8 r, g, b; + r = item.color[0] >> (8 - 5); + g = item.color[1] >> (8 - 6); + b = item.color[2] >> (8 - 5); + return (r << (5 + 6)) | (g << 5) | b; +} + +int +ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *dst = buf; + + for (;;) { + int i, j, k; + UINT16 color_min = 0, color_max = 0; + rgb color_min_rgb, color_max_rgb; + rgba block[16], *current_rgba; + + // Determine the min and max colors in this 4x4 block + int has_alpha_channel = + strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; + int first = 1; + int transparency = 0; + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) { + int x = state->x + i * im->pixelsize; + int y = state->y + j; + if (x >= state->xsize * im->pixelsize || y >= state->ysize) { + // The 4x4 block extends past the edge of the image + continue; + } + + current_rgba = &block[i + j * 4]; + for (k = 0; k < 3; k++) { + current_rgba->color[k] = + (UINT8)im->image[y][x + (im->pixelsize == 1 ? 0 : k)]; + } + if (has_alpha_channel) { + if ((UINT8)im->image[y][x + 3] == 0) { + current_rgba->alpha = 0; + transparency = 1; + continue; + } else { + current_rgba->alpha = 1; + } + } + + UINT16 color = encode_565(*current_rgba); + if (first || color < color_min) { + color_min = color; + } + if (first || color > color_max) { + color_max = color; + } + first = 0; + } + } + + if (transparency) { + *dst++ = color_min; + *dst++ = color_min >> 8; + } + *dst++ = color_max; + *dst++ = color_max >> 8; + if (!transparency) { + *dst++ = color_min; + *dst++ = color_min >> 8; + } + + color_min_rgb = decode_565(color_min); + color_max_rgb = decode_565(color_max); + for (i = 0; i < 4; i++) { + UINT8 l = 0; + for (j = 3; j > -1; j--) { + current_rgba = &block[i * 4 + j]; + if (transparency && !current_rgba->alpha) { + l |= 3 << (j * 2); + continue; + } + + float distance = 0; + int total = 0; + for (k = 0; k < 3; k++) { + float denom = + (float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]); + if (denom != 0) { + distance += + abs(current_rgba->color[k] - color_min_rgb.color[k]) / + denom; + total += 1; + } + } + if (total == 0) { + continue; + } + distance *= 6 / total; + if (transparency) { + if (distance < 1.5) { + // color_max + } else if (distance < 4.5) { + l |= 2 << (j * 2); // 1/2 * color_min + 1/2 * color_max + } else { + l |= 1 << (j * 2); // color_min + } + } else { + if (distance < 1) { + l |= 1 << (j * 2); // color_min + } else if (distance < 3) { + l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max + } else if (distance < 5) { + l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max + } else { + // color_max + } + } + } + *dst++ = l; + } + + state->x += im->pixelsize * 4; + + if (state->x >= state->xsize * im->pixelsize) { + state->x = 0; + state->y += 4; + if (state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + } + } + + return dst - buf; +} diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 0c2d3fc2e..0fc191d15 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -567,6 +567,8 @@ typedef int (*ImagingCodec)( extern int ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int +ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); From 9430bbe5a133c6e5ccb2ce23e0ea1f7f6adca0fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 13 Mar 2025 23:53:08 +1100 Subject: [PATCH 369/628] Added DXT5 saving --- Tests/test_file_dds.py | 29 +++- src/PIL/DdsImagePlugin.py | 28 ++-- src/encode.c | 11 +- src/libImaging/BcnEncode.c | 289 ++++++++++++++++++++++++------------- 4 files changed, 237 insertions(+), 120 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 7a6099ce7..17d88451f 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -398,21 +398,48 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None: def test_save_dxt1(tmp_path: Path) -> None: + # RGB out = str(tmp_path / "temp.dds") with Image.open(TEST_FILE_DXT1) as im: im.convert("RGB").save(out, pixel_format="DXT1") assert_image_similar_tofile(im, out, 1.84) + # RGBA im_alpha = im.copy() im_alpha.putpixel((0, 0), (0, 0, 0, 0)) im_alpha.save(out, pixel_format="DXT1") with Image.open(out) as reloaded: assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) + # L im_l = im.convert("L") im_l.save(out, pixel_format="DXT1") - assert_image_similar_tofile(im_l.convert("RGBA"), out, 9.25) + assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07) + # LA im_alpha.convert("LA").save(out, pixel_format="DXT1") with Image.open(out) as reloaded: assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) + + +def test_save_dxt5(tmp_path: Path) -> None: + # RGB + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DXT1) as im: + im.convert("RGB").save(out, pixel_format="DXT5") + assert_image_similar_tofile(im, out, 1.84) + + # RGBA + with Image.open(TEST_FILE_DXT5) as im_rgba: + im_rgba.save(out, pixel_format="DXT5") + assert_image_similar_tofile(im_rgba, out, 3.69) + + # L + im_l = im.convert("L") + im_l.save(out, pixel_format="DXT5") + assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07) + + # LA + im_la = im_rgba.convert("LA") + im_la.save(out, pixel_format="DXT5") + assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.32) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 718f376e8..2d097fd16 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -520,8 +520,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT bitcount = len(im.getbands()) * 8 - raw = im.encoderinfo.get("pixel_format") != "DXT1" - if raw: + pixel_format = im.encoderinfo.get("pixel_format") + if pixel_format in ("DXT1", "DXT5"): + codec_name = "bcn" + flags |= DDSD.LINEARSIZE + pitch = (im.width + 3) * 4 + args = pixel_format + rgba_mask = [0, 0, 0, 0] + pixel_flags = DDPF.FOURCC + fourcc = D3DFMT.DXT1 if pixel_format == "DXT1" else D3DFMT.DXT5 + else: codec_name = "raw" flags |= DDSD.PITCH pitch = (im.width * bitcount + 7) // 8 @@ -529,14 +537,14 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: alpha = im.mode[-1] == "A" if im.mode[0] == "L": pixel_flags = DDPF.LUMINANCE - rawmode = im.mode + args = im.mode if alpha: rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] else: rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] else: pixel_flags = DDPF.RGB - rawmode = im.mode[::-1] + args = im.mode[::-1] rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] if alpha: @@ -546,15 +554,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: pixel_flags |= DDPF.ALPHAPIXELS rgba_mask.append(0xFF000000 if alpha else 0) - fourcc = 0 - else: - codec_name = "bcn" - flags |= DDSD.LINEARSIZE - pitch = (im.width + 3) * 4 - rawmode = None - rgba_mask = [0, 0, 0, 0] - pixel_flags = DDPF.FOURCC - fourcc = D3DFMT.DXT1 + fourcc = D3DFMT.UNKNOWN fp.write( o32(DDS_MAGIC) + struct.pack( @@ -573,7 +573,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(codec_name, (0, 0) + im.size, 0, rawmode)]) + ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)]) def _accept(prefix: bytes) -> bool: diff --git a/src/encode.c b/src/encode.c index d27783f0c..e228237f2 100644 --- a/src/encode.c +++ b/src/encode.c @@ -27,6 +27,7 @@ #include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" +#include "libImaging/Bcn.h" #include "libImaging/Gif.h" #ifdef HAVE_UNISTD_H @@ -358,13 +359,21 @@ PyObject * PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - encoder = PyImaging_EncoderNew(0); + char *mode; + char *pixel_format; + if (!PyArg_ParseTuple(args, "ss", &mode, &pixel_format)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(sizeof(BCNSTATE)); if (encoder == NULL) { return NULL; } encoder->encode = ImagingBcnEncode; + ((BCNSTATE *)encoder->state.context)->pixel_format = pixel_format; + return (PyObject *)encoder; } diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 42a46358c..57353246a 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -17,8 +17,7 @@ typedef struct { } rgb; typedef struct { - UINT8 color[3]; - int alpha; + UINT8 color[4]; } rgba; static rgb @@ -46,116 +45,198 @@ encode_565(rgba item) { return (r << (5 + 6)) | (g << 5) | b; } +static void +encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_alpha) { + int i, j, k; + UINT16 color_min = 0, color_max = 0; + rgb color_min_rgb, color_max_rgb; + rgba block[16], *current_rgba; + + // Determine the min and max colors in this 4x4 block + int first = 1; + int transparency = 0; + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) { + int x = state->x + i * im->pixelsize; + int y = state->y + j; + if (x >= state->xsize * im->pixelsize || y >= state->ysize) { + // The 4x4 block extends past the edge of the image + continue; + } + + current_rgba = &block[i + j * 4]; + for (k = 0; k < 3; k++) { + current_rgba->color[k] = + (UINT8)im->image[y][x + (im->pixelsize == 1 ? 0 : k)]; + } + if (separate_alpha) { + if ((UINT8)im->image[y][x + 3] == 0) { + current_rgba->color[3] = 0; + transparency = 1; + continue; + } else { + current_rgba->color[3] = 1; + } + } + + UINT16 color = encode_565(*current_rgba); + if (first || color < color_min) { + color_min = color; + } + if (first || color > color_max) { + color_max = color; + } + first = 0; + } + } + + if (transparency) { + *dst++ = color_min; + *dst++ = color_min >> 8; + } + *dst++ = color_max; + *dst++ = color_max >> 8; + if (!transparency) { + *dst++ = color_min; + *dst++ = color_min >> 8; + } + + color_min_rgb = decode_565(color_min); + color_max_rgb = decode_565(color_max); + for (i = 0; i < 4; i++) { + UINT8 l = 0; + for (j = 3; j > -1; j--) { + current_rgba = &block[i * 4 + j]; + if (transparency && !current_rgba->color[3]) { + l |= 3 << (j * 2); + continue; + } + + float distance = 0; + int total = 0; + for (k = 0; k < 3; k++) { + float denom = + (float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]); + if (denom != 0) { + distance += + abs(current_rgba->color[k] - color_min_rgb.color[k]) / denom; + total += 1; + } + } + if (total == 0) { + continue; + } + if (transparency) { + distance *= 4 / total; + if (distance < 1) { + // color_max + } else if (distance < 3) { + l |= 2 << (j * 2); // 1/2 * color_min + 1/2 * color_max + } else { + l |= 1 << (j * 2); // color_min + } + } else { + distance *= 6 / total; + if (distance < 1) { + l |= 1 << (j * 2); // color_min + } else if (distance < 3) { + l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max + } else if (distance < 5) { + l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max + } else { + // color_max + } + } + } + *dst++ = l; + } +} + +static void +encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { + int i, j; + UINT8 alpha_min = 0, alpha_max = 0; + UINT8 block[16], current_alpha; + + // Determine the min and max colors in this 4x4 block + int first = 1; + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) { + int x = state->x + i * im->pixelsize; + int y = state->y + j; + if (x >= state->xsize * im->pixelsize || y >= state->ysize) { + // The 4x4 block extends past the edge of the image + continue; + } + + current_alpha = (UINT8)im->image[y][x + 3]; + block[i + j * 4] = current_alpha; + + if (first || current_alpha < alpha_min) { + alpha_min = current_alpha; + } + if (first || current_alpha > alpha_max) { + alpha_max = current_alpha; + } + first = 0; + } + } + + *dst++ = alpha_min; + *dst++ = alpha_max; + + float denom = (float)abs(alpha_max - alpha_min); + for (i = 0; i < 2; i++) { + UINT32 l = 0; + for (j = 7; j > -1; j--) { + current_alpha = block[i * 8 + j]; + if (!current_alpha) { + l |= 6 << (j * 3); + continue; + } else if (current_alpha == 255 || denom == 0) { + l |= 7 << (j * 3); + continue; + } + + float distance = abs(current_alpha - alpha_min) / denom * 10; + if (distance < 3) { + l |= 2 << (j * 3); // 4/5 * alpha_min + 1/5 * alpha_max + } else if (distance < 5) { + l |= 3 << (j * 3); // 3/5 * alpha_min + 2/5 * alpha_max + } else if (distance < 7) { + l |= 4 << (j * 3); // 2/5 * alpha_min + 3/5 * alpha_max + } else { + l |= 5 << (j * 3); // 1/5 * alpha_min + 4/5 * alpha_max + } + } + *dst++ = l; + *dst++ = l >> 8; + *dst++ = l >> 16; + } +} + int ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; + int n = strcmp(pixel_format, "DXT5") == 0 ? 3 : 1; + int has_alpha_channel = + strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; + UINT8 *dst = buf; for (;;) { - int i, j, k; - UINT16 color_min = 0, color_max = 0; - rgb color_min_rgb, color_max_rgb; - rgba block[16], *current_rgba; - - // Determine the min and max colors in this 4x4 block - int has_alpha_channel = - strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; - int first = 1; - int transparency = 0; - for (i = 0; i < 4; i++) { - for (j = 0; j < 4; j++) { - int x = state->x + i * im->pixelsize; - int y = state->y + j; - if (x >= state->xsize * im->pixelsize || y >= state->ysize) { - // The 4x4 block extends past the edge of the image - continue; - } - - current_rgba = &block[i + j * 4]; - for (k = 0; k < 3; k++) { - current_rgba->color[k] = - (UINT8)im->image[y][x + (im->pixelsize == 1 ? 0 : k)]; - } - if (has_alpha_channel) { - if ((UINT8)im->image[y][x + 3] == 0) { - current_rgba->alpha = 0; - transparency = 1; - continue; - } else { - current_rgba->alpha = 1; - } - } - - UINT16 color = encode_565(*current_rgba); - if (first || color < color_min) { - color_min = color; - } - if (first || color > color_max) { - color_max = color; - } - first = 0; - } - } - - if (transparency) { - *dst++ = color_min; - *dst++ = color_min >> 8; - } - *dst++ = color_max; - *dst++ = color_max >> 8; - if (!transparency) { - *dst++ = color_min; - *dst++ = color_min >> 8; - } - - color_min_rgb = decode_565(color_min); - color_max_rgb = decode_565(color_max); - for (i = 0; i < 4; i++) { - UINT8 l = 0; - for (j = 3; j > -1; j--) { - current_rgba = &block[i * 4 + j]; - if (transparency && !current_rgba->alpha) { - l |= 3 << (j * 2); - continue; - } - - float distance = 0; - int total = 0; - for (k = 0; k < 3; k++) { - float denom = - (float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]); - if (denom != 0) { - distance += - abs(current_rgba->color[k] - color_min_rgb.color[k]) / - denom; - total += 1; - } - } - if (total == 0) { - continue; - } - distance *= 6 / total; - if (transparency) { - if (distance < 1.5) { - // color_max - } else if (distance < 4.5) { - l |= 2 << (j * 2); // 1/2 * color_min + 1/2 * color_max - } else { - l |= 1 << (j * 2); // color_min - } - } else { - if (distance < 1) { - l |= 1 << (j * 2); // color_min - } else if (distance < 3) { - l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max - } else if (distance < 5) { - l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max - } else { - // color_max - } + if (n == 3) { + if (has_alpha_channel) { + encode_bc3_alpha(im, state, dst); + dst += 8; + } else { + for (int i = 0; i < 8; i++) { + *dst++ = 0xff; } } - *dst++ = l; } + encode_bc1_color(im, state, dst, n == 1 && has_alpha_channel); + dst += 8; state->x += im->pixelsize * 4; From 9f619b814f65a99c2107852027ecf7db4ddec7ec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 00:21:34 +1100 Subject: [PATCH 370/628] Added BC3 loading and saving --- Tests/test_file_dds.py | 14 ++++++++++++++ src/PIL/DdsImagePlugin.py | 17 +++++++++++++++-- src/libImaging/BcnEncode.c | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 17d88451f..3c7c8e604 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -12,6 +12,7 @@ from PIL import DdsImagePlugin, Image from .helper import ( assert_image_equal, assert_image_equal_tofile, + assert_image_similar, assert_image_similar_tofile, hopper, ) @@ -114,6 +115,19 @@ def test_sanity_ati1_bc4u(image_path: str) -> None: assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) +def test_dx10_bc3(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DXT5) as im: + im.save(out, pixel_format="BC3") + + with Image.open(out) as reloaded: + assert reloaded.format == "DDS" + assert reloaded.mode == "RGBA" + assert reloaded.size == (256, 256) + + assert_image_similar(im, reloaded, 3.69) + + @pytest.mark.parametrize( "image_path", ( diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 2d097fd16..a5e0a712b 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -419,6 +419,10 @@ class DdsImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.pixel_format = "BC1" n = 1 + elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM): + self._mode = "RGBA" + self.pixel_format = "BC3" + n = 3 elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM): self._mode = "L" self.pixel_format = "BC4" @@ -521,14 +525,18 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT bitcount = len(im.getbands()) * 8 pixel_format = im.encoderinfo.get("pixel_format") - if pixel_format in ("DXT1", "DXT5"): + if pixel_format in ("DXT1", "BC3", "DXT5"): codec_name = "bcn" flags |= DDSD.LINEARSIZE pitch = (im.width + 3) * 4 args = pixel_format rgba_mask = [0, 0, 0, 0] pixel_flags = DDPF.FOURCC - fourcc = D3DFMT.DXT1 if pixel_format == "DXT1" else D3DFMT.DXT5 + fourcc = {"DXT1": D3DFMT.DXT1, "BC3": D3DFMT.DX10, "DXT5": D3DFMT.DXT5}[ + pixel_format + ] + if fourcc == D3DFMT.DX10: + dxgi_format = DXGI_FORMAT.BC3_TYPELESS else: codec_name = "raw" flags |= DDSD.PITCH @@ -573,6 +581,11 @@ 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) ) + if fourcc == D3DFMT.DX10: + fp.write( + # dxgi_format, 2D resource, misc, array size, straight alpha + struct.pack("<5I", dxgi_format, 3, 0, 0, 1) + ) ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)]) diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 57353246a..66f9f39b1 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -218,7 +218,7 @@ encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { int ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; - int n = strcmp(pixel_format, "DXT5") == 0 ? 3 : 1; + int n = strcmp(pixel_format, "DXT1") == 0 ? 1 : 3; int has_alpha_channel = strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; From f1a61a1e76cfbc21cd5345f4b87067ace7347ddf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 12:50:30 +1100 Subject: [PATCH 371/628] Added DXT3 saving --- Tests/test_file_dds.py | 23 ++++++++++++++++++ src/PIL/DdsImagePlugin.py | 20 +++++++++------ src/encode.c | 9 +++---- src/libImaging/BcnEncode.c | 50 ++++++++++++++++++++++++++++++++------ 4 files changed, 83 insertions(+), 19 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 3c7c8e604..5ef9fbf05 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -436,6 +436,29 @@ def test_save_dxt1(tmp_path: Path) -> None: assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) +def test_save_dxt3(tmp_path: Path) -> None: + # RGB + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DXT3) as im: + im_rgb = im.convert("RGB") + im_rgb.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im_rgb.convert("RGBA"), out, 1.26) + + # RGBA + im.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im, out, 3.81) + + # L + im_l = im.convert("L") + im_l.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im_l.convert("RGBA"), out, 5.89) + + # LA + im_la = im.convert("LA") + im_la.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.44) + + def test_save_dxt5(tmp_path: Path) -> None: # RGB out = str(tmp_path / "temp.dds") diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index a5e0a712b..c30672c86 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -525,18 +525,24 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT bitcount = len(im.getbands()) * 8 pixel_format = im.encoderinfo.get("pixel_format") - if pixel_format in ("DXT1", "BC3", "DXT5"): + args: tuple[int] | str + if pixel_format in ("DXT1", "DXT3", "BC3", "DXT5"): codec_name = "bcn" flags |= DDSD.LINEARSIZE pitch = (im.width + 3) * 4 - args = pixel_format rgba_mask = [0, 0, 0, 0] pixel_flags = DDPF.FOURCC - fourcc = {"DXT1": D3DFMT.DXT1, "BC3": D3DFMT.DX10, "DXT5": D3DFMT.DXT5}[ - pixel_format - ] - if fourcc == D3DFMT.DX10: - dxgi_format = DXGI_FORMAT.BC3_TYPELESS + if pixel_format == "DXT1": + fourcc = D3DFMT.DXT1 + args = (1,) + elif pixel_format == "DXT3": + fourcc = D3DFMT.DXT3 + args = (2,) + else: + fourcc = D3DFMT.DXT5 if pixel_format == "DXT5" else D3DFMT.DX10 + args = (3,) + if fourcc == D3DFMT.DX10: + dxgi_format = DXGI_FORMAT.BC3_TYPELESS else: codec_name = "raw" flags |= DDSD.PITCH diff --git a/src/encode.c b/src/encode.c index e228237f2..7c365a74f 100644 --- a/src/encode.c +++ b/src/encode.c @@ -360,19 +360,18 @@ PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; char *mode; - char *pixel_format; - if (!PyArg_ParseTuple(args, "ss", &mode, &pixel_format)) { + int n; + if (!PyArg_ParseTuple(args, "si", &mode, &n)) { return NULL; } - encoder = PyImaging_EncoderNew(sizeof(BCNSTATE)); + encoder = PyImaging_EncoderNew(0); if (encoder == NULL) { return NULL; } encoder->encode = ImagingBcnEncode; - - ((BCNSTATE *)encoder->state.context)->pixel_format = pixel_format; + encoder->state.state = n; return (PyObject *)encoder; } diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 66f9f39b1..4e0da322e 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -10,8 +10,6 @@ #include "Imaging.h" -#include "Bcn.h" - typedef struct { UINT8 color[3]; } rgb; @@ -57,14 +55,18 @@ encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_a int transparency = 0; for (i = 0; i < 4; i++) { for (j = 0; j < 4; j++) { + current_rgba = &block[i + j * 4]; + int x = state->x + i * im->pixelsize; int y = state->y + j; if (x >= state->xsize * im->pixelsize || y >= state->ysize) { // The 4x4 block extends past the edge of the image + for (k = 0; k < 3; k++) { + current_rgba->color[k] = 0; + } continue; } - current_rgba = &block[i + j * 4]; for (k = 0; k < 3; k++) { current_rgba->color[k] = (UINT8)im->image[y][x + (im->pixelsize == 1 ? 0 : k)]; @@ -152,6 +154,36 @@ encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_a } } +static void +encode_bc2_block(Imaging im, ImagingCodecState state, UINT8 *dst) { + int i, j; + UINT8 block[16], current_alpha; + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) { + int x = state->x + i * im->pixelsize; + int y = state->y + j; + if (x >= state->xsize * im->pixelsize || y >= state->ysize) { + // The 4x4 block extends past the edge of the image + block[i + j * 4] = 0; + continue; + } + + current_alpha = (UINT8)im->image[y][x + 3]; + block[i + j * 4] = current_alpha; + } + } + + for (i = 0; i < 4; i++) { + UINT16 l = 0; + for (j = 3; j > -1; j--) { + current_alpha = block[i * 4 + j]; + l |= current_alpha << (j * 4); + } + *dst++ = l; + *dst++ = l >> 8; + } +} + static void encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { int i, j; @@ -166,6 +198,7 @@ encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { int y = state->y + j; if (x >= state->xsize * im->pixelsize || y >= state->ysize) { // The 4x4 block extends past the edge of the image + block[i + j * 4] = 0; continue; } @@ -217,17 +250,20 @@ encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { int ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; - int n = strcmp(pixel_format, "DXT1") == 0 ? 1 : 3; + int n = state->state; int has_alpha_channel = strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; UINT8 *dst = buf; for (;;) { - if (n == 3) { + if (n == 2 || n == 3) { if (has_alpha_channel) { - encode_bc3_alpha(im, state, dst); + if (n == 2) { + encode_bc2_block(im, state, dst); + } else { + encode_bc3_alpha(im, state, dst); + } dst += 8; } else { for (int i = 0; i < 8; i++) { From b0315cc6039e1eabacc36b7af0677f69378f26bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 12:56:32 +1100 Subject: [PATCH 372/628] Added BC2 loading and saving --- Tests/test_file_dds.py | 13 +++++++++++++ src/PIL/DdsImagePlugin.py | 18 ++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 5ef9fbf05..3239065d7 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -115,6 +115,19 @@ def test_sanity_ati1_bc4u(image_path: str) -> None: assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) +def test_dx10_bc2(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DXT3) as im: + im.save(out, pixel_format="BC2") + + with Image.open(out) as reloaded: + assert reloaded.format == "DDS" + assert reloaded.mode == "RGBA" + assert reloaded.size == (256, 256) + + assert_image_similar(im, reloaded, 3.81) + + def test_dx10_bc3(tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") with Image.open(TEST_FILE_DXT5) as im: diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index c30672c86..d65e3fc65 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -419,6 +419,10 @@ class DdsImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.pixel_format = "BC1" n = 1 + elif dxgi_format in (DXGI_FORMAT.BC2_TYPELESS, DXGI_FORMAT.BC2_UNORM): + self._mode = "RGBA" + self.pixel_format = "BC2" + n = 2 elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM): self._mode = "RGBA" self.pixel_format = "BC3" @@ -526,7 +530,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: bitcount = len(im.getbands()) * 8 pixel_format = im.encoderinfo.get("pixel_format") args: tuple[int] | str - if pixel_format in ("DXT1", "DXT3", "BC3", "DXT5"): + if pixel_format in ("DXT1", "BC2", "DXT3", "BC3", "DXT5"): codec_name = "bcn" flags |= DDSD.LINEARSIZE pitch = (im.width + 3) * 4 @@ -538,10 +542,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: elif pixel_format == "DXT3": fourcc = D3DFMT.DXT3 args = (2,) - else: - fourcc = D3DFMT.DXT5 if pixel_format == "DXT5" else D3DFMT.DX10 + elif pixel_format == "DXT5": + fourcc = D3DFMT.DXT5 args = (3,) - if fourcc == D3DFMT.DX10: + else: + fourcc = D3DFMT.DX10 + if pixel_format == "BC2": + args = (2,) + dxgi_format = DXGI_FORMAT.BC2_TYPELESS + else: + args = (3,) dxgi_format = DXGI_FORMAT.BC3_TYPELESS else: codec_name = "raw" From cd11792c15c60db34b1d506c816d753174422f86 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 20:16:33 +1100 Subject: [PATCH 373/628] Added BC5 saving --- Tests/test_file_dds.py | 20 +++++++++++++++++++- src/PIL/DdsImagePlugin.py | 13 +++++++++++-- src/libImaging/BcnEncode.c | 38 +++++++++++++++++++++++--------------- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 3239065d7..9a6042660 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -402,7 +402,7 @@ def test_not_implemented(test_file: str) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") im = hopper("HSV") - with pytest.raises(OSError): + with pytest.raises(OSError, match="cannot write mode HSV as DDS"): im.save(out) @@ -424,6 +424,13 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None: assert_image_equal_tofile(im, out) +def test_save_unsupported_pixel_format(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + im = hopper() + with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"): + im.save(out, pixel_format="UNKNOWN") + + def test_save_dxt1(tmp_path: Path) -> None: # RGB out = str(tmp_path / "temp.dds") @@ -493,3 +500,14 @@ def test_save_dxt5(tmp_path: Path) -> None: im_la = im_rgba.convert("LA") im_la.save(out, pixel_format="DXT5") assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.32) + + +def test_save_dx10_bc5(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im: + im.save(out, pixel_format="BC5") + assert_image_similar_tofile(im, out, 9.56) + + im = hopper("L") + with pytest.raises(OSError, match="only RGB mode can be written as BC5"): + im.save(out, pixel_format="BC5") diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index d65e3fc65..26307817c 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -530,7 +530,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: bitcount = len(im.getbands()) * 8 pixel_format = im.encoderinfo.get("pixel_format") args: tuple[int] | str - if pixel_format in ("DXT1", "BC2", "DXT3", "BC3", "DXT5"): + if pixel_format: codec_name = "bcn" flags |= DDSD.LINEARSIZE pitch = (im.width + 3) * 4 @@ -550,9 +550,18 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if pixel_format == "BC2": args = (2,) dxgi_format = DXGI_FORMAT.BC2_TYPELESS - else: + elif pixel_format == "BC3": args = (3,) dxgi_format = DXGI_FORMAT.BC3_TYPELESS + elif pixel_format == "BC5": + args = (5,) + dxgi_format = DXGI_FORMAT.BC5_TYPELESS + if im.mode != "RGB": + msg = "only RGB mode can be written as BC5" + raise OSError(msg) + else: + msg = f"cannot write pixel format {pixel_format}" + raise OSError(msg) else: codec_name = "raw" flags |= DDSD.PITCH diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 4e0da322e..2bad73b92 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -185,7 +185,7 @@ encode_bc2_block(Imaging im, ImagingCodecState state, UINT8 *dst) { } static void -encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { +encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst, int o) { int i, j; UINT8 alpha_min = 0, alpha_max = 0; UINT8 block[16], current_alpha; @@ -202,7 +202,7 @@ encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { continue; } - current_alpha = (UINT8)im->image[y][x + 3]; + current_alpha = (UINT8)im->image[y][x + o]; block[i + j * 4] = current_alpha; if (first || current_alpha < alpha_min) { @@ -226,12 +226,13 @@ encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { if (!current_alpha) { l |= 6 << (j * 3); continue; - } else if (current_alpha == 255 || denom == 0) { + } else if (current_alpha == 255) { l |= 7 << (j * 3); continue; } - float distance = abs(current_alpha - alpha_min) / denom * 10; + float distance = + denom == 0 ? 0 : abs(current_alpha - alpha_min) / denom * 10; if (distance < 3) { l |= 2 << (j * 3); // 4/5 * alpha_min + 1/5 * alpha_max } else if (distance < 5) { @@ -257,21 +258,28 @@ ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { UINT8 *dst = buf; for (;;) { - if (n == 2 || n == 3) { - if (has_alpha_channel) { - if (n == 2) { - encode_bc2_block(im, state, dst); + if (n == 5) { + encode_bc3_alpha(im, state, dst, 0); + dst += 8; + + encode_bc3_alpha(im, state, dst, 1); + } else { + if (n == 2 || n == 3) { + if (has_alpha_channel) { + if (n == 2) { + encode_bc2_block(im, state, dst); + } else { + encode_bc3_alpha(im, state, dst, 3); + } + dst += 8; } else { - encode_bc3_alpha(im, state, dst); - } - dst += 8; - } else { - for (int i = 0; i < 8; i++) { - *dst++ = 0xff; + for (int i = 0; i < 8; i++) { + *dst++ = 0xff; + } } } + encode_bc1_color(im, state, dst, n == 1 && has_alpha_channel); } - encode_bc1_color(im, state, dst, n == 1 && has_alpha_channel); dst += 8; state->x += im->pixelsize * 4; From 841ba163fd9fc6e1cf04837515c6208d67032a9a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Mar 2025 00:21:08 +1100 Subject: [PATCH 374/628] If every tile covers the image, only use the last offset --- src/PIL/TiffImagePlugin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2b471abac..39783f1f8 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1608,6 +1608,10 @@ class TiffImageFile(ImageFile.ImageFile): raise ValueError(msg) w = tilewidth + if w == xsize and h == ysize and self._planar_configuration != 2: + # Every tile covers the image. Only use the last offset + offsets = offsets[-1:] + for offset in offsets: if x + w > xsize: stride = w * sum(bps_tuple) / 8 # bytes per line @@ -1630,11 +1634,11 @@ class TiffImageFile(ImageFile.ImageFile): args, ) ) - x = x + w + x += w if x >= xsize: x, y = 0, y + h if y >= ysize: - x = y = 0 + y = 0 layer += 1 else: logger.debug("- unsupported data organization") From ba2c4291ea009a91e52512abb0258289b72d74d3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Mar 2025 19:22:15 +1100 Subject: [PATCH 375/628] Updated comment --- src/PIL/WebPImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index c2dde4431..1716a18cc 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -238,7 +238,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: cur_idx = im.tell() try: for ims in [im] + append_images: - # Get # of frames in this image + # Get number of frames in this image nfr = getattr(ims, "n_frames", 1) for idx in range(nfr): From 6cc5f1f0adee0ed79bd6a4595b90933939926645 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Mar 2025 20:58:40 +1100 Subject: [PATCH 376/628] Simplified code --- Tests/check_j2k_overflow.py | 2 +- Tests/check_large_memory.py | 2 +- Tests/check_large_memory_numpy.py | 2 +- Tests/helper.py | 8 ++- Tests/test_file_apng.py | 20 +++---- Tests/test_file_blp.py | 4 +- Tests/test_file_bmp.py | 12 ++-- Tests/test_file_bufrstub.py | 4 +- Tests/test_file_dds.py | 18 +++--- Tests/test_file_eps.py | 4 +- Tests/test_file_gif.py | 98 +++++++++++++++---------------- Tests/test_file_gribstub.py | 4 +- Tests/test_file_hdf5stub.py | 2 +- Tests/test_file_icns.py | 4 +- Tests/test_file_ico.py | 24 ++++---- Tests/test_file_im.py | 8 +-- Tests/test_file_jpeg.py | 38 ++++++------ Tests/test_file_jpeg2k.py | 6 +- Tests/test_file_libtiff.py | 66 ++++++++++----------- Tests/test_file_msp.py | 4 +- Tests/test_file_palm.py | 4 +- Tests/test_file_pcx.py | 4 +- Tests/test_file_pdf.py | 12 ++-- Tests/test_file_png.py | 30 +++++----- Tests/test_file_ppm.py | 36 ++++++------ Tests/test_file_sgi.py | 8 +-- Tests/test_file_spider.py | 2 +- Tests/test_file_tga.py | 18 +++--- Tests/test_file_tiff.py | 48 +++++++-------- Tests/test_file_tiff_metadata.py | 38 ++++++------ Tests/test_file_webp_alpha.py | 12 ++-- Tests/test_file_webp_animated.py | 18 +++--- Tests/test_file_webp_lossless.py | 2 +- Tests/test_file_webp_metadata.py | 2 +- Tests/test_file_wmf.py | 4 +- Tests/test_file_xbm.py | 4 +- Tests/test_image.py | 22 +++---- Tests/test_image_convert.py | 6 +- Tests/test_image_resize.py | 2 +- Tests/test_image_split.py | 4 +- Tests/test_imagefont.py | 2 +- Tests/test_imagesequence.py | 2 +- Tests/test_imagewin_pointers.py | 2 +- Tests/test_mode_i16.py | 2 +- Tests/test_pickle.py | 6 +- Tests/test_psdraw.py | 2 +- Tests/test_shell_injection.py | 2 +- Tests/test_tiff_ifdrational.py | 2 +- 48 files changed, 315 insertions(+), 311 deletions(-) diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index dbdd5a4f5..58566c4b2 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -9,6 +9,6 @@ from PIL import Image def test_j2k_overflow(tmp_path: Path) -> None: im = Image.new("RGBA", (1024, 131584)) - target = str(tmp_path / "temp.jpc") + target = tmp_path / "temp.jpc" with pytest.raises(OSError): im.save(target) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index a9ce79e57..c9feda3b1 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im = Image.new("L", (xdim, ydim), 0) im.save(f) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index f4ca8d0aa..458b0ab72 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -28,7 +28,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im = Image.fromarray(a, "L") im.save(f) diff --git a/Tests/helper.py b/Tests/helper.py index 764935f87..909fff879 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -13,6 +13,7 @@ import tempfile from collections.abc import Sequence from functools import lru_cache from io import BytesIO +from pathlib import Path from typing import Any, Callable import pytest @@ -95,7 +96,10 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) - def assert_image_equal_tofile( - a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None + a: Image.Image, + filename: str | Path, + msg: str | None = None, + mode: str | None = None, ) -> None: with Image.open(filename) as img: if mode: @@ -136,7 +140,7 @@ def assert_image_similar( def assert_image_similar_tofile( a: Image.Image, - filename: str, + filename: str | Path, epsilon: float, msg: str | None = None, ) -> None: diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index b9a036173..abd7d510b 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -345,7 +345,7 @@ def test_apng_sequence_errors(test_file: str) -> None: def test_apng_save(tmp_path: Path) -> None: with Image.open("Tests/images/apng/single_frame.png") as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file, save_all=True) with Image.open(test_file) as im: @@ -375,7 +375,7 @@ def test_apng_save(tmp_path: Path) -> None: def test_apng_save_alpha(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) @@ -393,7 +393,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: # frames with image data spanning multiple fdAT chunks (in this case # both the default image and first animation frame will span multiple # data chunks) - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/old-style-jpeg-compression.png") as im: frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] im.save( @@ -408,7 +408,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: def test_apng_save_duration_loop(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/apng/delay.png") as im: frames = [] durations = [] @@ -471,7 +471,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: def test_apng_save_disposal(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255)) @@ -572,7 +572,7 @@ def test_apng_save_disposal(tmp_path: Path) -> None: def test_apng_save_disposal_previous(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" size = (128, 64) blue = Image.new("RGBA", size, (0, 0, 255, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -594,7 +594,7 @@ def test_apng_save_disposal_previous(tmp_path: Path) -> None: def test_apng_save_blend(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255)) @@ -662,7 +662,7 @@ def test_apng_save_blend(tmp_path: Path) -> None: def test_apng_save_size(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im = Image.new("L", (100, 100)) im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))]) @@ -686,7 +686,7 @@ def test_seek_after_close() -> None: def test_different_modes_in_later_frames( mode: str, default_image: bool, duplicate: bool, tmp_path: Path ) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im = Image.new("L", (1, 1)) im.save( @@ -700,7 +700,7 @@ def test_different_modes_in_later_frames( def test_different_durations(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/apng/different_durations.png") as im: for _ in range(3): diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 9f2de8f98..9f50df22d 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -46,7 +46,7 @@ def test_invalid_file() -> None: def test_save(tmp_path: Path) -> None: - f = str(tmp_path / "temp.blp") + f = tmp_path / "temp.blp" for version in ("BLP1", "BLP2"): im = hopper("P") @@ -56,7 +56,7 @@ def test_save(tmp_path: Path) -> None: assert_image_equal(im.convert("RGB"), reloaded) with Image.open("Tests/images/transparent.png") as im: - f = str(tmp_path / "temp.blp") + f = tmp_path / "temp.blp" im.convert("P").save(f, blp_version=version) with Image.open(f) as reloaded: diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 2ff4160bd..64d2acaf5 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -17,7 +17,7 @@ from .helper import ( def test_sanity(tmp_path: Path) -> None: def roundtrip(im: Image.Image) -> None: - outfile = str(tmp_path / "temp.bmp") + outfile = tmp_path / "temp.bmp" im.save(outfile, "BMP") @@ -66,7 +66,7 @@ def test_small_palette(tmp_path: Path) -> None: colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] im.putpalette(colors) - out = str(tmp_path / "temp.bmp") + out = tmp_path / "temp.bmp" im.save(out) with Image.open(out) as reloaded: @@ -74,7 +74,7 @@ def test_small_palette(tmp_path: Path) -> None: def test_save_too_large(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.bmp") + outfile = tmp_path / "temp.bmp" with Image.new("RGB", (1, 1)) as im: im._size = (37838, 37838) with pytest.raises(ValueError): @@ -96,7 +96,7 @@ def test_dpi() -> None: def test_save_bmp_with_dpi(tmp_path: Path) -> None: # Test for #1301 # Arrange - outfile = str(tmp_path / "temp.jpg") + outfile = tmp_path / "temp.jpg" with Image.open("Tests/images/hopper.bmp") as im: assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) @@ -112,7 +112,7 @@ def test_save_bmp_with_dpi(tmp_path: Path) -> None: def test_save_float_dpi(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.bmp") + outfile = tmp_path / "temp.bmp" with Image.open("Tests/images/hopper.bmp") as im: im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) with Image.open(outfile) as reloaded: @@ -152,7 +152,7 @@ def test_dib_header_size(header_size: int, path: str) -> None: def test_save_dib(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.dib") + outfile = tmp_path / "temp.dib" with Image.open("Tests/images/clipboard.dib") as im: im.save(outfile) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index fc8920317..362578c56 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -43,7 +43,7 @@ def test_load() -> None: def test_save(tmp_path: Path) -> None: # Arrange im = hopper() - tmpfile = str(tmp_path / "temp.bufr") + tmpfile = tmp_path / "temp.bufr" # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): @@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None: im.load() assert handler.is_loaded() - temp_file = str(tmp_path / "temp.bufr") + temp_file = tmp_path / "temp.bufr" im.save(temp_file) assert handler.saved diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 9a6042660..3388fce16 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -116,7 +116,7 @@ def test_sanity_ati1_bc4u(image_path: str) -> None: def test_dx10_bc2(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT3) as im: im.save(out, pixel_format="BC2") @@ -129,7 +129,7 @@ def test_dx10_bc2(tmp_path: Path) -> None: def test_dx10_bc3(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT5) as im: im.save(out, pixel_format="BC3") @@ -400,7 +400,7 @@ def test_not_implemented(test_file: str) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" im = hopper("HSV") with pytest.raises(OSError, match="cannot write mode HSV as DDS"): im.save(out) @@ -416,7 +416,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None: ], ) def test_save(mode: str, test_file: str, tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(test_file) as im: assert im.mode == mode im.save(out) @@ -425,7 +425,7 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None: def test_save_unsupported_pixel_format(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" im = hopper() with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"): im.save(out, pixel_format="UNKNOWN") @@ -433,7 +433,7 @@ def test_save_unsupported_pixel_format(tmp_path: Path) -> None: def test_save_dxt1(tmp_path: Path) -> None: # RGB - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT1) as im: im.convert("RGB").save(out, pixel_format="DXT1") assert_image_similar_tofile(im, out, 1.84) @@ -458,7 +458,7 @@ def test_save_dxt1(tmp_path: Path) -> None: def test_save_dxt3(tmp_path: Path) -> None: # RGB - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT3) as im: im_rgb = im.convert("RGB") im_rgb.save(out, pixel_format="DXT3") @@ -481,7 +481,7 @@ def test_save_dxt3(tmp_path: Path) -> None: def test_save_dxt5(tmp_path: Path) -> None: # RGB - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT1) as im: im.convert("RGB").save(out, pixel_format="DXT5") assert_image_similar_tofile(im, out, 1.84) @@ -503,7 +503,7 @@ def test_save_dxt5(tmp_path: Path) -> None: def test_save_dx10_bc5(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im: im.save(out, pixel_format="BC5") assert_image_similar_tofile(im, out, 9.56) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index a0c2f9216..f5acc532c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -239,7 +239,7 @@ def test_transparency() -> None: def test_file_object(tmp_path: Path) -> None: # issue 479 with Image.open(FILE1) as image1: - with open(str(tmp_path / "temp.eps"), "wb") as fh: + with open(tmp_path / "temp.eps", "wb") as fh: image1.save(fh, "EPS") @@ -274,7 +274,7 @@ def test_1(filename: str) -> None: def test_image_mode_not_supported(tmp_path: Path) -> None: im = hopper("RGBA") - tmpfile = str(tmp_path / "temp.eps") + tmpfile = tmp_path / "temp.eps" with pytest.raises(ValueError): im.save(tmpfile) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index dbbffc675..fb1a636ed 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -228,7 +228,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None: def test_full_palette_second_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (1, 256)) full_palette_im = Image.new("P", (1, 256)) @@ -249,7 +249,7 @@ def test_full_palette_second_frame(tmp_path: Path) -> None: def test_roundtrip(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = hopper() im.save(out) with Image.open(out) as reread: @@ -258,7 +258,7 @@ def test_roundtrip(tmp_path: Path) -> None: def test_roundtrip2(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/403 - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open(TEST_GIF) as im: im2 = im.copy() im2.save(out) @@ -268,7 +268,7 @@ def test_roundtrip2(tmp_path: Path) -> None: def test_roundtrip_save_all(tmp_path: Path) -> None: # Single frame image - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = hopper() im.save(out, save_all=True) with Image.open(out) as reread: @@ -276,7 +276,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, save_all=True) with Image.open(out) as reread: @@ -284,7 +284,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: def test_roundtrip_save_all_1(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("1", (1, 1)) im2 = Image.new("1", (1, 1), 1) im.save(out, save_all=True, append_images=[im2]) @@ -329,7 +329,7 @@ def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: info = im.info.copy() - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, save_all=True) with Image.open(out) as reread: for header in important_headers: @@ -345,7 +345,7 @@ def test_palette_handling(tmp_path: Path) -> None: im = im.resize((100, 100), Image.Resampling.LANCZOS) im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) - f = str(tmp_path / "temp.gif") + f = tmp_path / "temp.gif" im2.save(f, optimize=True) with Image.open(f) as reloaded: @@ -356,7 +356,7 @@ def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.copy().save(out, "GIF", **kwargs) reloaded = Image.open(out) @@ -599,7 +599,7 @@ def test_previous_frame_loaded() -> None: def test_save_dispose(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#111"), @@ -627,7 +627,7 @@ def test_save_dispose(tmp_path: Path) -> None: def test_dispose2_palette(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Four colors: white, gray, black, red circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] @@ -661,7 +661,7 @@ def test_dispose2_palette(tmp_path: Path) -> None: def test_dispose2_diff(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # 4 frames: red/blue, red/red, blue/blue, red/blue circles = [ @@ -703,7 +703,7 @@ def test_dispose2_diff(tmp_path: Path) -> None: def test_dispose2_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [] @@ -729,7 +729,7 @@ def test_dispose2_background(tmp_path: Path) -> None: def test_dispose2_background_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [Image.new("RGBA", (1, 20))] @@ -747,7 +747,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: def test_dispose2_previous_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (100, 100)) im.info["transparency"] = 0 @@ -766,7 +766,7 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None: def test_dispose2_without_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (100, 100)) @@ -781,7 +781,7 @@ def test_dispose2_without_transparency(tmp_path: Path) -> None: def test_transparency_in_second_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/different_transparency.gif") as im: assert im.info["transparency"] == 0 @@ -811,7 +811,7 @@ def test_no_transparency_in_second_frame() -> None: def test_remapped_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (1, 2)) im2 = im.copy() @@ -829,7 +829,7 @@ def test_remapped_transparency(tmp_path: Path) -> None: def test_duration(tmp_path: Path) -> None: duration = 1000 - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") # Check that the argument has priority over the info settings @@ -843,7 +843,7 @@ def test_duration(tmp_path: Path) -> None: def test_multiple_duration(tmp_path: Path) -> None: duration_list = [1000, 2000, 3000] - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#111"), @@ -878,7 +878,7 @@ def test_multiple_duration(tmp_path: Path) -> None: def test_roundtrip_info_duration(tmp_path: Path) -> None: duration_list = [100, 500, 500] - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/transparent_dispose.gif") as im: assert [ frame.info["duration"] for frame in ImageSequence.Iterator(im) @@ -893,7 +893,7 @@ def test_roundtrip_info_duration(tmp_path: Path) -> None: def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/duplicate_frame.gif") as im: assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ 1000, @@ -911,7 +911,7 @@ def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: def test_identical_frames(tmp_path: Path) -> None: duration_list = [1000, 1500, 2000, 4000] - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), @@ -944,7 +944,7 @@ def test_identical_frames(tmp_path: Path) -> None: def test_identical_frames_to_single_frame( duration: int | list[int], tmp_path: Path ) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), @@ -961,7 +961,7 @@ def test_identical_frames_to_single_frame( def test_loop_none(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.save(out, loop=None) with Image.open(out) as reread: @@ -971,7 +971,7 @@ def test_loop_none(tmp_path: Path) -> None: def test_number_of_loops(tmp_path: Path) -> None: number_of_loops = 2 - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.save(out, loop=number_of_loops) with Image.open(out) as reread: @@ -987,7 +987,7 @@ def test_number_of_loops(tmp_path: Path) -> None: def test_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.info["background"] = 1 im.save(out) @@ -996,7 +996,7 @@ def test_background(tmp_path: Path) -> None: def test_webp_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Test opaque WebP background if features.check("webp"): @@ -1014,7 +1014,7 @@ def test_comment(tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.info["comment"] = b"Test comment text" im.save(out) @@ -1031,7 +1031,7 @@ def test_comment(tmp_path: Path) -> None: def test_comment_over_255(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") comment = b"Test comment text" while len(comment) < 256: @@ -1057,7 +1057,7 @@ def test_read_multiple_comment_blocks() -> None: def test_empty_string_comment(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/chi.gif") as im: assert "comment" in im.info @@ -1091,7 +1091,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: assert "comment" not in im.info # Test that a saved image keeps the comment - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/dispose_prev.gif") as im: im.save(out, save_all=True, comment="Test") @@ -1101,7 +1101,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: def test_version(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" def assert_version_after_save(im: Image.Image, version: bytes) -> None: im.save(out) @@ -1131,7 +1131,7 @@ def test_version(tmp_path: Path) -> None: def test_append_images(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Test appending single frame images im = Image.new("RGB", (100, 100), "#f00") @@ -1160,7 +1160,7 @@ def test_append_images(tmp_path: Path) -> None: def test_append_different_size_image(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGB", (100, 100)) bigger_im = Image.new("RGB", (200, 200), "#f00") @@ -1187,7 +1187,7 @@ def test_transparent_optimize(tmp_path: Path) -> None: im.frombytes(data) im.putpalette(palette) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, transparency=im.getpixel((252, 0))) with Image.open(out) as reloaded: @@ -1195,7 +1195,7 @@ def test_transparent_optimize(tmp_path: Path) -> None: def test_removed_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGB", (256, 1)) for x in range(256): @@ -1210,7 +1210,7 @@ def test_removed_transparency(tmp_path: Path) -> None: def test_rgb_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Single frame im = Image.new("RGB", (1, 1)) @@ -1232,7 +1232,7 @@ def test_rgb_transparency(tmp_path: Path) -> None: def test_rgba_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = hopper("P") im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) @@ -1249,7 +1249,7 @@ def test_background_outside_palettte(tmp_path: Path) -> None: def test_bbox(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGB", (100, 100), "#fff") ims = [Image.new("RGB", (100, 100), "#000")] @@ -1260,7 +1260,7 @@ def test_bbox(tmp_path: Path) -> None: def test_bbox_alpha(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) im.putpixel((0, 1), (255, 0, 0, 0)) @@ -1279,7 +1279,7 @@ def test_palette_save_L(tmp_path: Path) -> None: palette = im.getpalette() assert palette is not None - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_l.save(out, palette=bytes(palette)) with Image.open(out) as reloaded: @@ -1290,7 +1290,7 @@ def test_palette_save_P(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, palette=bytes((1, 2, 3, 4, 5, 6))) with Image.open(out) as reloaded: @@ -1306,7 +1306,7 @@ def test_palette_save_duplicate_entries(tmp_path: Path) -> None: im.putpalette((0, 0, 0, 0, 0, 0)) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) with Image.open(out) as reloaded: @@ -1321,7 +1321,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None: frame.putpalette(color) frames.append(frame) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" frames[0].save( out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] ) @@ -1344,7 +1344,7 @@ def test_palette_save_ImagePalette(tmp_path: Path) -> None: im = hopper("P") palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, palette=palette) with Image.open(out) as reloaded: @@ -1357,7 +1357,7 @@ def test_save_I(tmp_path: Path) -> None: im = hopper("I") - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out) with Image.open(out) as reloaded: @@ -1441,7 +1441,7 @@ def test_missing_background() -> None: def test_saving_rgba(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/transparent.png") as im: im.save(out) @@ -1452,7 +1452,7 @@ def test_saving_rgba(tmp_path: Path) -> None: @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im1 = Image.new("P", (100, 100)) d = ImageDraw.Draw(im1) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 02e464ff1..960e5f4be 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -43,7 +43,7 @@ def test_load() -> None: def test_save(tmp_path: Path) -> None: # Arrange im = hopper() - tmpfile = str(tmp_path / "temp.grib") + tmpfile = tmp_path / "temp.grib" # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): @@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None: im.load() assert handler.is_loaded() - temp_file = str(tmp_path / "temp.grib") + temp_file = tmp_path / "temp.grib" im.save(temp_file) assert handler.saved diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 024be9e80..50864009f 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -81,7 +81,7 @@ def test_handler(tmp_path: Path) -> None: im.load() assert handler.is_loaded() - temp_file = str(tmp_path / "temp.h5") + temp_file = tmp_path / "temp.h5" im.save(temp_file) assert handler.saved diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 94f16aeec..b6dc4bc19 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -43,7 +43,7 @@ def test_load() -> None: def test_save(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.icns") + temp_file = tmp_path / "temp.icns" with Image.open(TEST_FILE) as im: im.save(temp_file) @@ -60,7 +60,7 @@ def test_save(tmp_path: Path) -> None: def test_save_append_images(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.icns") + temp_file = tmp_path / "temp.icns" provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) with Image.open(TEST_FILE) as im: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 2f5e4ca5a..37bfd3f1f 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -41,7 +41,7 @@ def test_black_and_white() -> None: def test_palette(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") + temp_file = tmp_path / "temp.ico" im = Image.new("P", (16, 16)) im.save(temp_file) @@ -88,7 +88,7 @@ def test_save_to_bytes() -> None: def test_getpixel(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") + temp_file = tmp_path / "temp.ico" im = hopper() im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) @@ -101,8 +101,8 @@ def test_getpixel(tmp_path: Path) -> None: def test_no_duplicates(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - temp_file2 = str(tmp_path / "temp2.ico") + temp_file = tmp_path / "temp.ico" + temp_file2 = tmp_path / "temp2.ico" im = hopper() sizes = [(32, 32), (64, 64)] @@ -115,8 +115,8 @@ def test_no_duplicates(tmp_path: Path) -> None: def test_different_bit_depths(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - temp_file2 = str(tmp_path / "temp2.ico") + temp_file = tmp_path / "temp.ico" + temp_file2 = tmp_path / "temp2.ico" im = hopper() im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) @@ -132,8 +132,8 @@ def test_different_bit_depths(tmp_path: Path) -> None: assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) # Test that only matching sizes of different bit depths are saved - temp_file3 = str(tmp_path / "temp3.ico") - temp_file4 = str(tmp_path / "temp4.ico") + temp_file3 = tmp_path / "temp3.ico" + temp_file4 = tmp_path / "temp4.ico" im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) im.save( @@ -186,7 +186,7 @@ def test_save_256x256(tmp_path: Path) -> None: """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" # Arrange with Image.open("Tests/images/hopper_256x256.ico") as im: - outfile = str(tmp_path / "temp_saved_hopper_256x256.ico") + outfile = tmp_path / "temp_saved_hopper_256x256.ico" # Act im.save(outfile) @@ -202,7 +202,7 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None: """ # Arrange with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 - outfile = str(tmp_path / "temp_saved_python.ico") + outfile = tmp_path / "temp_saved_python.ico" # Act im.save(outfile) @@ -215,7 +215,7 @@ def test_save_append_images(tmp_path: Path) -> None: # append_images should be used for scaled down versions of the image im = hopper("RGBA") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) - outfile = str(tmp_path / "temp_saved_multi_icon.ico") + outfile = tmp_path / "temp_saved_multi_icon.ico" im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) with Image.open(outfile) as reread: @@ -235,7 +235,7 @@ def test_unexpected_size() -> None: def test_draw_reloaded(tmp_path: Path) -> None: with Image.open(TEST_ICO_FILE) as im: - outfile = str(tmp_path / "temp_saved_hopper_draw.ico") + outfile = tmp_path / "temp_saved_hopper_draw.ico" draw = ImageDraw.Draw(im) draw.line((0, 0) + im.size, "#f00") diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index d29998801..235914a2b 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -23,7 +23,7 @@ def test_sanity() -> None: def test_name_limit(tmp_path: Path) -> None: - out = str(tmp_path / ("name_limit_test" * 7 + ".im")) + out = tmp_path / ("name_limit_test" * 7 + ".im") with Image.open(TEST_IM) as im: im.save(out) assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") @@ -87,7 +87,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) def test_roundtrip(mode: str, tmp_path: Path) -> None: - out = str(tmp_path / "temp.im") + out = tmp_path / "temp.im" im = hopper(mode) im.save(out) assert_image_equal_tofile(im, out) @@ -98,7 +98,7 @@ def test_small_palette(tmp_path: Path) -> None: colors = [0, 1, 2] im.putpalette(colors) - out = str(tmp_path / "temp.im") + out = tmp_path / "temp.im" im.save(out) with Image.open(out) as reloaded: @@ -106,7 +106,7 @@ def test_small_palette(tmp_path: Path) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None: - out = str(tmp_path / "temp.im") + out = tmp_path / "temp.im" im = hopper("HSV") with pytest.raises(ValueError): im.save(out) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index a2481c336..8ab853b85 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -83,7 +83,7 @@ class TestFileJpeg: @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = Image.new("RGB", size) with pytest.raises(ValueError): im.save(f) @@ -194,7 +194,7 @@ class TestFileJpeg: icc_profile = im1.info["icc_profile"] assert len(icc_profile) == 3144 # Roundtrip via physical file. - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im1.save(f, icc_profile=icc_profile) with Image.open(f) as im2: assert im2.info.get("icc_profile") == icc_profile @@ -238,7 +238,7 @@ class TestFileJpeg: # Sometimes the meta data on the icc_profile block is bigger than # Image.MAXBLOCK or the image size. with Image.open("Tests/images/icc_profile_big.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" icc_profile = im.info["icc_profile"] # Should not raise OSError for image with icc larger than image size. im.save( @@ -250,11 +250,11 @@ class TestFileJpeg: ) with Image.open("Tests/images/flower2.jpg") as im: - f = str(tmp_path / "temp2.jpg") + f = tmp_path / "temp2.jpg" im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955) with Image.open("Tests/images/flower2.jpg") as im: - f = str(tmp_path / "temp3.jpg") + f = tmp_path / "temp3.jpg" im.save(f, progressive=True, quality=94, exif=b" " * 43668) def test_optimize(self) -> None: @@ -268,7 +268,7 @@ class TestFileJpeg: def test_optimize_large_buffer(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) @@ -288,13 +288,13 @@ class TestFileJpeg: assert im1_bytes >= im3_bytes def test_progressive_large_buffer(self, tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = self.gen_random_image((255, 255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) @@ -307,7 +307,7 @@ class TestFileJpeg: def test_large_exif(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = hopper() im.save(f, "JPEG", quality=90, exif=b"1" * 65533) @@ -335,7 +335,7 @@ class TestFileJpeg: assert exif[gps_index] == expected_exif_gps # Writing - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" exif = Image.Exif() exif[gps_index] = expected_exif_gps hopper().save(f, exif=exif) @@ -505,15 +505,15 @@ class TestFileJpeg: def test_quality_keep(self, tmp_path: Path) -> None: # RGB with Image.open("Tests/images/hopper.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im.save(f, quality="keep") # Grayscale with Image.open("Tests/images/hopper_gray.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im.save(f, quality="keep") # CMYK with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im.save(f, quality="keep") def test_junk_jpeg_header(self) -> None: @@ -726,7 +726,7 @@ class TestFileJpeg: def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: im = self.gen_random_image((512, 512)) - f = str(tmp_path / "temp.jpeg") + f = tmp_path / "temp.jpeg" im.save(f, quality=100, optimize=True) with Image.open(f) as reloaded: @@ -762,7 +762,7 @@ class TestFileJpeg: def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: # Arrange - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/hopper.tif") as im: # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -773,7 +773,7 @@ class TestFileJpeg: assert im.info["dpi"] == reloaded.info["dpi"] def test_save_dpi_rounding(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.jpg") + outfile = tmp_path / "temp.jpg" with Image.open("Tests/images/hopper.jpg") as im: im.save(outfile, dpi=(72.2, 72.2)) @@ -859,7 +859,7 @@ class TestFileJpeg: exif = im.getexif() assert exif[282] == 180 - out = str(tmp_path / "out.jpg") + out = tmp_path / "out.jpg" with warnings.catch_warnings(): warnings.simplefilter("error") @@ -1005,7 +1005,7 @@ class TestFileJpeg: assert im.getxmp() == {"xmpmeta": None} def test_save_xmp(self, tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = hopper() im.save(f, xmp=b"XMP test") with Image.open(f) as reloaded: @@ -1094,7 +1094,7 @@ class TestFileJpeg: @skip_unless_feature("jpg") class TestFileCloseW32: def test_fd_leak(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.jpg") + tmpfile = tmp_path / "temp.jpg" with Image.open("Tests/images/hopper.jpg") as im: im.save(tmpfile) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index e429610ad..916df2586 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -99,7 +99,7 @@ def test_bytesio(card: ImageFile.ImageFile) -> None: def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() - outfile = str(tmp_path / "temp_test-card.png") + outfile = tmp_path / "temp_test-card.png" im.save(outfile) assert_image_similar(im, card, 1.0e-3) @@ -213,7 +213,7 @@ def test_header_errors() -> None: def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp_layers.jp2") + outfile = tmp_path / "temp_layers.jp2" for quality_layers in [[100, 50, 10], (100, 50, 10), None]: card.save(outfile, quality_layers=quality_layers) @@ -289,7 +289,7 @@ def test_mct(card: ImageFile.ImageFile) -> None: def test_sgnd(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.jp2") + outfile = tmp_path / "temp.jp2" im = Image.new("L", (1, 1)) im.save(outfile) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index f284c3f2f..25d1f5712 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -39,7 +39,7 @@ class LibTiffTestCase: assert im._compression == "group4" # can we write it back out, in a different form. - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" im.save(out) out_bytes = io.BytesIO() @@ -123,7 +123,7 @@ class TestFileLibTiff(LibTiffTestCase): """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" rot = orig.transpose(Image.Transpose.ROTATE_90) assert rot.size == (500, 500) rot.save(out) @@ -151,7 +151,7 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("legacy_api", (False, True)) def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: """Test metadata writing through libtiff""" - f = str(tmp_path / "temp.tiff") + f = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper_g4.tif") as img: img.save(f, tiffinfo=img.tag) @@ -247,7 +247,7 @@ class TestFileLibTiff(LibTiffTestCase): # Extra samples really doesn't make sense in this application. del new_ifd[338] - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out, tiffinfo=new_ifd) @@ -313,7 +313,7 @@ class TestFileLibTiff(LibTiffTestCase): ) -> None: im = hopper() - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, tiffinfo=tiffinfo) with Image.open(out) as reloaded: @@ -347,13 +347,13 @@ class TestFileLibTiff(LibTiffTestCase): ) def test_osubfiletype(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: im.tag_v2[OSUBFILETYPE] = 1 im.save(outfile) def test_subifd(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: im.tag_v2[SUBIFD] = 10000 @@ -365,7 +365,7 @@ class TestFileLibTiff(LibTiffTestCase): ) -> None: monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) with Image.open(out) as reloaded: @@ -375,7 +375,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: # issue #1765 im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out, dpi=(72, 72)) with Image.open(out) as reloaded: @@ -383,7 +383,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_g3_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_g4_500.tif") as i: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" i.save(out, compression="group3") with Image.open(out) as reread: @@ -400,7 +400,7 @@ class TestFileLibTiff(LibTiffTestCase): assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" # out = "temp.le.tif" im.save(out) with Image.open(out) as reread: @@ -420,7 +420,7 @@ class TestFileLibTiff(LibTiffTestCase): assert b[0] == ord(b"\x01") assert b[1] == ord(b"\xe0") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out) with Image.open(out) as reread: assert reread.info["compression"] == im.info["compression"] @@ -430,7 +430,7 @@ class TestFileLibTiff(LibTiffTestCase): """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" orig.tag[269] = "temp.tif" orig.save(out) @@ -457,7 +457,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_blur(self, tmp_path: Path) -> None: # test case from irc, how to do blur on b/w image # and save to compressed tif. - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with Image.open("Tests/images/pport_g4.tif") as im: im = im.convert("L") @@ -470,7 +470,7 @@ class TestFileLibTiff(LibTiffTestCase): # Test various tiff compressions and assert similar image content but reduced # file sizes. im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out) size_raw = os.path.getsize(out) @@ -494,7 +494,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression="tiff_jpeg") with Image.open(out) as reloaded: @@ -502,7 +502,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_tiff_deflate_compression(self, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression="tiff_deflate") with Image.open(out) as reloaded: @@ -510,7 +510,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_quality(self, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with pytest.raises(ValueError): im.save(out, compression="tiff_lzw", quality=50) @@ -525,7 +525,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_cmyk_save(self, tmp_path: Path) -> None: im = hopper("CMYK") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression="tiff_adobe_deflate") assert_image_equal_tofile(im, out) @@ -534,7 +534,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_palette_save( self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out) @@ -546,7 +546,7 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with pytest.raises(OSError): im.save(out, compression=compression) @@ -686,7 +686,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_save_ycbcr(self, tmp_path: Path) -> None: im = hopper("YCbCr") - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile, compression="jpeg") with Image.open(outfile) as reloaded: @@ -713,7 +713,7 @@ class TestFileLibTiff(LibTiffTestCase): ) -> None: # issue 1597 with Image.open("Tests/images/rdf.tif") as im: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) # this shouldn't crash @@ -724,7 +724,7 @@ class TestFileLibTiff(LibTiffTestCase): # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. # The second is the total number of pages, zero means not available. - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" # Created by printing a page in Chrome to PDF, then: # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif # -dNOPAUSE /tmp/test.pdf -c quit @@ -736,7 +736,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_fd_duplication(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1651 - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" with open(tmpfile, "wb") as f: with open("Tests/images/g4-multi.tiff", "rb") as src: f.write(src.read()) @@ -779,7 +779,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc_profile = img.info["icc_profile"] - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" img.save(out, icc_profile=icc_profile) with Image.open(out) as reloaded: assert icc_profile == reloaded.info["icc_profile"] @@ -802,7 +802,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: # Arrange - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif # Contains JPEGTables (347) tag @@ -864,7 +864,7 @@ class TestFileLibTiff(LibTiffTestCase): self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: im = Image.new("F", (1, 1)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out) @@ -1008,7 +1008,7 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("compression", (None, "jpeg")) def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" tags = { TiffImagePlugin.TILEWIDTH: 256, @@ -1147,7 +1147,7 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression=compression) with Image.open(out) as im: @@ -1160,7 +1160,7 @@ class TestFileLibTiff(LibTiffTestCase): self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: im = hopper("RGB").resize((256, 256)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" if not argument: monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18) @@ -1176,13 +1176,13 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with pytest.raises(SystemError): im.save(out, compression=compression) def test_save_many_compressed(self, tmp_path: Path) -> None: im = hopper() - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" for _ in range(10000): im.save(out, compression="jpeg") diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index b0964aabe..8c91922bd 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -15,7 +15,7 @@ YA_EXTRA_DIR = "Tests/images/msp" def test_sanity(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.msp") + test_file = tmp_path / "temp.msp" hopper("1").save(test_file) @@ -84,7 +84,7 @@ def test_msp_v2() -> None: def test_cannot_save_wrong_mode(tmp_path: Path) -> None: # Arrange im = hopper() - filename = str(tmp_path / "temp.msp") + filename = tmp_path / "temp.msp" # Act/Assert with pytest.raises(OSError): diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 194f39b30..a1859bc33 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -14,7 +14,7 @@ from .helper import assert_image_equal, hopper, magick_command def helper_save_as_palm(tmp_path: Path, mode: str) -> None: # Arrange im = hopper(mode) - outfile = str(tmp_path / ("temp_" + mode + ".palm")) + outfile = tmp_path / ("temp_" + mode + ".palm") # Act im.save(outfile) @@ -25,7 +25,7 @@ def helper_save_as_palm(tmp_path: Path, mode: str) -> None: def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: - outfile = str(tmp_path / "temp.png") + outfile = tmp_path / "temp.png" rc = subprocess.call( magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT ) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 21c32268c..5d7fd1c1b 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -11,7 +11,7 @@ from .helper import assert_image_equal, hopper def _roundtrip(tmp_path: Path, im: Image.Image) -> None: - f = str(tmp_path / "temp.pcx") + f = tmp_path / "temp.pcx" im.save(f) with Image.open(f) as im2: assert im2.mode == im.mode @@ -31,7 +31,7 @@ def test_sanity(tmp_path: Path) -> None: _roundtrip(tmp_path, im) # Test an unsupported mode - f = str(tmp_path / "temp.pcx") + f = tmp_path / "temp.pcx" im = hopper("RGBA") with pytest.raises(ValueError): im.save(f) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 815686a52..bde1e3ab8 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -55,7 +55,7 @@ def test_save_alpha(tmp_path: Path, mode: str) -> None: def test_p_alpha(tmp_path: Path) -> None: # Arrange - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" with Image.open("Tests/images/pil123p.png") as im: assert im.mode == "P" assert isinstance(im.info["transparency"], bytes) @@ -80,7 +80,7 @@ def test_monochrome(tmp_path: Path) -> None: def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("PA") - outfile = str(tmp_path / "temp_PA.pdf") + outfile = tmp_path / "temp_PA.pdf" with pytest.raises(ValueError): im.save(outfile) @@ -89,7 +89,7 @@ def test_unsupported_mode(tmp_path: Path) -> None: def test_resolution(tmp_path: Path) -> None: im = hopper() - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile, resolution=150) with open(outfile, "rb") as fp: @@ -117,7 +117,7 @@ def test_resolution(tmp_path: Path) -> None: def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: im = hopper() - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile, "PDF", **params) with open(outfile, "rb") as fp: @@ -144,7 +144,7 @@ def test_save_all(tmp_path: Path) -> None: # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile, save_all=True) assert os.path.isfile(outfile) @@ -177,7 +177,7 @@ def test_save_all(tmp_path: Path) -> None: def test_multiframe_normal_save(tmp_path: Path) -> None: # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile) assert os.path.isfile(outfile) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index efd2e5cd9..f99ca91a3 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -68,7 +68,7 @@ def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile: @skip_unless_feature("zlib") class TestFilePng: - def get_chunks(self, filename: str) -> list[bytes]: + def get_chunks(self, filename: Path) -> list[bytes]: chunks = [] with open(filename, "rb") as fp: fp.read(8) @@ -89,7 +89,7 @@ class TestFilePng: assert version is not None assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" hopper("RGB").save(test_file) @@ -250,7 +250,7 @@ class TestFilePng: # each palette entry assert len(im.info["transparency"]) == 256 - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) # check if saved image contains same transparency @@ -271,7 +271,7 @@ class TestFilePng: assert im.info["transparency"] == 164 assert im.getpixel((31, 31)) == 164 - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) # check if saved image contains same transparency @@ -294,7 +294,7 @@ class TestFilePng: assert im.getcolors() == [(100, (0, 0, 0, 0))] im = im.convert("P") - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) # check if saved image contains same transparency @@ -315,7 +315,7 @@ class TestFilePng: im_rgba = im.convert("RGBA") assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) with Image.open(test_file) as test_im: @@ -329,7 +329,7 @@ class TestFilePng: def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" with Image.open(in_file) as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) def test_load_verify(self) -> None: @@ -488,7 +488,7 @@ class TestFilePng: im = hopper("P") im.info["transparency"] = 0 - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im.save(f) with Image.open(f) as im2: @@ -549,7 +549,7 @@ class TestFilePng: def test_chunk_order(self, tmp_path: Path) -> None: with Image.open("Tests/images/icc_profile.png") as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.convert("P").save(test_file, dpi=(100, 100)) chunks = self.get_chunks(test_file) @@ -661,7 +661,7 @@ class TestFilePng: def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: im = hopper("P") - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" im.save(out, bits=4, save_all=save_all) with Image.open(out) as reloaded: @@ -671,8 +671,8 @@ class TestFilePng: im = Image.new("P", (1, 1)) im.putpalette((1, 1, 1)) - out = str(tmp_path / "temp.png") - im.save(str(tmp_path / "temp.png")) + out = tmp_path / "temp.png" + im.save(tmp_path / "temp.png") with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 @@ -721,7 +721,7 @@ class TestFilePng: def test_exif_save(self, tmp_path: Path) -> None: # Test exif is not saved from info - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/exif.png") as im: im.save(test_file) @@ -741,7 +741,7 @@ class TestFilePng: ) def test_exif_from_jpg(self, tmp_path: Path) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: @@ -750,7 +750,7 @@ class TestFilePng: def test_exif_argument(self, tmp_path: Path) -> None: with Image.open(TEST_PNG_FILE) as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file, exif=b"exifstring") with Image.open(test_file) as reloaded: diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index c93a8c73a..41e2b5416 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -94,7 +94,7 @@ def test_16bit_pgm() -> None: def test_16bit_pgm_write(tmp_path: Path) -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: - filename = str(tmp_path / "temp.pgm") + filename = tmp_path / "temp.pgm" im.save(filename, "PPM") assert_image_equal_tofile(im, filename) @@ -106,7 +106,7 @@ def test_pnm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) - filename = str(tmp_path / "temp.pnm") + filename = tmp_path / "temp.pnm" im.save(filename) assert_image_equal_tofile(im, filename) @@ -117,7 +117,7 @@ def test_pfm(tmp_path: Path) -> None: assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) - filename = str(tmp_path / "tmp.pfm") + filename = tmp_path / "tmp.pfm" im.save(filename) assert_image_equal_tofile(im, filename) @@ -128,7 +128,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None: assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) - filename = str(tmp_path / "tmp.pfm") + filename = tmp_path / "tmp.pfm" im.save(filename) assert_image_equal_tofile(im, filename) @@ -194,8 +194,8 @@ def test_16bit_plain_pgm() -> None: def test_plain_data_with_comment( tmp_path: Path, header: bytes, data: bytes, comment_count: int ) -> None: - path1 = str(tmp_path / "temp1.ppm") - path2 = str(tmp_path / "temp2.ppm") + path1 = tmp_path / "temp1.ppm" + path2 = tmp_path / "temp2.ppm" comment = b"# comment" * comment_count with open(path1, "wb") as f1, open(path2, "wb") as f2: f1.write(header + b"\n\n" + data) @@ -207,7 +207,7 @@ def test_plain_data_with_comment( @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(data) @@ -218,7 +218,7 @@ def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(data) @@ -235,7 +235,7 @@ def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: ), ) def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(data) @@ -245,7 +245,7 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: def test_plain_ppm_value_negative(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n-1") @@ -255,7 +255,7 @@ def test_plain_ppm_value_negative(tmp_path: Path) -> None: def test_plain_ppm_value_too_large(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n256") @@ -270,7 +270,7 @@ def test_magic() -> None: def test_header_with_comments(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") @@ -279,7 +279,7 @@ def test_header_with_comments(tmp_path: Path) -> None: def test_non_integer_token(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6\nTEST") @@ -289,7 +289,7 @@ def test_non_integer_token(tmp_path: Path) -> None: def test_header_token_too_long(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6\n 01234567890") @@ -300,7 +300,7 @@ def test_header_token_too_long(tmp_path: Path) -> None: def test_truncated_file(tmp_path: Path) -> None: # Test EOF in header - path = str(tmp_path / "temp.pgm") + path = tmp_path / "temp.pgm" with open(path, "wb") as f: f.write(b"P6") @@ -316,7 +316,7 @@ def test_truncated_file(tmp_path: Path) -> None: def test_not_enough_image_data(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P2 1 2 255 255") @@ -327,7 +327,7 @@ def test_not_enough_image_data(tmp_path: Path) -> None: @pytest.mark.parametrize("maxval", (b"0", b"65536")) def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) @@ -350,7 +350,7 @@ def test_neg_ppm() -> None: def test_mimetypes(tmp_path: Path) -> None: - path = str(tmp_path / "temp.pgm") + path = tmp_path / "temp.pgm" with open(path, "wb") as f: f.write(b"P4\n128 128\n255") diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index e13a8019e..7d34fa4b5 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -73,11 +73,11 @@ def test_invalid_file() -> None: def test_write(tmp_path: Path) -> None: def roundtrip(img: Image.Image) -> None: - out = str(tmp_path / "temp.sgi") + out = tmp_path / "temp.sgi" img.save(out, format="sgi") assert_image_equal_tofile(img, out) - out = str(tmp_path / "fp.sgi") + out = tmp_path / "fp.sgi" with open(out, "wb") as fp: img.save(fp) assert_image_equal_tofile(img, out) @@ -95,7 +95,7 @@ def test_write16(tmp_path: Path) -> None: test_file = "Tests/images/hopper16.rgb" with Image.open(test_file) as im: - out = str(tmp_path / "temp.sgi") + out = tmp_path / "temp.sgi" im.save(out, format="sgi", bpc=2) assert_image_equal_tofile(im, out) @@ -103,7 +103,7 @@ def test_write16(tmp_path: Path) -> None: def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("LA") - out = str(tmp_path / "temp.sgi") + out = tmp_path / "temp.sgi" with pytest.raises(ValueError): im.save(out, format="sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index cdb7b3e0b..b64a629f5 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -51,7 +51,7 @@ def test_context_manager() -> None: def test_save(tmp_path: Path) -> None: # Arrange - temp = str(tmp_path / "temp.spider") + temp = tmp_path / "temp.spider" im = hopper() # Act diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index b6396bd64..f7b14beab 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -24,7 +24,7 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} @pytest.mark.parametrize("mode", _MODES) def test_sanity(mode: str, tmp_path: Path) -> None: def roundtrip(original_im: Image.Image) -> None: - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" original_im.save(out, rle=rle) with Image.open(out) as saved_im: @@ -76,7 +76,7 @@ def test_palette_depth_16(tmp_path: Path) -> None: assert im.palette.mode == "RGBA" assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png") - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" im.save(out) with Image.open(out) as reloaded: assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png") @@ -122,7 +122,7 @@ def test_cross_scan_line() -> None: def test_save(tmp_path: Path) -> None: test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" # Save im.save(out) @@ -141,7 +141,7 @@ def test_small_palette(tmp_path: Path) -> None: colors = [0, 0, 0] im.putpalette(colors) - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" im.save(out) with Image.open(out) as reloaded: @@ -155,7 +155,7 @@ def test_missing_palette() -> None: def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" with pytest.raises(OSError): im.save(out) @@ -172,7 +172,7 @@ def test_save_mapdepth() -> None: def test_save_id_section(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" # Check there is no id section im.save(out) @@ -202,7 +202,7 @@ def test_save_id_section(tmp_path: Path) -> None: def test_save_orientation(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" with Image.open(test_file) as im: assert im.info["orientation"] == -1 @@ -229,7 +229,7 @@ def test_save_rle(tmp_path: Path) -> None: with Image.open(test_file) as im: assert im.info["compression"] == "tga_rle" - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" # Save im.save(out) @@ -266,7 +266,7 @@ def test_save_l_transparency(tmp_path: Path) -> None: assert im.mode == "LA" assert im.getchannel("A").getcolors()[0][0] == num_transparent - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" im.save(out) with Image.open(out) as test_im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index e98e55aca..6962a5c98 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -31,7 +31,7 @@ except ImportError: class TestFileTiff: def test_sanity(self, tmp_path: Path) -> None: - filename = str(tmp_path / "temp.tif") + filename = tmp_path / "temp.tif" hopper("RGB").save(filename) @@ -112,11 +112,11 @@ class TestFileTiff: assert_image_equal_tofile(im, "Tests/images/hopper.tif") with Image.open("Tests/images/hopper_bigtiff.tif") as im: - outfile = str(tmp_path / "temp.tif") + outfile = 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") + outfile = tmp_path / "temp.tif" im = hopper() im.save(outfile, big_tiff=True) @@ -185,7 +185,7 @@ class TestFileTiff: assert im.info["dpi"] == (dpi, dpi) def test_save_float_dpi(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/hopper.tif") as im: dpi = (72.2, 72.2) im.save(outfile, dpi=dpi) @@ -220,12 +220,12 @@ class TestFileTiff: def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile) def test_save_unsupported_mode(self, tmp_path: Path) -> None: im = hopper("HSV") - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with pytest.raises(OSError): im.save(outfile) @@ -485,14 +485,14 @@ class TestFileTiff: assert gps[0] == b"\x03\x02\x00\x00" assert gps[18] == "WGS-84" - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() check_exif(exif) im.save(outfile, exif=exif) - outfile2 = str(tmp_path / "temp2.tif") + outfile2 = tmp_path / "temp2.tif" with Image.open(outfile) as im: exif = im.getexif() check_exif(exif) @@ -504,7 +504,7 @@ class TestFileTiff: check_exif(exif) def test_modify_exif(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() exif[264] = 100 @@ -533,7 +533,7 @@ class TestFileTiff: @pytest.mark.parametrize("mode", ("1", "L")) def test_photometric(self, mode: str, tmp_path: Path) -> None: - filename = str(tmp_path / "temp.tif") + filename = tmp_path / "temp.tif" im = hopper(mode) im.save(filename, tiffinfo={262: 0}) with Image.open(filename) as reloaded: @@ -612,7 +612,7 @@ class TestFileTiff: def test_with_underscores(self, tmp_path: Path) -> None: kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} - filename = str(tmp_path / "temp.tif") + filename = tmp_path / "temp.tif" hopper("RGB").save(filename, "TIFF", **kwargs) with Image.open(filename) as im: # legacy interface @@ -630,14 +630,14 @@ class TestFileTiff: with Image.open(infile) as im: assert im.getpixel((0, 0)) == pixel_value - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" im.save(tmpfile) assert_image_equal_tofile(im, tmpfile) def test_iptc(self, tmp_path: Path) -> None: # Do not preserve IPTC_NAA_CHUNK by default if type is LONG - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/hopper.tif") as im: im.load() assert isinstance(im, TiffImagePlugin.TiffImageFile) @@ -652,7 +652,7 @@ class TestFileTiff: assert 33723 not in im.tag_v2 def test_rowsperstrip(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im = hopper() im.save(outfile, tiffinfo={278: 256}) @@ -703,7 +703,7 @@ class TestFileTiff: with Image.open(infile) as im: assert im._planar_configuration == 2 - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile) with Image.open(outfile) as reloaded: @@ -718,7 +718,7 @@ class TestFileTiff: @pytest.mark.parametrize("mode", ("P", "PA")) def test_palette(self, mode: str, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im = hopper(mode) im.save(outfile) @@ -812,7 +812,7 @@ class TestFileTiff: im.info["icc_profile"] = "Dummy value" # Try save-load round trip to make sure both handle icc_profile. - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" im.save(tmpfile, "TIFF", compression="raw") with Image.open(tmpfile) as reloaded: assert b"Dummy value" == reloaded.info["icc_profile"] @@ -821,7 +821,7 @@ class TestFileTiff: im = hopper() assert "icc_profile" not in im.info - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" icc_profile = b"Dummy value" im.save(outfile, icc_profile=icc_profile) @@ -832,11 +832,11 @@ class TestFileTiff: with Image.open("Tests/images/hopper.bmp") as im: assert im.info["compression"] == 0 - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile) def test_discard_icc_profile(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/icc_profile.png") as im: assert "icc_profile" in im.info @@ -889,7 +889,7 @@ class TestFileTiff: ] def test_tiff_chunks(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" im = hopper() with open(tmpfile, "wb") as fp: @@ -911,7 +911,7 @@ class TestFileTiff: def test_close_on_load_exclusive(self, tmp_path: Path) -> None: # similar to test_fd_leak, but runs on unixlike os - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) @@ -923,7 +923,7 @@ class TestFileTiff: assert fp.closed def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) @@ -974,7 +974,7 @@ class TestFileTiff: @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: def test_fd_leak(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 36aabf4f8..0734d1db1 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -56,7 +56,7 @@ def test_rt_metadata(tmp_path: Path) -> None: info[ImageDescription] = text_data - f = str(tmp_path / "temp.tif") + f = tmp_path / "temp.tif" img.save(f, tiffinfo=info) @@ -128,7 +128,7 @@ def test_read_metadata() -> None: def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: - f = str(tmp_path / "temp.tiff") + f = tmp_path / "temp.tiff" del img.tag[278] img.save(f, tiffinfo=img.tag) @@ -163,7 +163,7 @@ def test_write_metadata(tmp_path: Path) -> None: def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.tif") as im: info = im.tag_v2 del info[278] @@ -210,7 +210,7 @@ def test_no_duplicate_50741_tag() -> None: def test_iptc(tmp_path: Path) -> None: - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.Lab.tif") as im: im.save(out) @@ -227,7 +227,7 @@ def test_writing_other_types_to_ascii( info[271] = value im = hopper() - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info) with Image.open(out) as reloaded: @@ -244,7 +244,7 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) info[700] = value - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info) with Image.open(out) as reloaded: @@ -263,7 +263,7 @@ def test_writing_other_types_to_undefined( info[33723] = value - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info) with Image.open(out) as reloaded: @@ -296,7 +296,7 @@ def test_empty_metadata() -> None: def test_iccprofile(tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1462 - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.iccprofile.tif") as im: im.save(out) @@ -317,13 +317,13 @@ def test_iccprofile_binary() -> None: def test_iccprofile_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as im: - outfile = str(tmp_path / "temp.png") + outfile = tmp_path / "temp.png" im.save(outfile) def test_iccprofile_binary_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: - outfile = str(tmp_path / "temp.png") + outfile = tmp_path / "temp.png" im.save(outfile) @@ -332,7 +332,7 @@ def test_exif_div_zero(tmp_path: Path) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() info[41988] = TiffImagePlugin.IFDRational(0, 0) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -351,7 +351,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -363,7 +363,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -381,7 +381,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -393,7 +393,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -406,7 +406,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -420,7 +420,7 @@ def test_ifd_signed_long(tmp_path: Path) -> None: info[37000] = -60000 - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -446,7 +446,7 @@ def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out) with Image.open(out) as reloaded: assert len(reloaded.tag_v2[34377]) == 70 @@ -480,7 +480,7 @@ def test_tag_group_data() -> None: def test_empty_subifd(tmp_path: Path) -> None: - out = str(tmp_path / "temp.jpg") + out = tmp_path / "temp.jpg" im = hopper() exif = im.getexif() diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c88fe3589..c573390c4 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -42,7 +42,7 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: Does it have the bits we expect? """ - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" # temp_file = "temp.webp" pil_image = hopper("RGBA") @@ -71,7 +71,7 @@ def test_write_rgba(tmp_path: Path) -> None: Does it have the bits we expect? """ - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) pil_image.save(temp_file) @@ -104,7 +104,7 @@ def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: half_transparent_image.putalpha(new_alpha) # save with transparent area preserved - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" half_transparent_image.save(temp_file, exact=True, lossless=True) with Image.open(temp_file) as reloaded: @@ -123,7 +123,7 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: should work, and be similar to the original file. """ - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" file_path = "Tests/images/transparent.gif" with Image.open(file_path) as im: im.save(temp_file) @@ -142,10 +142,10 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: def test_alpha_quality(tmp_path: Path) -> None: with Image.open("Tests/images/transparent.png") as im: - out = str(tmp_path / "temp.webp") + out = tmp_path / "temp.webp" im.save(out) - out_quality = str(tmp_path / "quality.webp") + out_quality = tmp_path / "quality.webp" im.save(out_quality, alpha_quality=50) with Image.open(out) as reloaded: with Image.open(out_quality) as reloaded_quality: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 967a0aae8..d4b1fda97 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -39,7 +39,7 @@ def test_write_animation_L(tmp_path: Path) -> None: with Image.open("Tests/images/iss634.gif") as orig: assert orig.n_frames > 1 - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" orig.save(temp_file, save_all=True) with Image.open(temp_file) as im: assert im.n_frames == orig.n_frames @@ -67,7 +67,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: are visually similar to the originals. """ - def check(temp_file: str) -> None: + def check(temp_file: Path) -> None: with Image.open(temp_file) as im: assert im.n_frames == 2 @@ -87,7 +87,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: with Image.open("Tests/images/anim_frame1.webp") as frame1: with Image.open("Tests/images/anim_frame2.webp") as frame2: - temp_file1 = str(tmp_path / "temp.webp") + temp_file1 = tmp_path / "temp.webp" frame1.copy().save( temp_file1, save_all=True, append_images=[frame2], lossless=True ) @@ -99,7 +99,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: ) -> Generator[Image.Image, None, None]: yield from ims - temp_file2 = str(tmp_path / "temp_generator.webp") + temp_file2 = tmp_path / "temp_generator.webp" frame1.copy().save( temp_file2, save_all=True, @@ -116,7 +116,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: """ durations = [0, 10, 20, 30, 40] - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" with Image.open("Tests/images/anim_frame1.webp") as frame1: with Image.open("Tests/images/anim_frame2.webp") as frame2: frame1.save( @@ -141,7 +141,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: def test_float_duration(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" with Image.open("Tests/images/iss634.apng") as im: assert im.info["duration"] == 70.0 @@ -159,7 +159,7 @@ def test_seeking(tmp_path: Path) -> None: """ dur = 33 - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" with Image.open("Tests/images/anim_frame1.webp") as frame1: with Image.open("Tests/images/anim_frame2.webp") as frame2: frame1.save( @@ -196,10 +196,10 @@ def test_alpha_quality(tmp_path: Path) -> None: with Image.open("Tests/images/transparent.png") as im: first_frame = Image.new("L", im.size) - out = str(tmp_path / "temp.webp") + out = tmp_path / "temp.webp" first_frame.save(out, save_all=True, append_images=[im]) - out_quality = str(tmp_path / "quality.webp") + out_quality = tmp_path / "quality.webp" first_frame.save( out_quality, save_all=True, append_images=[im], alpha_quality=50 ) diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 80429715e..5eaa4f599 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -13,7 +13,7 @@ RGB_MODE = "RGB" def test_write_lossless_rgb(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" hopper(RGB_MODE).save(temp_file, lossless=True) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index c68a20d7a..d1d3421ec 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -146,7 +146,7 @@ def test_write_animated_metadata(tmp_path: Path) -> None: exif_data = b"" xmp_data = b"" - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" with Image.open("Tests/images/anim_frame1.webp") as frame1: with Image.open("Tests/images/anim_frame2.webp") as frame2: frame1.save( diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 97469b77e..07c945848 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -59,7 +59,7 @@ def test_register_handler(tmp_path: Path) -> None: WmfImagePlugin.register_handler(handler) im = hopper() - tmpfile = str(tmp_path / "temp.wmf") + tmpfile = tmp_path / "temp.wmf" im.save(tmpfile) assert handler.methodCalled @@ -93,6 +93,6 @@ def test_load_set_dpi() -> None: def test_save(ext: str, tmp_path: Path) -> None: im = hopper() - tmpfile = str(tmp_path / ("temp" + ext)) + tmpfile = tmp_path / ("temp" + ext) with pytest.raises(OSError): im.save(tmpfile) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 44dd2541f..154f3dcc0 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -73,7 +73,7 @@ def test_invalid_file() -> None: def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper() - out = str(tmp_path / "temp.xbm") + out = tmp_path / "temp.xbm" with pytest.raises(OSError): im.save(out) @@ -81,7 +81,7 @@ def test_save_wrong_mode(tmp_path: Path) -> None: def test_hotspot(tmp_path: Path) -> None: im = hopper("1") - out = str(tmp_path / "temp.xbm") + out = tmp_path / "temp.xbm" hotspot = (0, 7) im.save(out, hotspot=hotspot) diff --git a/Tests/test_image.py b/Tests/test_image.py index d64816b1e..7f46cb7b0 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -187,14 +187,14 @@ class TestImage: for ext in (".jpg", ".jp2"): if ext == ".jp2" and not features.check_codec("jpg_2000"): pytest.skip("jpg_2000 not available") - temp_file = str(tmp_path / ("temp." + ext)) + temp_file = tmp_path / ("temp." + ext) im.save(Path(temp_file)) def test_fp_name(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.jpg") + temp_file = tmp_path / "temp.jpg" class FP(io.BytesIO): - name: str + name: Path if sys.version_info >= (3, 12): from collections.abc import Buffer @@ -225,7 +225,7 @@ class TestImage: def test_unknown_extension(self, tmp_path: Path) -> None: im = hopper() - temp_file = str(tmp_path / "temp.unknown") + temp_file = tmp_path / "temp.unknown" with pytest.raises(ValueError): im.save(temp_file) @@ -245,7 +245,7 @@ class TestImage: reason="Test requires opening an mmaped file for writing", ) def test_readonly_save(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.bmp") + temp_file = tmp_path / "temp.bmp" shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) with Image.open(temp_file) as im: @@ -728,7 +728,7 @@ class TestImage: # https://github.com/python-pillow/Pillow/issues/835 # Arrange test_file = "Tests/images/hopper.png" - temp_file = str(tmp_path / "temp.jpg") + temp_file = tmp_path / "temp.jpg" # Act/Assert with Image.open(test_file) as im: @@ -738,7 +738,7 @@ class TestImage: im.save(temp_file) def test_no_new_file_on_error(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.jpg") + temp_file = tmp_path / "temp.jpg" im = Image.new("RGB", (0, 0)) with pytest.raises(ValueError): @@ -805,7 +805,7 @@ class TestImage: assert exif[296] == 2 assert exif[11] == "gThumb 3.0.1" - out = str(tmp_path / "temp.jpg") + out = tmp_path / "temp.jpg" exif[258] = 8 del exif[274] del exif[282] @@ -827,7 +827,7 @@ class TestImage: assert exif[274] == 1 assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" - out = str(tmp_path / "temp.jpg") + out = tmp_path / "temp.jpg" exif[258] = 8 del exif[306] exif[274] = 455 @@ -846,7 +846,7 @@ class TestImage: exif = im.getexif() assert exif == {} - out = str(tmp_path / "temp.webp") + out = tmp_path / "temp.webp" exif[258] = 8 exif[40963] = 455 exif[305] = "Pillow test" @@ -868,7 +868,7 @@ class TestImage: exif = im.getexif() assert exif == {274: 1} - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" exif[258] = 8 del exif[274] exif[40963] = 455 diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 5f8b35c79..7d4f78c23 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -118,7 +118,7 @@ def test_trns_p(tmp_path: Path) -> None: im = hopper("P") im.info["transparency"] = 0 - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im_l = im.convert("L") assert im_l.info["transparency"] == 0 @@ -154,7 +154,7 @@ def test_trns_l(tmp_path: Path) -> None: im = hopper("L") im.info["transparency"] = 128 - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im_la = im.convert("LA") assert "transparency" not in im_la.info @@ -177,7 +177,7 @@ def test_trns_RGB(tmp_path: Path) -> None: im = hopper("RGB") im.info["transparency"] = im.getpixel((0, 0)) - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im_l = im.convert("L") assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 1166371b8..f700d20c0 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -171,7 +171,7 @@ class TestImagingCoreResize: # platforms. So if a future Pillow change requires that the test file # be updated, that is okay. im = hopper().resize((64, 64)) - temp_file = str(tmp_path / "temp.gif") + temp_file = tmp_path / "temp.gif" im.save(temp_file) with Image.open(temp_file) as reloaded: diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 3385f81f5..43068535e 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -45,9 +45,9 @@ def test_split_merge(mode: str) -> None: def test_split_open(tmp_path: Path) -> None: if features.check("zlib"): - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" else: - test_file = str(tmp_path / "temp.pcx") + test_file = tmp_path / "temp.pcx" def split_open(mode: str) -> int: hopper(mode).save(test_file) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4cce8f180..69533c2f8 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -124,7 +124,7 @@ def test_render_equal(layout_engine: ImageFont.Layout) -> None: def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: - tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) + tempfile = tmp_path / ("temp_" + chr(128) + ".ttf") try: shutil.copy(FONT_PATH, tempfile) except UnicodeEncodeError: diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 26b287bb4..da9e71692 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -10,7 +10,7 @@ from .helper import assert_image_equal, hopper, skip_unless_feature def test_sanity(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.im") + test_file = tmp_path / "temp.im" im = hopper("RGB") im.save(test_file) diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index c23a5c690..e8468e59f 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -88,7 +88,7 @@ if is_win32(): def test_pointer(tmp_path: Path) -> None: im = hopper() (width, height) = im.size - opath = str(tmp_path / "temp.png") + opath = tmp_path / "temp.png" imdib = ImageWin.Dib(im) hdr = BITMAPINFOHEADER() diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index e26f5d283..b78b7984f 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -44,7 +44,7 @@ def test_basic(tmp_path: Path, mode: str) -> None: im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) verify(im_out) # transform - filename = str(tmp_path / "temp.im") + filename = tmp_path / "temp.im" im_in.save(filename) with Image.open(filename) as im_out: diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 05c41a802..70661ecc1 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -18,7 +18,7 @@ def helper_pickle_file( ) -> None: # Arrange with Image.open(test_file) as im: - filename = str(tmp_path / "temp.pkl") + filename = tmp_path / "temp.pkl" if mode: im = im.convert(mode) @@ -87,7 +87,7 @@ def test_pickle_jpeg() -> None: def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange - filename = str(tmp_path / "temp.pkl") + filename = tmp_path / "temp.pkl" with Image.open("Tests/images/hopper.jpg") as im: im = im.convert("PA") @@ -151,7 +151,7 @@ def test_pickle_font_string(protocol: int) -> None: def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) - filename = str(tmp_path / "temp.pkl") + filename = tmp_path / "temp.pkl" # Act: roundtrip with open(filename, "wb") as f: diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index a743d831f..78f5632c5 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -35,7 +35,7 @@ def test_draw_postscript(tmp_path: Path) -> None: # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript # Arrange - tempfile = str(tmp_path / "temp.ps") + tempfile = tmp_path / "temp.ps" with open(tempfile, "wb") as fp: # Act ps = PSDraw.PSDraw(fp) diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index dd4fc46c3..4fd3aab5d 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -35,7 +35,7 @@ class TestShellInjection: @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg_filename(self, tmp_path: Path) -> None: for filename in test_filenames: - src_file = str(tmp_path / filename) + src_file = tmp_path / filename shutil.copy(TEST_JPG, src_file) with Image.open(src_file) as im: diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 13f1f9c80..30dc73654 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -65,7 +65,7 @@ def test_ifd_rational_save( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool ) -> None: im = hopper() - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" res = IFDRational(301, 1) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) From 700d36f2d2b351074a136243d7d72682bd3113e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 20 Mar 2025 00:11:18 +1100 Subject: [PATCH 377/628] Added release notes for #8807 --- docs/handbook/image-file-formats.rst | 6 ++++++ docs/releasenotes/11.2.0.rst | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index b0e20fa84..97599ace5 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -93,6 +93,12 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode. in ``P`` mode. +.. versionadded:: 11.2.0 + DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved:: + + im.save(out, pixel_format="DXT1") + + DIB ^^^ diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index f7e644cf3..3e977221e 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -66,6 +66,14 @@ libjpeg library, and what version of MozJPEG is being used:: features.check_feature("mozjpeg") # True or False features.version_feature("mozjpeg") # "4.1.1" for example, or None +Saving compressed DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3, +DXT5, BC2, BC3 and BC5 are supported:: + + im.save("out.dds", pixel_format="DXT1") + Other Changes ============= From acd8548f6e38b48c2af02d5081584472535afd52 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 20 Mar 2025 22:36:59 +1100 Subject: [PATCH 378/628] Removed FIXME --- src/PIL/Image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 884659882..e2a76dfe1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -548,7 +548,6 @@ class Image: def __init__(self) -> None: # FIXME: take "new" parameters / other image? - # FIXME: turn mode and size into delegating properties? self._im: core.ImagingCore | DeferredError | None = None self._mode = "" self._size = (0, 0) From 0888dc02ac9700acdaf5d07691203fb7e57a538a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 20 Mar 2025 23:10:09 +1100 Subject: [PATCH 379/628] Allow for two header fields and a comment --- Tests/images/full_gimp_palette.gpl | 260 +++++++++++++++++++++++++++++ Tests/test_file_gimppalette.py | 22 ++- src/PIL/GimpPaletteFile.py | 4 +- 3 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 Tests/images/full_gimp_palette.gpl diff --git a/Tests/images/full_gimp_palette.gpl b/Tests/images/full_gimp_palette.gpl new file mode 100644 index 000000000..004217210 --- /dev/null +++ b/Tests/images/full_gimp_palette.gpl @@ -0,0 +1,260 @@ +GIMP Palette +Name: fullpalette +Columns: 4 +# + 0 0 0 Index 0 + 1 1 1 Index 1 + 2 2 2 Index 2 + 3 3 3 Index 3 + 4 4 4 Index 4 + 5 5 5 Index 5 + 6 6 6 Index 6 + 7 7 7 Index 7 + 8 8 8 Index 8 + 9 9 9 Index 9 + 10 10 10 Index 10 + 11 11 11 Index 11 + 12 12 12 Index 12 + 13 13 13 Index 13 + 14 14 14 Index 14 + 15 15 15 Index 15 + 16 16 16 Index 16 + 17 17 17 Index 17 + 18 18 18 Index 18 + 19 19 19 Index 19 + 20 20 20 Index 20 + 21 21 21 Index 21 + 22 22 22 Index 22 + 23 23 23 Index 23 + 24 24 24 Index 24 + 25 25 25 Index 25 + 26 26 26 Index 26 + 27 27 27 Index 27 + 28 28 28 Index 28 + 29 29 29 Index 29 + 30 30 30 Index 30 + 31 31 31 Index 31 + 32 32 32 Index 32 + 33 33 33 Index 33 + 34 34 34 Index 34 + 35 35 35 Index 35 + 36 36 36 Index 36 + 37 37 37 Index 37 + 38 38 38 Index 38 + 39 39 39 Index 39 + 40 40 40 Index 40 + 41 41 41 Index 41 + 42 42 42 Index 42 + 43 43 43 Index 43 + 44 44 44 Index 44 + 45 45 45 Index 45 + 46 46 46 Index 46 + 47 47 47 Index 47 + 48 48 48 Index 48 + 49 49 49 Index 49 + 50 50 50 Index 50 + 51 51 51 Index 51 + 52 52 52 Index 52 + 53 53 53 Index 53 + 54 54 54 Index 54 + 55 55 55 Index 55 + 56 56 56 Index 56 + 57 57 57 Index 57 + 58 58 58 Index 58 + 59 59 59 Index 59 + 60 60 60 Index 60 + 61 61 61 Index 61 + 62 62 62 Index 62 + 63 63 63 Index 63 + 64 64 64 Index 64 + 65 65 65 Index 65 + 66 66 66 Index 66 + 67 67 67 Index 67 + 68 68 68 Index 68 + 69 69 69 Index 69 + 70 70 70 Index 70 + 71 71 71 Index 71 + 72 72 72 Index 72 + 73 73 73 Index 73 + 74 74 74 Index 74 + 75 75 75 Index 75 + 76 76 76 Index 76 + 77 77 77 Index 77 + 78 78 78 Index 78 + 79 79 79 Index 79 + 80 80 80 Index 80 + 81 81 81 Index 81 + 82 82 82 Index 82 + 83 83 83 Index 83 + 84 84 84 Index 84 + 85 85 85 Index 85 + 86 86 86 Index 86 + 87 87 87 Index 87 + 88 88 88 Index 88 + 89 89 89 Index 89 + 90 90 90 Index 90 + 91 91 91 Index 91 + 92 92 92 Index 92 + 93 93 93 Index 93 + 94 94 94 Index 94 + 95 95 95 Index 95 + 96 96 96 Index 96 + 97 97 97 Index 97 + 98 98 98 Index 98 + 99 99 99 Index 99 +100 100 100 Index 100 +101 101 101 Index 101 +102 102 102 Index 102 +103 103 103 Index 103 +104 104 104 Index 104 +105 105 105 Index 105 +106 106 106 Index 106 +107 107 107 Index 107 +108 108 108 Index 108 +109 109 109 Index 109 +110 110 110 Index 110 +111 111 111 Index 111 +112 112 112 Index 112 +113 113 113 Index 113 +114 114 114 Index 114 +115 115 115 Index 115 +116 116 116 Index 116 +117 117 117 Index 117 +118 118 118 Index 118 +119 119 119 Index 119 +120 120 120 Index 120 +121 121 121 Index 121 +122 122 122 Index 122 +123 123 123 Index 123 +124 124 124 Index 124 +125 125 125 Index 125 +126 126 126 Index 126 +127 127 127 Index 127 +128 128 128 Index 128 +129 129 129 Index 129 +130 130 130 Index 130 +131 131 131 Index 131 +132 132 132 Index 132 +133 133 133 Index 133 +134 134 134 Index 134 +135 135 135 Index 135 +136 136 136 Index 136 +137 137 137 Index 137 +138 138 138 Index 138 +139 139 139 Index 139 +140 140 140 Index 140 +141 141 141 Index 141 +142 142 142 Index 142 +143 143 143 Index 143 +144 144 144 Index 144 +145 145 145 Index 145 +146 146 146 Index 146 +147 147 147 Index 147 +148 148 148 Index 148 +149 149 149 Index 149 +150 150 150 Index 150 +151 151 151 Index 151 +152 152 152 Index 152 +153 153 153 Index 153 +154 154 154 Index 154 +155 155 155 Index 155 +156 156 156 Index 156 +157 157 157 Index 157 +158 158 158 Index 158 +159 159 159 Index 159 +160 160 160 Index 160 +161 161 161 Index 161 +162 162 162 Index 162 +163 163 163 Index 163 +164 164 164 Index 164 +165 165 165 Index 165 +166 166 166 Index 166 +167 167 167 Index 167 +168 168 168 Index 168 +169 169 169 Index 169 +170 170 170 Index 170 +171 171 171 Index 171 +172 172 172 Index 172 +173 173 173 Index 173 +174 174 174 Index 174 +175 175 175 Index 175 +176 176 176 Index 176 +177 177 177 Index 177 +178 178 178 Index 178 +179 179 179 Index 179 +180 180 180 Index 180 +181 181 181 Index 181 +182 182 182 Index 182 +183 183 183 Index 183 +184 184 184 Index 184 +185 185 185 Index 185 +186 186 186 Index 186 +187 187 187 Index 187 +188 188 188 Index 188 +189 189 189 Index 189 +190 190 190 Index 190 +191 191 191 Index 191 +192 192 192 Index 192 +193 193 193 Index 193 +194 194 194 Index 194 +195 195 195 Index 195 +196 196 196 Index 196 +197 197 197 Index 197 +198 198 198 Index 198 +199 199 199 Index 199 +200 200 200 Index 200 +201 201 201 Index 201 +202 202 202 Index 202 +203 203 203 Index 203 +204 204 204 Index 204 +205 205 205 Index 205 +206 206 206 Index 206 +207 207 207 Index 207 +208 208 208 Index 208 +209 209 209 Index 209 +210 210 210 Index 210 +211 211 211 Index 211 +212 212 212 Index 212 +213 213 213 Index 213 +214 214 214 Index 214 +215 215 215 Index 215 +216 216 216 Index 216 +217 217 217 Index 217 +218 218 218 Index 218 +219 219 219 Index 219 +220 220 220 Index 220 +221 221 221 Index 221 +222 222 222 Index 222 +223 223 223 Index 223 +224 224 224 Index 224 +225 225 225 Index 225 +226 226 226 Index 226 +227 227 227 Index 227 +228 228 228 Index 228 +229 229 229 Index 229 +230 230 230 Index 230 +231 231 231 Index 231 +232 232 232 Index 232 +233 233 233 Index 233 +234 234 234 Index 234 +235 235 235 Index 235 +236 236 236 Index 236 +237 237 237 Index 237 +238 238 238 Index 238 +239 239 239 Index 239 +240 240 240 Index 240 +241 241 241 Index 241 +242 242 242 Index 242 +243 243 243 Index 243 +244 244 244 Index 244 +245 245 245 Index 245 +246 246 246 Index 246 +247 247 247 Index 247 +248 248 248 Index 248 +249 249 249 Index 249 +250 250 250 Index 250 +251 251 251 Index 251 +252 252 252 Index 252 +253 253 253 Index 253 +254 254 254 Index 254 +255 255 255 Index 255 diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index ff9cc91c5..c122b37b3 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL.GimpPaletteFile import GimpPaletteFile @@ -22,9 +24,12 @@ def test_sanity() -> None: GimpPaletteFile(fp) -def test_get_palette() -> None: +@pytest.mark.parametrize( + "filename, size", (("custom_gimp_palette.gpl", 8), ("full_gimp_palette.gpl", 256)) +) +def test_get_palette(filename: str, size: int) -> None: # Arrange - with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: + with open("Tests/images/" + filename, "rb") as fp: palette_file = GimpPaletteFile(fp) # Act @@ -32,4 +37,15 @@ def test_get_palette() -> None: # Assert assert mode == "RGB" - assert len(palette) / 3 == 8 + assert len(palette) / 3 == size + + +def test_palette_limit() -> None: + with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: + data = fp.read() + + # Test that __init__ only reads 256 entries + data = data.replace(b"#\n", b"") + b" 0 0 0 Index 256" + b = BytesIO(data) + palette = GimpPaletteFile(b) + assert len(palette.palette) / 3 == 256 diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 0f079f457..a87487209 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -30,7 +30,7 @@ class GimpPaletteFile: raise SyntaxError(msg) palette: list[int] = [] - for _ in range(256): + for _ in range(256 + 3): s = fp.readline() if not s: break @@ -48,6 +48,8 @@ class GimpPaletteFile: raise ValueError(msg) palette += (int(v[i]) for i in range(3)) + if len(palette) == 768: + break self.palette = bytes(palette) From 510bc055774291d2380ea5d8e25faa84c7dfc637 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 20 Mar 2025 23:12:35 +1100 Subject: [PATCH 380/628] Added frombytes() to allow for unlimited parsing --- Tests/test_file_gimppalette.py | 24 +++++++++++++++++++----- src/PIL/GimpPaletteFile.py | 23 +++++++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index c122b37b3..c3d2bfeb7 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -16,11 +16,11 @@ def test_sanity() -> None: GimpPaletteFile(fp) with open("Tests/images/bad_palette_file.gpl", "rb") as fp: - with pytest.raises(SyntaxError): + with pytest.raises(SyntaxError, match="bad palette file"): GimpPaletteFile(fp) with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="bad palette entry"): GimpPaletteFile(fp) @@ -40,12 +40,26 @@ def test_get_palette(filename: str, size: int) -> None: assert len(palette) / 3 == size -def test_palette_limit() -> None: +def test_frombytes() -> None: with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: - data = fp.read() + full_data = fp.read() # Test that __init__ only reads 256 entries - data = data.replace(b"#\n", b"") + b" 0 0 0 Index 256" + data = full_data.replace(b"#\n", b"") + b" 0 0 0 Index 256" b = BytesIO(data) palette = GimpPaletteFile(b) assert len(palette.palette) / 3 == 256 + + # Test that frombytes() can read beyond that + palette = GimpPaletteFile.frombytes(data) + assert len(palette.palette) / 3 == 257 + + # Test that __init__ raises an error if a comment is too long + data = full_data[:-1] + b"a" * 100 + b = BytesIO(data) + with pytest.raises(SyntaxError, match="bad palette file"): + palette = GimpPaletteFile(b) + + # Test that frombytes() can read the data regardless + palette = GimpPaletteFile.frombytes(data) + assert len(palette.palette) / 3 == 256 diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index a87487209..379ffd739 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -16,6 +16,7 @@ from __future__ import annotations import re +from io import BytesIO from typing import IO @@ -24,13 +25,18 @@ class GimpPaletteFile: rawmode = "RGB" - def __init__(self, fp: IO[bytes]) -> None: + def _read(self, fp: IO[bytes], limit: bool = True) -> None: if not fp.readline().startswith(b"GIMP Palette"): msg = "not a GIMP palette file" raise SyntaxError(msg) palette: list[int] = [] - for _ in range(256 + 3): + i = 0 + while True: + if limit and i == 256 + 3: + break + + i += 1 s = fp.readline() if not s: break @@ -38,7 +44,7 @@ class GimpPaletteFile: # skip fields and comment lines if re.match(rb"\w+:|#", s): continue - if len(s) > 100: + if limit and len(s) > 100: msg = "bad palette file" raise SyntaxError(msg) @@ -48,10 +54,19 @@ class GimpPaletteFile: raise ValueError(msg) palette += (int(v[i]) for i in range(3)) - if len(palette) == 768: + if limit and len(palette) == 768: break self.palette = bytes(palette) + def __init__(self, fp: IO[bytes]) -> None: + self._read(fp) + + @classmethod + def frombytes(cls, data: bytes) -> GimpPaletteFile: + self = cls.__new__(cls) + self._read(BytesIO(data), False) + return self + def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode From 21ff960c9c796892e5e13fa8ca2d382565141539 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 08:51:41 +1100 Subject: [PATCH 381/628] Test that an unlimited number of lines is not read by __init__ --- Tests/test_file_gimppalette.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index c3d2bfeb7..08862113b 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -41,10 +41,17 @@ def test_get_palette(filename: str, size: int) -> None: def test_frombytes() -> None: - with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: - full_data = fp.read() + # Test that __init__ stops reading after 260 lines + with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: + custom_data = fp.read() + custom_data += b"#\n" * 300 + b" 0 0 0 Index 12" + b = BytesIO(custom_data) + palette = GimpPaletteFile(b) + assert len(palette.palette) / 3 == 8 # Test that __init__ only reads 256 entries + with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: + full_data = fp.read() data = full_data.replace(b"#\n", b"") + b" 0 0 0 Index 256" b = BytesIO(data) palette = GimpPaletteFile(b) From 8d440f734bbf1caac75cd3f94dcd737e401a21b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 20:39:36 +1100 Subject: [PATCH 382/628] Removed unused argument --- Tests/test_file_gif.py | 2 +- Tests/test_file_tga.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index fb1a636ed..ba0963e8c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1242,7 +1242,7 @@ def test_rgba_transparency(tmp_path: Path) -> None: assert_image_equal(hopper("P").convert("RGB"), reloaded) -def test_background_outside_palettte(tmp_path: Path) -> None: +def test_background_outside_palettte() -> None: with Image.open("Tests/images/background_outside_palette.gif") as im: im.seek(1) assert im.info["background"] == 255 diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index f7b14beab..a10d4d7ab 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -65,7 +65,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None: roundtrip(original_im) -def test_palette_depth_8(tmp_path: Path) -> None: +def test_palette_depth_8() -> None: with pytest.raises(UnidentifiedImageError): Image.open("Tests/images/p_8.tga") From 8d5505487766b3e1c31e90c41599fb84de694174 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 20:41:15 +1100 Subject: [PATCH 383/628] Reuse temp path --- 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 f99ca91a3..c969bd502 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -672,7 +672,7 @@ class TestFilePng: im.putpalette((1, 1, 1)) out = tmp_path / "temp.png" - im.save(tmp_path / "temp.png") + im.save(out) with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 From 9334bf040ef35f31c6b388e95d91f8fa8dcfc220 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 20:41:52 +1100 Subject: [PATCH 384/628] Do not cast unnecessarily --- Tests/test_image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 7f46cb7b0..d13e47602 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -187,8 +187,7 @@ class TestImage: for ext in (".jpg", ".jp2"): if ext == ".jp2" and not features.check_codec("jpg_2000"): pytest.skip("jpg_2000 not available") - temp_file = tmp_path / ("temp." + ext) - im.save(Path(temp_file)) + im.save(tmp_path / ("temp." + ext)) def test_fp_name(self, tmp_path: Path) -> None: temp_file = tmp_path / "temp.jpg" From c7e3158d515fe339f67e1b0313105ae9b88efa42 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 20:47:38 +1100 Subject: [PATCH 385/628] Added explicit test for opening and saving image with string --- Tests/test_image.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index d13e47602..f18d8489c 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -175,6 +175,13 @@ class TestImage: with Image.open(io.StringIO()): # type: ignore[arg-type] pass + def test_string(self, tmp_path: Path) -> None: + out = str(tmp_path / "temp.png") + im = hopper() + im.save(out) + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) + def test_pathlib(self, tmp_path: Path) -> None: with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: assert im.mode == "P" From bca693bd82ce1dab40a375d101c5292e3a275143 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 24 Mar 2025 17:33:45 +1100 Subject: [PATCH 386/628] Updated harfbuzz to 11.0.0 (#8830) 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 e9c54536e..51ba0cf28 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.4.0 +HARFBUZZ_VERSION=11.0.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index ea3d99394..2e9e18719 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.4.0", + "HARFBUZZ": "11.0.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", "LIBIMAGEQUANT": "4.3.4", From 053b5790e13030b9ac13a70489f48453dab7cb8f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:22:21 +1100 Subject: [PATCH 387/628] Added media_white_point (#8829) Co-authored-by: Andrew Murray --- docs/reference/ImageCms.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 96bd14dd3..4a1f5a3ee 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -286,6 +286,14 @@ can be easily displayed in a chromaticity diagram, for example). The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + .. py:attribute:: media_white_point + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + This tag specifies the media white point and is used for + generating absolute colorimetry. + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + .. py:attribute:: media_white_point_temperature :type: float | None From 14d495a519a4c444c3f3ef369c21ad45551f689b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:41:03 +0000 Subject: [PATCH 388/628] Update dependency cibuildwheel to v2.23.2 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index f2109ed61..db5f89c9a 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.23.1 +cibuildwheel==2.23.2 From b8abded99b4f77db591e270836156349a074c3bc Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 26 Mar 2025 00:31:49 +1100 Subject: [PATCH 389/628] Change back to actions/setup-python (#8833) Co-authored-by: Andrew Murray --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4ad88be9..006d574f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: Quansight-Labs/setup-python@v5 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true From 295a5e9bd7fe021df6ea3aa41d54738dcd3f8250 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 Jan 2025 19:23:04 +1100 Subject: [PATCH 390/628] Do not convert BC1 LUT to UINT32 --- src/libImaging/BcnDecode.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 9a41febc7..7b3d8f908 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -25,7 +25,6 @@ typedef struct { typedef struct { UINT16 c0, c1; - UINT32 lut; } bc1_color; typedef struct { @@ -40,13 +39,10 @@ typedef struct { #define LOAD16(p) (p)[0] | ((p)[1] << 8) -#define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) - static void bc1_color_load(bc1_color *dst, const UINT8 *src) { dst->c0 = LOAD16(src); dst->c1 = LOAD16(src + 2); - dst->lut = LOAD32(src + 4); } static rgba @@ -70,7 +66,7 @@ static void decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { bc1_color col; rgba p[4]; - int n, cw; + int n, o, cw; UINT16 r0, g0, b0, r1, g1, b1; bc1_color_load(&col, src); @@ -103,9 +99,11 @@ decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { p[3].b = 0; p[3].a = 0; } - for (n = 0; n < 16; n++) { - cw = 3 & (col.lut >> (2 * n)); - dst[n] = p[cw]; + for (n = 0; n < 4; n++) { + for (o = 0; o < 4; o++) { + cw = 3 & ((src + 4)[n] >> (2 * o)); + dst[n * 4 + o] = p[cw]; + } } } From e1f0def839776c131a8f297c7b05712d1cfbee75 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:43:07 +1100 Subject: [PATCH 391/628] Updated xz to 5.8.0, except on manylinux2014 (#8836) Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 51ba0cf28..461729a74 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -42,7 +42,11 @@ HARFBUZZ_VERSION=11.0.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 -XZ_VERSION=5.6.4 +if [[ $MB_ML_VER == 2014 ]]; then + XZ_VERSION=5.6.4 +else + XZ_VERSION=5.8.0 +fi TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_NG_VERSION=2.2.4 From 3c185d1f696dfbe20a5401a2f36edd392b2094d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:44:27 +1100 Subject: [PATCH 392/628] Do not load image during save if file extension is unknown (#8835) Co-authored-by: Andrew Murray --- Tests/test_file_hdf5stub.py | 2 +- src/PIL/Image.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 50864009f..e4f09a09c 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -43,7 +43,7 @@ def test_save() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_fp = BytesIO() - dummy_filename = "dummy.filename" + dummy_filename = "dummy.h5" # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e2a76dfe1..d338e4dfd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2510,13 +2510,6 @@ class Image: # only set the name for metadata purposes filename = os.fspath(fp.name) - # may mutate self! - self._ensure_mutable() - - save_all = params.pop("save_all", False) - self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} - self.encoderconfig: tuple[Any, ...] = () - preinit() filename_ext = os.path.splitext(filename)[1].lower() @@ -2531,6 +2524,13 @@ class Image: msg = f"unknown file extension: {ext}" raise ValueError(msg) from e + # may mutate self! + self._ensure_mutable() + + save_all = params.pop("save_all", False) + self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} + self.encoderconfig: tuple[Any, ...] = () + if format.upper() not in SAVE: init() if save_all: From 10ccbd7788532f72939d38c41f3f7303ff07b4da Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 28 Mar 2025 03:01:09 +1100 Subject: [PATCH 393/628] If append_images is populated, default save_all to True (#8781) Co-authored-by: Andrew Murray --- Tests/test_file_gif.py | 6 ++++++ docs/handbook/image-file-formats.rst | 26 ++++++++++++++------------ docs/handbook/tutorial.rst | 1 - src/PIL/Image.py | 8 ++++++-- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index ba0963e8c..376eab0c6 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1138,6 +1138,12 @@ def test_append_images(tmp_path: Path) -> None: ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] im.copy().save(out, save_all=True, append_images=ims) + with Image.open(out) as reread: + assert reread.n_frames == 3 + + # Test append_images without save_all + im.copy().save(out, append_images=ims) + with Image.open(out) as reread: assert reread.n_frames == 3 diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 97599ace5..c0b1a9d4e 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -235,8 +235,9 @@ following options are available:: im.save(out, save_all=True, append_images=[im1, im2, ...]) **save_all** - If present and true, all frames of the image will be saved. If - not, then only the first frame of a multiframe image will be saved. + If present and true, or if ``append_images`` is not empty, all frames of + the image will be saved. Otherwise, only the first frame of a multiframe + image will be saved. **append_images** A list of images to append as additional frames. Each of the @@ -723,8 +724,8 @@ Saving When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default only the first frame of a multiframe image will be saved. If the ``save_all`` -argument is present and true, then all frames will be saved, and the following -option will also be available. +argument is present and true, or if ``append_images`` is not empty, all frames +will be saved. **append_images** A list of images to append as additional pictures. Each of the @@ -934,7 +935,8 @@ Saving When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file will be saved. To save an APNG file (including a single frame APNG), the ``save_all`` -parameter must be set to ``True``. The following parameters can also be set: +parameter should be set to ``True`` or ``append_images`` should not be empty. The +following parameters can also be set: **default_image** Boolean value, specifying whether or not the base image is a default image. @@ -1163,7 +1165,8 @@ Saving The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: **save_all** - If true, Pillow will save all frames of the image to a multiframe tiff document. + If true, or if ``append_images`` is not empty, Pillow will save all frames of the + image to a multiframe tiff document. .. versionadded:: 3.4.0 @@ -1313,8 +1316,8 @@ Saving sequences When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default only the first frame of a multiframe image will be saved. If the ``save_all`` -argument is present and true, then all frames will be saved, and the following -options will also be available. +argument is present and true, or if ``append_images`` is not empty, all frames +will be saved, and the following options will also be available. **append_images** A list of images to append as additional frames. Each of the @@ -1616,15 +1619,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum **save_all** If a multiframe image is used, by default, only the first image will be saved. To save all frames, each frame to a separate page of the PDF, the ``save_all`` - parameter must be present and set to ``True``. + parameter should be present and set to ``True`` or ``append_images`` should not be + empty. .. versionadded:: 3.0.0 **append_images** A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each - of the images in the list can be single or multiframe images. The ``save_all`` - parameter must be present and set to ``True`` in conjunction with - ``append_images``. + of the images in the list can be single or multiframe images. .. versionadded:: 4.2.0 diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index f771ae7ae..f1a2849b8 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -534,7 +534,6 @@ You can create animated GIFs with Pillow, e.g. # Save the images as an animated GIF images[0].save( "animated_hopper.gif", - save_all=True, append_images=images[1:], duration=500, # duration of each frame in milliseconds loop=0, # loop forever diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d338e4dfd..67e068b06 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2527,13 +2527,17 @@ class Image: # may mutate self! self._ensure_mutable() - save_all = params.pop("save_all", False) + save_all = params.pop("save_all", None) self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} self.encoderconfig: tuple[Any, ...] = () if format.upper() not in SAVE: init() - if save_all: + if save_all or ( + save_all is None + and params.get("append_images") + and format.upper() in SAVE_ALL + ): save_handler = SAVE_ALL[format.upper()] else: save_handler = SAVE[format.upper()] From 1cb6c7c347a7509e0303c32cfe8f5d1e063be1b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 28 Mar 2025 23:27:39 +1100 Subject: [PATCH 394/628] Parametrize tests (#8838) Co-authored-by: Andrew Murray --- Tests/test_file_bmp.py | 28 ++++++++------------ Tests/test_file_sgi.py | 28 +++++++++++--------- Tests/test_file_tga.py | 60 ++++++++++++++++++++++-------------------- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 64d2acaf5..8a94011e8 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -15,25 +15,19 @@ from .helper import ( ) -def test_sanity(tmp_path: Path) -> None: - def roundtrip(im: Image.Image) -> None: - outfile = tmp_path / "temp.bmp" +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) +def test_sanity(mode: str, tmp_path: Path) -> None: + outfile = tmp_path / "temp.bmp" - im.save(outfile, "BMP") + im = hopper(mode) + im.save(outfile, "BMP") - with Image.open(outfile) as reloaded: - reloaded.load() - assert im.mode == reloaded.mode - assert im.size == reloaded.size - assert reloaded.format == "BMP" - assert reloaded.get_format_mimetype() == "image/bmp" - - roundtrip(hopper()) - - roundtrip(hopper("1")) - roundtrip(hopper("L")) - roundtrip(hopper("P")) - roundtrip(hopper("RGB")) + with Image.open(outfile) as reloaded: + reloaded.load() + assert im.mode == reloaded.mode + assert im.size == reloaded.size + assert reloaded.format == "BMP" + assert reloaded.get_format_mimetype() == "image/bmp" def test_invalid_file() -> None: diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 7d34fa4b5..da0965fa1 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -71,24 +71,26 @@ def test_invalid_file() -> None: SgiImagePlugin.SgiImageFile(invalid_file) -def test_write(tmp_path: Path) -> None: - def roundtrip(img: Image.Image) -> None: - out = tmp_path / "temp.sgi" - img.save(out, format="sgi") +def roundtrip(img: Image.Image, tmp_path: Path) -> None: + out = tmp_path / "temp.sgi" + img.save(out, format="sgi") + assert_image_equal_tofile(img, out) + + out = tmp_path / "fp.sgi" + with open(out, "wb") as fp: + img.save(fp) assert_image_equal_tofile(img, out) - out = tmp_path / "fp.sgi" - with open(out, "wb") as fp: - img.save(fp) - assert_image_equal_tofile(img, out) + assert not fp.closed - assert not fp.closed - for mode in ("L", "RGB", "RGBA"): - roundtrip(hopper(mode)) +@pytest.mark.parametrize("mode", ("L", "RGB", "RGBA")) +def test_write(mode: str, tmp_path: Path) -> None: + roundtrip(hopper(mode), tmp_path) - # Test 1 dimension for an L mode image - roundtrip(Image.new("L", (10, 1))) + +def test_write_L_mode_1_dimension(tmp_path: Path) -> None: + roundtrip(Image.new("L", (10, 1)), tmp_path) def test_write16(tmp_path: Path) -> None: diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index a10d4d7ab..8b6ed3ed2 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,8 +1,6 @@ from __future__ import annotations import os -from glob import glob -from itertools import product from pathlib import Path import pytest @@ -15,14 +13,27 @@ _TGA_DIR = os.path.join("Tests", "images", "tga") _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") -_MODES = ("L", "LA", "P", "RGB", "RGBA") _ORIGINS = ("tl", "bl") _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} -@pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode: str, tmp_path: Path) -> None: +@pytest.mark.parametrize( + "size_mode", + ( + ("1x1", "L"), + ("200x32", "L"), + ("200x32", "LA"), + ("200x32", "P"), + ("200x32", "RGB"), + ("200x32", "RGBA"), + ), +) +@pytest.mark.parametrize("origin", _ORIGINS) +@pytest.mark.parametrize("rle", (True, False)) +def test_sanity( + size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path +) -> None: def roundtrip(original_im: Image.Image) -> None: out = tmp_path / "temp.tga" @@ -36,33 +47,26 @@ def test_sanity(mode: str, tmp_path: Path) -> None: assert_image_equal(saved_im, original_im) - png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) + size, mode = size_mode + png_path = os.path.join(_TGA_DIR_COMMON, size + "_" + mode.lower() + ".png") + with Image.open(png_path) as reference_im: + assert reference_im.mode == mode - for png_path in png_paths: - with Image.open(png_path) as reference_im: - assert reference_im.mode == mode + path_no_ext = os.path.splitext(png_path)[0] + tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw") - path_no_ext = os.path.splitext(png_path)[0] - for origin, rle in product(_ORIGINS, (True, False)): - tga_path = "{}_{}_{}.tga".format( - path_no_ext, origin, "rle" if rle else "raw" - ) + with Image.open(tga_path) as original_im: + assert original_im.format == "TGA" + assert original_im.get_format_mimetype() == "image/x-tga" + if rle: + assert original_im.info["compression"] == "tga_rle" + assert original_im.info["orientation"] == _ORIGIN_TO_ORIENTATION[origin] + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() - with Image.open(tga_path) as original_im: - assert original_im.format == "TGA" - assert original_im.get_format_mimetype() == "image/x-tga" - if rle: - assert original_im.info["compression"] == "tga_rle" - assert ( - original_im.info["orientation"] - == _ORIGIN_TO_ORIENTATION[origin] - ) - if mode == "P": - assert original_im.getpalette() == reference_im.getpalette() + assert_image_equal(original_im, reference_im) - assert_image_equal(original_im, reference_im) - - roundtrip(original_im) + roundtrip(original_im) def test_palette_depth_8() -> None: From 722283e8199ab071906982d5efde51f2122ed5ae Mon Sep 17 00:00:00 2001 From: Adian Kozlica Date: Fri, 28 Mar 2025 16:43:10 +0100 Subject: [PATCH 395/628] Add KDE Wayland support for ImageGrab --- Tests/test_imagegrab.py | 4 +--- src/PIL/ImageGrab.py | 9 +++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 5cd510751..e8fd9524c 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -40,9 +40,7 @@ class TestImageGrab: @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") def test_grab_no_xcb(self) -> None: - if sys.platform not in ("win32", "darwin") and not shutil.which( - "gnome-screenshot" - ): + if sys.platform not in ("win32", "darwin") and not shutil.which("gnome-screenshot") and not shutil.which('spectacle'): with pytest.raises(OSError) as e: ImageGrab.grab() assert str(e.value).startswith("Pillow was built without XCB support") diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index fe27bfaeb..5ac0b6a21 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -80,11 +80,16 @@ def grab( if ( display_name is None and sys.platform not in ("darwin", "win32") - and shutil.which("gnome-screenshot") ): fh, filepath = tempfile.mkstemp(".png") os.close(fh) - subprocess.call(["gnome-screenshot", "-f", filepath]) + if shutil.which("gnome-screenshot"): + subprocess.call(["gnome-screenshot", "-f", filepath]) + elif shutil.which("spectacle"): + subprocess.call(["spectacle", "-n", "-b", "-f", "-o", filepath]) + else: + os.unlink(filepath) + raise im = Image.open(filepath) im.load() os.unlink(filepath) From eeb494abf714579c0744381eaaaab1fc1f5eac85 Mon Sep 17 00:00:00 2001 From: Adian Kozlica Date: Fri, 28 Mar 2025 17:18:09 +0100 Subject: [PATCH 396/628] Fix formatting --- Tests/test_imagegrab.py | 6 +++++- src/PIL/ImageGrab.py | 7 ++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index e8fd9524c..ab06f04e2 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -40,7 +40,11 @@ class TestImageGrab: @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") def test_grab_no_xcb(self) -> None: - if sys.platform not in ("win32", "darwin") and not shutil.which("gnome-screenshot") and not shutil.which('spectacle'): + if ( + sys.platform not in ("win32", "darwin") + and not shutil.which("gnome-screenshot") + and not shutil.which("spectacle") + ): with pytest.raises(OSError) as e: ImageGrab.grab() assert str(e.value).startswith("Pillow was built without XCB support") diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 5ac0b6a21..e79b4d651 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -77,10 +77,7 @@ def grab( raise OSError(msg) size, data = Image.core.grabscreen_x11(display_name) except OSError: - if ( - display_name is None - and sys.platform not in ("darwin", "win32") - ): + if display_name is None and sys.platform not in ("darwin", "win32"): fh, filepath = tempfile.mkstemp(".png") os.close(fh) if shutil.which("gnome-screenshot"): @@ -89,7 +86,7 @@ def grab( subprocess.call(["spectacle", "-n", "-b", "-f", "-o", filepath]) else: os.unlink(filepath) - raise + raise im = Image.open(filepath) im.load() os.unlink(filepath) From e685e2833e9e6de8f03cdc71ac9b58107c909768 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Mar 2025 18:27:02 +1100 Subject: [PATCH 397/628] Do not create temporary file if no utility is available --- src/PIL/ImageGrab.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e79b4d651..db2a8db06 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -78,15 +78,15 @@ def grab( size, data = Image.core.grabscreen_x11(display_name) except OSError: if display_name is None and sys.platform not in ("darwin", "win32"): + if shutil.which("gnome-screenshot"): + args = ["gnome-screenshot", "-f"] + elif shutil.which("spectacle"): + args = ["spectacle", "-n", "-b", "-f", "-o"] + else: + raise fh, filepath = tempfile.mkstemp(".png") os.close(fh) - if shutil.which("gnome-screenshot"): - subprocess.call(["gnome-screenshot", "-f", filepath]) - elif shutil.which("spectacle"): - subprocess.call(["spectacle", "-n", "-b", "-f", "-o", filepath]) - else: - os.unlink(filepath) - raise + subprocess.call(args + [filepath]) im = Image.open(filepath) im.load() os.unlink(filepath) From ae52f9f37d2bee5ccd2ce10fb9c3d6318e675b89 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 00:21:51 +1100 Subject: [PATCH 398/628] Added release notes for #8781 and #8837 (#8843) Co-authored-by: Andrew Murray --- docs/releasenotes/11.2.0.rst | 37 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index 3e977221e..d40d86f21 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -4,21 +4,12 @@ Security ======== -TODO -^^^^ +Undefined shift when loading compressed DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO - -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ - -TODO - -Backwards Incompatible Changes -============================== - -TODO -^^^^ +When loading some compressed DDS formats, an integer was bitshifted by 24 places to +generate the 32 bits of the lookup table. This was undefined behaviour, and has been +present since Pillow 3.4.0. Deprecations ============ @@ -36,10 +27,14 @@ an :py:class:`PIL.ImageFile.ImageFile` instance. API Changes =========== -TODO -^^^^ +``append_images`` no longer requires ``save_all`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Previously, ``save_all`` was required to in order to use ``append_images``. Now, +``save_all`` will default to ``True`` if ``append_images`` is not empty and the format +supports saving multiple frames:: + + im.save("out.gif", append_images=ims) API Additions ============= @@ -73,11 +68,3 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT5, BC2, BC3 and BC5 are supported:: im.save("out.dds", pixel_format="DXT1") - -Other Changes -============= - -TODO -^^^^ - -TODO From 6d42449788e4c05a76cc7c9c81a7c8b2a40d099e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:25:13 +1100 Subject: [PATCH 399/628] Allow loading of EMF images at a given DPI (#8536) Co-authored-by: Andrew Murray --- Tests/images/drawing_emf_ref_72_144.png | Bin 0 -> 984 bytes Tests/test_file_wmf.py | 14 ++++++++++++++ src/PIL/WmfImagePlugin.py | 24 ++++++++++++++---------- 3 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 Tests/images/drawing_emf_ref_72_144.png diff --git a/Tests/images/drawing_emf_ref_72_144.png b/Tests/images/drawing_emf_ref_72_144.png new file mode 100644 index 0000000000000000000000000000000000000000..000377b6c7b1e05688fc17d5da09d163a18c5b4b GIT binary patch literal 984 zcmV;}11J26P) z+m?eM3`K+g|If}tT8An^fFxX!{W??4_QVUcOTu}cFoF=ms9ghq-o8T!`G3$n3L4r; z;S(Tv7 z*g4bg5BZ5u>}=ZTECjnbkG7~Y!fVc;t>BC>n)hm}IU`)=UE0dd2#a~U_7G>J-@H+K zpfl2G-l#p+8R@B^MO*Hfv6kjaTC`_~8fk9zYVCQVM%pr{(;j{$OVW@;o%V#z&{S20 z_H6APQ(GHVd(QU0sJ*sPwP$ulswyOD&)nWI^g2n}^GA))>lB$n90)P+vi2$+jt~Pc zYp>GbRTQ+>iW;HRT+m)IYD&#H?G>X&ik0WISBx4dR=(Q}jL56@x*d+>>wnc=x5JTq z{odpet9ST^cZ;<4>K$IhoBYcr9ge)XB(%5haPTI##a(-=6B|hx-L);8*x*fWiy!R` zPi*Aj^`mX%#D-XV+o%EHVdv+zC0yGQuDvz4pF4cCC;yEGJ66)Z;o6pP?cIql<_Flj zjDxAPV_e%3u5Ag|wuEb2!nG~o+Lo}TeT_YBX8{;W*E8=kK*&u$uPh z0#pg#mvPzBzHn_zxV9x++Y+vA3D>rSYg^&~0E0trLup^5PB5h%oqCm9%f69=AWL)}qDpk;FvW&-2%W_7m4ewmZF(V~zdOPTrXJ z*G{sz_SAfY3qS45;(n1@>+kbrKnL=A$hI8>3{A})shuAu$f!EHj>I!TPwG& zL#U6Wa@E!;18=CRere*`4+zs%Pqp@J*S59>Y+8SNnpSTPpm8WNL*NZpvWrIT;jP}| z3_SzSf##jg&^g{7V&3lz{nHG}<}A*@GP|N?&gBeTlS(?~j5kiUxinGpzU3_C`9IR4e2CH??qha)B=@Nu*N0000 None: assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) + with Image.open("Tests/images/drawing.emf") as im: + assert im.size == (1625, 1625) + + if not hasattr(Image.core, "drawwmf"): + return + im.load(im.info["dpi"]) + assert im.size == (1625, 1625) + + with Image.open("Tests/images/drawing.emf") as im: + im.load((72, 144)) + assert im.size == (82, 164) + + assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref_72_144.png") + @pytest.mark.parametrize("ext", (".wmf", ".emf")) def test_save(ext: str, tmp_path: Path) -> None: diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 04abd52f0..f709d026b 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -80,8 +80,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): format_description = "Windows Metafile" def _open(self) -> None: - self._inch = None - # check placable header s = self.fp.read(80) @@ -89,10 +87,11 @@ class WmfStubImageFile(ImageFile.StubImageFile): # placeable windows metafile # get units per inch - self._inch = word(s, 14) - if self._inch == 0: + inch = word(s, 14) + if inch == 0: msg = "Invalid inch" raise ValueError(msg) + self._inch: tuple[float, float] = inch, inch # get bounding box x0 = short(s, 6) @@ -103,8 +102,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): # normalize size to 72 dots per inch self.info["dpi"] = 72 size = ( - (x1 - x0) * self.info["dpi"] // self._inch, - (y1 - y0) * self.info["dpi"] // self._inch, + (x1 - x0) * self.info["dpi"] // inch, + (y1 - y0) * self.info["dpi"] // inch, ) self.info["wmf_bbox"] = x0, y0, x1, y1 @@ -138,6 +137,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): self.info["dpi"] = xdpi else: self.info["dpi"] = xdpi, ydpi + self._inch = xdpi, ydpi else: msg = "Unsupported file format" @@ -153,13 +153,17 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _load(self) -> ImageFile.StubHandler | None: return _handler - def load(self, dpi: int | None = None) -> Image.core.PixelAccess | None: - if dpi is not None and self._inch is not None: + def load( + self, dpi: float | tuple[float, float] | None = None + ) -> Image.core.PixelAccess | None: + if dpi is not None: self.info["dpi"] = dpi x0, y0, x1, y1 = self.info["wmf_bbox"] + if not isinstance(dpi, tuple): + dpi = dpi, dpi self._size = ( - (x1 - x0) * self.info["dpi"] // self._inch, - (y1 - y0) * self.info["dpi"] // self._inch, + int((x1 - x0) * dpi[0] / self._inch[0]), + int((y1 - y0) * dpi[1] / self._inch[1]), ) return super().load() From 93cdfeb48879064314af70faf98726e09fb06ab0 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:25:57 +1100 Subject: [PATCH 400/628] Prevent TIFFRGBAImageBegin from applying image orientation (#8556) Co-authored-by: Andrew Murray --- Tests/test_file_libtiff.py | 11 +++++++++++ src/libImaging/TiffDecode.c | 1 + 2 files changed, 12 insertions(+) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 25d1f5712..9e63e9c10 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1026,6 +1026,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + def test_old_style_jpeg_orientation(self) -> None: + with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp: + data = fp.read() + + # Set EXIF Orientation to 2 + data = data[:102] + b"\x02" + data[103:] + + with Image.open(io.BytesIO(data)) as im: + im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + def test_open_missing_samplesperpixel(self) -> None: with Image.open( "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index e4da9162d..9a2db95b4 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -299,6 +299,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { return -1; } + img.orientation = ORIENTATION_TOPLEFT; img.req_orientation = ORIENTATION_TOPLEFT; img.col_offset = 0; From 140e426082bfe1ec84c5d6eb34a3c47eb1d89185 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:27:00 +1100 Subject: [PATCH 401/628] Added USE_RAW_ALPHA (#8602) Co-authored-by: Andrew Murray --- Tests/test_file_bmp.py | 10 ++++++++++ src/PIL/BmpImagePlugin.py | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 8a94011e8..757650711 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -224,3 +224,13 @@ def test_offset() -> None: # to exclude the palette size from the pixel data offset with Image.open("Tests/images/pal8_offset.bmp") as im: assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") + + +def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None: + with Image.open("Tests/images/bmp/g/rgb32.bmp") as im: + assert im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"] + assert im.mode == "RGB" + + monkeypatch.setattr(BmpImagePlugin, "USE_RAW_ALPHA", True) + with Image.open("Tests/images/bmp/g/rgb32.bmp") as im: + assert im.mode == "RGBA" diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index d60ea591a..43131cfe2 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -48,6 +48,8 @@ BIT2MODE = { 32: ("RGB", "BGRX"), } +USE_RAW_ALPHA = False + def _accept(prefix: bytes) -> bool: return prefix.startswith(b"BM") @@ -242,7 +244,9 @@ class BmpImageFile(ImageFile.ImageFile): msg = "Unsupported BMP bitfields layout" raise OSError(msg) elif file_info["compression"] == self.COMPRESSIONS["RAW"]: - if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset + if file_info["bits"] == 32 and ( + header == 22 or USE_RAW_ALPHA # 32-bit .cur offset + ): raw_mode, self._mode = "BGRA", "RGBA" elif file_info["compression"] in ( self.COMPRESSIONS["RLE8"], From 6bffa3a9d4fe3d5a50c505ec0637d70cc1e8d59b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:29:02 +1100 Subject: [PATCH 402/628] Only read until the offset of the next tile (#8609) Co-authored-by: Andrew Murray --- Tests/test_imagefile.py | 20 ++++++++++++++++++++ src/PIL/ImageFile.py | 9 +++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index c60a475a3..7622eea99 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -131,6 +131,26 @@ class TestImageFile: assert_image_equal(im1, im2) + def test_tile_size(self) -> None: + with open("Tests/images/hopper.tif", "rb") as im_fp: + data = im_fp.read() + + reads = [] + + class FP(BytesIO): + def read(self, size: int | None = None) -> bytes: + reads.append(size) + return super().read(size) + + fp = FP(data) + with Image.open(fp) as im: + assert len(im.tile) == 7 + + im.load() + + # Despite multiple tiles, assert only one tile caused a read of maxblock size + assert reads.count(im.decodermaxblock) == 1 + def test_raise_oserror(self) -> None: with pytest.warns(DeprecationWarning): with pytest.raises(OSError): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 1bf8a7e5f..9470a8dd7 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -345,7 +345,7 @@ class ImageFile(Image.Image): self.tile, lambda tile: (tile[0], tile[1], tile[3]) ) ] - for decoder_name, extents, offset, args in self.tile: + for i, (decoder_name, extents, offset, args) in enumerate(self.tile): seek(offset) decoder = Image._getdecoder( self.mode, decoder_name, args, self.decoderconfig @@ -358,8 +358,13 @@ class ImageFile(Image.Image): else: b = prefix while True: + read_bytes = self.decodermaxblock + if i + 1 < len(self.tile): + next_offset = self.tile[i + 1].offset + if next_offset > offset: + read_bytes = next_offset - offset try: - s = read(self.decodermaxblock) + s = read(read_bytes) except (IndexError, struct.error) as e: # truncated png/gif if LOAD_TRUNCATED_IMAGES: From 03dc994baaa98385ef313669dbfd6e5e80f86380 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:30:30 +1100 Subject: [PATCH 403/628] Check that _fp type is not DeferredError before use (#8640) --- src/PIL/DcxImagePlugin.py | 3 +++ src/PIL/FliImagePlugin.py | 3 +++ src/PIL/GifImagePlugin.py | 3 +++ src/PIL/ImImagePlugin.py | 3 +++ src/PIL/ImageFile.py | 2 +- src/PIL/MpoImagePlugin.py | 5 +++++ src/PIL/PngImagePlugin.py | 3 +++ src/PIL/PsdImagePlugin.py | 5 +++++ src/PIL/SpiderImagePlugin.py | 3 +++ src/PIL/TiffImagePlugin.py | 4 +++- 10 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index f67f27d73..aea661b9c 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -24,6 +24,7 @@ from __future__ import annotations from . import Image from ._binary import i32le as i32 +from ._util import DeferredError from .PcxImagePlugin import PcxImageFile MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? @@ -66,6 +67,8 @@ class DcxImageFile(PcxImageFile): def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.frame = frame self.fp = self._fp self.fp.seek(self._offset[frame]) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index b534b30ab..7c5bfeefa 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 from ._binary import i32le as i32 from ._binary import o8 +from ._util import DeferredError # # decoder @@ -134,6 +135,8 @@ class FliImageFile(ImageFile.ImageFile): self._seek(f) def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex if frame == 0: self.__frame = -1 self._fp.seek(self.__rewind) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 259e93f09..045ab1027 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -45,6 +45,7 @@ from . import ( from ._binary import i16le as i16 from ._binary import o8 from ._binary import o16le as o16 +from ._util import DeferredError if TYPE_CHECKING: from . import _imaging @@ -167,6 +168,8 @@ class GifImageFile(ImageFile.ImageFile): raise EOFError(msg) from e def _seek(self, frame: int, update_image: bool = True) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex if frame == 0: # rewind self.__offset = 0 diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 9f20b30f8..71b999678 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -31,6 +31,7 @@ import re from typing import IO, Any from . import Image, ImageFile, ImagePalette +from ._util import DeferredError # -------------------------------------------------------------------- # Standard tags @@ -290,6 +291,8 @@ class ImImageFile(ImageFile.ImageFile): def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.frame = frame diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 9470a8dd7..f5b72ee0d 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -167,7 +167,7 @@ class ImageFile(Image.Image): pass def _close_fp(self): - if getattr(self, "_fp", False): + if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError): if self._fp != self.fp: self._fp.close() self._fp = DeferredError(ValueError("Operation on closed image")) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index e08f80b6b..f7393eac0 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -32,6 +32,7 @@ from . import ( TiffImagePlugin, ) from ._binary import o32le +from ._util import DeferredError def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -125,11 +126,15 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self.readonly = 1 def load_seek(self, pos: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self._fp.seek(pos) def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp self.offset = self.__mpoffsets[frame] diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4fc6217e1..3e3cf6526 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -48,6 +48,7 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +from ._util import DeferredError if TYPE_CHECKING: from . import _imaging @@ -869,6 +870,8 @@ class PngImageFile(ImageFile.ImageFile): def _seek(self, frame: int, rewind: bool = False) -> None: assert self.png is not None + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.dispose: _imaging.ImagingCore | None dispose_extent = None diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 0aada8a06..f49aaeeb1 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -27,6 +27,7 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import si16be as si16 from ._binary import si32be as si32 +from ._util import DeferredError MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -148,6 +149,8 @@ class PsdImageFile(ImageFile.ImageFile): ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: layers = [] if self._layers_position is not None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self._fp.seek(self._layers_position) _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size)) layers = _layerinfo(_layer_data, self._layers_size) @@ -167,6 +170,8 @@ class PsdImageFile(ImageFile.ImageFile): def seek(self, layer: int) -> None: if not self._seek_check(layer): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex # seek to given layer (1..max) _, mode, _, tile = self.layers[layer - 1] diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index b26e1a996..62fa7be03 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -40,6 +40,7 @@ import sys from typing import IO, TYPE_CHECKING, Any, cast from . import Image, ImageFile +from ._util import DeferredError def isInt(f: Any) -> int: @@ -178,6 +179,8 @@ class SpiderImageFile(ImageFile.ImageFile): raise EOFError(msg) if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) self.fp = self._fp self.fp.seek(self.stkoffset) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 39783f1f8..ebe599cca 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -58,7 +58,7 @@ from ._binary import i32be as i32 from ._binary import o8 from ._deprecate import deprecate from ._typing import StrOrBytesPath -from ._util import is_path +from ._util import DeferredError, is_path from .TiffTags import TYPES if TYPE_CHECKING: @@ -1222,6 +1222,8 @@ class TiffImageFile(ImageFile.ImageFile): self._im = None def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp while len(self._frame_pos) <= frame: From e8a9b566036a041f7643d32bf9050ee31de8163c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:33:51 +1100 Subject: [PATCH 404/628] Improved connecting discontiguous corners (#8659) --- .../discontiguous_corners_polygon.png | Bin 533 -> 533 bytes Tests/test_imagedraw.py | 2 +- src/libImaging/Draw.c | 58 ++++++++---------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png index 1b58889c8f3ae45243a7509c907f1928534bcbde..8992a165758676b9ba7777347fb09a3b8c453e3d 100644 GIT binary patch delta 498 zcmV!!QU#x&NVZwfxr}B*1oAc++Wu^hDyMmFv3h`#y7( z^bX*wh={0kNqVm~ZJk<@{7}`frq38OUF0%$Ri9_ST)mw}n@*lB8`kG(v?=7-vMC9z zPn)FNOFlq0L$jBBfNY*-7kRd9wq_%Fw(Ja(TpUFHEepqypH5182bmeUaqnc-s_XgQ{+#-eC+wU`t#9JT*W=9jlj!u-K3$uKcEi$mwNKY+qig+H7V^#a7ga8(`}`_7Z^}zP zQ9q<-sx8egsg|Yw#I!ktma!{-pYw9{cA9Otc{FxdpQqV|m`7tnCY0Yc zh}>&F0UN>WHJ^ZuV|JNGW22dk=F!+W%xd#!?5C`Y&7-j;iGLTEM`LSqPco0j_UtvF zc|TaBnTP~M<};~0<^#KGm-vZS{wjg@Oab*XJUP5=3P_!s&8re=9eQD8B{9Jc+$-AK5ydi~mnpf?M*P557 z@N)B#eer(t%M?yAKiL-tnSV;*IP>a^$~!1re}Az#u=?k@{eksw9&mD^TA$$8<{7%T zU(d|ucb|eKly1+L^El`HZd84>FK6@E9$Xr)_T`*5+iTxtF+aS1Q}Pz|J-^GG59Kvq z$RE;k|Gt3?V-1fyE-1*Ls!QC1{o@oTsl+9J2Pa? o!@AA+ej3>3+2wVTOis1_06el#wiqRwUjP6A07*qoM6N<$f>{3oE&u=k diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 2767418ea..ffe9c0979 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1704,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None: BLACK, ) expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") - assert_image_similar_tofile(img, expected, 1) + assert_image_equal_tofile(img, expected) def test_polygon2() -> None: diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index ea6f8805e..d5aff8709 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -501,55 +501,49 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if (current->dx != 0 && j % 2 == 1 && - roundf(xx[j - 1]) == xx[j - 1]) { + } else if ((ymin == current->ymin || ymin == current->ymax) && + current->dx != 0) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; - if ((current->dx > 0 && other_edge->dx <= 0) || - (current->dx < 0 && other_edge->dx >= 0)) { + if ((ymin != other_edge->ymin && ymin != other_edge->ymax) || + other_edge->dx == 0) { continue; } // Check if the two edges join to make a corner - if (xx[j - 1] == - (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) { + if (roundf(xx[j - 1]) == + roundf( + (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; + int offset = ymin == current->ymax ? -1 : 1; adjacent_line_x = (ymin + offset - current->y0) * current->dx + current->x0; - adjacent_line_x_other_edge = - (ymin + offset - other_edge->y0) * other_edge->dx + - other_edge->x0; - if (ymin == current->ymax) { - if (current->dx > 0) { - xx[k] = - fmax( + if (ymin + offset >= other_edge->ymin && + ymin + offset <= other_edge->ymax) { + adjacent_line_x_other_edge = + (ymin + offset - other_edge->y0) * other_edge->dx + + other_edge->x0; + if (xx[j - 1] > adjacent_line_x + 1 && + xx[j - 1] > adjacent_line_x_other_edge + 1) { + xx[j - 1] = + roundf(fmax( adjacent_line_x, adjacent_line_x_other_edge - ) + + )) + 1; - } else { - xx[k] = - fmin( + } else if (xx[j - 1] < adjacent_line_x - 1 && + xx[j - 1] < adjacent_line_x_other_edge - 1) { + xx[j - 1] = + roundf(fmin( adjacent_line_x, adjacent_line_x_other_edge - ) - - 1; - } - } else { - if (current->dx > 0) { - xx[k] = fmin( - adjacent_line_x, adjacent_line_x_other_edge - ); - } else { - xx[k] = - fmax( - adjacent_line_x, adjacent_line_x_other_edge - ) + + )) - 1; } + break; } - break; } } } From 25653d2f87f0f0be370442836b472a43c1898b71 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:34:42 +1100 Subject: [PATCH 405/628] Corrected P mode save (#8685) --- Tests/test_file_palm.py | 6 +++++- src/PIL/PalmImagePlugin.py | 33 +++++++++------------------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index a1859bc33..58208ba99 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None: im.save(outfile) converted = open_with_magick(magick, tmp_path, outfile) + if mode == "P": + assert converted.mode == "P" + + im = im.convert("RGB") + converted = converted.convert("RGB") assert_image_equal(converted, im) @@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None: roundtrip(tmp_path, mode) -@pytest.mark.xfail(reason="Palm P image is wrong") def test_p_mode(tmp_path: Path) -> None: # Arrange mode = "P" diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index b33245376..15f712908 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -116,9 +116,6 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode == "P": - # we assume this is a color Palm image with the standard colormap, - # unless the "info" dict has a "custom-colormap" field - rawmode = "P" bpp = 8 version = 1 @@ -172,12 +169,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: compression_type = _COMPRESSION_TYPES["none"] flags = 0 - if im.mode == "P" and "custom-colormap" in im.info: - assert im.palette is not None - flags = flags & _FLAGS["custom-colormap"] - colormapsize = 4 * 256 + 2 - colormapmode = im.palette.mode - colormap = im.getdata().getpalette() + if im.mode == "P": + flags |= _FLAGS["custom-colormap"] + colormap = im.im.getpalette() + colors = len(colormap) // 3 + colormapsize = 4 * colors + 2 else: colormapsize = 0 @@ -196,22 +192,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # now write colormap if necessary - if colormapsize > 0: - fp.write(o16b(256)) - for i in range(256): + if colormapsize: + fp.write(o16b(colors)) + for i in range(colors): fp.write(o8(i)) - if colormapmode == "RGB": - fp.write( - o8(colormap[3 * i]) - + o8(colormap[3 * i + 1]) - + o8(colormap[3 * i + 2]) - ) - elif colormapmode == "RGBA": - fp.write( - o8(colormap[4 * i]) - + o8(colormap[4 * i + 1]) - + o8(colormap[4 * i + 2]) - ) + fp.write(colormap[3 * i : 3 * i + 3]) # now convert data to raw form ImageFile._save( From bce83ac800dd70c8b49fff9662a2352b2a388a0b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:36:36 +1100 Subject: [PATCH 406/628] Enable mmap on PyPy (#8840) --- src/PIL/Image.py | 2 ++ src/PIL/ImageFile.py | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 67e068b06..662afadf4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -621,6 +621,8 @@ class Image: more information. """ if getattr(self, "map", None): + if sys.platform == "win32" and hasattr(sys, "pypy_version_info"): + self.map.close() self.map: mmap.mmap | None = None # Instead of simply setting to None, we're setting up a diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f5b72ee0d..a7848c369 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -34,7 +34,6 @@ import itertools import logging import os import struct -import sys from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast from . import ExifTags, Image @@ -278,8 +277,6 @@ class ImageFile(Image.Image): self.map: mmap.mmap | None = None use_mmap = self.filename and len(self.tile) == 1 - # As of pypy 2.1.0, memory mapping was failing here. - use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") readonly = 0 From e053be3412ea6db562cd4f240471e79f6bdbae53 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 07:27:30 +1100 Subject: [PATCH 407/628] Updated version Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageGrab.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 6435e1a0c..671d1ccee 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -43,7 +43,7 @@ or the clipboard to a PIL image memory. :param handle: HWND, to capture a single window. Windows only. - .. versionadded:: 11.1.0 + .. versionadded:: 11.2.0 :return: An image .. py:function:: grabclipboard() From 382c3ab10d5583072a70250218d1bd5bef65ef8a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Mar 2025 11:16:05 +1100 Subject: [PATCH 408/628] spectacle may also be used on Linux --- docs/reference/ImageGrab.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index db2987eb0..1b81bc42c 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -16,8 +16,9 @@ or the clipboard to a PIL image memory. the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return - a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is - installed. To disable this behaviour, pass ``xdisplay=""`` instead. + a snapshot of the screen, ``gnome-screenshot`` or ``spectacle`` will be used as a + fallback if they are installed. To disable this behaviour, pass ``xdisplay=""`` + instead. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) From 14fb62e36c5e933faf9f88e9c0418f71be883a9c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 18:42:46 +1100 Subject: [PATCH 409/628] Assert image type (#8619) --- Tests/test_file_apng.py | 44 ++++++++++++++++++++++++++++++++ Tests/test_file_dcx.py | 2 ++ Tests/test_file_eps.py | 6 +++++ Tests/test_file_fli.py | 8 ++++++ Tests/test_file_fpx.py | 3 ++- Tests/test_file_gif.py | 25 ++++++++++++++++++ Tests/test_file_icns.py | 4 +++ Tests/test_file_ico.py | 5 ++++ Tests/test_file_im.py | 2 ++ Tests/test_file_jpeg.py | 10 ++++++++ Tests/test_file_jpeg2k.py | 2 ++ Tests/test_file_libtiff.py | 21 +++++++++++++++ Tests/test_file_mic.py | 5 +++- Tests/test_file_mpo.py | 7 ++++- Tests/test_file_png.py | 7 +++++ Tests/test_file_psd.py | 7 +++++ Tests/test_file_spider.py | 1 + Tests/test_file_tiff.py | 44 +++++++++++++++++++++++++++++--- Tests/test_file_tiff_metadata.py | 20 +++++++++++++++ Tests/test_file_webp_animated.py | 9 ++++++- Tests/test_file_webp_metadata.py | 3 ++- Tests/test_file_wmf.py | 3 +++ Tests/test_file_xpm.py | 1 + Tests/test_imagesequence.py | 4 ++- Tests/test_shell_injection.py | 1 + Tests/test_tiff_ifdrational.py | 1 + 26 files changed, 236 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index abd7d510b..a5734c202 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin # (referenced from https://wiki.mozilla.org/APNG_Specification) def test_apng_basic() -> None: with Image.open("Tests/images/apng/single_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated assert im.n_frames == 1 assert im.get_format_mimetype() == "image/apng" @@ -20,6 +21,7 @@ def test_apng_basic() -> None: assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/single_frame_default.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.is_animated assert im.n_frames == 2 assert im.get_format_mimetype() == "image/apng" @@ -52,6 +54,7 @@ def test_apng_basic() -> None: ) def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -59,31 +62,37 @@ def test_apng_fdat(filename: str) -> None: def test_apng_dispose() -> None: with Image.open("Tests/images/apng/dispose_op_none.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_background.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) @@ -91,21 +100,25 @@ def test_apng_dispose() -> None: def test_apng_dispose_region() -> None: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 255, 255) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -132,6 +145,7 @@ def test_apng_dispose_op_previous_frame() -> None: # ], # ) with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (255, 0, 0, 255) @@ -145,26 +159,31 @@ def test_apng_dispose_op_background_p_mode() -> None: def test_apng_blend() -> None: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 2) assert im.getpixel((64, 32)) == (0, 255, 0, 2) with Image.open("Tests/images/apng/blend_op_over.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 97) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -178,6 +197,7 @@ def test_apng_blend_transparency() -> None: def test_apng_chunk_order() -> None: with Image.open("Tests/images/apng/fctl_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -233,24 +253,28 @@ def test_apng_num_plays() -> None: def test_apng_mode() -> None: with Image.open("Tests/images/apng/mode_16bit.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "RGBA" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 128, 191) assert im.getpixel((64, 32)) == (0, 0, 128, 191) with Image.open("Tests/images/apng/mode_grayscale.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "L" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == 128 assert im.getpixel((64, 32)) == 255 with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "LA" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (128, 191) assert im.getpixel((64, 32)) == (128, 191) with Image.open("Tests/images/apng/mode_palette.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGB") @@ -258,6 +282,7 @@ def test_apng_mode() -> None: assert im.getpixel((64, 32)) == (0, 255, 0) with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGBA") @@ -265,6 +290,7 @@ def test_apng_mode() -> None: assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGBA") @@ -274,25 +300,31 @@ def test_apng_mode() -> None: def test_apng_chunk_errors() -> None: with Image.open("Tests/images/apng/chunk_no_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) @@ -300,26 +332,31 @@ def test_apng_chunk_errors() -> None: def test_apng_syntax_errors() -> None: with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.raises(OSError): im.load() with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated im.load() # we can handle this case gracefully with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated im.load() @@ -339,6 +376,7 @@ def test_apng_syntax_errors() -> None: def test_apng_sequence_errors(test_file: str) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() @@ -349,6 +387,7 @@ def test_apng_save(tmp_path: Path) -> None: im.save(test_file, save_all=True) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.load() assert not im.is_animated assert im.n_frames == 1 @@ -364,6 +403,7 @@ def test_apng_save(tmp_path: Path) -> None: ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.load() assert im.is_animated assert im.n_frames == 2 @@ -403,6 +443,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: append_images=frames, ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() @@ -445,6 +486,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 1 assert "duration" not in im.info @@ -456,6 +498,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: duration=[500, 100, 150], ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 @@ -466,6 +509,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: frame.info["duration"] = 300 frame.save(test_file, save_all=True, append_images=[frame, different_frame]) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index ab6b9f983..e9d88dd39 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -69,12 +69,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index f5acc532c..b484a8cfa 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -86,6 +86,8 @@ simple_eps_file_with_long_binary_data = ( def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: + assert isinstance(image, EpsImagePlugin.EpsImageFile) + image.load(scale=scale) assert image.mode == "RGB" assert image.size == expected_size @@ -227,6 +229,8 @@ def test_showpage() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_transparency() -> None: with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: + assert isinstance(plot_image, EpsImagePlugin.EpsImageFile) + plot_image.load(transparency=True) assert plot_image.mode == "RGBA" @@ -308,6 +312,7 @@ def test_render_scale2() -> None: # Zero bounding box with Image.open(FILE1) as image1_scale2: + assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile) image1_scale2.load(scale=2) with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: image1_scale2_compare = image1_scale2_compare.convert("RGB") @@ -316,6 +321,7 @@ def test_render_scale2() -> None: # Non-zero bounding box with Image.open(FILE2) as image2_scale2: + assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile) image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: image2_scale2_compare = image2_scale2_compare.convert("RGB") diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 2f39adc69..81df1ab0b 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -22,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" def test_sanity() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + im.load() assert im.mode == "P" assert im.size == (128, 128) @@ -29,6 +31,8 @@ def test_sanity() -> None: assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + assert im.mode == "P" assert im.size == (320, 200) assert im.format == "FLI" @@ -112,16 +116,19 @@ def test_palette_chunk_second() -> None: def test_n_frames() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 384 assert im.is_animated def test_eoferror() -> None: with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -166,6 +173,7 @@ def test_seek_tell() -> None: def test_seek() -> None: with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) im.seek(50) assert_image_equal_tofile(im, "Tests/images/a_fli.png") diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index e32f30a01..8d8064692 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -22,10 +22,11 @@ def test_sanity() -> None: def test_close() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: - pass + assert isinstance(im, FpxImagePlugin.FpxImageFile) assert im.ole.fp.closed im = Image.open("Tests/images/input_bw_one_band.fpx") + assert isinstance(im, FpxImagePlugin.FpxImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 376eab0c6..20d58a9dd 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -402,6 +402,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None: def test_seek() -> None: with Image.open("Tests/images/dispose_none.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) frame_count = 0 try: while True: @@ -446,10 +447,12 @@ def test_seek_rewind() -> None: def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.is_animated == (n_frames != 1) # Test is_animated after n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) @@ -459,6 +462,7 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(1) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 5 assert_image_equal(im, expected) @@ -466,17 +470,20 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(3) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.is_animated assert_image_equal(im, expected) with Image.open("Tests/images/comment_after_only_frame.gif") as im: expected = Image.new("P", (1, 1)) + assert isinstance(im, GifImagePlugin.GifImageFile) assert not im.is_animated assert_image_equal(im, expected) def test_eoferror() -> None: with Image.open(TEST_GIF) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -495,6 +502,7 @@ def test_first_frame_transparency() -> None: def test_dispose_none() -> None: with Image.open("Tests/images/dispose_none.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -518,6 +526,7 @@ def test_dispose_none_load_end() -> None: def test_dispose_background() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -571,6 +580,7 @@ def test_transparent_dispose( def test_dispose_previous() -> None: with Image.open("Tests/images/dispose_prev.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -608,6 +618,7 @@ def test_save_dispose(tmp_path: Path) -> None: for method in range(4): im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for _ in range(2): img.seek(img.tell() + 1) assert img.disposal_method == method @@ -621,6 +632,7 @@ def test_save_dispose(tmp_path: Path) -> None: ) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 @@ -743,6 +755,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) with Image.open(out) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 3 @@ -924,6 +937,8 @@ def test_identical_frames(tmp_path: Path) -> None: out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that the first three frames were combined assert reread.n_frames == 2 @@ -953,6 +968,8 @@ def test_identical_frames_to_single_frame( im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that all frames were combined assert reread.n_frames == 1 @@ -1139,12 +1156,14 @@ def test_append_images(tmp_path: Path) -> None: im.copy().save(out, save_all=True, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Test append_images without save_all im.copy().save(out, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending using a generator @@ -1154,6 +1173,7 @@ def test_append_images(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=im_generator(ims)) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending single and multiple frame images @@ -1162,6 +1182,7 @@ def test_append_images(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 10 @@ -1262,6 +1283,7 @@ def test_bbox(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 @@ -1274,6 +1296,7 @@ def test_bbox_alpha(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 @@ -1425,6 +1448,7 @@ def test_extents( ) -> None: monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) with Image.open("Tests/images/" + test_file) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.size == (100, 100) # Check that n_frames does not change the size @@ -1472,4 +1496,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: im1.save(out, save_all=True, append_images=[im2], **params) with Image.open(out) as reloaded: + assert isinstance(reloaded, GifImagePlugin.GifImageFile) assert reloaded.n_frames == 2 diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index b6dc4bc19..2dabfd2f3 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -69,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None: assert_image_similar_tofile(im, temp_file, 1) with Image.open(temp_file) as reread: + assert isinstance(reread, IcnsImagePlugin.IcnsImageFile) reread.size = (16, 16) reread.load(2) assert_image_equal(reread, provided_im) @@ -90,6 +91,7 @@ def test_sizes() -> None: # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected with Image.open(TEST_FILE) as im: + assert isinstance(im, IcnsImagePlugin.IcnsImageFile) for w, h, r in im.info["sizes"]: wr = w * r hr = h * r @@ -118,6 +120,7 @@ def test_older_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow2.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h) im2.load(r) assert im2.mode == "RGBA" @@ -135,6 +138,7 @@ def test_jp2_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow3.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h) im2.load(r) assert im2.mode == "RGBA" diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 37bfd3f1f..5d2ace35e 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -77,6 +77,7 @@ def test_save_to_bytes() -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert im.mode == reloaded.mode @@ -94,6 +95,7 @@ def test_getpixel(tmp_path: Path) -> None: im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) with Image.open(temp_file) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.load() reloaded.size = (32, 32) @@ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert "RGBA" == reloaded.mode @@ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: def test_incorrect_size() -> None: with Image.open(TEST_ICO_FILE) as im: + assert isinstance(im, IcoImagePlugin.IcoImageFile) with pytest.raises(ValueError): im.size = (1, 1) @@ -219,6 +223,7 @@ def test_save_append_images(tmp_path: Path) -> None: im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) with Image.open(outfile) as reread: + assert isinstance(reread, IcoImagePlugin.IcoImageFile) assert_image_equal(reread, hopper("RGBA")) reread.size = (32, 32) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 235914a2b..55c6b7305 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -68,12 +68,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 8ab853b85..79f0ec1a8 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -91,6 +91,7 @@ class TestFileJpeg: def test_app(self) -> None: # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") assert im.applist[1] == ( "COM", @@ -316,6 +317,8 @@ class TestFileJpeg: def test_exif_typeerror(self) -> None: with Image.open("Tests/images/exif_typeerror.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise a TypeError im._getexif() @@ -500,6 +503,7 @@ class TestFileJpeg: def test_mp(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im._getmp() is None def test_quality_keep(self, tmp_path: Path) -> None: @@ -558,12 +562,14 @@ class TestFileJpeg: with Image.open(test_file) as im: im.save(b, "JPEG", qtables=[[n] * 64] * n) with Image.open(b) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization assert max(reloaded.quantization[0]) <= 255 with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) qtables = im.quantization reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) assert im.quantization == reloaded.quantization @@ -663,6 +669,7 @@ class TestFileJpeg: def test_load_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == 2 assert len(im.quantization[0]) == 64 assert max(im.quantization[0]) > 255 @@ -705,6 +712,7 @@ class TestFileJpeg: @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self) -> None: with Image.open(TEST_FILE) as img: + assert isinstance(img, JpegImagePlugin.JpegImageFile) img.load_djpeg() assert_image_similar_tofile(img, TEST_FILE, 5) @@ -909,6 +917,7 @@ class TestFileJpeg: def test_photoshop_malformed_and_multiple(self) -> None: with Image.open("Tests/images/app13-multiple.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert "photoshop" in im.info assert 24 == len(im.info["photoshop"]) apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] @@ -1084,6 +1093,7 @@ class TestFileJpeg: def test_deprecation(self) -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) with pytest.warns(DeprecationWarning): assert im.huffman_ac == {} with pytest.warns(DeprecationWarning): diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 916df2586..4095bfaf2 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -228,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None: out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 1 im.load() assert_image_similar(im, card, 13) out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 3 im.load() assert_image_similar(im, card, 0.4) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9e63e9c10..9916215fb 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -36,6 +36,7 @@ class LibTiffTestCase: im.load() im.getdata() + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._compression == "group4" # can we write it back out, in a different form. @@ -153,6 +154,7 @@ class TestFileLibTiff(LibTiffTestCase): """Test metadata writing through libtiff""" f = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) img.save(f, tiffinfo=img.tag) if legacy_api: @@ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase): ] with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) if legacy_api: reloaded = loaded.tag.named() else: @@ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase): # Exclude ones that have special meaning # that we're already testing them with Image.open("Tests/images/hopper_g4.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) for tag in im.tag_v2: try: del core_items[tag] @@ -317,6 +321,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, tiffinfo=tiffinfo) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) for tag, value in tiffinfo.items(): reloaded_value = reloaded.tag_v2[tag] if ( @@ -349,12 +354,14 @@ class TestFileLibTiff(LibTiffTestCase): def test_osubfiletype(self, tmp_path: Path) -> None: outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[OSUBFILETYPE] = 1 im.save(outfile) def test_subifd(self, tmp_path: Path) -> None: outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[SUBIFD] = 10000 # Should not segfault @@ -369,6 +376,7 @@ class TestFileLibTiff(LibTiffTestCase): hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag" @@ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase): """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: + assert isinstance(orig, TiffImagePlugin.TiffImageFile) + out = tmp_path / "temp.tif" orig.tag[269] = "temp.tif" orig.save(out) with Image.open(out) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] @@ -541,6 +552,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: # colormap/palette tag + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) @@ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.seek(0) assert im.size == (10, 10) assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) @@ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase): # issue #862 monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) frames = im.n_frames assert frames == 3 for _ in range(frames): @@ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase): def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert not im.tag.next im.load() assert not im.tag.next @@ -690,21 +705,25 @@ class TestFileLibTiff(LibTiffTestCase): im.save(outfile, compression="jpeg") with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) def test_exif_ifd(self) -> None: out = io.BytesIO() with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[34665] == 125456 im.save(out, "TIFF") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 34665 not in reloaded.tag_v2 im.save(out, "TIFF", tiffinfo={34665: 125456}) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 @@ -786,6 +805,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) @@ -1090,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 274 in im.tag_v2 im.load() diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 9a6f13ea3..9aeb306e4 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -30,11 +30,13 @@ def test_sanity() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.n_frames == 1 def test_is_animated() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert not im.is_animated @@ -55,10 +57,11 @@ def test_seek() -> None: def test_close() -> None: with Image.open(TEST_FILE) as im: - pass + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.ole.fp.closed im = Image.open(TEST_FILE) + assert isinstance(im, MicImagePlugin.MicImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 6b4f6423b..73838ef44 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -6,7 +6,7 @@ from typing import Any import pytest -from PIL import Image, ImageFile, MpoImagePlugin +from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin from .helper import ( assert_image_equal, @@ -80,6 +80,7 @@ def test_context_manager() -> None: def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.applist[0][0] == "APP1" assert im.applist[1][0] == "APP2" assert im.applist[1][1].startswith( @@ -220,12 +221,14 @@ def test_seek(test_file: str) -> None: def test_n_frames() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -239,6 +242,8 @@ def test_eoferror() -> None: def test_adopt_jpeg() -> None: with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + with pytest.raises(ValueError): MpoImagePlugin.MpoImageFile.adopt(im) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c969bd502..0f0886ab8 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -576,6 +576,7 @@ class TestFilePng: def test_read_private_chunks(self) -> None: with Image.open("Tests/images/exif.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.private_chunks == [(b"orNT", b"\x01")] def test_roundtrip_private_chunk(self) -> None: @@ -598,6 +599,7 @@ class TestFilePng: def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/hopper.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert "comment" in im.text for k, v in { "date:create": "2014-09-04T09:37:08+03:00", @@ -607,15 +609,19 @@ class TestFilePng: # Raises a SyntaxError in load_end with Image.open("Tests/images/broken_data_stream.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) 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 isinstance(im, PngImagePlugin.PngImageFile) assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} # Raises a UnicodeDecodeError in load_end with Image.open("Tests/images/truncated_image.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) + # The file is truncated with pytest.raises(OSError): im.text @@ -726,6 +732,7 @@ class TestFilePng: im.save(test_file) with Image.open(test_file) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) assert reloaded._getexif() is None # Test passing in exif diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 1793c269d..38a88cd17 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -59,17 +59,21 @@ def test_invalid_file() -> None: def test_n_frames() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 1 assert not im.is_animated for path in [test_file, "Tests/images/negative_layer_count.psd"]: with Image.open(path) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + # PSD seek index starts at 1 rather than 0 n_frames = im.n_frames + 1 @@ -119,11 +123,13 @@ def test_rgba() -> None: def test_negative_top_left_layer() -> None: with Image.open("Tests/images/negative_top_left_layer.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.layers[0][2] == (-50, -50, 50, 50) def test_layer_skip() -> None: with Image.open("Tests/images/five_channels.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 1 @@ -175,5 +181,6 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None: def test_layer_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) with pytest.raises(SyntaxError): im.layers diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index b64a629f5..3b3c3b4a5 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -96,6 +96,7 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, SpiderImagePlugin.SpiderImageFile) assert im.n_frames == 1 assert not im.is_animated diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 6962a5c98..502d9df9a 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -9,7 +9,13 @@ from types import ModuleType import pytest -from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + TiffImagePlugin, + UnidentifiedImageError, +) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( @@ -113,6 +119,7 @@ class TestFileTiff: with Image.open("Tests/images/hopper_bigtiff.tif") as im: outfile = tmp_path / "temp.tif" + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) def test_bigtiff_save(self, tmp_path: Path) -> None: @@ -121,11 +128,13 @@ class TestFileTiff: im.save(outfile, big_tiff=True) with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) 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 isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2._bigtiff is True def test_seek_too_large(self) -> None: @@ -140,6 +149,8 @@ class TestFileTiff: def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -153,6 +164,8 @@ class TestFileTiff: def test_xyres_fallback_tiff(self) -> None: filename = "Tests/images/compression.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -167,6 +180,8 @@ class TestFileTiff: def test_int_resolution(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -181,6 +196,7 @@ class TestFileTiff: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) @@ -198,6 +214,7 @@ class TestFileTiff: with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) with Image.open(b) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45 @@ -213,10 +230,12 @@ class TestFileTiff: TiffImagePlugin.PREFIXES.pop() def test_bad_exif(self) -> None: - with Image.open("Tests/images/hopper_bad_exif.jpg") as i: + with Image.open("Tests/images/hopper_bad_exif.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise struct.error. with pytest.warns(UserWarning): - i._getexif() + im._getexif() def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") @@ -307,11 +326,13 @@ class TestFileTiff: ) def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) def test_eoferror(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -355,19 +376,24 @@ class TestFileTiff: def test_frame_order(self) -> None: # A frame can't progress to itself after reading with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 1 # A frame can't progress to a frame that has already been read with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 2 # Frames don't have to be in sequence with Image.open("Tests/images/multipage_out_of_order.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 3 def test___str__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # Act ret = str(im.ifd) @@ -378,6 +404,8 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 interface v2_tags = { 256: 55, @@ -417,6 +445,7 @@ class TestFileTiff: def test__delitem__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) len_before = len(dict(im.ifd)) del im.ifd[256] len_after = len(dict(im.ifd)) @@ -449,6 +478,7 @@ class TestFileTiff: def test_ifd_tag_type(self) -> None: with Image.open("Tests/images/ifd_tag_type.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 0x8825 in im.tag_v2 def test_exif(self, tmp_path: Path) -> None: @@ -537,6 +567,7 @@ class TestFileTiff: im = hopper(mode) im.save(filename, tiffinfo={262: 0}) with Image.open(filename) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[262] == 0 assert_image_equal(im, reloaded) @@ -615,6 +646,8 @@ class TestFileTiff: filename = tmp_path / "temp.tif" hopper("RGB").save(filename, "TIFF", **kwargs) with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 @@ -701,6 +734,7 @@ class TestFileTiff: def test_planar_configuration_save(self, tmp_path: Path) -> None: infile = "Tests/images/tiff_tiled_planar_raw.tif" with Image.open(infile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._planar_configuration == 2 outfile = tmp_path / "temp.tif" @@ -733,6 +767,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 3 # Test appending images @@ -743,6 +778,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 # Test appending using a generator @@ -754,6 +790,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 def test_fixoffsets(self) -> None: @@ -864,6 +901,7 @@ class TestFileTiff: def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert list(im.get_photoshop_blocks().keys()) == [ 1061, 1002, diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 0734d1db1..884868345 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -61,6 +61,7 @@ def test_rt_metadata(tmp_path: Path) -> None: img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None: info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata() -> None: with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, @@ -128,6 +131,7 @@ def test_read_metadata() -> None: def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) f = tmp_path / "temp.tiff" del img.tag[278] img.save(f, tiffinfo=img.tag) @@ -135,6 +139,7 @@ def test_write_metadata(tmp_path: Path) -> None: original = img.tag_v2.named() with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) reloaded = loaded.tag_v2.named() ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] @@ -165,6 +170,7 @@ def test_write_metadata(tmp_path: Path) -> None: def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) info = im.tag_v2 del info[278] @@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG @@ -231,6 +238,7 @@ def test_writing_other_types_to_ascii( im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[271] == expected @@ -248,6 +256,7 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[700] == b"\x01" @@ -267,6 +276,7 @@ def test_writing_other_types_to_undefined( im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[33723] == b"1" @@ -311,6 +321,7 @@ def test_iccprofile_binary() -> None: # but probably won't be able to save it. with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.tagtype[34675] == 1 assert im.info["icc_profile"] @@ -336,6 +347,7 @@ def test_exif_div_zero(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 0 == reloaded.tag_v2[41988].numerator assert 0 == reloaded.tag_v2[41988].denominator @@ -355,6 +367,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -367,6 +380,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -385,6 +399,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -397,6 +412,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -410,6 +426,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 2**31 - 1 == reloaded.tag_v2[37380].numerator assert -1 == reloaded.tag_v2[37380].denominator @@ -424,6 +441,7 @@ def test_ifd_signed_long(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[37000] == -60000 @@ -444,11 +462,13 @@ def test_empty_values() -> None: def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) out = tmp_path / "temp.tiff" im.save(out) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[34377]) == 70 assert isinstance(reloaded.tag_v2[34377], bytes) diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index d4b1fda97..503761374 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest from packaging.version import parse as parse_version -from PIL import Image, features +from PIL import GifImagePlugin, Image, WebPImagePlugin, features from .helper import ( assert_image_equal, @@ -22,10 +22,12 @@ def test_n_frames() -> None: """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" with Image.open("Tests/images/hopper.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open("Tests/images/iss634.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 42 assert im.is_animated @@ -37,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None: """ with Image.open("Tests/images/iss634.gif") as orig: + assert isinstance(orig, GifImagePlugin.GifImageFile) assert orig.n_frames > 1 temp_file = tmp_path / "temp.webp" orig.save(temp_file, save_all=True) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == orig.n_frames # Compare first and last frames to the original animated GIF @@ -69,6 +73,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: def check(temp_file: Path) -> None: with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 2 # Compare first frame to original @@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated @@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index d1d3421ec..7543d22da 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -6,7 +6,7 @@ from types import ModuleType import pytest -from PIL import Image +from PIL import Image, WebPImagePlugin from .helper import mark_if_feature_version, skip_unless_feature @@ -110,6 +110,7 @@ def test_read_no_exif() -> None: test_buffer.seek(0) with Image.open(test_buffer) as webp_image: + assert isinstance(webp_image, WebPImagePlugin.WebPImageFile) assert not webp_image._getexif() diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index a752f8013..dcf5f000f 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -89,6 +89,7 @@ def test_load_float_dpi() -> None: def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) assert im.size == (82, 82) if hasattr(Image.core, "drawwmf"): @@ -102,10 +103,12 @@ def test_load_set_dpi() -> None: if not hasattr(Image.core, "drawwmf"): return + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) im.load(im.info["dpi"]) assert im.size == (1625, 1625) with Image.open("Tests/images/drawing.emf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) im.load((72, 144)) assert im.size == (82, 164) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 26afe93f4..73c62a44d 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -30,6 +30,7 @@ def test_invalid_file() -> None: def test_load_read() -> None: # Arrange with Image.open(TEST_FILE) as im: + assert isinstance(im, XpmImagePlugin.XpmImageFile) dummy_bytes = 1 # Act diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index da9e71692..7b9ac80bc 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from PIL import Image, ImageSequence, TiffImagePlugin +from PIL import Image, ImageSequence, PsdImagePlugin, TiffImagePlugin from .helper import assert_image_equal, hopper, skip_unless_feature @@ -31,6 +31,7 @@ def test_sanity(tmp_path: Path) -> None: def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) i = ImageSequence.Iterator(im) for index in range(im.n_frames): assert i[index] == next(i) @@ -42,6 +43,7 @@ def test_iterator() -> None: def test_iterator_min_frame() -> None: with Image.open("Tests/images/hopper.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) i = ImageSequence.Iterator(im) for index in range(1, im.n_frames): assert i[index] == next(i) diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 4fd3aab5d..03e92b5b9 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -39,6 +39,7 @@ class TestShellInjection: shutil.copy(TEST_JPG, src_file) with Image.open(src_file) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im.load_djpeg() @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 30dc73654..42d06b896 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -72,4 +72,5 @@ def test_ifd_rational_save( im.save(out, dpi=(res, res), compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) From 80d5b421ebd8b8c20c39889862764822366ef183 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 22:13:21 +1100 Subject: [PATCH 410/628] Do not import type checking Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageGrab.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 0113ebcbf..e978b5d23 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -22,10 +22,9 @@ import shutil import subprocess import sys import tempfile -from typing import TYPE_CHECKING - from . import Image +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageWin From d2683e052f341a5f53c7bb440af7fba577e0f4d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 30 Mar 2025 11:13:48 +0000 Subject: [PATCH 411/628] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/ImageGrab.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e978b5d23..42abdf8c7 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -22,6 +22,7 @@ import shutil import subprocess import sys import tempfile + from . import Image TYPE_CHECKING = False From 4236b583a1578c089cd06538a9ded20bcba1a863 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Mar 2025 22:16:16 +1100 Subject: [PATCH 412/628] Do not import TYPE_CHECKING --- src/PIL/GifImagePlugin.py | 3 ++- src/PIL/Image.py | 10 ++-------- src/PIL/ImageDraw.py | 3 ++- src/PIL/ImageFile.py | 3 ++- src/PIL/ImageFilter.py | 3 ++- src/PIL/ImageFont.py | 3 ++- src/PIL/ImagePalette.py | 3 ++- src/PIL/ImageQt.py | 3 ++- src/PIL/ImageTk.py | 3 ++- src/PIL/JpegImagePlugin.py | 3 ++- src/PIL/PSDraw.py | 5 ++++- src/PIL/PdfParser.py | 3 ++- src/PIL/PngImagePlugin.py | 3 ++- src/PIL/SpiderImagePlugin.py | 4 +++- src/PIL/TiffImagePlugin.py | 3 ++- src/PIL/_typing.py | 3 ++- 16 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 045ab1027..4392c4cb9 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union +from typing import IO, Any, Literal, NamedTuple, Union from . import ( Image, @@ -47,6 +47,7 @@ from ._binary import o8 from ._binary import o16le as o16 from ._util import DeferredError +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging from ._typing import Buffer diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 662afadf4..19b22342a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,14 +41,7 @@ import warnings from collections.abc import Callable, Iterator, MutableMapping, Sequence from enum import IntEnum from types import ModuleType -from typing import ( - IO, - TYPE_CHECKING, - Any, - Literal, - Protocol, - cast, -) +from typing import IO, Any, Literal, Protocol, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -218,6 +211,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): # -------------------------------------------------------------------- # Registries +TYPE_CHECKING = False if TYPE_CHECKING: import mmap from xml.etree.ElementTree import Element diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c2ed9034d..e6c7b0298 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -35,7 +35,7 @@ import math import struct from collections.abc import Sequence from types import ModuleType -from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast +from typing import Any, AnyStr, Callable, Union, cast from . import Image, ImageColor from ._deprecate import deprecate @@ -44,6 +44,7 @@ from ._typing import Coords # experimental access to the outline API Outline: Callable[[], Image.core._Outline] = Image.core.outline +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageDraw2, ImageFont diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index a7848c369..c5d6383a5 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -34,12 +34,13 @@ import itertools import logging import os import struct -from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast +from typing import IO, Any, NamedTuple, cast from . import ExifTags, Image from ._deprecate import deprecate from ._util import DeferredError, is_path +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import StrOrBytesPath diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 05829d0c6..b9ed54ab2 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -20,8 +20,9 @@ import abc import functools from collections.abc import Sequence from types import ModuleType -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import Any, Callable, cast +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging from ._typing import NumpyArray diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c8f05fbb7..ebe510ba9 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,12 +34,13 @@ import warnings from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast +from typing import IO, Any, BinaryIO, TypedDict, cast from . import Image, features from ._typing import StrOrBytesPath from ._util import DeferredError, is_path +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageFile from ._imaging import ImagingFont diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 183f85526..103697117 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -19,10 +19,11 @@ from __future__ import annotations import array from collections.abc import Sequence -from typing import IO, TYPE_CHECKING +from typing import IO from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile +TYPE_CHECKING = False if TYPE_CHECKING: from . import Image diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 2cc40f855..df7a57b65 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,11 +19,12 @@ from __future__ import annotations import sys from io import BytesIO -from typing import TYPE_CHECKING, Any, Callable, Union +from typing import Any, Callable, Union from . import Image from ._util import is_path +TYPE_CHECKING = False if TYPE_CHECKING: import PyQt6 import PySide6 diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index e6a9d8eea..3a4cb81e9 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -28,10 +28,11 @@ from __future__ import annotations import tkinter from io import BytesIO -from typing import TYPE_CHECKING, Any +from typing import Any from . import Image, ImageFile +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import CapsuleType diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 9465d8e2d..cc1d54b93 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,7 @@ import subprocess import sys import tempfile import warnings -from typing import IO, TYPE_CHECKING, Any +from typing import IO, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -52,6 +52,7 @@ from ._binary import o16be as o16 from ._deprecate import deprecate from .JpegPresets import presets +TYPE_CHECKING = False if TYPE_CHECKING: from .MpoImagePlugin import MpoImageFile diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 02939d26b..7fd4c5c94 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -17,10 +17,13 @@ from __future__ import annotations import sys -from typing import IO, TYPE_CHECKING +from typing import IO from . import EpsImagePlugin +TYPE_CHECKING = False + + ## # Simple PostScript graphics interface. diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 41b38ebbf..73d8c21c0 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,7 +8,7 @@ import os import re import time import zlib -from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union +from typing import IO, Any, NamedTuple, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -251,6 +251,7 @@ class PdfArray(list[Any]): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" +TYPE_CHECKING = False if TYPE_CHECKING: _DictBase = collections.UserDict[Union[str, bytes], Any] else: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 3e3cf6526..f3815a122 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -40,7 +40,7 @@ import warnings import zlib from collections.abc import Callable from enum import IntEnum -from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast +from typing import IO, Any, NamedTuple, NoReturn, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -50,6 +50,7 @@ from ._binary import o16be as o16 from ._binary import o32be as o32 from ._util import DeferredError +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 62fa7be03..868019e80 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -37,11 +37,13 @@ from __future__ import annotations import os import struct import sys -from typing import IO, TYPE_CHECKING, Any, cast +from typing import IO, Any, cast from . import Image, ImageFile from ._util import DeferredError +TYPE_CHECKING = False + def isInt(f: Any) -> int: try: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ebe599cca..88af9162e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,7 +50,7 @@ import warnings from collections.abc import Iterator, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast +from typing import IO, Any, Callable, NoReturn, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -61,6 +61,7 @@ from ._typing import StrOrBytesPath from ._util import DeferredError, is_path from .TiffTags import TYPES +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import Buffer, IntegralLike diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 34a9a81e1..373938e71 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -3,8 +3,9 @@ from __future__ import annotations import os import sys from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union +from typing import Any, Protocol, TypeVar, Union +TYPE_CHECKING = False if TYPE_CHECKING: from numbers import _IntegralLike as IntegralLike From b4a480ff2cc3e418c04993a54f43b16df9174c28 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 31 Mar 2025 00:31:56 +1100 Subject: [PATCH 413/628] Corrected documentation --- docs/reference/ImageGrab.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 671d1ccee..54d66db13 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -9,7 +9,7 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 -.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) +.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None, window=None) Take a snapshot of the screen. The pixels inside the bounding box are returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, @@ -40,7 +40,7 @@ or the clipboard to a PIL image memory. .. versionadded:: 7.1.0 - :param handle: + :param window: HWND, to capture a single window. Windows only. .. versionadded:: 11.2.0 From 25af4f1841c837c8e7ad370f4b001409c1b221c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 31 Mar 2025 00:32:35 +1100 Subject: [PATCH 414/628] Added release notes --- docs/releasenotes/11.2.0.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index d40d86f21..9f27d8456 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -51,6 +51,15 @@ aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") +Specify window in ImageGrab on Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the +HWND:: + + from PIL import ImageGrab + ImageGrab.grab(window=hwnd) + Check for MozJPEG ^^^^^^^^^^^^^^^^^ From 81be8d54103d008dcfb7d75edc5ca43d0f78afd5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 31 Mar 2025 05:16:25 +1100 Subject: [PATCH 415/628] Fixed unclosed file warning (#8847) --- Tests/test_image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index f18d8489c..c2e850c36 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -230,10 +230,10 @@ class TestImage: assert_image_similar(im, reloaded, 20) def test_unknown_extension(self, tmp_path: Path) -> None: - im = hopper() temp_file = tmp_path / "temp.unknown" - with pytest.raises(ValueError): - im.save(temp_file) + with hopper() as im: + with pytest.raises(ValueError): + im.save(temp_file) def test_internals(self) -> None: im = Image.new("L", (100, 100)) From f673f3e543881ae283b25ef7db06fa828a4e353a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 31 Mar 2025 05:16:50 +1100 Subject: [PATCH 416/628] Close file handle on error (#8846) --- src/PIL/TarIO.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 779288b1c..86490a496 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -35,12 +35,16 @@ class TarIO(ContainerIO.ContainerIO[bytes]): while True: s = self.fh.read(512) if len(s) != 512: + self.fh.close() + msg = "unexpected end of tar file" raise OSError(msg) name = s[:100].decode("utf-8") i = name.find("\0") if i == 0: + self.fh.close() + msg = "cannot find subfile" raise OSError(msg) if i > 0: From e995eef424cb996ebf933b690d30c1834b99999d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 31 Mar 2025 22:31:24 +0300 Subject: [PATCH 417/628] Replace deprecated classifier with licence expression (PEP 639) --- .ci/install.sh | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 62677005e..fbb6c28b5 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -50,7 +50,7 @@ if [[ $(uname) != CYGWIN* ]]; then # Pyroma uses non-isolated build and fails with old setuptools if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then # To match pyproject.toml - python3 -m pip install "setuptools>=67.8" + python3 -m pip install "setuptools>=77" fi # webp diff --git a/pyproject.toml b/pyproject.toml index 780a938a3..3f9c916c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "backend" requires = [ - "setuptools>=67.8", + "setuptools>=77", ] backend-path = [ "_custom_build", @@ -14,14 +14,14 @@ readme = "README.md" keywords = [ "Imaging", ] -license = { text = "MIT-CMU" } +license = "MIT-CMU" +license-files = [ "LICENSE" ] authors = [ { name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, ] requires-python = ">=3.9" classifiers = [ "Development Status :: 6 - Mature", - "License :: OSI Approved :: CMU License (MIT-CMU)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", From d8a0cb5db104cc5d9acc6b4ba1ba871636132f51 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 31 Mar 2025 22:53:51 +0300 Subject: [PATCH 418/628] Work around pyroma test --- Tests/test_pyroma.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2f7fe22e..8235daf32 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -23,5 +23,11 @@ def test_pyroma() -> None: ) else: - # Should have a perfect score - assert rating == (10, []) + # Should have a perfect score, but pyroma does not support PEP 639 yet. + assert rating == ( + 9, + [ + "Your package does neither have a license field " + "nor any license classifiers." + ], + ) From 999d9a7f0cab002c78c93c3790307474256f7d90 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:09:09 +1100 Subject: [PATCH 419/628] Updated xz to 5.8.0 on manylinux2014 by removing po4a dependency (#8848) --- .github/workflows/wheels-dependencies.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4858f6d69..2e842df64 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -42,11 +42,7 @@ HARFBUZZ_VERSION=11.0.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 -if [[ $MB_ML_VER == 2014 ]]; then - XZ_VERSION=5.6.4 -else - XZ_VERSION=5.8.0 -fi +XZ_VERSION=5.8.0 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 @@ -56,6 +52,20 @@ BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +if [[ $MB_ML_VER == 2014 ]]; then + function build_xz { + if [ -e xz-stamp ]; then return; fi + yum install -y gettext-devel + fetch_unpack https://tukaani.org/xz/xz-$XZ_VERSION.tar.gz + (cd xz-$XZ_VERSION \ + && ./autogen.sh --no-po4a \ + && ./configure --prefix=$BUILD_PREFIX \ + && make -j4 \ + && make install) + touch xz-stamp + } +fi + function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi # This essentially duplicates the Homebrew recipe From 7d50816f0a6e607b04f9bdc8af7482a29ba578e3 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Tue, 1 Apr 2025 00:13:21 -0400 Subject: [PATCH 420/628] Add AVIF plugin (decoder + encoder using libavif) (#5201) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .ci/install.sh | 5 +- .github/workflows/macos-install.sh | 7 + .github/workflows/test-mingw.yml | 1 + .github/workflows/test-windows.yml | 6 +- .github/workflows/wheels-dependencies.sh | 43 +- .github/workflows/wheels.yml | 5 + Tests/check_wheel.py | 8 +- Tests/images/avif/exif.avif | Bin 0 -> 16078 bytes Tests/images/avif/hopper-missing-pixi.avif | Bin 0 -> 5435 bytes Tests/images/avif/hopper.avif | Bin 0 -> 3077 bytes Tests/images/avif/hopper.heif | Bin 0 -> 3555 bytes Tests/images/avif/hopper_avif_write.png | Bin 0 -> 30311 bytes Tests/images/avif/icc_profile.avif | Bin 0 -> 6460 bytes Tests/images/avif/icc_profile_none.avif | Bin 0 -> 3303 bytes Tests/images/avif/rot0mir0.avif | Bin 0 -> 16357 bytes Tests/images/avif/rot0mir1.avif | Bin 0 -> 17157 bytes Tests/images/avif/rot1mir0.avif | Bin 0 -> 17182 bytes Tests/images/avif/rot1mir1.avif | Bin 0 -> 16588 bytes Tests/images/avif/rot2mir0.avif | Bin 0 -> 17001 bytes Tests/images/avif/rot2mir1.avif | Bin 0 -> 16387 bytes Tests/images/avif/rot3mir0.avif | Bin 0 -> 16568 bytes Tests/images/avif/rot3mir1.avif | Bin 0 -> 17290 bytes Tests/images/avif/star.avifs | Bin 0 -> 29724 bytes Tests/images/avif/star.gif | Bin 0 -> 2900 bytes Tests/images/avif/star.png | Bin 0 -> 3844 bytes Tests/images/avif/transparency.avif | Bin 0 -> 6441 bytes Tests/images/avif/xmp_tags_orientation.avif | Bin 0 -> 6686 bytes Tests/test_file_avif.py | 778 +++++++++++++++++ depends/install_libavif.sh | 64 ++ docs/handbook/image-file-formats.rst | 79 +- docs/installation/building-from-source.rst | 29 +- docs/reference/features.rst | 1 + docs/reference/plugins.rst | 8 + docs/releasenotes/11.2.0.rst | 9 + setup.py | 19 + src/PIL/AvifImagePlugin.py | 292 +++++++ src/PIL/Image.py | 2 + src/PIL/__init__.py | 1 + src/PIL/_avif.pyi | 3 + src/PIL/features.py | 2 + src/_avif.c | 908 ++++++++++++++++++++ wheels/dependency_licenses/AOM.txt | 26 + wheels/dependency_licenses/DAV1D.txt | 23 + wheels/dependency_licenses/LIBAVIF.txt | 387 +++++++++ wheels/dependency_licenses/LIBYUV.txt | 29 + wheels/dependency_licenses/RAV1E.txt | 25 + wheels/dependency_licenses/SVT-AV1.txt | 26 + winbuild/build.rst | 1 + winbuild/build_prepare.py | 28 + 49 files changed, 2807 insertions(+), 8 deletions(-) create mode 100644 Tests/images/avif/exif.avif create mode 100644 Tests/images/avif/hopper-missing-pixi.avif create mode 100644 Tests/images/avif/hopper.avif create mode 100644 Tests/images/avif/hopper.heif create mode 100644 Tests/images/avif/hopper_avif_write.png create mode 100644 Tests/images/avif/icc_profile.avif create mode 100644 Tests/images/avif/icc_profile_none.avif create mode 100644 Tests/images/avif/rot0mir0.avif create mode 100644 Tests/images/avif/rot0mir1.avif create mode 100644 Tests/images/avif/rot1mir0.avif create mode 100644 Tests/images/avif/rot1mir1.avif create mode 100644 Tests/images/avif/rot2mir0.avif create mode 100644 Tests/images/avif/rot2mir1.avif create mode 100644 Tests/images/avif/rot3mir0.avif create mode 100644 Tests/images/avif/rot3mir1.avif create mode 100644 Tests/images/avif/star.avifs create mode 100644 Tests/images/avif/star.gif create mode 100644 Tests/images/avif/star.png create mode 100644 Tests/images/avif/transparency.avif create mode 100644 Tests/images/avif/xmp_tags_orientation.avif create mode 100644 Tests/test_file_avif.py create mode 100755 depends/install_libavif.sh create mode 100644 src/PIL/AvifImagePlugin.py create mode 100644 src/PIL/_avif.pyi create mode 100644 src/_avif.c create mode 100644 wheels/dependency_licenses/AOM.txt create mode 100644 wheels/dependency_licenses/DAV1D.txt create mode 100644 wheels/dependency_licenses/LIBAVIF.txt create mode 100644 wheels/dependency_licenses/LIBYUV.txt create mode 100644 wheels/dependency_licenses/RAV1E.txt create mode 100644 wheels/dependency_licenses/SVT-AV1.txt diff --git a/.ci/install.sh b/.ci/install.sh index 62677005e..83d5df01c 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ - sway wl-clipboard libopenblas-dev + sway wl-clipboard libopenblas-dev nasm fi python3 -m pip install --upgrade pip @@ -62,6 +62,9 @@ if [[ $(uname) != CYGWIN* ]]; then # raqm pushd depends && ./install_raqm.sh && popd + # libavif + pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd + # extra test images pushd depends && ./install_extra_test_images.sh && popd else diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 6aa59a4ac..099f4a582 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -6,6 +6,8 @@ if [[ "$ImageOS" == "macos13" ]]; then brew uninstall gradle maven fi brew install \ + aom \ + dav1d \ freetype \ ghostscript \ jpeg-turbo \ @@ -14,6 +16,8 @@ brew install \ libtiff \ little-cms2 \ openjpeg \ + rav1e \ + svt-av1 \ webp export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" @@ -27,5 +31,8 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install numpy +# libavif +pushd depends && ./install_libavif.sh && popd + # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bb6d7dc37..5a83c16c3 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,6 +60,7 @@ jobs: mingw-w64-x86_64-gcc \ mingw-w64-x86_64-ghostscript \ mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libavif \ mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libraqm \ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index a780c7835..0c3f44e96 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -42,7 +42,7 @@ jobs: # Test the oldest Python on 32-bit - { python-version: "3.9", architecture: "x86", os: "windows-2019" } - timeout-minutes: 30 + timeout-minutes: 45 name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) @@ -145,6 +145,10 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" + - name: Build dependencies / libavif + if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64' + run: "& winbuild\\build\\build_dep_libavif.cmd" + # for FreeType WOFF2 font support - name: Build dependencies / brotli if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2e842df64..2f2e75b6c 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -25,7 +25,7 @@ else MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_VER=${AUDITWHEEL_POLICY:9} fi -PLAT=$CIBW_ARCHS +PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" # Define custom utilities source wheels/multibuild/common_utils.sh @@ -51,6 +51,7 @@ LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +LIBAVIF_VERSION=1.2.1 if [[ $MB_ML_VER == 2014 ]]; then function build_xz { @@ -116,6 +117,45 @@ function build_harfbuzz { touch harfbuzz-stamp } +function build_libavif { + if [ -e libavif-stamp ]; then return; fi + + python3 -m pip install meson ninja + + if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then + build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 + fi + + # For rav1e + curl https://sh.rustup.rs -sSf | sh -s -- -y + . "$HOME/.cargo/env" + if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then + yum install -y perl + if [[ "$MB_ML_VER" == 2014 ]]; then + yum install -y perl-IPC-Cmd + fi + fi + + local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) + (cd $out_dir \ + && CMAKE_POLICY_VERSION_MINIMUM=3.5 cmake \ + -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ + -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + -DAVIF_CODEC_AOM=LOCAL \ + -DAVIF_CODEC_DAV1D=LOCAL \ + -DAVIF_CODEC_RAV1E=LOCAL \ + -DAVIF_CODEC_SVT=LOCAL \ + -DENABLE_NASM=ON \ + -DCMAKE_MODULE_PATH=/tmp/cmake/Modules \ + . \ + && make install) + touch libavif-stamp +} + function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then @@ -150,6 +190,7 @@ function build { build_tiff fi + build_libavif build_libpng build_lcms2 build_openjpeg diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 1fe6badae..2a8594f49 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -160,6 +160,11 @@ jobs: & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} shell: pwsh + - name: Update rust + if: matrix.cibw_arch == 'AMD64' + run: | + rustup update + - name: Build wheels run: | setlocal EnableDelayedExpansion diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 8ba40ba3f..582fc92c2 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,6 +1,7 @@ from __future__ import annotations import platform +import struct import sys from PIL import features @@ -9,7 +10,7 @@ from .helper import is_pypy def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} # tkinter is not available in cibuildwheel installed CPython on Windows try: @@ -19,6 +20,11 @@ def test_wheel_modules() -> None: except ImportError: expected_modules.remove("tkinter") + # libavif is not available on Windows for x86 and ARM64 architectures + if sys.platform == "win32": + if platform.machine() == "ARM64" or struct.calcsize("P") == 4: + expected_modules.remove("avif") + assert set(features.get_supported_modules()) == expected_modules diff --git a/Tests/images/avif/exif.avif b/Tests/images/avif/exif.avif new file mode 100644 index 0000000000000000000000000000000000000000..07964487f3cb9ae2a801dfaf63a01f0ab570cf09 GIT binary patch literal 16078 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 Z_~Q~NBps!GIgGP2wZlMPQgBD`{{tjqpyL1l literal 0 HcmV?d00001 diff --git a/Tests/images/avif/hopper-missing-pixi.avif b/Tests/images/avif/hopper-missing-pixi.avif new file mode 100644 index 0000000000000000000000000000000000000000..c793ade1c21da30de4937836db18887d311b739f GIT binary patch literal 5435 zcmXv}1y~eZuwA;lJ4Cu0SC9}XNs&fkSzyVP4(XEaZjh9vnA?zxU4l z?wpx9^PRaD001yrx_Cm(+#r^KXZ&M(h^2r%#LQAvMnLAdwQdh~G5e=IgWcM~&gp+s z000UBx%_|rkL@7l|8Ij2g}B)Nry)H@dWfAP=wI~~06=;^{~`bh3jiP$e9qY+5QqPE z|EEwt7hvRn!_PBjZu|mrj&_d!t<;A=ouJQsAOs3>d}dOJGZg%9@EQL9cX7?!1mpr3 zpDO@>1mtMvWu`GCl#@tJFWRwZ~BDdTH@5NKzuZ(;bs{-&Z zvM##SJWN>@y=nonity8Y7UZC_Z(k)-3SA_)$5C6#Yw=E}x0R!|KE{uASxjFY5c#5| z+F8o6x#${T)uyTxV5hO_pp!9BW(!h;zts4Ryj$fdr_r==&OH`Yx5AYP)^aWT z7LjdIT!a|!FA`~5*l%m+nddS=TuXmsl!j5Z&!Tq@(8o zn*nd2W`gK6w(#OG)&a)6=3KT+rd=Y@K3#?G@cjVoq54X3a}~-?_xe260kjagx5gPW z*O{?8=Pw(>2a~Mz@ur9KO%I13(-O{Rcvop!HwJ)(WuJ7jZ$Dvxazod2?P?YY-Hx7O zwl$d7w3&ZYUmG^PhVPdu; znN)39p0Ekm-#iUkaLcq(0TY+a|JIsdsGAgBb6sPYx<#~)d3X*ISVLjZq)wedQ&+*% zizDk?2H`#%iN)}{C1pSO{em!~uPxp=GYGwR_oySe>^q8a>^a6epywV`k1#J#RLQFX z6!ulSOzRavz5V6KyLEAUFxo~fSn-lk5;lF*vC<%T3=i7~OE^vZCt zBMl-A5 z5doLyRc3n?7xm%`I#J)P{jEdgiQ6%0DV$Eo-`-Br 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 Tojj2C+Idjr^nh=P!vB8&%2-KI literal 0 HcmV?d00001 diff --git a/Tests/images/avif/hopper.avif b/Tests/images/avif/hopper.avif new file mode 100644 index 0000000000000000000000000000000000000000..87e4394f0596e22b50895bef01121f676d731ed1 GIT binary patch literal 3077 zcmXw02{;q*8{RbJjYLwW-kIARpiTM319_cqs4(q~A& zQC=ALBaZ=DU(bNx|E2%{8tH-ge}2RP$eaIf!;MB_Q2#t2BZ?pcf;^61h5!JNagHJY zCd?0=3B^5y&Iq*zx1X8N}%xreu%+;5?GR9zg-YfgS-7jLuvDmUxDA5{X8+A9)Ng z76Sp9a3IE3e^8$87(M_yn;`Vvp>ONCg2JwW7AHdFki?K-&~cc+_$;IbG93rlIS|xKc3inyZx#1C zrcfGZZsLuJ%9?fh@r?!g&VRY$c7&F*0B3ebTmHzp33H*SL80&LsPCsM_fD!MrM^ zRA01jcaN!=(<6u8S%v;Lof9D6k|AbZlN?dkviIkKshja~B+34qoMfBT*{Gtg7Z#Yb zUzd9vewG{YSgLfx6NzY}gA5YG6AbY*sX}!Ik3!QVLYg{!1k}!>f3VWsM86{*=IoOQDzS&S-dac9% zGQ-Jb@6(1SiawSZBj-O;cRed**D>2}A4gv=W^%HrM?PfM=d9%Nb$Td(-S}Ga*`)avgE7 z1eF&Vdetw;#rOj|y;u5*SA`NcC1QS``z-`XE=tF&6sM&WH}@g(YLa5NG{3*g7+ZRm z4V8XA|1odp(tw5y%iliH~LidFb0 zh&!FnS57%>R+6;{{LA?_vjI`dWJ*!?BKir1zo?gw&s-<@8K)F9bBz}6^U>>zz7%`* zxQ=z;U4BrO=*bb@Qb=Q?wE6^)!7WB1V z0Mh!i@k8z2GE+W^#6){H9h6}AWUTihX}4GIjJ>MjSA5hLczbY-Wgk9ICo}m*TFX*n zs~KS;JhKl7KFGpF|Kar#i0GSemWYzrza}24HW;iC&i2Enp%fUPB@LzmdpXOU-2`pm zBB<5W%O6wK`WMnv^F`*pw*SHg?wa4P-juh?^{;9Cq{Ncle04a;pfF>u6+Wf1=?k>= zsXz)5;it7nJ`rv5$n=81H<}mDyN-V1Sb6TWW(RaF@*+o*SBHf{W#9IoJG*M11RaLy zwk}mXa&#P!I5VEDdVU{$a{(D&3EIvjwWT*`+w9&iPjR02xk^o3BWlUkE~Mi1$kj!o z={&+A=AHKEHDK$I#v$q#{YCsYYS%qe{YMRS0moC!RK(u|Ky{mZ)7lE-S0Yq-e5~(2 z*}$yOj>&=*er+KP@P<}`7>%1zdsX<5O0_U*W{b07Aa4TD9W16`ocN8s*_0-fj^L%S z!o5OIKk#FYywvSfK>^Oc>1bDd_L)2;Kgsv&r$So&jzjnU?;sb}^N?q3?E*tpe@*kj z=RnvF4d=!kMKS$FCEIhMhcKvwri6VIYqNwT>KzL}T)lf8E@1Ec=yQAJZ1s4)6>uwd zqA9{WKa@YXaaXour{O?px<98-knl9(L;NTvWb+V=c~MYQZL=?1!Yj$=%7Q0m9Xr8_ zBs*_A{ZS}?eNNg>ept+JT(3*5EaZ4v9M^N9fw_f}#XQq*?>PkCuFKF|ptPq4;vUZtn-zT**%alz2fjZb(U{)g@)`9zg+SQZ+2a2!0o+uC^N!v!7%03Jgef@h?W|9_I?T0cp%qH zOHcSrqMS=1>4%!xv|D2iM@pK!MfQUITse3fOsZ^Q_cmwwQAog*LwuASxiuHiX?1f@ zwy5KP*oTw6MK&V13VF@EtLmPm?$13e+VopWJTzt-tWI%>BxSeXQoctiGd@`Do)w2! zKFjT2@b%>3d%776h(>;hD{)1F0MH(*{HgX$g=8!(vf1M{9sOA62GEtC&rHVXq~_3Q z-!h9wjHL*>OKV<&y!CU|QX`T-`Ss9eMDomuCXPtza}ECQsz1b5Ht-&MNlv$>!sCia zm&I)_8w1d2H8o{nf4I+qYjo6*$7G00{)I(zbru4dKT}fje*Udc?EAZ&(o9!cBOe#P zEcSbK_9xcfq_NM&hnjNl7MT-=X#n*3U!tWx=-=aEay={8G3uLyH`w~=$IV2XCn)a( zQ}%exttd&|wQd1=xGaugc)8;OB{tfizKiIW9ZmSFw>frI3Uj-7u@}p-VO1lAFDAdN<;u zLVl}JdPb`0kv(&z^ECQbww7FUuG8dsLVz@>+Hx4n7;$ghlN5O6c!q%c4?B(LkxR?e2I2yeU$vZ0|AW9dDsR;yJ zxz8Etw)l>U84hYlfo>|?OpNBAwrvwJ(%pX9#quE{%yEYpImpa0cWoov-;j7$$h$Ah zMED!+5OrG&;I%|x2N}1dsE}^p=4|_ZzC&czTV7bd6xJ(9)GbIiO%!Xv3AVOmYW9n& zs6VQ1WA1k_6Fzx;R7YQ0#bN{&vADRE)HNTp-0AyM<|d9**iE1zM!HL+YcRT-kAc#yC^--$MkpXH z2uMmO{Nsz~`QLlbdCz&zx%WQz004lyw(j1}_BKc>01*yITj9&t0cl|i004CyY}_p_ zbt2g8t8zruUIZ zN8%%w1)|Xr6C09$GkXs!;mdFg>FnxE1u4{&Rc`08oR# z#A_H}Knmc+#bpBskZ|dUo27*W#zri1DFFXI`hS(Yn_#e^V^LDZYFE%@>)$R6LTn~e z1io{vfxrL&z-=ohw5y{P+KVVt05TM3UJ4*`a&jWkBb^=o zn;mfgSAt1ESBO7%cd)h~j*h~f3y8ZN6#No@pH(sO+rvBUUgWoy>dm_N4pQ?nRGRYU z8B+%8{%X?o@1-pz81u6o#*=1fUw?Gv{%m5~?|U8le}YKeNW0>P+5;;nLI=(Q-ILG4 z;;xQrIXT9rX`~%YoqLS`PEfs4!hYg1e3wD%moUhAO`fG9~7+7CuvV6qhsH=6} z013Y*l$y%9pMX~%E)dHZN-FdARQ9#tQ>3SzYBWz>cY5^PuFlTP-$wsu~Djc9N>ecqV2SLo6S>y#pL@#ZlJ`%#?u_s^1M zF%L~r{e5B{;|(21Iy2wpg;;W(=dTN)r8P>0{otMt2)!!|>29%Ug_E)cBEQh6H%&U^ zQ%)Tg4~tC+ODDGMzHZm%kB*x<(gPeKmlk%yB*#gB3AN7m(@oDpYX$vOt|@(~&W7qK zfpwvV;j%l+^$?OxE7YGEVJQ`v!5+E#Pv@vc+EOXeGO0NB!=AENx^z=AZi=`U!`z4= z9xdy#J7iRv4A0=HOWg8kg{QuI=ztV6>0!f&a{9%`7v)h$(+_``^s0$g6^`bnxX3ev zoVWBe>0T`{=g>`w4iWX9i3xtmCv10KIsgXB!*89e^&)CVJF_T2-6DS;nI|%|wCH6U zWJw=gmHs|)a9#0D%lQk06V6zUl_5Z(R!=Z3Sw`nGeDim2sD=V6XN>M^JjAJtIZxoo zOXfK&v-teQ43B>VHfDyGl!covVImZ+sG(5L5k!$&Qa=)W-U=bVHWuYz#qHKG&pV>r z*`1tOux|2tWZpR0B{Du%z2++|mL`EW{fnxM=Ib48D$>%2qqNO`j6*SZ|`^kS#HJ-RKEW7eO=fv0OGfL5iVi=?d7vhvOK;ODoUDM=BpuJ;xd__rk zPFF7xDBCXhNi{mv9if760_GE=_E0z+S||ZbT4@ zy3}*`7+&RHAJ?ueFFzI3WwuRzMpAXLXYI*Jy?s$MD`)tDDxy~OqWIMT4S`ZFhjYk8 z9*2DK^IX+nHp3+#ZPk-6DlsRYeac)bzV%LMbZaHzqcPJ&-b>~^2kbYanTIOMs$=ap zesrI}B}9wVR=lI8zBO~_f{q=Gw`OSq@b6u<74Xrg>pUbIHyNX3dLE^%tbB3~+Q3}@ zxQr9AB0!iA*_rI)Za>jtd;ZxN5)ywl~Vyb2qK|gkUa9un$bg^3}=arm}6@ob3H*(@mSx zR+svOb?a}gtUCHK!&nQgt-+yVJtjMTzXCvG=Z&jQ@6WBX9~Isoo4x+v5X@BV47jeu z4-ptNud>y%4H$S@b-b#qZ!>xY=(1*r>yLxOs_@Eu1B&a8cXJsUgZ3sy%LwIc;xC2u zXD3Vg8vGqM(O2epg_^E@;IOa@APk+l`5XUePW7%VMa0(nS88%Vu5YA?MNkgPXZA9i zF08-;dBq7rWoqpH(PS4tMGwKPd(@i4`tDvS?5sIuN8OzVKZ=xJ<#o<+*X_vd zpyJpwPfj>0$vgAZ=wVrH1A%kgFY&eDhwVQUjD^zT?NoN{m*;o{E8iLPZ&QvPfJ2om?3Z9-&>>T_b-)uW}p4?7CYj3o~?Ge+-I?;cRAd+ub_L z`5BxDovEX$H8!YudsR%l1$AH-hVQcc8dkeicn)px99ECuyU89bya_XWTGs)zI|^4rCL9uabmay+SA@sM>Rm0;{YW=KHSwwcGuH zg(VxPjLi@)6_*xtG2DIp)_;S8e9!s3!x;LK!-`ey{pGe&NM}s*a9WQhJNY;R|{8nTrfn$+v&aXI@`J zHh(SQadQ@_!+}OwLZKYZ?LhJ3{Ey~m_7Qm1csj)nx&|ngbGk8@=OkM*>eCiq^ta>I zK%Vm?v((M!lD7QvYW7V{#RawvGHu@?C!+N?wz8(zBl7ElsYq0WSLxqyQyNmn$6^eA zl*z$_^J%}yzUmm;mGno|b@3!+9*^7$HPFul2AxrcxL;)N;7^OShm`5ESnn(zAhg!1 z6dx6ORf7Sd;uoeOMevh8ozwKn+s&3;=QFwrHSd$i_xfcW2_B3QQ1lFnVo-Jw)O?t z_F^;C&5)fP67d3!1unTVtM>%~iHJ%MOj^EQY_2imijEKbWJ=Ps07f_a)I(Q~GyecW)m1p!_ zYR&1HJl0}u=M$xChX`aph>X&kR4cjqby|rL0;`nb$WcGqm%dZZ zcwUVaTjES0Ym3LErr8>5N159v~04JlD6`@_>PWnLxs+ zHJ2Nhx*z`)i@6Xo4Z}ac3w#%I{dx3x;75H!Y0~R8Kj{0Mn-d8oFYnz5cOwZ4L`pnd z8%*sf3he&miWEWxR7;2fdJeO%%`s}0h9f3$fpK=^r$P_fZ3ad@(??~de@NX8Q?~3C zvTYzKw&xQZFHQytPi8_MOq)fvRLK58Iex+}iG@?pT2Hd-fK(<}I;Cp)K9Z-MDF}3! ey3`G4a>V#pn^&w7}i~P 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 KMNUMnLSTYt*17Qj literal 0 HcmV?d00001 diff --git a/Tests/images/avif/icc_profile.avif b/Tests/images/avif/icc_profile.avif new file mode 100644 index 0000000000000000000000000000000000000000..658cfec176e8576bfd28711d879c2cfff4564915 GIT binary patch literal 6460 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 HF5mwF+?@?J literal 0 HcmV?d00001 diff --git a/Tests/images/avif/icc_profile_none.avif b/Tests/images/avif/icc_profile_none.avif new file mode 100644 index 0000000000000000000000000000000000000000..c73e70a3a525c68a16f5c2bbc075b69ab2704cd5 GIT binary patch literal 3303 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 PMJg=(O0BBIq_F-6#F!Ng literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot0mir0.avif b/Tests/images/avif/rot0mir0.avif new file mode 100644 index 0000000000000000000000000000000000000000..f57203093656d8c0bf5caaa95928f2447e9e8992 GIT binary patch literal 16357 zcmXwgV~}V))9u){ZR?C}+qP}nwr$&+4;W?yO$b$)8j@oooOA00L8I z4|@Yw3sZo9@}IV`FlDr{Fff%7U=;iZec6~e8~o?{Cluz!){g(*1OTwNFmnDs`#)`M zVfcSx;Os4&ZT{y2{MX`HSlb!>*AoT+0Q~p;*8%{N008g~{Ie-6ENuTb_WwLs|2if> z|B?T33|tu)h3u^D{x_tvg}tNwKU~?u-pKBsMzL_RH~EhU0089wk6!i9K(M!PxA+eM zhJb+hr(g_R8HEA>Vg7@VjqI!)ZH=tm|3!uc0Q3I`L0Z^Y{4X|!g}sfzf24o%Z-$_N zKtUiO|K>MtV{G7z1ONqvVM_k3L;}Ksg}_wrgJFvxuO5ovPyrbO0B}Z-G!FO@Xot!{ zlM|#A7`-C}NYw9c3tkL+IynC$7jv>S%3GnO4U4A={^e(xgJ|4BWBI8L@6RF#DxH!LbZYXw)yO)z{GnNKagZ>LUxAcGdC+>&l>03*Dbd}?bqrSb_NE46-&LUxE2b-%%>1a^1xic<7tk`qTA72EZV#u?-ez11_7K9 z&7Mu-el$1l>B(FgUt7vY+7ThT-hcjL2&L*tE_ zI8N}|&RcX)41~83O0;)LDjW!4nVBTJeuo{w4+@1=b;u<nHu_58*8k z&W&+7LamGLbs>PM)XgNSln?y(+h%>0e8Z7}xs=1w!WTYQu6MTSY#Fz6^JXCPO5f(L zS5jIW8wVc8_eNG6q|a~>eAlN_AwA`cJ{eHa-w=tqklnAAu1Jn6cJRgMD5-e@Y}`)5 zEXB0my{pC~vi$7`-_F5-zx$Q@a7#Kig)Z za90U+P)>mQZ{zkIj=y9J*TUboSe!8ZjXXg z$6u`*MGB5UnZ7W+zWm(ir+2s5*cQ>n|$Tnwd z*OFWgfoe5BGHpc>PE2y0gf~_u^rChM`Sk7fuA}1;M1$ib2Ku z$2c>csx7T($N<-8_ZooE!pm3qK|O59M()>d-?s)ds_Nv<7PF13?30d z%!h?HUqCgz!i*9gaXwTy)1ZG6=1SZK!JpWxwh-vnKP0YC61|}B)kO0oJ3JsysbX$- z?#ZIe(Llyb$lRMKzMP z+|2z_MTn?aW5KFU-`5U&F%@TJWw;mHtE{j(Z1W_+?a`OJ2~{NkE&*; zX#_PjTsWDzTA|m3z;RSn3#vN)WGj4&3y{go2S_GJc6YOx(XN43H8~dYaPdHkxzvui$XK^O&{t3Ve~O$P6I>jZ-7|T#UCUR({i|*W@ARH zC@SM)(k^QrF@0v3`*t;g93sEGXrUa-%)$>+3dVC3L>CdVx5rS?&Zt* ziLgUx#SAA&Nh%QUVQP4}V3*l7}6B2^_*f9htDE`n=MA(ffHN8L!BiW}{M%?^yE^>92|9fsncvJ>P9i&OpD zmsE8whIERzi6f{v1%iI<1#pc7WOZ~{yj6tQ3)|~F&?K|W6?UFVh*6xA7=1dw;U&8z z!T-kS!JvuLX^(ON=T0e`KT!>3)zQ|UF|(;IsP~W;jSqnGjBRy*xF^sEBi4*nYxmwP zinkA>_u+Pn{&(-P#NGGW3ckq4!WQ3TC8nr?kzC;a?bU z%IPF^&+`XOY5bhav#^JrN3PsYsyQ1y3$XGnB0b<&;@hoVgiB&%K zzLx1p7H?ysG)Afbw7m!bjn#H3lV}p(0d7^C(PrrQ2%VrnsmAuq>QQ6^_c~`wea4=c zem|ib>f&T;R^ISkJwg3SGf-gcyI@OZDvLMxp~H}?e*|Su+v+2ta^n1BV=wBjMyq%s zSl}W>;zPTH@Kjwc2Nt(-pRZ-BNiiiJ*hxoy>7rp?GK3m#WI}3 zv`=F(CS!v)VcmXCcOxL%aV-t{3R2Mob{n8gXK5C>d-CSrr)lcYK9&{=n6jyU<#76| zIBrHss+ZHvs;E8%;A7n`RypE{VAPZZASbfMtI>5)1!_4(#HK_OmaQ+ikMN_4V^ag; zqdph_@sNarF-}%~(!Sfu-D7in>O$PcB|aT^PL_&b@toXX4QcPwl|9;5@$XUPa&2m$0x2e4dJ|hs53XzoVF;uq^6M zs4J#6_|H=qp^I#kqT>%+&04BEhcZSvl{D86Nx0>l0?Vb|d4gN)x&jOiP5COur6cCb z;&pKo@0p;z*QmT|+TY7)){h2qkn#M0%KJm5`le>}*#UoZ>oJ8QH?am_xA!}^bH+Oz zub*$tpm39PCS7?wFvZ?k%v#Bhq-{<~U*c)a;{kK`0&keF+j;jsB)UtxheE+Pbx&E8 zCA<%^E3BCOFTJ(J8O*{m9QBg;{7{`Rx#{OD5hHFSf%!F-DC^QkM!BOlV1KW*7Mxcg z8@2x)S$6ZB61&eQ6!;Tb4QFPQZ(wg`K+LFkH3C^?ypjXUp(~o@$4yq&-dQX|yMkd= zN@eWT^_55F#C9gj_16<@|INTo7!Sw}XqZ-PCcF{EJN(@vWh zzP{%QS)-uDCdVja8F~Jq5xU4OA~1(Id4s?lQrB$U;R1vEVkF0sK+jGt3-vsJ=M>?# zo{%!Lckdsk>i%OjP6&MGbceL9FTJDfSvil3y2Z}ze3LSJ@MSQshfrDGzqPIvBiJ@9 zLcwjPYJWi`#f}{aExaTfI6|z{7r|a?&jL*f+>Gd5NOBxN6h9W6SkwfcTj7tl-?1Jm z%@YcqmbxV#_GpFqi-D)#UgT6=subU4Ror0g(~0`N2EgUr2Hq$`oYtzf$W=xzY)4!0 zcaFv7m;i$Q$SA^k0t%c9P=gf)(?3ELN@k8yKi1HaC*y;X7rHr#i|6KY?rFELU z7&bUjDSil;Wb2SwA$VCly#*Ik`vEF}4 zVrgF+(I{(c{d5o8-z-7eK3BPkO%d=mkm}tgT9Hlwjw|Q0Qvhw1v7(COQ?WSqCI?%5 zL4REvEKzq<5LSBWOc(WiOEbP%aTP@YLTiB#x=vX~s1#`ejf#x1}3j z?|VcU4p@~ZZ}05u152ZfMdXB_KIX%Yxrs0ND@aD_;pKZ!l^Q{#OR-mbkQ0>e_3n&! zLoHLCO^D41i=G)@Qg^y7I(e|h0Z_JOfrw*#8Tsb*A;S&3gSPDrjfyNeHlX(suQJt7zFX@Yo! z^|b^Kmcb4qh0%X$m zl?vZ`mHw_KP)x7LS2@K8qospKq^Q!TMPR&(96aOx{k664yF8 zfu!pe;pXP$ujAH~FGc#Z;t_rsUv*gi(YsoiNW5F1c`?c^(|INpxa*b#TdXZ116iUt z18ZmRmU+=cj@c9d6bs+a7D>y8h2iXt8Zm$MZi}htx1BeF1DCxWAePC^mHN7hH#Rrp z|B{aVrL%-jMU~`z{1*#E6^HkeTHPYqYf(?F$Sqyvbaum>u1h01lEjwYVMpCD>0HUd zVtJ1?bmKrQ%+L$3h=NmN(n_foephprw{kK8`~@tgY3!AAYz2dJ`pCu=lN!MxMjR62 z(Fq9QvBXIGN0`

^8J(Gy(!}!{bPRhFLa|Ic7}c+<^efXti4%P|EOYm2_<`adJ-v`_*)$t*;!Q%o*(@wf%^TmOO9#8c_nKA`zN4xW@LyLkE8M#iTP@0 z-*cTXI{ncZ^gbbL3${#(;izoV=?bNqFG!or1`rEBMVKb4K_#$V0IWFsBH!S|4t7dBDx;ni&I$Arw!+WP z-R1jE2Y-BAM6N%jotDd8T^!pY-ciT%ao+KQ-!z&w9ZB=2%>pJWWPxs$W%Y}4#EW?B zd6M(!lC?Y;_*7cW{(naFYlD@9;4&`I*ZHdHi7!u(oZQuHXsdFnU?>by$y4i|zUi=K z^w;?jWy8}ITpx0#3YH@(csH+A&%`Yw5P?A}n^cVg+Rw=T*acdFLcxJe z^|{>?e|NJ0kqtz({5z1;UY>hHL*p*n1r7+LiGO(UkD7qs=p+*2nPA~TCK~vRTik~% z8fW!u`p4#11xY>=bD9!iqrCtp!H5aEi)R=azDV8~p}YzEvz38qBYccDbeP+~y>K?( zZ8d$e&qhvl8US#-%y5TiAVLlqqu=;pC7yYvpI-BxIEqD@t*!j6Qv)*C0g{0%{b2m| zA1iBcJ49DheB+7ruho?=$}@>6xWpQ)BOUR_!6Fz!yF9Pk^rBcD-Q0UW%Q2+ZmocKc z#3f)873+wJXR|G+smv`=W74wKms-$m--1>St(j3-p2P_9XUJq?;c)$$UC_!b zhHUON()=?enM4fZk-*(5>)ckJIUblIY?du$3AXnYBa}nD|(oEcI z(K;eUHT2CAFI{hrieLnC`rtvXpxEY$jZ!NcQ{uP2EGQKE?hU z=}LeGvL?h>fX!y-EoQ9}GSF)gzn6Z?{na?uhkR+RWx4Z+z- z2dEXfJ+m8x7)clN(e4q8O69@95UZosAn&)f5)+#~*)!+qZGN!#ovQ?R!AoTcOba@hDR6xy!&eKe+=@=frW z6IUM^{IT3t;EDHkzbkAAyqK+{HUz!37ao{mO9ywHkxmdVH&zVfx&cg*>T~a1h{N+) z3N!p&f18Hriq?L`@RxiFRtvzzuvUU}65w0M|08dOD4kA^PLW=aI9bS&QFRWrD?ub!HwV&T{zy*J|G~crBVl3-5}C z5{l8b_E|aPYioJirRnj`OB%P@0Owls8j zHmVpQlKA3IhQWsgiF?RqYcAf*Y%azr*^Mv?E$@>F);LW9hdv2}|A2qAYYmSc6A z73=;d<9%b_UBQCgegv406BAptm&&APu5(Bq$j1}GswMJn%88DVdlJ=26THe1pqrv2 zn&c6%4N)A;5i8H=>NTgqgD1s;A&{t@Nk?zNB(Dt226A-jZrxAC~=v?-8C{oAvr zn0Rl^mqYh39&BeQvggJbny9I4u*3?fTV&p>W>ox}gy80AvS?C3{8q29p)ByuGnC}R zRizHwaBNl~{L@DI79QMB);yRka?aLFluunu{Dg$VKViBX5Lh1&P1G;4NFRn)^jZ=8NXX1?tgZQH?cv zp;=OCwef736?%M09*@(v97?OHrKO)feKj$#aC%Feiacenlo9*oy|=1E!Lq{KXZ1R3 zLG1{znbFO3+Cz)h=cC9V1rTzz86SlLaG%N>hU$BXwyAGuw#C_=;J>}sX z-^l{nnA`#9OMueT^eO|nA4je0@RIr|O;t{G=(F`|2)R|$UmZbR@j^1qS;Uyb$*t?k zKK?^aNQHCY?kNJ_q#-QW(JHGWtzOLG^cgFPjN9+Q(;xN~|IdP9NQY8E9pDmo^=`fI z?HXY1#QaxM*hqjS11?+=dP%j+nT`z%#a&z9x2ew^tfN~I*^srP^mZiu#iOvZY+;8S z>HJWcDyYp&es*`^^;4-grnSpbje}4se_rt~H+?~MM4O5@lVOmf8gpwOD*3D~2)RpQ zYPn-cP&ss&ngOrGMo)hZjmt+9QP{hMat>W;yqtCT6%~w(*|Nb9cOCb;Y;uhjP zKqCY0Ep{Hs*Y@gB+LexzLNhc4WP;H*Ta3ZOwSHpw{rnCB7-vvZ@sLr0>&Zp7#5#ZXSsM5tt)<_SYvoZ8Ub?=;SmI`CXyB zrU^H9>(^G$UNy_dHnD@_{StG7oq7?o9`?>q~;ty1U zgR*YF`#q4ST|vNt5i~-sqF)Yvh1HMlHvv0~cpi=}o@{D>7@ubXlAOa_gwKqi*82z= zOMGWQ)@L7foTsZiSU^JqfREVXgx(0F{djszO8(){q1aAr1Aoxw0vQq6RWQ?#_Ac(f zShYFJQJ^N5%Q6`%561U(nFEh=crtS+v!~UW&VOR4>O}Io7Vs*pJGze!aSD_AA~UM; zVMhSFcSwU1)ch*X;d+G`d;zY?%H8P-IGTr++3_0So2MrAIzv-weK4)8VHp{I;HT)I zznb?ii+$%<1*$Wg)!^szwsUJt}ZuT5G7mKfcTN19T zJ&qFD8h5&45%bqcRc#<)MYYvBF}=8jq=J^OJ-Q_>zAIxRKeJGo=93w`bX##7l39V& z*^!M(p3O%i9Dpyb!^A-UK5{z*xKb!th~vAAzf7-6MD!Do#hv30mrC-#MOEh}Bq^35 z4JxH2hVy-J23O$6%H`2DG8lX8l&y2%1cBE}ggcU5+&+56*H`PQo)MRE&G=E{T23?; zJr2!?%H%(QR($OEZo|}b8?PKPv z>ClgaWj9sFTVCXe996xZ$uC$OE?=EWL7VFVTpm1n<*9I)he&KYgz%cUiCD9_y57kI z#-*SY-9=IhAkY`^4Wil51k4c15d|_FM0*naz!Z028XOTZfg$pD3HxbNgeqxXovk5d zP<0xVtSuz#7PTkZ>~{gG%Nx5g=24( zl1)Jw?H1c9X;VX6tle0<<__75I%CNrsRzjJo2-YQX~S#?0gYjmX@b(9uOq2@LXCRq0JxhR zN9!O$;J)U-mS1E7X**$;b9->quyaEQLa8kPl&kjZrg9PxJq}>Y^j2t_o z>gl&0Fl0OsW0)e^MPmSjk*OC_&0P7(vIB~0bc$Ehx|w1&ME6I<=Sj3cuqvn51icxg zrItyX3bK!<(VWC^)w}mLZo~8(X5mE<`nsWoPjj}ZrYXhS!cFGK7sU+>ZcfnALtQFL zbV3RRq7SdshbJ^AfUaDLGU&QLyYoFae?R7`1xi7-(|O?>vlCh(;wBZ$$b4Q$+fSj~WN{I_Ab zWvxB*a{1K^Sc^iv6q7+p!}}(gR-};oA*!zKzQzklK{BS1W zr;Ug7Vw5PYbC+_U|W)nB2ip4mfo+V9> zZn3$~aJ~}2IFx@9cW<|^V_Zq_1$JxGLmGz9PjhGqlJ4T2>@W80Q>@Q`jO;v9v}-cV z2TA2&vxS#WEDFN_Akq$18qD|}Ie0OK$Ts~Uu)vA%l(L;O-*K6NJu0dE1x!mevGmfE z;CA>Y_NgQl3+jEUQVp+vRov;zxm&GY(2BEZo?|V3k}&=GEFRxFT}mP&a$nqg@{_&x z@n>6LG~H1eb%F8VFf;sYAO_i>BC7!e^S8Iz-W_24}`RI_^S% z1s)cW9Qu_8EYb6!G|nVsM`9kq5GQ&XART%I^b`xl&I50PiOyN8ciL%<{Ii!{W}lz2 zr52o~C2^Gos?#4-GEHq1*r?zvN3%@ki#LYr5zu~HuDZVbn4foj%`BRt8fVCD3hP`E z3D7Gi%wQk-O!sn{<-y(Q)q5K$MQ7cNX0&Kt4p9%WahIV8Jck{gS(M(Jxm_~hIyX$s2W9l1>2AelAA!Z1$P9(kWsd<(OH4^ zyhZ5fz55eC&(5ybB~mhBISNtyt@ncuKew@R#e=sD0D}1p#2-;F{-qXdy@yzS&z$D8 zn}nOak5n)JqmLvLI#hgEZjkzoGOiZ;j>o1*M>JvgJ-=|hZvOK`h24u+Oc zCe()gh0i@aGGAOHr*&+0&ixn)Nh^@SE&axM*0b-w~TW_4iGfBczqK0Z|Lvp z@6x%v?pG&#MuqPW-+TOLvqmR?(sh#qCr0!dGxYSz4=hvcoILuhe7@o&OmA)E<~FCY-7T zgv54s?&(jGRI!V&1U1E-hvB4Q6=+N}Eed$Z%Maus6a??p*NwzKY?B zRbi@_DWhCp!mOp8uGA~EWSbCp*V@ryMHG*@AVF_vX97Kk;>r1LLod_G3=;$b+v;HV znwu_|Q0=i$6~ZUd8tgYroL+%G7%R*53QECSRyh;s#f-FkE(>URho5k-;rLKj<&1s* zE$K}+OYL##&D7)fuzqO3p6zs1%=C-(r#DWq!z~D?Ww(lndg(Yk1S=DFHT~G^?T}Br z+FI^4N7B8A=B*EP7pV4pXfc9bY*%@>7fdr&`!jxpy%N^=a1BUOyG}5 zpYmXU?Ov&&!4TM%;(>A^pdDa{l&)jcc|%holN(}9lI#q}CW}y3wrAKxC7^ht^{3LeiF%UuphB z{L|!P7|&6B<7zXUfKpKR?huW_v>0+2*ftHIZW~G&VJjGNnqz!WvzyE4yVHfMOttDj zHA?6=FviiXKb8eUia%*lWPRrxKVd207baSAxjzhl*9XmMKXqpnBHx*KL&hm@N4OQ| z8rpqBXe)zbXVQY0z>dlUQQ|P%c6;%_!?f z^|z}%x(5zt`_%RWRmQ^3r994LjHrFL9+S@P^4BoBWiEQ5Pg%AGPPmb(SSMg<##i-* zdFFh=7B0S1U0AM8heyW?3=XZI4CA5LoX?iI3m$I`maRJ3#Q|&vf#ynbj6{t$p3CO; zG!=5>n5!^3Nt$GBh51(p84(|BoLJ5{dZKERCXx><4wsfkMCvmfp!#z`nT8zvEY`g) z^lctgfGJc$PRKAR+Em}a^(%gCu0_-Y{}neFER%{mufS=6tI~xVi`;+Q2MpG5ws>?M zHY2R1`ynKqMqx>VX4>Futhe|FZJq=MRamkV|BmI%G|)huSzBk-Q^{cft*G!yl7{M~ z*NxU9wCQbNIFK^k11ae^Hbh$C_XLa4(yr3^zYjFp}p#-C%_8<$*cR~DXl7Zkr5^M0KM zY}?tZWj{ao%5pF=a)beq4YybN5s9npMAHp1IK34#8q=hsIu0u$K?89eO!&;qJ~r!t z+z^YGzFm<20h{%R)c!T6l)QyO6yhtaySSXA2g9*(N~%O13S5%!oZiX zU>8s;VZB^!=qv$38%X=4zc5v(MaOM9$!!*rh-{u3(!~~<*IlSUMmM6^K;NZ==VtSR z;2W{H#|<4?ON_Yx$0zLHmVBDWIoh0&BCv=b{=Dy18K3r^lbtz8Txb6wdSRwblQrS5 zYGG7>Xw6!;XhZQ~(LH`C$*S`5z~5RR!Fc3*y=^5<#|EF+40voiz+gDJ|@5>5u! zWXy)AZH0HbME~EpZG-D^8Jb(ms zzmk?$tWQmCz{d{8?iUPHT zXr~ye!=8#x7#n;x8{r~}0u)$Zc(Slus&)HYN)g*!DA^{xWkv)qK;Feg_HwIud)afg zV_MTpD`ez6B_jb4p%_-J7~^o{OQk;J^c2M$kevWy7ucB3GaPeq{XM~gWAgxyIfKhA zMUWa20b(Qj`ghof9S@vd;Lu|88hh~Np9jO6AmVH9^g+xOkFe>aRIhzoyNDwI2I68_ zPZ-$RpB#&74I{t(qQXWbyzIH6<>h$?bCG_2L3Li{-~e6n@Ps&(kOEqr%6UQ1PIhyl zL$n7gFFMF=hzXf^>{JXYTHp;k@5M+r~U zrIz!N#fDpGd_^f#M8)z&)3+F?ig&i&LB`)or4;J3Gjg-hFZ*Y*Fo`M2Jj}2pU_8(N5*73b0samMDs_aiv0rI z;f{M}bcR^`LpzHAlgjS+QN*B?O*ApvGmRMGnKjj`-pg8&4iTz#Xg6r4qf=wS@2Z<9 zS#wfYBRnRL=d^eVcn*t%nsmE#n=|mYK-V( zr^tY>`>Z+9puKSS3{;bVQm2c$yixx`p)H~_BZsJ3YlZJMTii*L)wc$v9pwZ+o-1+c zCj`qtFd@c4qew^E6IS)Qm4ner?dUJ#+X6$UW*8_f(J$W%3VYno`SFCT7e#b|MlB>g zCDpOq;CrF+-HV#u(0|7;9laVl!Wr3n_QaRZ!m1={ykJDyyEaFi8_lM@(B?>`;X&$F z_UzuPXX(bYPM1!Pr3d!7(svLhx5ORyU$BUfUU6+$@^GY9%y?Y|vxAB&~ zOt>>gQEmAU+~SkWnjdmuARn&&M#|8F{28*}(6&tXNLx`0qJOnUlkoA1-^fgI_*Aep zxNk`u6xofGD`##1^QIjv*c?g8I|>$qfDTj-xDz`AIKq!kebdwcXzNU1?QEGu^yn43s+oH6@}An{K)iH8v9_ zQCAK1m$x!`J_f8_^zg1<^dT(uiMo6o5u=;rISonM!kev&yiZRx;=>x_{V(A+@UFKj zL^<0*N9QVH=2I1aH+mGCK2_g6uwytL>ioCuCy*wyJaMXAVeUd9PV$OTQXo9rM=5$+@to42c|G&e~BG`qd1r{s56fYrK znhnhqdS8RJ-JenKp;Ok_`LzPSU2S1mE3_`_5bV&tg)w-Kkcj#8oCbu*w5XlungNLg zg%{DLEY5{ciOop4x|jx8Ce^-l%7Yt#_p9Ce+s(XL>*_b<9F#TfN{>8Pj^BQNOc@@{ zL*nh)0N-M03(kSGMqxeAmRF-Q6pDDBi#G0fljw)zXsCTi4Vg1sCmCHe5?4)Ie3`Y$ zoGPI8G{{$UMO>Y9ht>pw^n|K3V}E(f+P1&_XpcxV6UjR{k;q!%|sE= z(ic2>n5G5M@RHXbRlJEMLHw!rpBr`}lfF1jz0$~IZq*XG!k*j)rZ2mwz%tR&5yS%o#%;G|c5j!3B}^0 zW{Qr@Nu8vt4WpJ2sdmw9TpRBSHY3ZindsblaRo09;T|gWJMVnOZl{4)1Iv54Y#q4i z(S1`+!tQ&!kOXER&*Sbf;>TT4|LKe?cg3#<(4JwV>l4Xw3cy^+H-Iq60kPtfuLhx7 z!?n@~Pwk+x9HF+9A(Kk?&~#^ z^R!Sa%{r|)7=M9=&aBe0Yv1z5GA7`LxpU`Bh>KAAmc0!Dzwp^jxGyr>wC&sY0sq)6 za$Nmk$mRBiaxk9g1qN3U< zVi}wJFmVl&ln@lU`gs_n-eXHfmD)=6kP5x7I9?sXK@suP5+f-d2j0efqU~umQZ2wW z_O+TGaFnvA`Xl-V(l7_gna9w%&T!&d*LzT*TZ#;Fv#p9BVd4KLS+Oqz)xs-NA{8K$ zr_VE7m*YpQEpi~0d^NW`w)9Az`G{ed#h2Wt8zwVu`d)oZXoon)Z=|Xm;PfPjIz%1`Ft(?y#}iJc=ZT++j(jUOyw!p)3S+D0Bo zI!ujDdaIrU+NV1mB_b^Ft_)3$1AxZ0y-z-n)*j77RjS*77C0;V359l%v*%xKbD0Hy z2CYHs^C^Wms8TOWt>%e#lqSlDf?o|U&pGMy@}~zS;KSle$lScQOhd;_J7(90c)CFd zyU!4cXgZ%EU-7K?1jx-3e+evVO^=VOJ+P0CUR=fSK=Mvul?*BO;_pVmjnJ^}Tu*Bu zokt5kKX5bX7`vWe7exfWyI_<-fWHgE%XxiPgs<1Bc$Gfk1TohQR8KHN{jT#2GI5Y~ z;miqO)v27V$w;!J2x^u!L%duTJf~;#u9Wp$dV)q@KGi&S*&33;0gMerU>LNyv!v?c z8tdeOf8EKe#yo=hUqyEMTqa(#l*d_Nm6t(qTa^Ol@FLWh?YlF+N%7Ex&SdlHga=tVmrsIKSj$z zOV+8tbsQ%9xBD~>S6*0Z0AY|aXCn7gPesb$UQ@9`G$xV zM3<=3BX0|}tgc65h~M;LhQKUN_&W2zhwKYwaJ$5sG`V-m12%rrm7B6I1i95-vIOgW zxuxcl=aD<{EU~S{;B}1CPZtsq#cOW!uK@AvJg|H;1Mxa0 zTUCN%{cgX0BJuB;hcLQef8)PPn0U?|il~rM&O;9BA`#;~!h^AgFurb?4g8SD9zf*! zYP8+dSxZ|z(rrO0Ji<-1otbVBXn~c@NW0~1At#1SwgwD@wPSx(*$FVyWYhFLqai%t zbStF~@i2Ovf`geQ-B)@_{mLW1?T0W9Qt@k_dU)F!yQDUjZw!Z!z`EWXhNyq{x3?-5~wQS(V7z9lsm4OAF(DQUiEJ)5$}5thh1|I&b2m9w2#% zI8K@G=)n&W$$cW`pO2adrS2S5eOtx0$IE;-F`!Z!mF+;C`aGKuoz;2bjKhl0#dld- zs!1Q)-p5xX6gcm08>k35aeNxlpD&Olu5r+!fEKchw9M4NPDvmZa}xyCl|u@OX&6ln zS5sQ>5{s9NNg@MHLZqb|5`4YXqOFe5mvgGI1NW$>Cb;g2@1Ng~xKFQUj&=DC@TJpE zF3_?Va!3d>i>0urZ#vU~=Y8<(_V!nJq&dgdB9)$Q)v&lGzZkA=wJVr}aCdflKQnP{ zyGc(H`Vwam-^?Zn5p!L~8X6LX-&MPk7ba5uNA&X%ec~YK6r$l!i$I&Rd*S_z2_H?O zq7+O#v(s1d6v5Fyy?%w0%(+e)@kJmeSI#T+(>85(;hAEUiGK=mIhcN*M{S&DL~?cKC2+|O{yh|fHe{VF_Aw zG<>nfEB;7B)>&H!+z@jUXGpZ~^IQ{*+_UqaJhl3unx&<7VWR>CkwFK`ZcgYbNdd0h zxh?1$?7jaJzR2vHA`Pn%E_i+4SkUvwP6+#%R}a(OCCp^x^t1N>Rz2&i93uL4DdDr6 zm07XbNKTtN%E$<*jsa$oOJpUUV2h?|*;QALzPl6}>5R`q$d5u2=hclg_Inm2J1*2X zJhV4-esP1x5bxUKx;_o|2BbDVjtb|vuPe6OvToFeO)C?azer);@grw-A7OTy z6}5gB1l#+4;S(Nu|5SQ5=^&(M&cZBk;?o_MGSl^Rz072Cx$2YG+4s>*aaN&IEX`3Y zgXivLD?l1hBO#Qpco11ioj>pf+N|2JGyVRpDT5HEVhM|`PN?D<(B}lU1Hd~#_Y~Ke zg80l&Wwl~hnG6h4*j@*OG>>0=_0~!AQ1n5?v?)(BnK;Z ztN$2KC@83Z0B7XNBoY7&_aB970A!fetQ_o&{^5V} zFG6r&kRY&-fAO8$nHo8x0KvdunNxhJkb<#dBQehg0spc`Qq&AZ>IjXD0Rpnb-=nCv zhw8F!>q^?jA|_}YUx{l#3G~ZaEomPzQC}SM zd*6HiHLl8uIDYUhzn81C;Vl8?ZZE=v_Snx2YyPIPE75r)dP486nTc`h-83DwM?M2D zMZ=qCtbM+ncI|B+yWq_7)JPJ@(WPav8&czu<9=_g|3kmBOQ!D+p@{&_VHCi8ra0vv zp)vyHw;Dyl4rv(tD!oC*7dr)EiY%SeUEQoSZ$cXVQk}cOeUrAnZ}|<;_fVL5Q@bes zv~jUx0Qd0FZmIVe*@eiBAk7?mjG!nYMWdFhf1HDmST#nx<(8=NbpZB@`5{sO=eJed zP0HBBv-wcJt`PWvv&XB}|h^d6zYq zYKQ70UA^B8jgMRYW^OW7?e)18oz(jw1;8u>7D9?P@kVCfFRTuTzkKHHTQ$u z`DFMvqm|KAF9Q@4F3<*}ujKe-uaL~N+ZE_v*ZSMYR$#7FN~goT1I3hfu@#<4qn*cM zq%&;G-(d3rs_`vTqUYdfI4Iq7zo{Qb6P!C2dAu33lvB;VjDcQ5EF<09SxCK(p+04M zQrs5Fvk&Y$`8`dD5u8o@5#GvT+0R?LG8!~!6M|jzAx-gBDe@MHbhtp{emlU&QX-}` z0>t*T$;3l}XnksvGApapu)u?{P_dj09~#*xTGh$E9*+`)%b5JcPs#TrN+MYFobGfT zM4_uA4ngjDQauC7KyF@pV&WxqZ7HiXV_{;qu-|v0}i4{h*nCQm_W1*oSvLiUl2)UK*C;H~GUOxnL z7#(4!TkN(e&3fs;EHU2kCHq%n@k%o9Sb%=c_`s*!YoE3Q@PlzZ;=e{v@lCEoZX$;n3KenZbffDUn1n^cgCDW-U`C?h2;;CyC8C~xk z8D1Xb)mU!79YM!b123p(SI-sT7M&MY({04icC9f<&ht%;vl#A9M;i$SR!=YB)^vk` zMir=HvEAF|q^x{-sDF{tHv6^5?>)ydUujpFot9oslEEPuQ^Ua zRlo&P2kOcSN2GOP9B60a_DYs3e7@_HJzFt01%*txV7^%-X=5Z0VO1ts)?4M5Y^)|~ z#cXkNC=uI{Zw3AkzU>dFw0Wko;889%F~kCVAiQbzbHr z+ofi;l7^cAtB>rc$=a(k&R|Ho)*zbOM09AiNp?kCPHHiRz<-RvHc?1C`OP7K{Uif0-%g|DY(5e9^Ju@*~;$ih216s@=4&Sj;U~hLydZ4-?Q(; zG_@pq=Gr^B_2b&+tSmd!e2SyMXx?a<=i$xA8t4NYtm8+GHOD{}`gJyZPC_e9pCF}x z8=VSr3;e;GgoODhL_^TUROFh}fl1Z+RG}05lDe7luxLRM6@(9n*a^sBKr%ExB=59g&XQ~(8H z2y_`)h5abxIT+Ow2FQ;vprgIXTkK&)+&ftlbEs%{IT0}R@*;8{J^ZpFngrhj&v^A4 zpBEQ<0V4AJS{ z;3cF0gHw}-dTs-znI{KRY|XrFb6p2hQi)%q@@bWFVdRQ=%o|wOsXY-)iRwwn^fhF> zK^;NuW_>UnyGn7DW)(PKhWx{Z03fP3YlOS)Wz8WVtRq3eix@d=992qs305Z2%bfRQ zd(ZWV9O(#)qRo{Dvd?E02q`TH#z9i90M1#`^W=%MN4M+5a49%t^R}T;MGI?WAOdkS zkf#KSb^l?a=nYC#nw110gU!Id#iRQ}t4iIyDs5e}Ww9EwU&F^Ib6a&`C6TtJW+}Fa z4LvfREHBeR1I1rkh!! z+AXBodg94SoiehyoYJr)qSQl4E-wZ#$A2mLThiQ@7W|oh!A*83u{1HrO=g$8r8c8M zwDibqFWQg7+m@zzL9+FEpiF4N(LXk<>-Wqz0a1}HJ(BJiqPa{vf1{>vdLk$+cW$%K z5MQ}t6Gzxon^0LJZfIGYWgxazVN)IORDfy>+ur;&`7$FxD<9}>%>v4=uuE2V%0MDr zi>p{aj7BX#k$B=ToV$Gx@vy4U+a6h4NkOR}*M}OS9JjxKJjmk2@JQf3s>amoH%3A@bMV^`xG=A^WY6%w)U zLW?k3Wft91PFVWDrocAA=)Zr%?|4P_mK*Z0D3JY9;RkSYYGR^`_^;`bfX;ftQOt?# zz+!}11+CVyXcUxr{O0x_3`D3epf;x$?&eBuk}3A#m&!F6$#D>$b2kc54Ok9*catk_ z@l;Oe_C<_>!4U`R_IbtoHjuTwQN3nR1{*OfkbF#5nOzHG$|$lSheGSd@a^dP*snf> zv&()>IcdGxDCXt%mj#~My+o%@Xc8oIIXi{b15fqd{@ghUP*ZDUR(miCG79Z^TgkB1lXI zk3)TJ@iPP4g1r4Q9+?%bA)Ag(8IwyG5dTTZvfuk)V$&u> z!AE}P(lAbKAXlkN4tno0ziL-j*0s-n`8hj&sb*>{VbDMdwjgjkTwa7+it^U%o9s6`eU`fh?sj7)7K)cKF27@U9 ziJakG)oS&OT=8PnLxrTb*~1Tzr7wLhN)UNLC-`vFfXe0oNz#ofW`I|0;R#99Pp6IZ z%sX$}F2y7cuMe_0nd?@l9g8k^J0|?N+@M7+D|zdaCPP>P5lcZuP|s5wP9G5jbLg~ym#9wRk$ zcfXE3DO>E!DW}MowH4tS>XI&^NAPql=*z(kLcsvEaZzPz%?i$&5_ijX75SVg=*g>e zQf{Zi=3Z#QAgSv zmc^=rBGXy~RWnUuq#i=uuKJA{s6W3JsH2P3YK3(_*2rBvV;0yV#m7oVw^f+t)_LX~J-obeV>$ z&dvzsx6V}hR0qskk-wm2K^v@+ecQC?)^p1JSuoU~C@T#6L?(7s0Fe6}<aQ~`+Ln@yaS-zRu$2Hi-bfc<{TtP2&Vn8!NVVZj2zK=ADJ~G*!vzxwi22u1FsaPJ z;h!a|=76TZf)9cVy&1Oodh9l>Gd3yZ!Zac0y!9sHyAim7)c#=2z<7n}gMT&LYS#}J z38!oGf}609V$p^Q{&|E}tE zSblW_^|}{RG$fXb@8Hm!@#Ht2-w8@>5eg^*lFuwiQzCRZ8MiV9jE?z1I)JDTM*5I* zv{5qsK%p)o<~P#lRVe5ItXd1PtWU<`*j)((J6t*0!NdiGDm9>cuC-^UbI4RKhWWhs zR4!5{Wo_e?WE}WmsB*sYnoWkbIlIQ!ld%lSJbb+x2X4Yk09sfr z%5xvHgj%`U@eOYJY?F)rIpMR3j1T0}bEXtBcxq1lE48?>MU5Sxq2pDkdM6%lA7pYm z9HYxjEBMhU19n0Vv4VRD(`g*z!_XIgLb{a^8uQDrGst;RFeJ01T?U!$nbboKI;@Ab!81mwK&x$Yy}!!xFA4;gcSfBi*_0e_>l;sy0F zs0XG`et*M4-k&G2^#!TV=LK6a->6T_+bz9V0fwgM$jN#%(*Py9-h2Gn*!Y?^!)=kS zCvI_C58C_C+0dy+t^VGn;{EG359FcozUoZ+W@TlhsuM059-w z`;F0fz7>ugCu#wRar_X&wBh`bD#cPO3~1$M1k&z1+oP04I)YSC`Gr!dI7StWRNpTh z!Ht1yhM`az=F9;itYJm}P+Khe3ded(Ehs#W+3pa>3b;gU;05=By{#k1%?k?cC9Gxe zm-NYnCh$r0{I0kZI95xCvvx%Sf^4o7>I1y4Yu#^^zNlxHQxM;DieUn{$T;Nm;0RAS zKPiE1?xwm$?6LzVHl_2=X4chP^YacnQsn|e905B7z|e|}=ybthCA!h22S(sIx{#<_ z+Vh&qK%`>U*Sp5jj912L$9E{i{s@JLfYZM-@X0VryumQJ@8GcBL`PSJzCV%C;`wfl z^Tjm+CX5q{f)5e9BY>z%0`u!dJ|oU~Bh?SJ|NLn&x&FA5)%_z%;_Rn1avvVEzl(X| zM60L`ax~o~_QH+1d04l7Y(qd_EJvT%earPG3|k|`zIJiPs3YnT%6YprAfJN*8u%EX z{+*z=8wv6WnvZL;B2huWsU{&D4~u^zoDDQCYY5`1`dvH%+F?LaSjhiSdhkcx%vHHs5l^&J zxxcGm@&LNd?RQm^2_f2^om4?4xI5DifNCW;0VZk|TFo7Fg$hrC*lFOTo2|2Kp0lSX zNX;FkEtbVy5BU32>tsat`D19t_B(hu~e0$4T5BIrZGk9`wp6dPW&p zi`TnnrER~DfkqY%Y8@7`E~z22Jv0$IrATR_$-jm$2byR^%wJ-4*?|PCZT_IirWHP! zG+AybdtXy`nquVi;708@g=)7$Y!I6Km{yzFq8l+a;G^qzHefddUoRzTPOdRbs`MIf z{JJyCC%_<%_bc_bX1b+`uc$7{VqJT(m_s;KjFBZZaY-YI*>fdQRll7j1icl$hB!vO zbqZMepLM>1N0B>@(jC=G*Lj0!gpI3uq26$$Ww8P1uouyZC-z-sF6rP_&dRwt^{9Oq z-Jtl!Jh)-h3r3e$12A;l^C%6T+x-Ll$x=U!<&~&dt;qZBej0pS<38=}Zrm+#PsFRb zsoqAWbGvAh(7y2!ZI4!~5k(7fzi7cX5K73E#$(m0{7!V4#XmooQI_VKN6R!6Wec(lpT7znwHeJ} z%DU^LUxC4nE@UjDi6-y6;~2zakC1r@>+%3Uma0e<#@;0L{5d!onjN4$HYg9{Pi&Y! z_fWtj`$r4133t!3_r)2Rf9u%540FMr?>9%Vxh?kYjsiQ&UXPCA$GUv|!X?yZ5(!Yg zqVd$fjSwczOz8oj{zW^3v@e#(NT_)XhuQg$arq$xAVvpx-70Vugtm#gkhC@?HV$8* z&S2Q$R%$KleB#>cM(l<<5upbOo`fk{PN5`^dvK1i#;UkPQ$tj<)*Jc#6OHzJPisK0 zt_G@DO-7N3H0jEv6s22HXd6?wO#imk?r1Lt5^*8FJSgRwY)vl|?ZE#{MVqNTZ8?|_|ERh3Gc8KbjD5QQLv>-V-;h0BSr&K7X0^f%R z@kKHzB$cN}|M0%8&{9`dwdM}x{oN~`svSHI=fm8mH;sCnVTAY++XoT5%pn|f?|l#YX`uHh#{5(SklgU0mL!_avZ}UxC=A;^Io{a* z%netHi#mhPQDX6BG7rxmpxCrgNxzIoC0+rFtX0)A`O0wdadk97(e~=B$xad`b$pUB zGLzWa);@8ugf0WjV$Prb62yyFS56)FPlQFhd{F3smNr_mY5J&YlBmV z^ABxE(#VGWFW*^fHnhkKP-Xs!54L_ta)>3`&_4*jVW81o3Tx9h2H$yQ72O4DnaLH} z^~YWD(}sTfyWz5EC~tR$;|X$obfQeT-iLt#1mR@(un`z_t!IYfaa*2sNlctLrZa=g zuGJU@+LEs(Wf$Hp5TFh9nK)L42stg|-c{tB!I&HUob*id@dDaI>c5*1mRNGZIHV+( zKU=mpy6%oUT(6B)$?M=Fz^rNBwcEA9{Aedhf@srnONJg~L<|hko+?Ee*1EWxSi;$g z?y#-V<4AvnXOJ^s!mK@+{+_>PGc@qed$V4aaz=BURrFII6c-eORst~x$s`POPJ@R4 za@4*i|E(c)F9St+FB^3CI>L!}^jKE6O-PmwuF!>lcx3(9&dPrLt2I17jz1yh!K1a> zwaV+}OPb6?{ZRCKLr(GUaxZ9eQ|x3A_x@-IYP(7~roIMXJh*i}^Ffh33b%Gsm;eJJ zVPkVxc4yBXO=B_d0}yxADg2pxR3-OU((^!9KzzOu3r2|&ys~B5` zsT+kna5W2`@jVUKXyz%GsTWqGJu;?&XW}1e+EBXEw)*E-f+?5nD_B}k5_qNWbHMcw1ozrHIhR@sQ6b-ltup$nGf_y&Z%5cbb zFuOFo*y=GDui&#*rTJ#3 zws=_$M$&1@(%?m|;+r=!^qkl+!49^eR3@bo1204?0vydeZCrH9U0%P=|ZX9}#`L$%Zf$=wgl(Iarw| zs-vJspEA|AMN&ofk|qHxw4SN22F9;!D$cT=y9g$x^zEdn^9WbID8K6L5E`f3OP+f> z2R!mf7Hc-ysX8sEsC%dItrtaULriWPw)5wG&%BJ??CKDC7gruwnH}iKC?W zzIP5NHY5a|Uy-4&qbEi#Q)3@s#rE7QQ{(C!a{)ib$xsRCc>8!IB z8Sw6796hh!evcHp?_Kme5YWJqIF1zCv6JOzKgj_Eu_gt=)7Q!Z>X6mZr(X6&>Ml{v z{)+7?riY%ZW$J4^h{Zr66>^5;p&hG;gBn=a8+WA_m(Hye!FLDIOXZ^-k0i7u^G&PS z?_S_hKaQM_p=vMW_@s6jYotyNhJT3=y|ME>qZCjzL}NSQqZMt;mT-*~=nJ zP{aY_to|$+0aGS0wG*=3#&B0M`M3xh9fK3uL?&fr6se#FSVxaL)4c6(&ELGGLKW2# z$fwUP{myEXn>Bg<<}97=o}MF&K4*rFE%8Sw7mHvkP~NZ?E%;gjFqQe3xf&rryLuE~ngc{6CK z@d1I#7SdvGrmh2BgMq$0=aYFNl5|E;0wHw4iF@u$qg~DLhnm8@RR6Z}zM-!i=!xna zK^{f^FDc8;>Iz8_v7O+qZ`RBZ*kKg23I9~0m&Ic_OWHh6d(BO1a;Y87j^l(A9af&d zINe3l+6+n-xJ5Y6wueIH??v0?ciK&y0t3_!CJN*Qxm44=4L1Aa0XhNW2r|4VBAGP8 ziZZ_lwm_+yU8@YxgFrGY4*Y~N^vSjfW=54Gta?h6uFkr)Us0|Ub8^Qd+KlerTX_STwV1v5%+wI~sZqTYR$Ao?o(2Uk8D4<0mn`z9hx$?3(dQ%4yXCNvqh8MMholYvsJ*R{m@l`Wq(o!F+)pH>)W1k32R z_d{=G)Crye0Xu4?h%kkzYD3SLk0l2JukJH&y-`efnk1eDmoKPC`wr+vi`s6mn&z1P zgK_yB32C0ZA7k_?oB$T0@FY%QHvB|HWi~Qr$xl)1Jzj($+tqy=ljgbT$N-M>X_;)( z^v|oWeR>X|14WZD&O$r$A{ayO5^_qtFEJ$|BomTHW5xw(i&`=6!$$6jmYc=|%1=>z z?0sJDhf>wQ5r}bm5hiIs{!J85ji=+XRUpTE@P0Kx-TyOF3O=8sA0&bnw0C9U ztd+a_WR`}v;?i=qqBNl?C6x3{_d%QI7t`T`h}FB2c;4pJ9-6?9zMs3;etXl=mf})5 zmaY+GE#ze3Jl#AwOMF~Dg5q)s`|Fl&TTC;B;)(uoaTAwZB!xTbG4#RDe^yg^(n{cx z9M9W?FXbAf&A~a0s#9J~L~>$3z=o=g4D{t;_qO-63b9LI_2VOw_LF(729t_43C77| zW)$RJPfp1?nw&?JGu%EyjB}Nt!v}gG7T{0_@Wm$y-_cZ(kWMePx>xb?LR1hzo{Oza zsky@!UYJnDs|g*xF>a}}xFj+x`>1H91%W|Yuu$C|z~}0ls@+@p(635^768{!_jjX@ z5L^?um35?CL+Sg~px!sjx3lScAIg?Ebqlf}7s1r!mx8D*#8+^XD=1tj|)+_gtO{f-w|j!d0Hek9GEGZ6z4 zc`KT)oOD&v88L3NJo%6oMN(@w7SW?jcI>-0`zgJ>qFa@GT>_M|6cbm6SBJszaA=pB zezBmZ_H=epT@cbtSYg~f_Bsx@3yg!@6~U_c#6Zhb_`57VZyof>oS6luTbM#FL)Q}e zKp(}zqU-{r3rqX3fZBvYSlRIGF98sKL4Q0e%D89Ex9hBdX05i!k7aeq$*BvHuWRRN zWJE_(%Imfc@jl&B2NFF*p3|i&Jq`4>KwLsH>t;?H?<@45mylVPFON&rO1hl|Ui++6 z3V767BEir~CX(Gb-=>CtS%sk(vGJ3*Ts@aqvJb4>DC8=rSynr0d>ojY6jCc@uYz3e zU9t!i`~VM1jlJZj7)j@l5%_i1BgC?5w=V$WyLLbB@#axy21h-rb?*omHGov+6p{2D z&J}T~b~tTX+i4Ji@Gw-XajK8wnNNOd31*p1fne@P5`TOC(19pt(Ql+`12GKgk76RF zW`TE_InI=Iz##X{^zO-^1W}Xafp$&3g7A7=yz|}YIv}pmT-TI5?>#B|I0cC9{jSfQ zcsK?-bpTc5xJZ|{W1CR=y9jD{ zk}EQSZx@v9+kO&uK=R2(-s6pH=v&g(&}A8esUNlt_8s!yZnB;&I-c`KjC=@QmYURG zqgKN!+}PTSHMEY4e>CUkNr*{)5)U%O`p|%E#d<2?HGxX(;Ki=^i&Uz&38^WQY;3e{cRA~Ln#jzW#kT=ql(Lg zWrOfDtj_x+qEq+-AhVOPxNLaeJNw**r8egZn{n18%eVi6<{q$Q(-Hz_aI+R*&Q~x}RNwH=rWyP8+gUOg&J<8XQLbM#U3y)u z)!4+mPAg{YHTnYrIyIfm%2=y$BMBlJttyz@zrsJDSF0Ll>(|szWXSm&6}$|7JwZLo zX_+r|>SHj!o_ZiuucAOIFb^hpxI8)`yy6sQ&Qqbop+^sca7JyEdB$HLvlp#u58@slJmxCx4apx9`CFlAUXqr7jsSP!3s zd?b8XeHqK`Vpd&m+Ns%MX`_;u6XYO>WU!FblSp9twN$pmlHbYx3?Z@Gd=9*1t8}|` zX3jQ+KP-jC2S4MvlF?Ddfnh*IgzMb8z;*EWf;v{ ztACs3R4U_E_$Iu%`~m?5n}ZU6eqyk~MaQ>UW~U9pf}7=WI1k1^z}%}cgA+e66fT0^ zM=W>IV`VfCf7|XJrESeLgWmHW<~@GS}8^vIkl|F7J0Ien~)uhNk4gbgUbil2g)w& z7`sEd@KrdZ7POKz{nSoL+G3EfbH0jT-v&L-+9U{aO~%8aHC=OXhn z8kOm8UK}-@Dn^>1#h*I%ltu{f%2rZQ5bvr6=bDHQW;7z*spaG!2j&&bsrrWogr-sW z1v%8UuFYhXlgm9`G{qiS1kpP{x*h3!b~!!WhJyI|Tm%rpclZBw@SIdHtyv}i z5DMYl2aVno+j(GcOk!%$g_7%gemfpy9%!hB9-sm+6t&!&h}VX2WAU*i3!Qlyw?Mat z8(YGS&^ml2Nd%8sM>jD&)>_kPj3=GOEb7{a+(l1ICOd1s<21R6E@ARg2q>dR%L6SO z$c2k;6t+D25yX%lYJ8V|7q^y2j#)8uNjYmv2fcRVOpJIWL1*4+lJF|i4)-os-DR7d zf*Cqknn;6l*d1fgt-hI<*Q@Ge#u$)0aisX2G7i|v7z@x4QUOffG36`ZXnbVl!H3H= zcdiSC^{;^9tZ^O%*&_!_!)kC>@#5jKi=q_U{F5}`dT^E^p^j`0r zZBUYDeo^!xb{f%=x{ZICT&?%J^~Tt*+2A@DYxDwy*j4_Vo;Q;NvYQI1Ez~M%eyF+IKJM&Nwh+C?3rMcSG9h3JT*RhkD5c5jV)O>XK^ zpmewN0z1v~>Ho%Q)xXR3h`=+jrh|T_y{5*ffplhYcFl^G&sU;@uBr7X-GPXT3P}xp z^bxjG@UwKJl#%Y*n1p?LGGBJeW2WXm?g-?N_{E|jG&tBYifT-ufS0xuO0%vcvJ0uO zbJN+3%b{R~bu6{9 zPVmlHH<1fir|BPoR~d37H(|m&XO@0;lBg40G6Q>&B4!m*NlVRjYm*`jpAltL^m>I< zO&*3mutH_LeQD8~U*RuYp|2JA>bXE7bfunIGC!EsMEN%FcDm?%HnCjnIQ_Ys=MV*5 zb}>qXmkZKIB5#G}CoggcScmW;-@{`N3;8#x$d4(1emD?={}n!#N-wpoBucMKI7d}? zl7M`NvK=M-1*U}fP1|kLhVH0l6Pt{6dvpl!{EK^P!$iY_np=)SrcPl*m4-2TM|^PP zMDP4@jD&d_+n5~~5!yLwzG(Sb!R1h)N+V;mSjj_`AOfUOQ+B2zMHT~Pitp^A#@+=UdpJ+ zX?XbI4%*;b+|Ze@mj@Mho!@{)u>Q2Smmp#EIN)xWbOGjUP1^i0ARM(Tc(C!y=@5%d z0<1ud-6GI!{JdT3&@MHO`Jl=jyeKHg`2Nj83101t0EOGF+qXnv?bg# zL2hD^+hzmk*)d}5>BYE3`!#*JBZ3|~Ix+T8#`(*kHhv~xs$n3E zd;x6wD;KvL{cKM_Efkhd<5vSL@pi33_Jh*oH9$=wi@j<#JBDlVtPMp)%n5=Ce|GEs z<|J|B4^O5dX>u614F!1Z$Tu>CDw7X`SMJ+UlT_>-M0X)g+g-3-sw^ITHzb7;dh{2z z8}*Yz;SI+lOenTFLAdoTTZPAj!(^mdU<+~39sxYq%mbQC2?SSp@K!tg)B}VGhE)`k z^lO@6%=@2Z8}0h)UR$EEGgjuAZAT}`X|1d}9T}w`x@fr+r#}V7;~YrZc}ncT0$hrV zCu$5h;3V<E~|Q{+#thY=IWh4t7&3EIJ#J03tub z>uWUO=&82i-6C8TBDBIa$s`Gmvdw4_kpA*}aQ(oxD8%8Wm%R!vRP3zrTc$3r*l?HN zdV-->3C@>&JN7`tr2`evM6BkT=e=wAgmXCs`DyD4J;-x?l1_x(zJDWL?4~pfMz@>*s6hY;+!2)R^9ju+eWe z?!;7%0fX6j^Egj+ooTCjb0lafl4&--5sEyOKyZmHcQ!~)yhG33x>zfV&5q_YhlQLF z0h#L3a}Pfy_samB4kbuK2u_d6zNXtq!8y}BYFRj=vY6HQ{1WqR3|sjjW|y@boF{zV zWS!~lT=E;WzVd6T?@`CC28a8w^Z1F+2%yGcRCNBtG=C}J^h-`k^O_8br^1Q2Q+uCb z_g@h}+8DwB*4#`*oI=FaK#;9n+O-WpXoHig74Ff_o|crzH&$*Y=4MJ4ESMm_QOSw! z-;fuyJbP(#Zap(~R(iMGaIg&R9_Bk-ta&11=t)*a35)oCmG#Hen%>p<4dJX7MgC5n z)d~s6@%^*aBOQB4;~UqnO$YPZfndy-v8oOyZR9Eg(y~9#g)SCFX{g}g%VRs5lN1nI zLh`1--P7oG`oOFAn*jtzc^Rc=R_0kv$zisQXKMJtv^YSXr`pnRhwr?2H523bPQ{~kNjS>`Dm)&22;13LI| zS}6ik#K9vs7h&!rSyL5ZqA}cgkNhp-kkJw&I-a?T`kjy($k3c<9&`1lZ@$^O1xz;5 zqZc+9q*kH5^Cjw|?va{IwjH6e+AMa8q4!HMhWAB_mb!;gZaD?!;)%|jez4;?Z%6$W zR#m>kT%vvd^9P)Bx@x91W2oMa5#^?y@kfvYcOw|SQ$WZjemY;S*A!+kpSPBNq(~;L zt+JykksXbLY|ibW)NaIj_d*x$!w4~oT=4r=LN;L;q%^i!j}_6&_&ydY5)Yu`61zPU zx7#Hvj^wtk*fk6w4C1t1)#Ry{K7vIg8%4%FV=leJ@=H)VVP+DaMf&NOxgqqZC9I5o zPS>U0^*3Lp;aV2vG5ElJF2(zd)#t{(9kgJLRVGd(Py1J9L->ufKIa-pr!vst#qN;l zURS*k*~>lGFZ9T06#q?OZ<;`h1n?k~z2Vex8?ZyVclGhdo(lYANY`v~po^DMMpo1` z;qj9?{w$ZwEaT%6B%}Z1)zERhHmtheoLK~Je|OCA)J}z-{ldzms$X|)0vLB|MurEr z{ppMylR#5Sy(b*iGONTtsftV`l=MuY=;i)HQUhc!aiZh3fE=iX;(?(hB!f7Hbi_lH$>+V{kLcc_15{Y zhvpV3hs!&Rjoo0*j@6o3(0sa*Bb7y=`M~xIywYn$)Jm(f_BM zFx!|`2m~hZ(oN_5tw{Bwp-C6I;g)a$uiZ|0TT?zk!=lHt7<0xs<8BgCEyXewXTs2% z39;O*ZV%QN?-<-R5jSTTktX(lS_u)3?7HhQF%UiM;f8XE3GDA(M&JydN$g((l$1dTyho|zK_!SE z(6fH=il;V6Zx)E5R4FiTb<7f;%GCH0o)Bzb>0oW?!G5)oc|6GJkf-}@m5TO;!sk9A z2OCCxul47}A8xc)(-cc_XpNvX;0=IG!7xi6l8p}nIaYlIL2{^S0)P5vuRFai)c-g* zGWY@RB0g!UlmgJt)j9}1C@R^Ap0Xg@hd~Jy;8_>9UtCFo1lOGpJGz-n+C{?INBMgB7q=B7n)!?&8hNI8-E!)XZqG4Yj}g_afZj~G<=t^*fEurYnfD& zy>B{ zj~94Y7dHxyrQePJ`ur)2KBFP0Ov?l+8Jb42N>Hft-Ud=ECV(p?y1yMDC@v)q` zTna8GBYdXD4>cK#D@M@Xrg(mfh1iIA{a!&09~gve0cbBtE$=?WN7PT?T@+E4Rl)$2 z%>6Gr>K4a*T@V<3|6rfPX+0|6Lu0^goOVFft)HEb4~V-%r;HbiYqRtPiu<)UDjMPt z&RHh(_akDTbAQmGvb06ZQ6dh#$>X@$de{7(cm#IgwOCHw213+tLQP`T!oIpckUMMx zdl8)>kdI6FOJvvP&jn`lK4L`mZ6boht(0A|abH#8#-^`)8}}1LLYvW5Mr%g!ubM8R zY!2TeC!(8lYA!|iK9>-JrnrV$s#me$X(p;BQ2HY=VfU6V$u}22L`(xo9j*tAn{v0# zC0EJZ2CD3}l>nI5a-52p=Np*%deixIa_KPe`^>^}h=Npjdfj;vVlL+SyRay#v(04) zG5Nzf7(&Ptafn@BQAwf!^zCri`VhRlNljW z)|FY0kd86sZq*u{F_vh<7XQ~Qz{?^Z%X?MC0W_rj_QsldZ|_`)77>-p1&Q?N=+Plk zL~lG7e^Dot%IY%+{{-({z=yVL)^cWou<2QV9=~oAhnW}|xL$wsgdtDQV{=m*C3pwt zeNoyY?v0IRMxI+564Ee>sytlODUlPpq(_p%WxqxTTp7cWX#|8*#UXYi)u`ay56X}g zE~bnyWrA?~_r9+!*d%s(mWhYYWJNw=tZ7r8n449NL z1ia{CL1crQdg-&Tm_qgbb3c+b7RUIO@*EE8-2e^f3;vdXH*!P_67u#tO%&WBm>Fm^CpZk6PdH$dDjo z7kpfU&n(Rv!Pd!$l7_z|}O2`^oUIgJOiCwXS9d%XKo68 z1&~c$y^D%9pzUqQn6uzE!{?uP;l7;fk$y|;flC!PoDu|!^=|1j5WNmOa1o6rzlE_& zYWFGtK}A3CAh~G(d-km-M|-2|!GigM)oA4Rg9_HdRR5K|`~qa?|*QJKUEJw{HC&*;Ai6ByqXi!aJ%rA*XhO= z;-7dZ88Iw+5AW~!;IK?!xSW4j&459vQeotKhOMdNvk(NI5TmBhI+=%8_&cOuC$7LE z2r4tjTuce>Jzm!oNuDzuzf}A&kdf@@@p9F2)bK-@#+iq}2YlmtG#@Ay5J?>3iXi`G zq~r^y1FmgAO69S^cFgLVpOwmyTPHFO`7?G zCV%)>B6?9W5#nU-%~uXSG1OamPnC0l<`B3V_tW8+iADx7N9)T_SfmdTQFiWLuADa2 zKABjXEqZlKN(LDz1YznqR@1w(hK*$shJ^lvdygGAjBMs(phA6m>Fiz}iF^i8%St@4 z$3rt`?F25%GC5?(!v=~-_Ro$igKFxuP-Keve5yUUom~ZcI=JK9f(@Y%Lz|o_$twVm zgRmsSvSe!yhXA63XyFqJ={u%7b)@E zv0WT44(}yyc~!>;u1^YGe|CNKMTeFuPSHv$G(OQJ^?4;z7#VP$!v-O2ZF~Gfj%Et3 zn|5`BH6DLwON_QsA#^scxj1fN(PoBOawm|{ z(Jk>)JE8Rwgh<`VUQYpUiq16eHkfCtd~0S;lri|pw&rtoLqk9TeG>4-Iw0*j<$($3 zpIyA@f_)YrfZ-(&JPw~3Y9$3_*}{ibPVa$H`J2N0=y)8`36%N)07C#=5mg+jt+2?*)-Y#qS*Jg_zDSASXhq8bWe~rq)H{K;#Mo-0D(5{;Y_-R1oks z2MlqinMiBrKHJ0g$bB_B0d$L4Uk%MLuZ+)U=ASY{o7Qc}v1_eW~l3!8*)s_0dP5% z*ffPp#+sR*0QooG=1kUU;U(rVeiZSpDsBjVYfFz|22Kf5ceHajyQU^Oy8wr9^A~rN z7DZO7yz9!<4DVsL1%C5VlCWo*aH4;F7IX`7w+ZTh(!Ev{LfsOj;7e3ohR9hq_#!%h z6OpBFx(ldyhm6*lXvV~S+F7Lp4m={mtg3`6gg387ocYqJ^z=J;g3iT}bDVH8kdI5w z_H-MwB4U{R`N4>@TAX#UEAq|`B1ko`EhdWW6y)h}VHCqO# z-_Y}yXq8WWDtb+iaGmT?5EV-#QWo#eRM`JZA=VksS*eJh`JGvq$_`pwvn3&uCSyuF z>Gt0#@-Wrb&JVwxp`*v{zT3@wB)AWFSN!CV@jMP4z=84N&%rcin()3fdjAw^iLURx zQPDTgBGt@1Am9HbZ=QR;`HaDy{7(J2cCh}@h9yTcO;mj1tAqLuCqFE*u(kCxeS?Y=Ra8IxMLrUu~7p6`|F z{L{0f2lT%54bvi&mK!j#a z?skSQ7G^;ISXv|?4MAYoBVP3|0WQSorST}|JncP zKNd#+mj=Pk!pZu7G2nkUzQrF~P|pe~^N);2<&W1OlVkASq~sFmZ;$L<0d~ zlk9$tj)pMhskURJjCL+}`|M0#Z=nYws(w&~OINc93+VQ7^rUl$0CVvzj$`2%sXyc@ z*Ig3;sW!2Ub!C@TJv}rGt90k^&7&Z5?BDNAL(C)qAGnF~)J@QUudc{fD*Ah~nCjpcm==svIS5l8gcY>Er%#%J0`s>$mX8RKJ)XJ}xnk z+@xsW$D#ng{3`S1N`#Pqe^3~VHh=|=_WGD%G~Y;Z-K_G1e!}z46)`+jrGvZ_-qFp^ zJ{Q$NjEH$rZ}nFoD<|IWC6Cahat5Gg+b!q_t?peekRiucNW?<+MQ{bv^0eH!cT8e( z1`TlBW}ZM$qK4ztn>Qr1C2=}MrG`3v`Q2(?pRW><V zA>RLbB`o1ev{_%wPY@BGnCcC%eJl#{KDHzJLXNN&#XmWmxxAx9K-*8~LTo(~3Nr5a z%~2jUDfpF0B@GHNjL!q0cGAIIUlHA|v7oqxj3GxLyig_<$zBUpdjYn%p2yO7^Jb>|QydJ32SJ+{b(um8IT1*i(GSVt!t4a;jvDj z)XC{))c(nKpF2V)(`s0r9t->20)D2cNoLp@N^H)<62ZjRZzKVdiwC~qvo+qpDc&lz zUT#btxEK85x3I+2`VszY9BW7sPwvc<7(H#kNqo$A)Z~b>e2IQAcG~ToG;fxpfca`q zZs)5bSdHz>>E0&a)Qu2P4Ua{<3W8+K>x4~X2Lt?*SFH(DOV~RKKhnN!fDK-<*kN|{ z3!-*?d?_^+fyN^IMdFjKk2z?5UVax!4{Lh9*1BSwY7VUGZ&u(uO1kz<3_Wz`%eT~V zhYM%%Lm=D~$0uWc$+OR1$YFPOT95t2KD!B*sK#U;t*mv8OofvV3QOX&ZqmC@zZE&4 zP;J!C+4W`$x!^c@2lN*}D~`H{J)u5nG>(a}HH)BdZIJr*`G_{Y&O9p4#GvxRDxUvC^5P^Jc7rJY51aw@_m6WXWy6;F}z<7&8ke| zc-!`(vla$%6Uk4eqzjMJmU-1y25GwGTe~eP6<*~H@|Zt-E+aXzw^Brf=CAC#2&JJa>;3{@P6PKr&Q~JZOASBbAk4ArzA2qi?%GyfyC?^7IFfR1SAg zoBx)%)!A8`ntYIK!_O6+B3$={CBye+YB>}`_aOe>IV{CsisbzCvNt! z-=$|OV;c|u9Rac(@mU^c9eK3?>44%3^cOY>-CW2noB!pJ)8Le8Bg5DUjPy)Q&5PrB zeRaM^ZA>zFv5ZHc18ro2Wgj@HhH|eZY_cQ#x0x>rEM^HWe{nE98nC0@RQxfaVeiT# z2McZLsm6ZC1UlSjxBICb=pw#sW)ET04TEVBng?Hz8c7t(s-~DW+vq&jB{}s1-0GG= zlydJPf`-DpFvfSX5$R~KP)dD27b}31^2teai^co_PwLt!pZ8;(#cAhroB-xGx+e${ zaq-4JKuG!`T+fP4QBgVo+q@Gh@&+S$S( zX{L}TFOAiWNLwOaF^|cTyLc+G`*z~#?NF$hHh#-axB~}`w4>+ZEe{Y?@>m-%SK+)^ z`mldWh0ifgMW9pJ(lfgX8^sJ|t#rPR12V~F&I|1XSkIkxQQSumF>`c3z7Cn|Bpl`{ z6*fgt92qzl=)uuQpiM^@k#7cYuJr(CvS1I(8M_y&N5pGps9>n!qFtcJ5hCG@ZC3Sp zx$>QI>y|NH_@HkvVB%v$HuR6L-}*^Xq^K*U&7J#_Wj*o}YKfpbd9u-Xx{G$Qll^2bYUUNGT;>gf=by3^c{c5$Z&V)6Ep-JN)br^w1oEA5=E{7jam96U~m2t(gIM z)I=4{dyw3`*(o#o-X*hmKlC(T=YZdy`_yue^`51?=F`)gi#@<)=GA*_8eX}I$iUpVOK!nKdu(HSwblk)gPplJ7s;sb_w#3!!x zGaTy-(k4$;6RB3uX=FS2EJbc!;k#Kn26nXjl2LZX&AI-Rr-7f58V}PshM;mVS-oC4 z1%t{~YOvjH&mBOcAg+zqP?w>cJip(Soyh3xQ_c5rY&XV&Y}cG8P0}ycsxrf&Se`xJ zP2hMmOYEXl(`|K_r4<<`gA0udxkM}6FGvi)eL3@;UR^p-c>~r?J!Wj!?|=@>5RK&Z zVki_T=dZc@O@1but!>DB5Jh3a{Jv(h&kZ=jC#aGye zIQu9$bYKiU2if=j%`RE~^=JrYJ&PML$=XZ0&WfENhZ@j}Tn}YWnSrYlqyNgFks;Cf z*4k)C+e)0F9Mklq>nN}D`sY2~S?29-yr+#U;};v!#(nb6j6ws8RuHiFyZ{F15#>6_ z@Fz>_i$5NVY9ouF1M4W-nT08Fi12Fw?BAzj1Qw>h^$$jTPs`dJM?=nv-W}LhkeZe5 zEfGpL?9(0ltHa$7@;8@;6u4d5QiT3&so7surP34?O5IgPlNuFF#X=N|H0w%b+icq^ zTXTakk0WajACRigni4=`=)lNFUtX%r&PgpDk(w?iA7o<$*q-*h?5lYX-5Xwezk9*_ zCgG|*1eGu^EzN68AamfdwLsvxA6_(xeDx>5b6Ozz-yhD{?hsW>=Vw2jz(@ll2JVkr zg~*@d=p?HTxDQk}xa$oUr(5QxrV*sfr5aNYP4A4lOa%p`Fa8Jvx&yZi)+&^ z5^H#f(hREu`c+?71?tcZs_WV`SoXi>NTwEc(k?S&as!YLhAs1PAJLht*luox?1iMo zw%Qx7z&&8#5ySO*pC9liq+)TDE@~U_VfnW8dSeKM7`KlFZQENMGd$K$@xBT#!saRgbzdUXaM)_R2 z+A>q3itBJb`fg^VvMZLi#|7}9ZNq>Xp`S#PzhIRuN-B5M`ct$?x)hKob%L10ho2g&tnvRT6Ay>CjTF}$5 z-oI(h-c(kVhV1z2i49lC$@)naYqCmtoWzF%+_@>m9N;ZRcaAogjt_T{z z<=9t7S$g)jzv4X${9DBAR=<1AXh<8vdk!3`8C>WVTO43cKoF(IyzvKbfCY%{9*=BTxsqu<047A(n0oOqoiVu!vweN(#T2gEnaiddY@evl=$u+p)h0+%b! z_PNrbeu)y{$yiqC*h|+G{Xw(V53aLoP;uUoEcx?|ZLNwWS#jtO%-G zb=n+d+SbJh;+n-Cq~DKiO{(lX7>t3r$vYBravMc99M<(dI-(|X+SVjlze-JzkBH;h z?zT##b^2$N`0odsd9rktGsf@Oe&^(*WqCNVl|q#- zaM`fAPV4bk-vc*3@f7B7gwLL@g~iTAC$cTUuBRghk`br-dAasYLFH5f84<@_idX;V zQ^$>2gKicMr^nxjbrd#?WKP)C9%7FeL>uqu8(*{w&F1S&748^VPIIIdtX=9;GVOv0 z)A`YRf7+KPLUG3~K;9(jk)g;`a5{t0xQ4QS)$O`JxhUGM?r$iHjDL=Ri}unWJi-bV zADRiPR3)f{mF5={>wL7@LUsG{ax)mMHSpL$7;PjCh-e;n@!KC35V`bRJ;y2r(;>yn zX#7a-zNmR}Kl&2uH8(CYLmdj4j3KX;c;K?FgwE3wd4UR--$kTFcSS;zoUxt6U7u|} z6nId*>tQ#In)p?_?@$THbWTI^CD$Wqw*d7rR~b_4SBw&Q;rw_d6r^1(@60VYShtZG zHX9~dSw$%M0kyJ@+&(O~VyYr$JQ@9!`48sKJk=Whl2#679pv|>hiR!carhWkV!mPu z%|}`aKmL=H8FwQdeo3lOvJd+GuQ$MT>6mnt35+*{;}X#D|-& zew~c4#2ykIi6f%W%J~L<9PwWF0xO5wG54Vqmf<%bN*ii~xp5Z=YWN{RjRlvTvV28% z6Ji}H;g%K>MojoQganiTfiE7JOK3y>4TeoP#N+(}|8tWvJ|PJk<)d8={N(&Aj4i=5 zishh*6?o(y85(lL?O=&;BoptN&pLc9TY8g028jn34ubs`dhqwK+*}WRegI)%W~X&4 zeg+8AF#W&xD|QO?D+^ftwZH2NHjcYxUz@$|k<6RnrPxV>ou#HdO*;enqpk<3%cB=NVstjwoi zQ3_2Y%+Jj&!pws)y>=R5M05J?cz-xMH0H7J%2k%9bi_nCv>jKP1i2aouy*g19$Eet zfEz!8HZS}nX7k^k)U6=^XW4L6GsmJ3%(_{|GN&YRN!M9ik{hn2=02>bFhH5?sv@u~ z)ZdQ%Do#fmleZT{-LH90T!Z^o{{H$gb4|r;^8v}i49$%E<1=v?39?IV;4-@G#$_Bk zGU`HsV5212ZaTxKUQ6y7yGWA>ac7OMx|?OsUGfT-soUE zT%c=_FzXN>cMz2=GO=gHcmTmeqp?C_aq-KnVX$IDlY@a@R;uxJ+nBSN9z&f&(!)!} zH5bY=`O&1!wn9w%QwEmIqZwBJq914hQ~XTv#2k>~F0SWJ;aphcGE)}J!pi|rF))q& zywTqQD+F4(>Ay1EF{@m75~AWwnmW1uSh+gfmcGqU$81qX12gao#)b?*jqhR2Nkm#$ zPGO6>&4Bc_=#M|u`>tgoe_hYpqq6Q;=u=zCcbhUvwtgVtrPcHo@yg=0ah%F8?)%}k z!?z#JPCx^mH__`V57Q8eriXAeR;OqDBUfF8RuvzqZZGRg`N)#;wN(Bi+qaJMTzb$j z(rOO5fr##fn&rQ)TH>CI{vM_ipl4bVOlLt%1e$V)CJV*3Dg99>X#B!pYU1tVXWqqN zogyhj1B2g~As>xh)gqB_x7D?>G^HZ)44nsllrqE?kH_-f8_mE0N|pDTd;g-7S00I# z_BUQL&w%+?As?UROw){d0)LEKkZrv3=%b(|@9H*9!3_G4ar;{&ObcHk|L?qc93$n~ z-H5$asL5QTaqT8ri|WhF`&&LAK;s932-Y=V%ppW?`QmAO@olo3p-Uh4SW;wr8jGiT zx^_C!o5IIP5nJ)v33R*S?yI z%g=Ue!mahxTy@#w{lMhfkpSsWn7~VPJ$*ult&6y3?o^ucaJ82H1E|8ccCBYZ5YTBV zA%vv*hV`@D+utY=r($ApVtnGPP?i3INlws$`}IGse)mP`{7T#CyL=IIVl&CLJrnz0 zd%nkNJNyao0l54($Gj$ItTGmTrT|}c{ezRbdK4C4&cGt!(_gNrN_vr-JqWGGz%Grx z&1CJacsV+(o0jWR6iR)8zv!xOYOzHabvOl+f3uCiy6B<9Z~N^j_Ak+sGh_a8EA+=4 z?-6}UA+tK1!%^^W4SsZoz;chD`=37V82LxsP!?I4(P?d#K!nSW{qA9ih9qDp*rovzw{x9(hRRh`mV)>W1|? z0Fa)!O+xTWotl`41&hdfkN3UzsB!wSanT62ixqnh(EtE;e=Bq4<8A~$I?vUTxm^|_l>`nI|yVpLQynM7+#$7ia~?^(56@R9)v7H*G+=v|8*@kuRV zVNT}PQ%@Z(=nGQwUG{*o&S@e?!l}m3zM-U6l=H+&=H-CNXAXIWN(SUwQ`q!XpoeR*~1)mShuZIY5o?4U}_hG z@B+ic7)NPRugB4J`U&q&{c-}%^nxDgTC#!*$XRkqze9~H(ED-L)jiws282nFRUysU zr1*Mb<5v|rf6K|?ZXK>feMX?Bw=cYj5_ziQ%@Oqoj*qGhbDVD>DEnQx1h@0^j zM9-5~t!W={AOMU0CD&Edlt@`Cyu+py0l24jFc)x~2Kv=(lJ(tnW1O-Wjkt7-ksDn| zPNzS=ubXuFDwDrGq5shb)depMO9r(Re;a4dok9Mk{%PYw8SvPk53z#b_3GH&o=e)# z&6UI}ZC;~j*&tm+(KajL4q1ah&7_$6T>~(GN8c-ifOkTMnK|>cX~mt00nB)r_*<#-~y`PdR@kwin6EceZ^VqYoZ_+U;!=;iS}B=XTFh#iS3O6E%c!YOXn$=EUeTxXIT;R##NdQEWOiGYi8BJe@P3d z=sSN5urZMepF*6``Kxh*taPj$VkyuTOGeTd)j5y3Pg;6JSRzT@#cmD$Yg4OXtzhlSKCauQoJOBaJVeU+ZVHIHfPVV6mxv`a`KzsYtw0$j5VjF+bSfiJU5^l-gUc3Wr{gPG^FLd@coKO}tzfTQ5TCSeK0a z1=d(@P+Our^zOxcoLkhuQ`yFyt<--KLD<_+VlND!{3hl5cb(%~--7fq)1YKZu z4eU`o$Lc&Czm-AYJ)kT1uhaLHl>o>}%sPh9Rv zR}g~Q;!%X;zHN3MEWa&kvF;OK7;0b`1Z7zFS|*3#OhMa|x1#Eu$!h6JUeh$B&AU5s!EmKotXHRj79Zo*y;Men_l9`?{hOo?HFI1Op4N z@4_x)0Z$pV6564SIHa3t>1T4JxN|;^hbMCkqnGnkE0WZ-5)nfTqdX`3vA%76$D6dJ zEvMnH+cB&ZN2oX*o7ol^R8q?l=e8Leyk9EaX<__qD$V;fwe}}xh61^@1B!Q1Rw%<^ z>7F<|p=5r{%YVEumBWwQsJ$>d01&w#ULGA*>S-i96~)ERV7FX4hFuzs73qP4V2rPH z#5G0+FLJvsIY-yJrlUG%w?PLp-yC;sYl`-+F21U5k=8}=3%rhaGskqee=t9^Jbp!L z#^fu^A<2b7xF*gYt_(#2l@Kx;xK9g&ZvGVmf<1texQ8VhaWQ|_Yg4Ql9#!3=XoB*M z1@vGFd*K%jCAhM=0LK2P-Y##bQr}!pW(l{%@7A!86BY%v1Rv-4{JJfYa&P!rPjsC+_%M#?4-gSxWVr|Q^T}n%Bf?ZffMAlv<@!t z2ADEL4B~D@Q74MUWYT?g*t4 zdeuy%$ajhQQrF#eRA23`+Dl$A-hf+%wgT>ke6c1tWrbGAS}H+a@PO>z#>hr!88T=B z>00m7LAqke+$A~j{Vckj6MfP}5p-bw_<3__Oc$A)K{jfmqhZWJ3S^|^HYz*xO7NXs z;w7XI@&jsd5(V-|)fGhR^z)mhz+QDc9k_UA55?AJH+24#2CuB@6GU*~y}e}5L2@W4 z$V^UtSrTD&i{vRH);p#o4IrJD0Kb(XiXLO*uPzbkbajLr7T3^3OqO_p}h+sq3V|HUIBAb1|9h@eP(B0s6| z%b0>vT|rHi$(bEh@unVl6AUxS%zsurgem`78=w*#9S5;l$ZsxpK^4FN%73c57ubnk zw^kop*E$zarl!8AjGOL$YT)>oZ}9`kXf=#kF)@2(^wz@Pc0xw|6bw5g_lq@apaIxa z1&aE+d(596U^hpDC7M7B!3rB4p@BV#4iDosY339*89Vebr%AA9+MMh99&?oT34Dn+ zzUmc7CDwfrdir5SiTA0&SIGXJ++2PK;CMbgeiR=5{*H!Rc!0XL0uCq{Pe9>W0{G^0 ze!H`M^Ut$RrFo|JhsDzDDC;l9+>uBG?kWdA@Q*JX6S@m(NQZE&VwkuV<2BC?3%xPc z1~8&C=HCx&yjgeAZ{To~&lvP&1M@g?UEhXRRu)?07jd?}rY|DSuL*nZDlzXAn6wz+ zP`QjY$C{GUg|=X>&Z)hTLh9wwl>KxcB#8?slPdkv$KafS6KezNdw;-l7G;LY#?WeM zhvXSnM1$OYSyT0Fc1EMkb`x)dIFcoUx$9+XW$PiHd&nDH7D*gNY2xAt&E)d8p1ErK z>={^XM!KTk6*jq(I1AY#4PL4w*M#<8uq&^9qwnFRP?X4H-FMmfqBDLtYF3me?td%L$=83(}sBEi8^fXFsjkuR*e;@t}5VBCOyugD->>o1s;pN;4?~( z&BdafliBZvXSIsS)kX@!S*G!OZCjoX?8o4EWSwNb8J7|BR9=)dsjVcNQbzF@m02D? z%o$d2{$-}7*)=;fcHfRn0qsbW+TDQNY}|(^Y<~`%V}BG8@rlnWhItvUGSPWbSNS|w zGEfkGJ|7x^;xz$X0D9a+91UZ<#9vpR&*BEvfGHHJ7r#-QqKWt2N;`B;N9h5{mpX zkg(Zgd@o|wGm0;VGWDt>&nX5Yi~-iAHku3#6d|68I#?mrJ{+f!K=xab66~tgnN%V~+Gi!EL8|UFC$eLqD{UYjI zO%mXkLhv|Ir4cAWs@ks&B-!AsU+#}(0>VY5Sr6XS#WZ!~2YQ4O=D1&SaTM;Lv*g^P z_*HtVytyrtm_J|+sO?V6ic~qpI({f2KJ&@S^MWkDFXT@hz`b)IF*%qXF($}x0MILO z@V413H8Tj2Ld-CVg{VpLR|!Ou zfK4TJ|5^kH3iG4ME@>r(?rUWAx{;=ITX-&j1^D$r=QcPMcSJ_Ul?1GxHV*f;4 zjO%Ah0M|o*FIvP@^D$(=ojVYyPTu2VM8pRI<{zKwn*4ca&Nq4QroWgWf+rI@A`1g1 z*>wzr!jsKux51=%r^BOlr$I^{emz$_53?xmrxz#QiluT}B zB*<#=U}bf_V1H#22QCWzPGXWqAf4+4m7S%BRBqRPGdM3A`u=h5-0*psu?cFNF6=jF z6D&$YQhiE;yN4Wj58={qm7E(^!Ia%=Au43Ozs{_+Tr)!wK$9pdxz<5AWHae_jI29o zCea?2g+9WN4yEzi1A%1l=2Gl?uaC5@pz``X=KiH}=^jGS>k{h*JM-Ra;-bYKie)WC{Y^f^57#HIEsa zRW;IFN^yM-dgfvLjGvZ(A!>z^!t%2J*O$dn>#yTukKM2sx~szWy00ns8+Y->Su__x zM3fYp;Hb(ZbK%TzH@N*F6vsIp4^qlQBAP>SWf@cTn)(t|`f=~IVrkOWr^82Jx8oy( z6rky+A^h!SVLK)${x%#MEwG5iU44%dP8Z$p0jQz$(`75NKTq z4AGydD9S<754+z_y(2lB$ah|`qYuDo*EQNmc(<25xoqX7Tkv1pY)F=y%OGGC zz`fa5k%C)CQ>H^t4?C2{%etTI+>#C~KD#c*K2kD|8jL0QyBEf^Ri!eHqYEaR!mRfz040pw#VkAIMrv$#iv?7(NFE z*sx&!l9*`OBj=Rc9&64f`0C>y0tg2#K(xrdXM^$`M(RhpMOQNtT>#F z7C?Fq3_A|ICmgN%x6Iq~%L8U-oX)qIoYLftQETujxuL@Gq!Rs|&xiCW)y>(yr)tCn zrhc_RH77!Nxxb2HBTwhF4d$B9f5eRS%Smf@9h{TEHU%nx7&K9yVMC90W2x#d-)(Mi7!Qa9x&OjeyhGceG#)&lGk~h0{K=?@poQo$=nt5g=di|f{?s1I}eO1dXCAjBH ztg{TIyA68zh1v%b$v!T%hK=;^cT6{M#glO?=k<56$6jzAopWgIqVl%256*}$Yvpe2cC25h{xN6Su`IQWN#TL;>RD9*GZSBX1 zsH2C3XbF=%6y-oX2YEjE-+I3l;cYcH)t%AYj4_7h{XuwKyFR8D(NFADDq;}ns%W9> zhJm4^7@@_w3j9%4$viq|db;@;(YZ4ZyIvJBoi`9V2NEGW7=SWcml`;(_? zd933pk^#s138GncY3`bc5ZyS0Qnt|JZ3{}PzBhs>caY1tFXn?M$43OAhUI+7G|rqP zm&K|lT&|K?Q#}fd$RW&-Ox2FKB9u3RLDiF0vK~kC1>m!*D%e@Sk4mr@|E*}1DwAH> z2rE{Kj)fja; z_uiR@yOzm_rF*d2u5*aO1uTHXt^(XzRS8DEtZ(ldx%LpP1%Zn?HL}R6Eno9;@wqwK znQ0?@wso0Zs{pJRt$auRFeOMsd6mT-3wo2$;`he3)FTtWu>QA8ms(^#NHZZ^$~|O& z1-=2vGslODVJ?HCbCM&9OLoX|N3Iiu>Dc7&e&Q&|rG4D>PT;}PY%hPfUcg@7R5Vpc zr_^h07fIc{P++hJipxi)q3K|xp!rsLNN9G<)>JI%yA#l!Kws|QDK@U>4iT#?d<|>C zhjeB5UJH(&*Yi3>|7v!J?48Njt&Yf}f^P#Xsd8}<04lgXd}h}fCd4RB!x_&V&W-zw z#`rlgg75;i$D@JsgbNTrw`=L#I*GkkOet!wh_+d=9DKzG?}o~(&fcG=1t zaNog-JW%-ISRX|;aDpEUBy45|0TXT~nDXc+R2ke5n@Tq#KG@FpN@91r50n%6F#hzj zcDa^aa;jvO(!6%;JnZ>&Z8V!%YpVc&vHb94Z!z%ET9@1G;P50naTlo}>>tiaginJnbV_zKwipBTxO0uW z(9+?WQrffNx?l>7d(U}ZYc5*zq|sSKWS9z9SH7Ty)M45kDzUJ3hq9Iw!*g%B+XFpd zf!X!d!drzGsv%2Np_?5QJfS@F$K8r+J6}K#Qt9BB!JkJ({9huy? zvqLtB_^Ts^+<7CqSTX#Wrv~%Rmfbgf&Z0E*X@+Dr_Vt}5mZLFU^{R{3xXC#oZdw@t zH!GqA-|j-eVY=BZ+9VVZdY?rJqD2`zc%%-4v}8jRn}i_?z^+c*#e`_Hu#fh7ym3Xp zTd|7dwWy15Lyn35KJ}FK&S#L!Q!c)HnrzD6wj&(JI?9j-i|w??+3({$*2sQ-kc3ceO!9&hT3jDeqPQ;g<`!eOB4Q_f%&5kh0T3;Y)SNqN0aXc11bOS$p_^PBg1PX;xm`=kc6%F zykxI-&9&F?mQ>P!)2uE~0eSnGRm@acFvXTIT{)=q>IXuNsj#J^OUi3)83KK|(+iD4 zalM%7n}bcSd(#V?B^X+i*q_Lxgs?>tTUviWp+e$r`kXTN#`q zfUDs?GNk`xC{N3>E%Mu^3vY1t!5)u{!**EXM=(=vnRFaJq3I-*x>!2~XopOl)Nh>>*>caxN zkJ+Zb4od_GdsW!dTsHgk!M(%AlvSB*N;k!Pa?|Fe2r(yRFi~}=`MDhOF{u!&sE4!Q zcwX-E#4B>;%`4=y##E@;HIT7nILCfX*qK4~f5>8DWK$!{(h-C>&>nU#FPzLIUx#{o z38I)t6Y6>9mZiaCAMMrHWCY3#f2YcPQ-spW41k{jR)iRP1Sj^OoMy+pB6ToS?t^It zl73es%ADnyU-{BW!4hfWbDvlwauXjcQ_k|bf$mI*_D}9TJdMt0V}Re>=P>c}ZmnSz zb7y|6nxR1FZ3)Wx!3{qI>uAyPz*ObSmvo;Wek{~wL!|iMk(YICwsa%=T9xZot|-kW zdlzR8QlsMY*b!-7UA4{R+5Z%rJzcqUKW_x7AnX!T14&+5zI*WJ4>!hN7pxWTX|r&w zw`CcoG_7F@+6;wVNTf+kZt6Yo8R&y{*6SzxL8FUerI^wUB&FHAzUI#(fA_JG z*EK;VQ-1N=?=jP1IIUIa6yvE0<)YZ>iI(gf1=9Nwhp|D)g;mmK1=&f{=~X7rym>kd zg2e^yp4_7ra#Z+@bfZ=MC6q&ctqP7PaP-;3=tTrQ zP^1Tk5|Q!gDfFG2fTv6L%w9+p*#+XmuPFJY&+rUWgC0w5Rwhji2~epl^RiWB?_D~; za9y*zN-~D8@C~(8IfGO=TpRc>rhDiYi!n}u4QfSZ*sc(e3?PhU2I28K4gW?o9aK0$ zq8Pc~vB;UbvOOb|r=mTfVxnydvVHPq@qB?8CCj!3nb6h6Ws{shZU)L90O%PGuHxsl zy4{hs4Sq7qg~;;-K-K}##cyLQHsMm5kQ}x@yzovHhvpwn@eI*@EzQfmKG75{YL6FQ zq`z|+UQ#pP1!Gv!Q`Z_JnJyB_2)La`_R-y7tYTFy77e$F5Vaz>oI&w2xj+#QfKBiQ zb^zCBc?8XCNP2UQW=y2{aRZFdc<+dr(;@%7+136b;4t?ROZi=EQmvF?Bqx_w#-QZ5 z@qxOkvzU!1&H!ytIWNTn&}FT$^|Soua(4P9yryR?lJ_-gg2VGHT*LQqSA~==!VDri zTD1>$s6{dBry$>7lo-^9@{8$s#Gh!qMNaIu9)}#=ysyj_4-|FW-r9<#G{pQLYk@n(i zUUS$(Z?vk|4;ViHh!%(+qN4^ia`dYiWox5B>pJ~#E>;n`EB$2x@1~uz&wT+!QTRn3 z-m=1h`gdr6o}W#^G~8BBsLgOC_H$+nB+tKUoB6&)rdl;3m9nhx*Gb|vDjTR2$|TL6oy zBNn3EG;y6pzG|jS`iK%wIY30>DWiYRItX=A*|>kTh*1pPGz{6-UgZeK0A+fSjO66{ zkRYgi*&0C!-l9gpE2&ovqBV=QUi@B3N~~(QSepMZAVlZhnK9Yu6}U*sw3*Z*q8=@o zAM2!!GbJwI*HFpR&>4z?eB+*5+@WzJ5+YCOuUmFx3`$+`z|!ka|4j*r6uh0)L>HY^ zs2PU z`-CEO$bR^9o6Vu-|chWJw+mC&3z?hKs>LsERHI$pz>4#y*bEyaX)d{jGgmRV%rwrP4+`6 zxBXHBW&hltpeS(K3e69Cd*YjyK8aY-qrWJ4B{}1M6;*&Z2XFdI8>Mel^TlqfxTdCn z9u;f>#FS6Q{uF1{&I74^`f5u{%K6@QtG&p8f9PQAMfjVgXpQe49*wtcjrcgmf1{OB!1OW>q&Y&Vc$B)0_LZsxyKyzHyKfVl>;)Dg*S>Lv zJN8d$`o->UcL=o6_VghGTP)|A=c2|zXgrcGSeWJsa(c^Tps&k5T0~05+wXS?3(KvC z*Jgwn-Wy~Q*z9PwbR)2-k-w!vGD|vvCtPE0LZ0>CB{pM;wwU!)baNd+wUfx~)>8eSJc${JHygTmm_EVO@ z(L4}q_5+!az<(>n?kct@@7=Q*6LnqSJ_H5va=qd|VSIZejSJg!PU>AxQ84`cZ=P=o z(_&CgRDm`_2~jzeoO8cYL-T>{v&3nkHIjjU)@<&@LZX;BuLsuzSS=0qWt(5k4ciyiz4V0E1QWVVA1tuipJ1@I$ z@h3AEX#Gm-oLs4kw zz5UD>Icd?cZz7;uT>h%x;YbJ>=ozeQ&nlX=7S50J*sGMZrjfd=zXn2g4R-hiWizPT zm82xkv}Pu{P`HQd=u;%C^QavRslZ7zM(EiE)PT*ehj&7T#*yZlvdqEv$S%jDgk;Ko zhmAqu9;r7ba1SOm`FpbUmM?_Lx8cVSvx`Jh`M1C`#4EQ-k$-MUENZ0vwP#1@pg6&; zs>*qDzrN{qt0c*${1$L!=7eX#pSt5KII}w;&2FAFK^Z<)3huhRDm)iWXIM*<%gA%k z2A=Q^^@*Mhd~Rx&75W3rAdQMgGIOuMKzi7HJIhqhZrGjg^L@M6M}5Svd9xGkOppGM zxQ9l7&01dC`bu9SmZl?079*V*a~E0tUb z)#T>G2Fj1yae*}YpUqrn zQ3#F;elzFEaB8YB4a)Fvl&QmzLk_o~ZQZ-8{rt`>)=FUb%bz|-o^m&?r&E259pz=V zHlGCm!u$h@T@LF8{8#%yG=X#1LXvC=8*)`#RvQz;w*Z@4R+g+_HM2E?qvQxH3G#$} zV>HLi>>RaH*oJJoI$-4QK9!RVmCeckq{Vj8u|h9MPAKWlYKC?I@t$&;LWCu7mjz*B zni=JTKa0ahr}@A!Y0DJ@PtgN2BTrCt$RabEU_6k@cc;@?+RBO4(ba$cLF89DhI4@-N?y-Tbr8XW6x3^U>Q6$6e(djRk(U}H~12CN$Q%IP> z7=r^Wp8pr80$BZszb~F> zP>CdaXPleECUrgx=O~v#(dXH?g>J1|_34QPzm+ge0$LbqR=uY6QJ~s?NrL$huQWY$ zn4ltYW+rgvhIl=@=xcpg*>sbp$duhAp}cr)D}OTG)W8y5%|MFs@-RuLMC8x*1EJzm zjEjGKb<3m&Iya#};~U{Pi4NhM%Na!NfpzBJilwZ4`t^Fd(^>fTWl3mW;x5#037IC3 z7And-*rcUF(Qx`fYOlUDm9yYv-HfN9$e)t_oiJKJDz2{?T(88hB5*nFlg^y_%-zxV zlu-z+e@1UXK6yN|C|MgE%w4_hf>4YnY)y523rfeZDug1cZ(HQlJDatC9B~3mpO_i+$mz>RCix=8Qbx1(6uNP`=pUqE0Kp*EIZ;^J3O>65>tSLqo+~Nr8R#WDf$$F`}hU3uA@Um z1OANTiWqGeHab;l;A^E86KJ(_{u~pmBbe1V3u447(^}y)dpqB*nk<3PZ(#n+9k%}K zxLW4^x;#X5Fn5D7w~e%_hT1|*BVMv4`I>7{bn5wj%*@lOimkWm@Sy?;E`4?#>Q=x0 RmVbqbo3jf9uwz-@Eq)%)BzFJ+ literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot1mir1.avif b/Tests/images/avif/rot1mir1.avif new file mode 100644 index 0000000000000000000000000000000000000000..0181b088cec5012953258419b42cbc90ea44b948 GIT binary patch literal 16588 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot2mir0.avif b/Tests/images/avif/rot2mir0.avif new file mode 100644 index 0000000000000000000000000000000000000000..ddaa02f3f890f0cd65035a89294893956c2a30fd GIT binary patch literal 17001 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/rot3mir1.avif b/Tests/images/avif/rot3mir1.avif new file mode 100644 index 0000000000000000000000000000000000000000..ccd8861d5ae9378bcb6613f6391f2c4270c5f706 GIT binary patch literal 17290 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/star.avifs b/Tests/images/avif/star.avifs new file mode 100644 index 0000000000000000000000000000000000000000..f2753395f8c3c131267eb4d1846ca9a21a6a47a3 GIT binary patch literal 29724 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 literal 0 HcmV?d00001 diff --git a/Tests/images/avif/star.png b/Tests/images/avif/star.png new file mode 100644 index 0000000000000000000000000000000000000000..468dcde005da39fc6807cfda46036dc78a1efe1c GIT binary patch literal 3844 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 CD~X2y literal 0 HcmV?d00001 diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py new file mode 100644 index 000000000..392a4bbd5 --- /dev/null +++ b/Tests/test_file_avif.py @@ -0,0 +1,778 @@ +from __future__ import annotations + +import gc +import os +import re +import warnings +from collections.abc import Generator, Sequence +from contextlib import contextmanager +from io import BytesIO +from pathlib import Path +from typing import Any + +import pytest + +from PIL import ( + AvifImagePlugin, + Image, + ImageDraw, + ImageFile, + UnidentifiedImageError, + features, +) + +from .helper import ( + PillowLeakTestCase, + assert_image, + assert_image_similar, + assert_image_similar_tofile, + hopper, + skip_unless_feature, +) + +try: + from PIL import _avif + + HAVE_AVIF = True +except ImportError: + HAVE_AVIF = False + + +TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" + + +def assert_xmp_orientation(xmp: bytes, expected: int) -> None: + assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected + + +def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile: + out = BytesIO() + im.save(out, "AVIF", **options) + return Image.open(out) + + +def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} decode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason + ) + + +def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator: + reason = f"{codec_name} encode not available" + return pytest.mark.skipif( + not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason + ) + + +def is_docker_qemu() -> bool: + try: + init_proc_exe = os.readlink("/proc/1/exe") + except (FileNotFoundError, PermissionError): + return False + return "qemu" in init_proc_exe + + +class TestUnsupportedAvif: + def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + with pytest.warns(UserWarning): + with pytest.raises(UnidentifiedImageError): + with Image.open(TEST_AVIF_FILE): + pass + + def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE) + + +@skip_unless_feature("avif") +class TestFileAvif: + def test_version(self) -> None: + version = features.version_module("avif") + assert version is not None + assert re.search(r"^\d+\.\d+\.\d+$", version) + + def test_codec_version(self) -> None: + assert AvifImagePlugin.get_codec_version("unknown") is None + + for codec_name in ("aom", "dav1d", "rav1e", "svt"): + codec_version = AvifImagePlugin.get_codec_version(codec_name) + if _avif.decoder_codec_available( + codec_name + ) or _avif.encoder_codec_available(codec_name): + assert codec_version is not None + assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version) + else: + assert codec_version is None + + def test_read(self) -> None: + """ + Can we read an AVIF file without error? + Does it have the bits we expect? + """ + + with Image.open(TEST_AVIF_FILE) as image: + assert image.mode == "RGB" + assert image.size == (128, 128) + assert image.format == "AVIF" + assert image.get_format_mimetype() == "image/avif" + image.getdata() + + # generated with: + # avifdec hopper.avif hopper_avif_write.png + assert_image_similar_tofile( + image, "Tests/images/avif/hopper_avif_write.png", 11.5 + ) + + def test_write_rgb(self, tmp_path: Path) -> None: + """ + Can we write a RGB mode file to avif without error? + Does it have the bits we expect? + """ + + temp_file = tmp_path / "temp.avif" + + im = hopper() + im.save(temp_file) + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "RGB" + assert reloaded.size == (128, 128) + assert reloaded.format == "AVIF" + reloaded.getdata() + + # avifdec hopper.avif avif/hopper_avif_write.png + assert_image_similar_tofile( + reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02 + ) + + # This test asserts that the images are similar. If the average pixel + # difference between the two images is less than the epsilon value, + # then we're going to accept that it's a reasonable lossy version of + # the image. + assert_image_similar(reloaded, im, 8.62) + + def test_AvifEncoder_with_invalid_args(self) -> None: + """ + Calling encoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifEncoder() + + def test_AvifDecoder_with_invalid_args(self) -> None: + """ + Calling decoder functions with no arguments should result in an error. + """ + with pytest.raises(TypeError): + _avif.AvifDecoder() + + def test_invalid_dimensions(self, tmp_path: Path) -> None: + test_file = tmp_path / "temp.avif" + im = Image.new("RGB", (0, 0)) + with pytest.raises(ValueError): + im.save(test_file) + + def test_encoder_finish_none_error( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Save should raise an OSError if AvifEncoder.finish returns None""" + + class _mock_avif: + class AvifEncoder: + def __init__(self, *args: Any) -> None: + pass + + def add(self, *args: Any) -> None: + pass + + def finish(self) -> None: + return None + + monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif) + + im = Image.new("RGB", (150, 150)) + test_file = tmp_path / "temp.avif" + with pytest.raises(OSError): + im.save(test_file) + + def test_no_resource_warning(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + with warnings.catch_warnings(): + warnings.simplefilter("error") + + im.save(tmp_path / "temp.avif") + + @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) + def test_accept_ftyp_brands(self, major_brand: bytes) -> None: + data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand + assert AvifImagePlugin._accept(data) is True + + def test_file_pointer_could_be_reused(self) -> None: + with open(TEST_AVIF_FILE, "rb") as blob: + with Image.open(blob) as im: + im.load() + with Image.open(blob) as im: + im.load() + + def test_background_from_gif(self, tmp_path: Path) -> None: + with Image.open("Tests/images/chi.gif") as im: + original_value = im.convert("RGB").getpixel((1, 1)) + + # Save as AVIF + out_avif = tmp_path / "temp.avif" + im.save(out_avif, save_all=True) + + # Save as GIF + out_gif = tmp_path / "temp.gif" + with Image.open(out_avif) as im: + im.save(out_gif) + + with Image.open(out_gif) as reread: + reread_value = reread.convert("RGB").getpixel((1, 1)) + difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) + assert difference <= 3 + + def test_save_single_frame(self, tmp_path: Path) -> None: + temp_file = tmp_path / "temp.avif" + with Image.open("Tests/images/chi.gif") as im: + im.save(temp_file) + with Image.open(temp_file) as im: + assert im.n_frames == 1 + + def test_invalid_file(self) -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + AvifImagePlugin.AvifImageFile(invalid_file) + + def test_load_transparent_rgb(self) -> None: + test_file = "Tests/images/avif/transparency.avif" + with Image.open(test_file) as im: + assert_image(im, "RGBA", (64, 64)) + + # image has 876 transparent pixels + assert im.getchannel("A").getcolors()[0] == (876, 0) + + def test_save_transparent(self, tmp_path: Path) -> None: + im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + test_file = tmp_path / "temp.avif" + im.save(test_file) + + # check if saved image contains the same transparency + with Image.open(test_file) as im: + assert_image(im, "RGBA", (10, 10)) + assert im.getcolors() == [(100, (0, 0, 0, 0))] + + def test_save_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert "icc_profile" not in im.info + + with Image.open("Tests/images/avif/icc_profile.avif") as with_icc: + expected_icc = with_icc.info["icc_profile"] + assert expected_icc is not None + + im = roundtrip(im, icc_profile=expected_icc) + assert im.info["icc_profile"] == expected_icc + + def test_discard_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile.avif") as im: + im = roundtrip(im, icc_profile=None) + assert "icc_profile" not in im.info + + def test_roundtrip_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile.avif") as im: + expected_icc = im.info["icc_profile"] + + im = roundtrip(im) + assert im.info["icc_profile"] == expected_icc + + def test_roundtrip_no_icc_profile(self) -> None: + with Image.open("Tests/images/avif/icc_profile_none.avif") as im: + assert "icc_profile" not in im.info + + im = roundtrip(im) + assert "icc_profile" not in im.info + + def test_exif(self) -> None: + # With an EXIF chunk + with Image.open("Tests/images/avif/exif.avif") as im: + exif = im.getexif() + assert exif[274] == 1 + + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + exif = im.getexif() + assert exif[274] == 3 + + @pytest.mark.parametrize("use_bytes", [True, False]) + @pytest.mark.parametrize("orientation", [1, 2]) + def test_exif_save( + self, + tmp_path: Path, + use_bytes: bool, + orientation: int, + ) -> None: + exif = Image.Exif() + exif[274] = orientation + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, exif=exif_data if use_bytes else exif) + + with Image.open(test_file) as reloaded: + if orientation == 1: + assert "exif" not in reloaded.info + else: + assert reloaded.info["exif"] == exif_data + + def test_exif_without_orientation(self, tmp_path: Path) -> None: + exif = Image.Exif() + exif[272] = b"test" + exif_data = exif.tobytes() + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, exif=exif) + + with Image.open(test_file) as reloaded: + assert reloaded.info["exif"] == exif_data + + def test_exif_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(SyntaxError): + im.save(test_file, exif=b"invalid") + + @pytest.mark.parametrize( + "rot, mir, exif_orientation", + [ + (0, 0, 4), + (0, 1, 2), + (1, 0, 5), + (1, 1, 7), + (2, 0, 2), + (2, 1, 4), + (3, 0, 7), + (3, 1, 5), + ], + ) + def test_rot_mir_exif( + self, rot: int, mir: int, exif_orientation: int, tmp_path: Path + ) -> None: + with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im: + exif = im.getexif() + assert exif[274] == exif_orientation + + test_file = tmp_path / "temp.avif" + im.save(test_file, exif=exif) + with Image.open(test_file) as reloaded: + assert reloaded.getexif()[274] == exif_orientation + + def test_xmp(self) -> None: + with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: + xmp = im.info["xmp"] + assert_xmp_orientation(xmp, 3) + + def test_xmp_save(self, tmp_path: Path) -> None: + xmp_arg = "\n".join( + [ + '', + '', + ' ', + ' ', + " ", + "", + '', + ] + ) + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, xmp=xmp_arg) + + with Image.open(test_file) as reloaded: + xmp = reloaded.info["xmp"] + assert_xmp_orientation(xmp, 1) + + def test_tell(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + assert im.tell() == 0 + + def test_seek(self) -> None: + with Image.open(TEST_AVIF_FILE) as im: + im.seek(0) + + with pytest.raises(EOFError): + im.seek(1) + + @pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:2:0", "4:0:0"]) + def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, subsampling=subsampling) + + def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, subsampling="foo") + + @pytest.mark.parametrize("value", ["full", "limited"]) + def test_encoder_range(self, tmp_path: Path, value: str) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, range=value) + + def test_encoder_range_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, range="foo") + + @skip_unless_avif_encoder("aom") + def test_encoder_codec_param(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + im.save(test_file, codec="aom") + + def test_encoder_codec_invalid(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, codec="foo") + + @skip_unless_avif_decoder("dav1d") + def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, codec="dav1d") + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize( + "advanced", + [ + { + "aq-mode": "1", + "enable-chroma-deltaq": "1", + }, + (("aq-mode", "1"), ("enable-chroma-deltaq", "1")), + [("aq-mode", "1"), ("enable-chroma-deltaq", "1")], + ], + ) + def test_encoder_advanced_codec_options( + self, advanced: dict[str, str] | Sequence[tuple[str, str]] + ) -> None: + with Image.open(TEST_AVIF_FILE) as im: + ctrl_buf = BytesIO() + im.save(ctrl_buf, "AVIF", codec="aom") + test_buf = BytesIO() + im.save( + test_buf, + "AVIF", + codec="aom", + advanced=advanced, + ) + assert ctrl_buf.getvalue() != test_buf.getvalue() + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234]) + def test_encoder_advanced_codec_options_invalid( + self, tmp_path: Path, advanced: dict[str, str] | int + ) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, codec="aom", advanced=advanced) + + @skip_unless_avif_decoder("aom") + def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom") + + with Image.open(TEST_AVIF_FILE) as im: + assert im.size == (128, 128) + + @skip_unless_avif_encoder("rav1e") + def test_encoder_codec_cannot_decode( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e") + + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + + def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo") + + with pytest.raises(ValueError): + with Image.open(TEST_AVIF_FILE): + pass + + @skip_unless_avif_encoder("aom") + def test_encoder_codec_available(self) -> None: + assert _avif.encoder_codec_available("aom") is True + + def test_encoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _avif.encoder_codec_available() + + @skip_unless_avif_decoder("dav1d") + def test_encoder_codec_available_cannot_decode(self) -> None: + assert _avif.encoder_codec_available("dav1d") is False + + def test_encoder_codec_available_invalid(self) -> None: + assert _avif.encoder_codec_available("foo") is False + + def test_encoder_quality_valueerror(self, tmp_path: Path) -> None: + with Image.open(TEST_AVIF_FILE) as im: + test_file = tmp_path / "temp.avif" + with pytest.raises(ValueError): + im.save(test_file, quality="invalid") + + @skip_unless_avif_decoder("aom") + def test_decoder_codec_available(self) -> None: + assert _avif.decoder_codec_available("aom") is True + + def test_decoder_codec_available_bad_params(self) -> None: + with pytest.raises(TypeError): + _avif.decoder_codec_available() + + @skip_unless_avif_encoder("rav1e") + def test_decoder_codec_available_cannot_decode(self) -> None: + assert _avif.decoder_codec_available("rav1e") is False + + def test_decoder_codec_available_invalid(self) -> None: + assert _avif.decoder_codec_available("foo") is False + + def test_p_mode_transparency(self, tmp_path: Path) -> None: + im = Image.new("P", size=(64, 64)) + draw = ImageDraw.Draw(im) + draw.rectangle(xy=[(0, 0), (32, 32)], fill=255) + draw.rectangle(xy=[(32, 32), (64, 64)], fill=255) + + out_png = tmp_path / "temp.png" + im.save(out_png, transparency=0) + with Image.open(out_png) as im_png: + out_avif = tmp_path / "temp.avif" + im_png.save(out_avif, quality=100) + + with Image.open(out_avif) as expected: + assert_image_similar(im_png.convert("RGBA"), expected, 0.17) + + def test_decoder_strict_flags(self) -> None: + # This would fail if full avif strictFlags were enabled + with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im: + assert im.size == (128, 128) + + @skip_unless_avif_encoder("aom") + @pytest.mark.parametrize("speed", [-1, 1, 11]) + def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None: + test_file = tmp_path / "temp.avif" + hopper().save(test_file, codec="aom", speed=speed) + + @skip_unless_avif_encoder("svt") + def test_svt_optimizations(self, tmp_path: Path) -> None: + test_file = tmp_path / "temp.avif" + hopper().save(test_file, codec="svt", speed=1) + + +@skip_unless_feature("avif") +class TestAvifAnimation: + @contextmanager + def star_frames(self) -> Generator[list[Image.Image], None, None]: + with Image.open("Tests/images/avif/star.png") as f: + yield [f, f.rotate(90), f.rotate(180), f.rotate(270)] + + def test_n_frames(self) -> None: + """ + Ensure that AVIF format sets n_frames and is_animated attributes + correctly. + """ + + with Image.open(TEST_AVIF_FILE) as im: + assert im.n_frames == 1 + assert not im.is_animated + + with Image.open("Tests/images/avif/star.avifs") as im: + assert im.n_frames == 5 + assert im.is_animated + + def test_write_animation_P(self, tmp_path: Path) -> None: + """ + Convert an animated GIF to animated AVIF, then compare the frame + count, and ensure the frames are visually similar to the originals. + """ + + with Image.open("Tests/images/avif/star.gif") as original: + assert original.n_frames > 1 + + temp_file = tmp_path / "temp.avif" + original.save(temp_file, save_all=True) + with Image.open(temp_file) as im: + assert im.n_frames == original.n_frames + + # Compare first frame in P mode to frame from original GIF + assert_image_similar(im, original.convert("RGBA"), 2) + + # Compare later frames in RGBA mode to frames from original GIF + for frame in range(1, original.n_frames): + original.seek(frame) + im.seek(frame) + assert_image_similar(im, original, 2.54) + + def test_write_animation_RGBA(self, tmp_path: Path) -> None: + """ + Write an animated AVIF from RGBA frames, and ensure the frames + are visually similar to the originals. + """ + + def check(temp_file: Path) -> None: + with Image.open(temp_file) as im: + assert im.n_frames == 4 + + # Compare first frame to original + assert_image_similar(im, frame1, 2.7) + + # Compare second frame to original + im.seek(1) + assert_image_similar(im, frame2, 4.1) + + with self.star_frames() as frames: + frame1 = frames[0] + frame2 = frames[1] + temp_file1 = tmp_path / "temp.avif" + frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:]) + check(temp_file1) + + # Test appending using a generator + def imGenerator( + ims: list[Image.Image], + ) -> Generator[Image.Image, None, None]: + yield from ims + + temp_file2 = tmp_path / "temp_generator.avif" + frames[0].copy().save( + temp_file2, + save_all=True, + append_images=imGenerator(frames[1:]), + ) + check(temp_file2) + + def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None: + temp_file = tmp_path / "temp.avif" + frame1 = Image.new("RGB", (100, 100)) + frame2 = Image.new("RGB", (150, 150)) + with pytest.raises(ValueError): + frame1.save(temp_file, save_all=True, append_images=[frame2]) + + def test_heif_raises_unidentified_image_error(self) -> None: + with pytest.raises(UnidentifiedImageError): + with Image.open("Tests/images/avif/hopper.heif"): + pass + + @pytest.mark.parametrize("alpha_premultiplied", [False, True]) + def test_alpha_premultiplied( + self, tmp_path: Path, alpha_premultiplied: bool + ) -> None: + temp_file = tmp_path / "temp.avif" + color = (200, 200, 200, 1) + im = Image.new("RGBA", (1, 1), color) + im.save(temp_file, alpha_premultiplied=alpha_premultiplied) + + expected = (255, 255, 255, 1) if alpha_premultiplied else color + with Image.open(temp_file) as reloaded: + assert reloaded.getpixel((0, 0)) == expected + + def test_timestamp_and_duration(self, tmp_path: Path) -> None: + """ + Try passing a list of durations, and make sure the encoded + timestamps and durations are correct. + """ + + durations = [1, 10, 20, 30, 40] + temp_file = tmp_path / "temp.avif" + with self.star_frames() as frames: + frames[0].save( + temp_file, + save_all=True, + append_images=(frames[1:] + [frames[0]]), + duration=durations, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated + + # Check that timestamps and durations match original values specified + timestamp = 0 + for frame in range(im.n_frames): + im.seek(frame) + im.load() + assert im.info["duration"] == durations[frame] + assert im.info["timestamp"] == timestamp + timestamp += durations[frame] + + def test_seeking(self, tmp_path: Path) -> None: + """ + Create an animated AVIF file, and then try seeking through frames in + reverse-order, verifying the timestamps and durations are correct. + """ + + duration = 33 + temp_file = tmp_path / "temp.avif" + with self.star_frames() as frames: + frames[0].save( + temp_file, + save_all=True, + append_images=(frames[1:] + [frames[0]]), + duration=duration, + ) + + with Image.open(temp_file) as im: + assert im.n_frames == 5 + assert im.is_animated + + # Traverse frames in reverse, checking timestamps and durations + timestamp = duration * (im.n_frames - 1) + for frame in reversed(range(im.n_frames)): + im.seek(frame) + im.load() + assert im.info["duration"] == duration + assert im.info["timestamp"] == timestamp + timestamp -= duration + + def test_seek_errors(self) -> None: + with Image.open("Tests/images/avif/star.avifs") as im: + with pytest.raises(EOFError): + im.seek(-1) + + with pytest.raises(EOFError): + im.seek(42) + + +MAX_THREADS = os.cpu_count() or 1 + + +@skip_unless_feature("avif") +class TestAvifLeaks(PillowLeakTestCase): + mem_limit = MAX_THREADS * 3 * 1024 + iterations = 100 + + @pytest.mark.skipif( + is_docker_qemu(), reason="Skipping on cross-architecture containers" + ) + def test_leak_load(self) -> None: + with open(TEST_AVIF_FILE, "rb") as f: + im_data = f.read() + + def core() -> None: + with Image.open(BytesIO(im_data)) as im: + im.load() + gc.collect() + + self._test_leak(core) diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh new file mode 100755 index 000000000..fc10d3e54 --- /dev/null +++ b/depends/install_libavif.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -eo pipefail + +version=1.2.1 + +./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz + +pushd libavif-$version + +if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then + PREFIX=$(brew --prefix) +else + PREFIX=/usr +fi + +PKGCONFIG=${PKGCONFIG:-pkg-config} + +LIBAVIF_CMAKE_FLAGS=() +HAS_DECODER=0 +HAS_ENCODER=0 + +if $PKGCONFIG --exists aom; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) + HAS_ENCODER=1 + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists libgav1; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) + HAS_DECODER=1 +fi + +if $PKGCONFIG --exists rav1e; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) + HAS_ENCODER=1 +fi + +if $PKGCONFIG --exists SvtAv1Enc; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) + HAS_ENCODER=1 +fi + +if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) +fi + +cmake \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MACOSX_RPATH=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + "${LIBAVIF_CMAKE_FLAGS[@]}" \ + . + +sudo make install + +popd diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index c0b1a9d4e..bfa462c04 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``. Fully supported formats ----------------------- +AVIF +^^^^ + +Pillow reads and writes AVIF files, including AVIF sequence images. +It is only possible to save 8-bit AVIF images, and all AVIF images are decoded +as 8-bit RGB(A). + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**quality** + Integer, 0-100, defaults to 75. 0 gives the smallest size and poorest + quality, 100 the largest size and best quality. + +**subsampling** + If present, sets the subsampling for the encoder. Defaults to ``4:2:0``. + Options include: + + * ``4:0:0`` + * ``4:2:0`` + * ``4:2:2`` + * ``4:4:4`` + +**speed** + Quality/speed trade-off (0=slower/better, 10=fastest). Defaults to 6. + +**max_threads** + Limit the number of active threads used. By default, there is no limit. If the aom + codec is used, there is a maximum of 64. + +**range** + YUV range, either "full" or "limited". Defaults to "full". + +**codec** + AV1 codec to use for encoding. Specific values are "aom", "rav1e", and + "svt", presuming the chosen codec is available. Defaults to "auto", which + will choose the first available codec in the order of the preceding list. + +**tile_rows** / **tile_cols** + For tile encoding, the (log 2) number of tile rows and columns to use. + Valid values are 0-6, default 0. Ignored if "autotiling" is set to true. + +**autotiling** + Split the image up to allow parallelization. Enabled automatically if "tile_rows" + and "tile_cols" both have their default values of zero. + +**alpha_premultiplied** + Encode the image with premultiplied alpha. Defaults to ``False``. + +**advanced** + Codec specific options. + +**icc_profile** + The ICC Profile to include in the saved file. + +**exif** + The exif data to include in the saved file. + +**xmp** + The XMP data to include in the saved file. + +Saving sequences +~~~~~~~~~~~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save` to write an AVIF file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +options will also be available. + +**append_images** + A list of images to append as additional frames. Each of the + images in the list can be single or multiframe images. + +**duration** + The display duration of each frame, in milliseconds. Pass a single + integer for a constant duration, or a list or tuple to set the + duration for each frame separately. + BLP ^^^ @@ -242,7 +319,7 @@ following options are available:: **append_images** A list of images to append as additional frames. Each of the images in the list can be single or multiframe images. - This is currently supported for GIF, PDF, PNG, TIFF, and WebP. + This is supported for AVIF, GIF, PDF, PNG, TIFF and WebP. It is also supported for ICO and ICNS. If images are passed in of relevant sizes, they will be used instead of scaling down the main image. diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 2790bc2e6..9f953e718 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -89,6 +89,14 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. +* **libavif** provides support for the AVIF format. + + * Pillow requires libavif version **1.0.0** or greater. + * libavif is merely an API that wraps AVIF codecs. If you are compiling + libavif from source, you will also need to install both an AVIF encoder + and decoder, such as rav1e and dav1d, or libaom, which both encodes and + decodes AVIF images. + .. tab:: Linux If you didn't build Python from source, make sure you have Python's @@ -117,6 +125,12 @@ Many of Pillow's features require external libraries: To install libraqm, ``sudo apt-get install meson`` and then see ``depends/install_raqm.sh``. + Build prerequisites for libavif on Ubuntu are installed with:: + + sudo apt-get install cmake ninja-build nasm + + Then see ``depends/install_libavif.sh`` to build and install libavif. + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ @@ -148,7 +162,15 @@ Many of Pillow's features require external libraries: The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - brew install libjpeg libraqm libtiff little-cms2 openjpeg webp + brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp + + If you would like to use libavif with more codecs than just aom, then + instead of installing libavif through Homebrew directly, you can use + Homebrew to install libavif's build dependencies:: + + brew install aom dav1d rav1e svt-av1 + + Then see ``depends/install_libavif.sh`` to install libavif. .. tab:: Windows @@ -187,7 +209,8 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm + mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libavif .. tab:: FreeBSD @@ -199,7 +222,7 @@ Many of Pillow's features require external libraries: Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif Then see ``depends/install_raqm_cmake.sh`` to install libraqm. diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 0e173fe87..c5d89b838 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -21,6 +21,7 @@ Support for the following modules can be checked: * ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. * ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. * ``webp``: WebP image support. +* ``avif``: AVIF image support. .. autofunction:: PIL.features.check_module .. autofunction:: PIL.features.version_module diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 454b94d8c..c789f5757 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,6 +1,14 @@ Plugin reference ================ +:mod:`~PIL.AvifImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.AvifImagePlugin + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL.BmpImagePlugin` Module --------------------------------- diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index d40d86f21..dbaa8a4a4 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -68,3 +68,12 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT5, BC2, BC3 and BC5 are supported:: im.save("out.dds", pixel_format="DXT1") + +Other Changes +============= + +Reading and writing AVIF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now read and write AVIF images. If you are building Pillow from source, this +will require libavif 1.0.0 or later. diff --git a/setup.py b/setup.py index 9fac993b1..9d69b1d6e 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ configuration: dict[str, list[str]] = {} PILLOW_VERSION = get_version() +AVIF_ROOT = None FREETYPE_ROOT = None HARFBUZZ_ROOT = None FRIBIDI_ROOT = None @@ -306,6 +307,7 @@ class pil_build_ext(build_ext): "jpeg2000", "imagequant", "xcb", + "avif", ] required = {"jpeg", "zlib"} @@ -481,6 +483,7 @@ class pil_build_ext(build_ext): # # add configured kits for root_name, lib_name in { + "AVIF_ROOT": "avif", "JPEG_ROOT": "libjpeg", "JPEG2K_ROOT": "libopenjp2", "TIFF_ROOT": ("libtiff-5", "libtiff-4"), @@ -846,6 +849,12 @@ class pil_build_ext(build_ext): if _find_library_file(self, "xcb"): feature.set("xcb", "xcb") + if feature.want("avif"): + _dbg("Looking for avif") + if _find_include_file(self, "avif/avif.h"): + if _find_library_file(self, "avif"): + feature.set("avif", "avif") + for f in feature: if not feature.get(f) and feature.require(f): if f in ("jpeg", "zlib"): @@ -934,6 +943,14 @@ class pil_build_ext(build_ext): else: self._remove_extension("PIL._webp") + if feature.get("avif"): + libs = [feature.get("avif")] + if sys.platform == "win32": + libs.extend(["ntdll", "userenv", "ws2_32", "bcrypt"]) + self._update_extension("PIL._avif", libs) + else: + self._remove_extension("PIL._avif") + tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] self._update_extension("PIL._imagingtk", tk_libs) @@ -976,6 +993,7 @@ class pil_build_ext(build_ext): (feature.get("lcms"), "LITTLECMS2"), (feature.get("webp"), "WEBP"), (feature.get("xcb"), "XCB (X protocol)"), + (feature.get("avif"), "LIBAVIF"), ] all = 1 @@ -1018,6 +1036,7 @@ ext_modules = [ Extension("PIL._imagingft", ["src/_imagingft.c"]), Extension("PIL._imagingcms", ["src/_imagingcms.c"]), Extension("PIL._webp", ["src/_webp.c"]), + Extension("PIL._avif", ["src/_avif.c"]), Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), Extension("PIL._imagingmath", ["src/_imagingmath.c"]), Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py new file mode 100644 index 000000000..b2c5ab15d --- /dev/null +++ b/src/PIL/AvifImagePlugin.py @@ -0,0 +1,292 @@ +from __future__ import annotations + +import os +from io import BytesIO +from typing import IO + +from . import ExifTags, Image, ImageFile + +try: + from . import _avif + + SUPPORTED = True +except ImportError: + SUPPORTED = False + +# Decoder options as module globals, until there is a way to pass parameters +# to Image.open (see https://github.com/python-pillow/Pillow/issues/569) +DECODE_CODEC_CHOICE = "auto" +# Decoding is only affected by this for libavif **0.8.4** or greater. +DEFAULT_MAX_THREADS = 0 + + +def get_codec_version(codec_name: str) -> str | None: + versions = _avif.codec_versions() + for version in versions.split(", "): + if version.split(" [")[0] == codec_name: + return version.split(":")[-1].split(" ")[0] + return None + + +def _accept(prefix: bytes) -> bool | str: + if prefix[4:8] != b"ftyp": + return False + major_brand = prefix[8:12] + if major_brand in ( + # coding brands + b"avif", + b"avis", + # We accept files with AVIF container brands; we can't yet know if + # the ftyp box has the correct compatible brands, but if it doesn't + # then the plugin will raise a SyntaxError which Pillow will catch + # before moving on to the next plugin that accepts the file. + # + # Also, because this file might not actually be an AVIF file, we + # don't raise an error if AVIF support isn't properly compiled. + b"mif1", + b"msf1", + ): + if not SUPPORTED: + return ( + "image file could not be identified because AVIF support not installed" + ) + return True + return False + + +def _get_default_max_threads() -> int: + if DEFAULT_MAX_THREADS: + return DEFAULT_MAX_THREADS + if hasattr(os, "sched_getaffinity"): + return len(os.sched_getaffinity(0)) + else: + return os.cpu_count() or 1 + + +class AvifImageFile(ImageFile.ImageFile): + format = "AVIF" + format_description = "AVIF image" + __frame = -1 + + def _open(self) -> None: + if not SUPPORTED: + msg = "image file could not be opened because AVIF support not installed" + raise SyntaxError(msg) + + if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available( + DECODE_CODEC_CHOICE + ): + msg = "Invalid opening codec" + raise ValueError(msg) + self._decoder = _avif.AvifDecoder( + self.fp.read(), + DECODE_CODEC_CHOICE, + _get_default_max_threads(), + ) + + # Get info from decoder + self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = ( + self._decoder.get_info() + ) + self.is_animated = self.n_frames > 1 + + if icc: + self.info["icc_profile"] = icc + if xmp: + self.info["xmp"] = xmp + + if exif_orientation != 1 or exif: + exif_data = Image.Exif() + if exif: + exif_data.load(exif) + original_orientation = exif_data.get(ExifTags.Base.Orientation, 1) + else: + original_orientation = 1 + if exif_orientation != original_orientation: + exif_data[ExifTags.Base.Orientation] = exif_orientation + exif = exif_data.tobytes() + if exif: + self.info["exif"] = exif + self.seek(0) + + def seek(self, frame: int) -> None: + if not self._seek_check(frame): + return + + # Set tile + self.__frame = frame + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] + + def load(self) -> Image.core.PixelAccess | None: + if self.tile: + # We need to load the image data for this frame + data, timescale, pts_in_timescales, duration_in_timescales = ( + self._decoder.get_frame(self.__frame) + ) + self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale)) + self.info["duration"] = round(1000 * (duration_in_timescales / timescale)) + + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + + return super().load() + + def load_seek(self, pos: int) -> None: + pass + + def tell(self) -> int: + return self.__frame + + +def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + _save(im, fp, filename, save_all=True) + + +def _save( + im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False +) -> None: + info = im.encoderinfo.copy() + if save_all: + append_images = list(info.get("append_images", [])) + else: + append_images = [] + + total = 0 + for ims in [im] + append_images: + total += getattr(ims, "n_frames", 1) + + quality = info.get("quality", 75) + if not isinstance(quality, int) or quality < 0 or quality > 100: + msg = "Invalid quality setting" + raise ValueError(msg) + + duration = info.get("duration", 0) + subsampling = info.get("subsampling", "4:2:0") + speed = info.get("speed", 6) + max_threads = info.get("max_threads", _get_default_max_threads()) + codec = info.get("codec", "auto") + if codec != "auto" and not _avif.encoder_codec_available(codec): + msg = "Invalid saving codec" + raise ValueError(msg) + range_ = info.get("range", "full") + tile_rows_log2 = info.get("tile_rows", 0) + tile_cols_log2 = info.get("tile_cols", 0) + alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) + autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) + + icc_profile = info.get("icc_profile", im.info.get("icc_profile")) + exif_orientation = 1 + if exif := info.get("exif"): + if isinstance(exif, Image.Exif): + exif_data = exif + else: + exif_data = Image.Exif() + exif_data.load(exif) + if ExifTags.Base.Orientation in exif_data: + exif_orientation = exif_data.pop(ExifTags.Base.Orientation) + exif = exif_data.tobytes() if exif_data else b"" + elif isinstance(exif, Image.Exif): + exif = exif_data.tobytes() + + xmp = info.get("xmp") + + if isinstance(xmp, str): + xmp = xmp.encode("utf-8") + + advanced = info.get("advanced") + if advanced is not None: + if isinstance(advanced, dict): + advanced = advanced.items() + try: + advanced = tuple(advanced) + except TypeError: + invalid = True + else: + invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced) + if invalid: + msg = ( + "advanced codec options must be a dict of key-value string " + "pairs or a series of key-value two-tuples" + ) + raise ValueError(msg) + + # Setup the AVIF encoder + enc = _avif.AvifEncoder( + im.size, + subsampling, + quality, + speed, + max_threads, + codec, + range_, + tile_rows_log2, + tile_cols_log2, + alpha_premultiplied, + autotiling, + icc_profile or b"", + exif or b"", + exif_orientation, + xmp or b"", + advanced, + ) + + # Add each frame + frame_idx = 0 + frame_duration = 0 + cur_idx = im.tell() + is_single_frame = total == 1 + try: + for ims in [im] + append_images: + # Get number of frames in this image + nfr = getattr(ims, "n_frames", 1) + + for idx in range(nfr): + ims.seek(idx) + + # Make sure image mode is supported + frame = ims + rawmode = ims.mode + if ims.mode not in {"RGB", "RGBA"}: + rawmode = "RGBA" if ims.has_transparency_data else "RGB" + frame = ims.convert(rawmode) + + # Update frame duration + if isinstance(duration, (list, tuple)): + frame_duration = duration[frame_idx] + else: + frame_duration = duration + + # Append the frame to the animation encoder + enc.add( + frame.tobytes("raw", rawmode), + frame_duration, + frame.size, + rawmode, + is_single_frame, + ) + + # Update frame index + frame_idx += 1 + + if not save_all: + break + + finally: + im.seek(cur_idx) + + # Get the final output from the encoder + data = enc.finish() + if data is None: + msg = "cannot write file as AVIF (encoder returned None)" + raise OSError(msg) + + fp.write(data) + + +Image.register_open(AvifImageFile.format, AvifImageFile, _accept) +if SUPPORTED: + Image.register_save(AvifImageFile.format, _save) + Image.register_save_all(AvifImageFile.format, _save_all) + Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) + Image.register_mime(AvifImageFile.format, "image/avif") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 19b22342a..60850f4ff 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1520,6 +1520,8 @@ class Image: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") + if not xmp_tags and (xmp_tags := self.info.get("xmp")): + xmp_tags = xmp_tags.decode("utf-8") if xmp_tags: match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 09546fe63..6e4c23f89 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -25,6 +25,7 @@ del _version _plugins = [ + "AvifImagePlugin", "BlpImagePlugin", "BmpImagePlugin", "BufrStubImagePlugin", diff --git a/src/PIL/_avif.pyi b/src/PIL/_avif.pyi new file mode 100644 index 000000000..e27843e53 --- /dev/null +++ b/src/PIL/_avif.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/features.py b/src/PIL/features.py index ae7ea4255..573f1d412 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -17,6 +17,7 @@ modules = { "freetype2": ("PIL._imagingft", "freetype2_version"), "littlecms2": ("PIL._imagingcms", "littlecms_version"), "webp": ("PIL._webp", "webpdecoder_version"), + "avif": ("PIL._avif", "libavif_version"), } @@ -288,6 +289,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), + ("avif", "AVIF"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), diff --git a/src/_avif.c b/src/_avif.c new file mode 100644 index 000000000..eabd9958e --- /dev/null +++ b/src/_avif.c @@ -0,0 +1,908 @@ +#define PY_SSIZE_T_CLEAN + +#include +#include "avif/avif.h" + +// Encoder type +typedef struct { + PyObject_HEAD avifEncoder *encoder; + avifImage *image; + int first_frame; +} AvifEncoderObject; + +static PyTypeObject AvifEncoder_Type; + +// Decoder type +typedef struct { + PyObject_HEAD avifDecoder *decoder; + Py_buffer buffer; +} AvifDecoderObject; + +static PyTypeObject AvifDecoder_Type; + +static int +normalize_tiles_log2(int value) { + if (value < 0) { + return 0; + } else if (value > 6) { + return 6; + } else { + return value; + } +} + +static PyObject * +exc_type_for_avif_result(avifResult result) { + switch (result) { + case AVIF_RESULT_INVALID_EXIF_PAYLOAD: + case AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION: + return PyExc_ValueError; + case AVIF_RESULT_INVALID_FTYP: + case AVIF_RESULT_BMFF_PARSE_FAILED: + case AVIF_RESULT_TRUNCATED_DATA: + case AVIF_RESULT_NO_CONTENT: + return PyExc_SyntaxError; + default: + return PyExc_RuntimeError; + } +} + +static uint8_t +irot_imir_to_exif_orientation(const avifImage *image) { + uint8_t axis = image->imir.axis; + int imir = image->transformFlags & AVIF_TRANSFORM_IMIR; + int irot = image->transformFlags & AVIF_TRANSFORM_IROT; + if (irot) { + uint8_t angle = image->irot.angle; + if (angle == 1) { + if (imir) { + return axis ? 7 // 90 degrees anti-clockwise then swap left and right. + : 5; // 90 degrees anti-clockwise then swap top and bottom. + } + return 6; // 90 degrees anti-clockwise. + } + if (angle == 2) { + if (imir) { + return axis + ? 4 // 180 degrees anti-clockwise then swap left and right. + : 2; // 180 degrees anti-clockwise then swap top and bottom. + } + return 3; // 180 degrees anti-clockwise. + } + if (angle == 3) { + if (imir) { + return axis + ? 5 // 270 degrees anti-clockwise then swap left and right. + : 7; // 270 degrees anti-clockwise then swap top and bottom. + } + return 8; // 270 degrees anti-clockwise. + } + } + if (imir) { + return axis ? 2 // Swap left and right. + : 4; // Swap top and bottom. + } + return 1; // Default orientation ("top-left", no-op). +} + +static void +exif_orientation_to_irot_imir(avifImage *image, int orientation) { + // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A + // Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 + // sections 6.5.10 and 6.5.12. + switch (orientation) { + case 2: // The 0th row is at the visual top of the image, and the 0th column is + // the visual right-hand side. + image->transformFlags |= AVIF_TRANSFORM_IMIR; + image->imir.axis = 1; + break; + case 3: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual right-hand side. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 2; + break; + case 4: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual left-hand side. + image->transformFlags |= AVIF_TRANSFORM_IMIR; + break; + case 5: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 1; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 + break; + case 6: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 3; + break; + case 7: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 3; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 + break; + case 8: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags |= AVIF_TRANSFORM_IROT; + image->irot.angle = 1; + break; + } +} + +static int +_codec_available(const char *name, avifCodecFlags flags) { + avifCodecChoice codec = avifCodecChoiceFromName(name); + if (codec == AVIF_CODEC_CHOICE_AUTO) { + return 0; + } + const char *codec_name = avifCodecName(codec, flags); + return (codec_name == NULL) ? 0 : 1; +} + +PyObject * +_decoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_DECODE); + return PyBool_FromLong(is_available); +} + +PyObject * +_encoder_codec_available(PyObject *self, PyObject *args) { + char *codec_name; + if (!PyArg_ParseTuple(args, "s", &codec_name)) { + return NULL; + } + int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_ENCODE); + return PyBool_FromLong(is_available); +} + +PyObject * +_codec_versions(PyObject *self, PyObject *args) { + char buffer[256]; + avifCodecVersions(buffer); + return PyUnicode_FromString(buffer); +} + +static int +_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { + Py_ssize_t i, size; + PyObject *keyval, *py_key, *py_val; + if (!PyTuple_Check(opts)) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + size = PyTuple_GET_SIZE(opts); + + for (i = 0; i < size; i++) { + keyval = PyTuple_GetItem(opts, i); + if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + py_key = PyTuple_GetItem(keyval, 0); + py_val = PyTuple_GetItem(keyval, 1); + if (!PyUnicode_Check(py_key) || !PyUnicode_Check(py_val)) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + const char *key = PyUnicode_AsUTF8(py_key); + const char *val = PyUnicode_AsUTF8(py_val); + if (key == NULL || val == NULL) { + PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); + return 1; + } + + avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting advanced codec options failed: %s", + avifResultToString(result) + ); + return 1; + } + } + return 0; +} + +// Encoder functions +PyObject * +AvifEncoderNew(PyObject *self_, PyObject *args) { + unsigned int width, height; + AvifEncoderObject *self = NULL; + avifEncoder *encoder = NULL; + + char *subsampling; + int quality; + int speed; + int exif_orientation; + int max_threads; + Py_buffer icc_buffer; + Py_buffer exif_buffer; + Py_buffer xmp_buffer; + int alpha_premultiplied; + int autotiling; + int tile_rows_log2; + int tile_cols_log2; + + char *codec; + char *range; + + PyObject *advanced; + int error = 0; + + if (!PyArg_ParseTuple( + args, + "(II)siiissiippy*y*iy*O", + &width, + &height, + &subsampling, + &quality, + &speed, + &max_threads, + &codec, + &range, + &tile_rows_log2, + &tile_cols_log2, + &alpha_premultiplied, + &autotiling, + &icc_buffer, + &exif_buffer, + &exif_orientation, + &xmp_buffer, + &advanced + )) { + return NULL; + } + + // Create a new animation encoder and picture frame + avifImage *image = avifImageCreateEmpty(); + if (image == NULL) { + PyErr_SetString(PyExc_ValueError, "Image creation failed"); + error = 1; + goto end; + } + + // Set these in advance so any upcoming RGB -> YUV use the proper coefficients + if (strcmp(range, "full") == 0) { + image->yuvRange = AVIF_RANGE_FULL; + } else if (strcmp(range, "limited") == 0) { + image->yuvRange = AVIF_RANGE_LIMITED; + } else { + PyErr_SetString(PyExc_ValueError, "Invalid range"); + error = 1; + goto end; + } + if (strcmp(subsampling, "4:0:0") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; + } else if (strcmp(subsampling, "4:2:0") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420; + } else if (strcmp(subsampling, "4:2:2") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422; + } else if (strcmp(subsampling, "4:4:4") == 0) { + image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; + } else { + PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling); + error = 1; + goto end; + } + + // Validate canvas dimensions + if (width == 0 || height == 0) { + PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); + error = 1; + goto end; + } + image->width = width; + image->height = height; + + image->depth = 8; + image->alphaPremultiplied = alpha_premultiplied ? AVIF_TRUE : AVIF_FALSE; + + encoder = avifEncoderCreate(); + if (!encoder) { + PyErr_SetString(PyExc_MemoryError, "Can't allocate encoder"); + error = 1; + goto end; + } + + int is_aom_encode = strcmp(codec, "aom") == 0 || + (strcmp(codec, "auto") == 0 && + _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE)); + encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads; + + encoder->quality = quality; + + if (strcmp(codec, "auto") == 0) { + encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO; + } else { + encoder->codecChoice = avifCodecChoiceFromName(codec); + } + if (speed < AVIF_SPEED_SLOWEST) { + speed = AVIF_SPEED_SLOWEST; + } else if (speed > AVIF_SPEED_FASTEST) { + speed = AVIF_SPEED_FASTEST; + } + encoder->speed = speed; + encoder->timescale = (uint64_t)1000; + + encoder->autoTiling = autotiling ? AVIF_TRUE : AVIF_FALSE; + if (!autotiling) { + encoder->tileRowsLog2 = normalize_tiles_log2(tile_rows_log2); + encoder->tileColsLog2 = normalize_tiles_log2(tile_cols_log2); + } + + if (advanced != Py_None && _add_codec_specific_options(encoder, advanced)) { + error = 1; + goto end; + } + + self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); + error = 1; + goto end; + } + self->first_frame = 1; + + avifResult result; + if (icc_buffer.len) { + result = avifImageSetProfileICC(image, icc_buffer.buf, icc_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting ICC profile failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + // colorPrimaries and transferCharacteristics are ignored when an ICC + // profile is present, so set them to UNSPECIFIED. + image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + } else { + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + } + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + + if (exif_buffer.len) { + result = avifImageSetMetadataExif(image, exif_buffer.buf, exif_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting EXIF data failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + } + + if (xmp_buffer.len) { + result = avifImageSetMetadataXMP(image, xmp_buffer.buf, xmp_buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting XMP data failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + } + + if (exif_orientation > 1) { + exif_orientation_to_irot_imir(image, exif_orientation); + } + + self->image = image; + self->encoder = encoder; + +end: + PyBuffer_Release(&icc_buffer); + PyBuffer_Release(&exif_buffer); + PyBuffer_Release(&xmp_buffer); + + if (error) { + if (image) { + avifImageDestroy(image); + } + if (encoder) { + avifEncoderDestroy(encoder); + } + if (self) { + PyObject_Del(self); + } + return NULL; + } + + return (PyObject *)self; +} + +PyObject * +_encoder_dealloc(AvifEncoderObject *self) { + if (self->encoder) { + avifEncoderDestroy(self->encoder); + } + if (self->image) { + avifImageDestroy(self->image); + } + Py_RETURN_NONE; +} + +PyObject * +_encoder_add(AvifEncoderObject *self, PyObject *args) { + uint8_t *rgb_bytes; + Py_ssize_t size; + unsigned int duration; + unsigned int width; + unsigned int height; + char *mode; + unsigned int is_single_frame; + int error = 0; + + avifRGBImage rgb; + avifResult result; + + avifEncoder *encoder = self->encoder; + avifImage *image = self->image; + avifImage *frame = NULL; + + if (!PyArg_ParseTuple( + args, + "y#I(II)sp", + (char **)&rgb_bytes, + &size, + &duration, + &width, + &height, + &mode, + &is_single_frame + )) { + return NULL; + } + + if (image->width != width || image->height != height) { + PyErr_Format( + PyExc_ValueError, + "Image sequence dimensions mismatch, %ux%u != %ux%u", + image->width, + image->height, + width, + height + ); + return NULL; + } + + if (self->first_frame) { + // If we don't have an image populated with yuv planes, this is the first frame + frame = image; + } else { + frame = avifImageCreateEmpty(); + if (image == NULL) { + PyErr_SetString(PyExc_ValueError, "Image creation failed"); + return NULL; + } + + frame->width = width; + frame->height = height; + frame->colorPrimaries = image->colorPrimaries; + frame->transferCharacteristics = image->transferCharacteristics; + frame->matrixCoefficients = image->matrixCoefficients; + frame->yuvRange = image->yuvRange; + frame->yuvFormat = image->yuvFormat; + frame->depth = image->depth; + frame->alphaPremultiplied = image->alphaPremultiplied; + } + + avifRGBImageSetDefaults(&rgb, frame); + + if (strcmp(mode, "RGBA") == 0) { + rgb.format = AVIF_RGB_FORMAT_RGBA; + } else { + rgb.format = AVIF_RGB_FORMAT_RGB; + } + + result = avifRGBImageAllocatePixels(&rgb); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Pixel allocation failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + + if (rgb.rowBytes * rgb.height != size) { + PyErr_Format( + PyExc_RuntimeError, + "rgb data has incorrect size: %u * %u (%u) != %u", + rgb.rowBytes, + rgb.height, + rgb.rowBytes * rgb.height, + size + ); + error = 1; + goto end; + } + + // rgb.pixels is safe for writes + memcpy(rgb.pixels, rgb_bytes, size); + + Py_BEGIN_ALLOW_THREADS; + result = avifImageRGBToYUV(frame, &rgb); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion to YUV failed: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + + uint32_t addImageFlags = + is_single_frame ? AVIF_ADD_IMAGE_FLAG_SINGLE : AVIF_ADD_IMAGE_FLAG_NONE; + + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderAddImage(encoder, frame, duration, addImageFlags); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to encode image: %s", + avifResultToString(result) + ); + error = 1; + goto end; + } + +end: + if (&rgb) { + avifRGBImageFreePixels(&rgb); + } + if (!self->first_frame) { + avifImageDestroy(frame); + } + + if (error) { + return NULL; + } + self->first_frame = 0; + Py_RETURN_NONE; +} + +PyObject * +_encoder_finish(AvifEncoderObject *self) { + avifEncoder *encoder = self->encoder; + + avifRWData raw = AVIF_DATA_EMPTY; + avifResult result; + PyObject *ret = NULL; + + Py_BEGIN_ALLOW_THREADS; + result = avifEncoderFinish(encoder, &raw); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to finish encoding: %s", + avifResultToString(result) + ); + avifRWDataFree(&raw); + return NULL; + } + + ret = PyBytes_FromStringAndSize((char *)raw.data, raw.size); + + avifRWDataFree(&raw); + + return ret; +} + +// Decoder functions +PyObject * +AvifDecoderNew(PyObject *self_, PyObject *args) { + Py_buffer buffer; + AvifDecoderObject *self = NULL; + avifDecoder *decoder; + + char *codec_str; + avifCodecChoice codec; + int max_threads; + + avifResult result; + + if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) { + return NULL; + } + + if (strcmp(codec_str, "auto") == 0) { + codec = AVIF_CODEC_CHOICE_AUTO; + } else { + codec = avifCodecChoiceFromName(codec_str); + } + + self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type); + if (!self) { + PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); + PyBuffer_Release(&buffer); + return NULL; + } + + decoder = avifDecoderCreate(); + if (!decoder) { + PyErr_SetString(PyExc_MemoryError, "Can't allocate decoder"); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + decoder->maxThreads = max_threads; + // Turn off libavif's 'clap' (clean aperture) property validation. + decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID; + // Allow the PixelInformationProperty ('pixi') to be missing in AV1 image + // items. libheif v1.11.0 and older does not add the 'pixi' item property to + // AV1 image items. + decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; + decoder->codecChoice = codec; + + result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Setting IO memory failed: %s", + avifResultToString(result) + ); + avifDecoderDestroy(decoder); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + + result = avifDecoderParse(decoder); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode image: %s", + avifResultToString(result) + ); + avifDecoderDestroy(decoder); + PyBuffer_Release(&buffer); + PyObject_Del(self); + return NULL; + } + + self->decoder = decoder; + self->buffer = buffer; + + return (PyObject *)self; +} + +PyObject * +_decoder_dealloc(AvifDecoderObject *self) { + if (self->decoder) { + avifDecoderDestroy(self->decoder); + } + PyBuffer_Release(&self->buffer); + Py_RETURN_NONE; +} + +PyObject * +_decoder_get_info(AvifDecoderObject *self) { + avifDecoder *decoder = self->decoder; + avifImage *image = decoder->image; + + PyObject *icc = NULL; + PyObject *exif = NULL; + PyObject *xmp = NULL; + PyObject *ret = NULL; + + if (image->xmp.size) { + xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size); + } + + if (image->exif.size) { + exif = + PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); + } + + if (image->icc.size) { + icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); + } + + ret = Py_BuildValue( + "(II)IsSSIS", + image->width, + image->height, + decoder->imageCount, + decoder->alphaPresent ? "RGBA" : "RGB", + NULL == icc ? Py_None : icc, + NULL == exif ? Py_None : exif, + irot_imir_to_exif_orientation(image), + NULL == xmp ? Py_None : xmp + ); + + Py_XDECREF(xmp); + Py_XDECREF(exif); + Py_XDECREF(icc); + + return ret; +} + +PyObject * +_decoder_get_frame(AvifDecoderObject *self, PyObject *args) { + PyObject *bytes; + PyObject *ret; + Py_ssize_t size; + avifResult result; + avifRGBImage rgb; + avifDecoder *decoder; + avifImage *image; + uint32_t frame_index; + + decoder = self->decoder; + + if (!PyArg_ParseTuple(args, "I", &frame_index)) { + return NULL; + } + + result = avifDecoderNthImage(decoder, frame_index); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Failed to decode frame %u: %s", + frame_index, + avifResultToString(result) + ); + return NULL; + } + + image = decoder->image; + + avifRGBImageSetDefaults(&rgb, image); + + rgb.depth = 8; + rgb.format = decoder->alphaPresent ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; + + result = avifRGBImageAllocatePixels(&rgb); + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Pixel allocation failed: %s", + avifResultToString(result) + ); + return NULL; + } + + Py_BEGIN_ALLOW_THREADS; + result = avifImageYUVToRGB(image, &rgb); + Py_END_ALLOW_THREADS; + + if (result != AVIF_RESULT_OK) { + PyErr_Format( + exc_type_for_avif_result(result), + "Conversion from YUV failed: %s", + avifResultToString(result) + ); + avifRGBImageFreePixels(&rgb); + return NULL; + } + + if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) { + PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size"); + return NULL; + } + + size = rgb.rowBytes * rgb.height; + + bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); + avifRGBImageFreePixels(&rgb); + + ret = Py_BuildValue( + "SKKK", + bytes, + decoder->timescale, + decoder->imageTiming.ptsInTimescales, + decoder->imageTiming.durationInTimescales + ); + + Py_DECREF(bytes); + + return ret; +} + +/* -------------------------------------------------------------------- */ +/* Type Definitions */ +/* -------------------------------------------------------------------- */ + +// AvifEncoder methods +static struct PyMethodDef _encoder_methods[] = { + {"add", (PyCFunction)_encoder_add, METH_VARARGS}, + {"finish", (PyCFunction)_encoder_finish, METH_NOARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifEncoder type definition +static PyTypeObject AvifEncoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder", + .tp_basicsize = sizeof(AvifEncoderObject), + .tp_dealloc = (destructor)_encoder_dealloc, + .tp_methods = _encoder_methods, +}; + +// AvifDecoder methods +static struct PyMethodDef _decoder_methods[] = { + {"get_info", (PyCFunction)_decoder_get_info, METH_NOARGS}, + {"get_frame", (PyCFunction)_decoder_get_frame, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +// AvifDecoder type definition +static PyTypeObject AvifDecoder_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifDecoder", + .tp_basicsize = sizeof(AvifDecoderObject), + .tp_dealloc = (destructor)_decoder_dealloc, + .tp_methods = _decoder_methods, +}; + +/* -------------------------------------------------------------------- */ +/* Module Setup */ +/* -------------------------------------------------------------------- */ + +static PyMethodDef avifMethods[] = { + {"AvifDecoder", AvifDecoderNew, METH_VARARGS}, + {"AvifEncoder", AvifEncoderNew, METH_VARARGS}, + {"decoder_codec_available", _decoder_codec_available, METH_VARARGS}, + {"encoder_codec_available", _encoder_codec_available, METH_VARARGS}, + {"codec_versions", _codec_versions, METH_NOARGS}, + {NULL, NULL} +}; + +static int +setup_module(PyObject *m) { + if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) { + return -1; + } + + PyObject *d = PyModule_GetDict(m); + PyObject *v = PyUnicode_FromString(avifVersion()); + PyDict_SetItemString(d, "libavif_version", v ? v : Py_None); + Py_XDECREF(v); + + return 0; +} + +PyMODINIT_FUNC +PyInit__avif(void) { + PyObject *m; + + static PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + .m_name = "_avif", + .m_size = -1, + .m_methods = avifMethods, + }; + + m = PyModule_Create(&module_def); + if (setup_module(m) < 0) { + Py_DECREF(m); + return NULL; + } + +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + + return m; +} diff --git a/wheels/dependency_licenses/AOM.txt b/wheels/dependency_licenses/AOM.txt new file mode 100644 index 000000000..3a2e46c26 --- /dev/null +++ b/wheels/dependency_licenses/AOM.txt @@ -0,0 +1,26 @@ +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt new file mode 100644 index 000000000..875b138ec --- /dev/null +++ b/wheels/dependency_licenses/DAV1D.txt @@ -0,0 +1,23 @@ +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt new file mode 100644 index 000000000..350eb9d15 --- /dev/null +++ b/wheels/dependency_licenses/LIBAVIF.txt @@ -0,0 +1,387 @@ +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBYUV.txt b/wheels/dependency_licenses/LIBYUV.txt new file mode 100644 index 000000000..c911747a6 --- /dev/null +++ b/wheels/dependency_licenses/LIBYUV.txt @@ -0,0 +1,29 @@ +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/RAV1E.txt b/wheels/dependency_licenses/RAV1E.txt new file mode 100644 index 000000000..3d6c825c4 --- /dev/null +++ b/wheels/dependency_licenses/RAV1E.txt @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2017-2023, the rav1e contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/SVT-AV1.txt b/wheels/dependency_licenses/SVT-AV1.txt new file mode 100644 index 000000000..532a982b3 --- /dev/null +++ b/wheels/dependency_licenses/SVT-AV1.txt @@ -0,0 +1,26 @@ +Copyright (c) 2019, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/winbuild/build.rst b/winbuild/build.rst index aae78ce12..3c20c7d17 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -61,6 +61,7 @@ Run ``build_prepare.py`` to configure the build:: --no-imagequant skip GPL-licensed optional dependency libimagequant --no-fribidi, --no-raqm skip LGPL-licensed optional dependency FriBiDi + --no-avif skip optional dependency libavif Arguments can also be supplied using the environment variables PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2e9e18719..e4901859e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,6 +116,7 @@ V = { "HARFBUZZ": "11.0.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", + "LIBAVIF": "1.2.1", "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.47", "LIBWEBP": "1.5.0", @@ -378,6 +379,26 @@ DEPS: dict[str, dict[str, Any]] = { ], "bins": [r"*.dll"], }, + "libavif": { + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", + "filename": f"libavif-{V['LIBAVIF']}.zip", + "license": "LICENSE", + "build": [ + f"{sys.executable} -m pip install meson", + *cmds_cmake( + "avif_static", + "-DBUILD_SHARED_LIBS=OFF", + "-DAVIF_LIBSHARPYUV=LOCAL", + "-DAVIF_LIBYUV=LOCAL", + "-DAVIF_CODEC_AOM=LOCAL", + "-DAVIF_CODEC_DAV1D=LOCAL", + "-DAVIF_CODEC_RAV1E=LOCAL", + "-DAVIF_CODEC_SVT=LOCAL", + ), + cmd_xcopy("include", "{inc_dir}"), + ], + "libs": ["avif.lib"], + }, } @@ -683,6 +704,11 @@ def main() -> None: action="store_true", help="skip LGPL-licensed optional dependency FriBiDi", ) + parser.add_argument( + "--no-avif", + action="store_true", + help="skip optional dependency libavif", + ) args = parser.parse_args() arch_prefs = ARCHITECTURES[args.architecture] @@ -723,6 +749,8 @@ def main() -> None: disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] + if args.no_avif or args.architecture != "AMD64": + disabled += ["libavif"] prefs = { "architecture": args.architecture, From 81412212016a70eb160460e26dc552a0f8a8c153 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 08:35:19 +1100 Subject: [PATCH 421/628] Allow cmake<4 when building libtiff --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e4901859e..b45148ee8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -235,6 +235,7 @@ DEPS: dict[str, dict[str, Any]] = { "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ) ], "headers": [r"libtiff\tiff*.h"], From 348bf6550d3937d14bbd04251c12bed6dfed9eec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 16:33:55 +1100 Subject: [PATCH 422/628] Allow cmake<4 when building libavif --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b45148ee8..e118cd994 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -395,6 +395,7 @@ DEPS: dict[str, dict[str, Any]] = { "-DAVIF_CODEC_DAV1D=LOCAL", "-DAVIF_CODEC_RAV1E=LOCAL", "-DAVIF_CODEC_SVT=LOCAL", + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ), cmd_xcopy("include", "{inc_dir}"), ], From 5c76e7ec17813eefaa1fdd8948e0165ee644e11f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 1 Apr 2025 07:10:45 +0100 Subject: [PATCH 423/628] Image -> Arrow support (#8330) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .ci/install.sh | 3 + .github/workflows/macos-install.sh | 3 + .github/workflows/test-windows.yml | 4 + Tests/test_arrow.py | 164 ++++++++++++++++ Tests/test_pyarrow.py | 112 +++++++++++ docs/reference/Image.rst | 3 + docs/reference/arrow_support.rst | 88 +++++++++ docs/reference/block_allocator.rst | 3 + docs/reference/internal_design.rst | 1 + pyproject.toml | 5 + setup.py | 1 + src/PIL/Image.py | 80 ++++++++ src/_imaging.c | 115 +++++++++++ src/libImaging/Arrow.c | 299 +++++++++++++++++++++++++++++ src/libImaging/Arrow.h | 48 +++++ src/libImaging/Imaging.h | 38 ++++ src/libImaging/Storage.c | 199 ++++++++++++++++++- 17 files changed, 1165 insertions(+), 1 deletion(-) create mode 100644 Tests/test_arrow.py create mode 100644 Tests/test_pyarrow.py create mode 100644 docs/reference/arrow_support.rst create mode 100644 src/libImaging/Arrow.c create mode 100644 src/libImaging/Arrow.h diff --git a/.ci/install.sh b/.ci/install.sh index 83d5df01c..ba32eab04 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -36,6 +36,9 @@ python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma +# optional test dependency, only install if there's a binary package. +# fails on beta 3.14 and PyPy +python3 -m pip install --only-binary=:all: pyarrow || true if [[ $(uname) != CYGWIN* ]]; then python3 -m pip install numpy diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 099f4a582..94e3d5d08 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -30,6 +30,9 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install numpy +# optional test dependency, only install if there's a binary package. +# fails on beta 3.14 and PyPy +python3 -m pip install --only-binary=:all: pyarrow || true # libavif pushd depends && ./install_libavif.sh && popd diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 0c3f44e96..bf8ec2f2c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -88,6 +88,10 @@ jobs: run: | python3 -m pip install PyQt6 + - name: Install PyArrow dependency + run: | + python3 -m pip install --only-binary=:all: pyarrow || true + - name: Install dependencies id: install run: | diff --git a/Tests/test_arrow.py b/Tests/test_arrow.py new file mode 100644 index 000000000..b86c77b9a --- /dev/null +++ b/Tests/test_arrow.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import pytest + +from PIL import Image + +from .helper import hopper + + +@pytest.mark.parametrize( + "mode, dest_modes", + ( + ("L", ["I", "F", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), + ("I", ["L", "F"]), # Technically I;32 can work for any 4x8bit storage. + ("F", ["I", "L", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), + ("LA", ["L", "F"]), + ("RGB", ["L", "F"]), + ("RGBA", ["L", "F"]), + ("RGBX", ["L", "F"]), + ("CMYK", ["L", "F"]), + ("YCbCr", ["L", "F"]), + ("HSV", ["L", "F"]), + ), +) +def test_invalid_array_type(mode: str, dest_modes: list[str]) -> None: + img = hopper(mode) + for dest_mode in dest_modes: + with pytest.raises(ValueError): + Image.fromarrow(img, dest_mode, img.size) + + +def test_invalid_array_size() -> None: + img = hopper("RGB") + + assert img.size != (10, 10) + with pytest.raises(ValueError): + Image.fromarrow(img, "RGB", (10, 10)) + + +def test_release_schema() -> None: + # these should not error out, valgrind should be clean + img = hopper("L") + schema = img.__arrow_c_schema__() + del schema + + +def test_release_array() -> None: + # these should not error out, valgrind should be clean + img = hopper("L") + array, schema = img.__arrow_c_array__() + del array + del schema + + +def test_readonly() -> None: + img = hopper("L") + reloaded = Image.fromarrow(img, img.mode, img.size) + assert reloaded.readonly == 1 + reloaded._readonly = 0 + assert reloaded.readonly == 1 + + +def test_multiblock_l_image() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + + with pytest.raises(ValueError): + (schema, arr) = img.__arrow_c_array__() + + +def test_multiblock_rgba_image() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + + with pytest.raises(ValueError): + (schema, arr) = img.__arrow_c_array__() + + +def test_multiblock_l_schema() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + + with pytest.raises(ValueError): + img.__arrow_c_schema__() + + +def test_multiblock_rgba_schema() -> None: + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + + with pytest.raises(ValueError): + img.__arrow_c_schema__() + + +def test_singleblock_l_image() -> None: + Image.core.set_use_block_allocator(1) + + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, 2 * (block_size // 4096)) + img = Image.new("L", size, 128) + assert img.im.isblock() + + (schema, arr) = img.__arrow_c_array__() + assert schema + assert arr + + Image.core.set_use_block_allocator(0) + + +def test_singleblock_rgba_image() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + assert img.im.isblock() + + (schema, arr) = img.__arrow_c_array__() + assert schema + assert arr + Image.core.set_use_block_allocator(0) + + +def test_singleblock_l_schema() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in single channel mode + size = (4096, 2 * block_size // 4096) + img = Image.new("L", size, 128) + assert img.im.isblock() + + schema = img.__arrow_c_schema__() + assert schema + Image.core.set_use_block_allocator(0) + + +def test_singleblock_rgba_schema() -> None: + Image.core.set_use_block_allocator(1) + block_size = Image.core.get_block_size() + + # check a 2 block image in 4 channel mode + size = (4096, (block_size // 4096) // 2) + img = Image.new("RGBA", size, (128, 127, 126, 125)) + assert img.im.isblock() + + schema = img.__arrow_c_schema__() + assert schema + Image.core.set_use_block_allocator(0) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py new file mode 100644 index 000000000..ece9f8f26 --- /dev/null +++ b/Tests/test_pyarrow.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from typing import Any # undone + +import pytest + +from PIL import Image + +from .helper import ( + assert_deep_equal, + assert_image_equal, + hopper, +) + +pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") + +TEST_IMAGE_SIZE = (10, 10) + + +def _test_img_equals_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None +) -> None: + assert img.height * img.width == len(arr) + px = img.load() + assert px is not None + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + if mask: + for ix, elt in enumerate(mask): + pixel = px[x, y] + assert isinstance(pixel, tuple) + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) + + +# really hard to get a non-nullable list type +fl_uint8_4_type = pyarrow.field( + "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) +).type + + +@pytest.mark.parametrize( + "mode, dtype, mask", + ( + ("L", pyarrow.uint8(), None), + ("I", pyarrow.int32(), None), + ("F", pyarrow.float32(), None), + ("LA", fl_uint8_4_type, [0, 3]), + ("RGB", fl_uint8_4_type, [0, 1, 2]), + ("RGBA", fl_uint8_4_type, None), + ("RGBX", fl_uint8_4_type, None), + ("CMYK", fl_uint8_4_type, None), + ("YCbCr", fl_uint8_4_type, [0, 1, 2]), + ("HSV", fl_uint8_4_type, [0, 1, 2]), + ), +) +def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + arr = pyarrow.array(img) + _test_img_equals_pyarray(img, arr, mask) + assert arr.type == dtype + + reloaded = Image.fromarrow(arr, mode, img.size) + + assert reloaded + + assert_image_equal(img, reloaded) + + +def test_lifetime() -> None: + # valgrind shouldn't error out here. + # arrays should be accessible after the image is deleted. + + img = hopper("L") + + arr_1 = pyarrow.array(img) + arr_2 = pyarrow.array(img) + + del img + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + +def test_lifetime2() -> None: + # valgrind shouldn't error out here. + # img should remain after the arrays are collected. + + img = hopper("L") + + arr_1 = pyarrow.array(img) + arr_2 = pyarrow.array(img) + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + img2 = img.copy() + px = img2.load() + assert px # make mypy happy + assert isinstance(px[0, 0], int) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index bc3758218..a3ba8cfd8 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -79,6 +79,7 @@ Constructing images .. autofunction:: new .. autofunction:: fromarray +.. autofunction:: fromarrow .. autofunction:: frombytes .. autofunction:: frombuffer @@ -370,6 +371,8 @@ Protocols .. autoclass:: SupportsArrayInterface :show-inheritance: +.. autoclass:: SupportsArrowArrayInterface + :show-inheritance: .. autoclass:: SupportsGetData :show-inheritance: diff --git a/docs/reference/arrow_support.rst b/docs/reference/arrow_support.rst new file mode 100644 index 000000000..4a5c45e86 --- /dev/null +++ b/docs/reference/arrow_support.rst @@ -0,0 +1,88 @@ +.. _arrow-support: + +============= +Arrow Support +============= + +`Arrow `__ +is an in-memory data exchange format that is the spiritual +successor to the NumPy array interface. It provides for zero-copy +access to columnar data, which in our case is ``Image`` data. + +The goal with Arrow is to provide native zero-copy interoperability +with any Arrow provider or consumer in the Python ecosystem. + +.. warning:: Zero-copy does not mean zero allocation -- the internal + memory layout of Pillow images contains an allocation for row + pointers, so there is a non-zero, but significantly smaller than a + full-copy memory cost to reading an Arrow image. + + +Data Formats +============ + +Pillow currently supports exporting Arrow images in all modes +**except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to +line-length packing in these modes making for non-continuous memory. + +For single-band images, the exported array is width*height elements, +with each pixel corresponding to the appropriate Arrow type. + +For multiband images, the exported array is width*height fixed-length +four-element arrays of uint8. This is memory compatible with the raw +image storage of four bytes per pixel. + +Mode ``1`` images are exported as one uint8 byte/pixel, as this is +consistent with the internal storage. + +Pillow will accept, but not produce, one other format. For any +multichannel image with 32-bit storage per pixel, Pillow will accept +an array of width*height int32 elements, which will then be +interpreted using the mode-specific interpretation of the bytes. + +The image mode must match the Arrow band format when reading single +channel images. + +Memory Allocator +================ + +Pillow's default memory allocator, the :ref:`block_allocator`, +allocates up to a 16 MB block for images by default. Larger images +overflow into additional blocks. Arrow requires a single continuous +memory allocation, so images allocated in multiple blocks cannot be +exported in the Arrow format. + +To enable the single block allocator:: + + from PIL import Image + Image.core.set_use_block_allocator(1) + +Note that this is a global setting, not a per-image setting. + +Unsupported Features +==================== + +* Table/dataframe protocol. We support a single array. +* Null markers, producing or consuming. Null values are inferred from + the mode, e.g. RGB images are stored in the first three bytes of + each 32-bit pixel, and the last byte is an implied null. +* Schema negotiation. There is an optional schema for the requested + datatype in the Arrow source interface. We ignore that + parameter. +* Array metadata. + +Internal Details +================ + +Python Arrow C interface: +https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html + +The memory that is exported from the Arrow interface is shared -- not +copied, so the lifetime of the memory allocation is no longer strictly +tied to the life of the Python object. + +The core imaging struct now has a refcount associated with it, and the +lifetime of the core image struct is now divorced from the Python +image object. Creating an arrow reference to the image increments the +refcount, and the imaging struct is only released when the refcount +reaches zero. diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index 1abe5280f..f4d27e24e 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -1,3 +1,6 @@ + +.. _block_allocator: + Block Allocator =============== diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 99a18e9ea..041177953 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -9,3 +9,4 @@ Internal Reference block_allocator internal_modules c_extension_debugging + arrow_support diff --git a/pyproject.toml b/pyproject.toml index 780a938a3..856419215 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,10 @@ optional-dependencies.fpx = [ optional-dependencies.mic = [ "olefile", ] +optional-dependencies.test-arrow = [ + "pyarrow", +] + optional-dependencies.tests = [ "check-manifest", "coverage>=7.4.2", @@ -67,6 +71,7 @@ optional-dependencies.tests = [ "pytest-timeout", "trove-classifiers>=2024.10.12", ] + optional-dependencies.typing = [ "typing-extensions; python_version<'3.10'", ] diff --git a/setup.py b/setup.py index 9d69b1d6e..5ecd6b816 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ _IMAGING = ("decode", "encode", "map", "display", "outline", "path") _LIB_IMAGING = ( "Access", "AlphaComposite", + "Arrow", "Resample", "Reduce", "Bands", diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 60850f4ff..233df592c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -577,6 +577,14 @@ class Image: def mode(self) -> str: return self._mode + @property + def readonly(self) -> int: + return (self._im and self._im.readonly) or self._readonly + + @readonly.setter + def readonly(self, readonly: int) -> None: + self._readonly = readonly + def _new(self, im: core.ImagingCore) -> Image: new = Image() new.im = im @@ -728,6 +736,16 @@ class Image: new["shape"], new["typestr"] = _conv_type_shape(self) return new + def __arrow_c_schema__(self) -> object: + self.load() + return self.im.__arrow_c_schema__() + + def __arrow_c_array__( + self, requested_schema: object | None = None + ) -> tuple[object, object]: + self.load() + return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__()) + def __getstate__(self) -> list[Any]: im_data = self.tobytes() # load image first return [self.info, self.mode, self.size, self.getpalette(), im_data] @@ -3201,6 +3219,18 @@ class SupportsArrayInterface(Protocol): raise NotImplementedError() +class SupportsArrowArrayInterface(Protocol): + """ + An object that has an ``__arrow_c_array__`` method corresponding to the arrow c + data interface. + """ + + def __arrow_c_array__( + self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037 + ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037 + raise NotImplementedError() + + def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: """ Creates an image memory from an object exporting the array interface @@ -3289,6 +3319,56 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) +def fromarrow(obj: SupportsArrowArrayInterface, mode, size) -> Image: + """Creates an image with zero-copy shared memory from an object exporting + the arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + + If the data representation of the ``obj`` is not compatible with + Pillow internal storage, a ValueError is raised. + + Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + + As with array support, when converting Pillow images to arrays, + only pixel values are transferred. This means that P and PA mode + images will lose their palette. + + :param obj: Object with an arrow_c_array interface + :param mode: Image mode. + :param size: Image size. This must match the storage of the arrow object. + :returns: An Image object + + Note that according to the Arrow spec, both the producer and the + consumer should consider the exported array to be immutable, as + unsynchronized updates will potentially cause inconsistent data. + + See: :ref:`arrow-support` for more detailed information + + .. versionadded:: 11.2.0 + + """ + if not hasattr(obj, "__arrow_c_array__"): + msg = "arrow_c_array interface not found" + raise ValueError(msg) + + (schema_capsule, array_capsule) = obj.__arrow_c_array__() + _im = core.new_arrow(mode, size, schema_capsule, array_capsule) + if _im: + return Image()._new(_im) + + msg = "new_arrow returned None without an exception" + raise ValueError(msg) + + def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: """Creates an image instance from a QImage image""" from . import ImageQt diff --git a/src/_imaging.c b/src/_imaging.c index 330a7eef4..72f122143 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -230,6 +230,93 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); } +/* -------------------------------------------------------------------- */ +/* Arrow HANDLING */ +/* -------------------------------------------------------------------- */ + +PyObject * +ArrowError(int err) { + if (err == IMAGING_CODEC_MEMORY) { + return ImagingError_MemoryError(); + } + if (err == IMAGING_ARROW_INCOMPATIBLE_MODE) { + return ImagingError_ValueError("Incompatible Pillow mode for Arrow array"); + } + if (err == IMAGING_ARROW_MEMORY_LAYOUT) { + return ImagingError_ValueError( + "Image is in multiple array blocks, use imaging_new_block for zero copy" + ); + } + return ImagingError_ValueError("Unknown error"); +} + +void +ReleaseArrowSchemaPyCapsule(PyObject *capsule) { + struct ArrowSchema *schema = + (struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema"); + if (schema->release != NULL) { + schema->release(schema); + } + free(schema); +} + +PyObject * +ExportArrowSchemaPyCapsule(ImagingObject *self) { + struct ArrowSchema *schema = + (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + int err = export_imaging_schema(self->image, schema); + if (err == 0) { + return PyCapsule_New(schema, "arrow_schema", ReleaseArrowSchemaPyCapsule); + } + free(schema); + return ArrowError(err); +} + +void +ReleaseArrowArrayPyCapsule(PyObject *capsule) { + struct ArrowArray *array = + (struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array"); + if (array->release != NULL) { + array->release(array); + } + free(array); +} + +PyObject * +ExportArrowArrayPyCapsule(ImagingObject *self) { + struct ArrowArray *array = + (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + int err = export_imaging_array(self->image, array); + if (err == 0) { + return PyCapsule_New(array, "arrow_array", ReleaseArrowArrayPyCapsule); + } + free(array); + return ArrowError(err); +} + +static PyObject * +_new_arrow(PyObject *self, PyObject *args) { + char *mode; + int xsize, ysize; + PyObject *schema_capsule, *array_capsule; + PyObject *ret; + + if (!PyArg_ParseTuple( + args, "s(ii)OO", &mode, &xsize, &ysize, &schema_capsule, &array_capsule + )) { + return NULL; + } + + // ImagingBorrowArrow is responsible for retaining the array_capsule + ret = + PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) + ); + if (!ret) { + return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); + } + return ret; +} + /* -------------------------------------------------------------------- */ /* EXCEPTION REROUTING */ /* -------------------------------------------------------------------- */ @@ -3655,6 +3742,10 @@ static struct PyMethodDef methods[] = { /* Misc. */ {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, + /* arrow */ + {"__arrow_c_schema__", (PyCFunction)ExportArrowSchemaPyCapsule, METH_VARARGS}, + {"__arrow_c_array__", (PyCFunction)ExportArrowArrayPyCapsule, METH_VARARGS}, + {NULL, NULL} /* sentinel */ }; @@ -3722,6 +3813,11 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { ); } +static PyObject * +_getattr_readonly(ImagingObject *self, void *closure) { + return PyLong_FromLong(self->image->read_only); +} + static struct PyGetSetDef getsetters[] = { {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, @@ -3729,6 +3825,7 @@ static struct PyGetSetDef getsetters[] = { {"id", (getter)_getattr_id}, {"ptr", (getter)_getattr_ptr}, {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, + {"readonly", (getter)_getattr_readonly}, {NULL} }; @@ -3983,6 +4080,21 @@ _set_blocks_max(PyObject *self, PyObject *args) { Py_RETURN_NONE; } +static PyObject * +_set_use_block_allocator(PyObject *self, PyObject *args) { + int use_block_allocator; + if (!PyArg_ParseTuple(args, "i:set_use_block_allocator", &use_block_allocator)) { + return NULL; + } + ImagingMemorySetBlockAllocator(&ImagingDefaultArena, use_block_allocator); + Py_RETURN_NONE; +} + +static PyObject * +_get_use_block_allocator(PyObject *self, PyObject *args) { + return PyLong_FromLong(ImagingDefaultArena.use_block_allocator); +} + static PyObject * _clear_cache(PyObject *self, PyObject *args) { int i = 0; @@ -4104,6 +4216,7 @@ static PyMethodDef functions[] = { {"fill", (PyCFunction)_fill, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS}, {"new_block", (PyCFunction)_new_block, METH_VARARGS}, + {"new_arrow", (PyCFunction)_new_arrow, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ @@ -4190,9 +4303,11 @@ static PyMethodDef functions[] = { {"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS}, {"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS}, {"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS}, + {"get_use_block_allocator", (PyCFunction)_get_use_block_allocator, METH_VARARGS}, {"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS}, {"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS}, {"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS}, + {"set_use_block_allocator", (PyCFunction)_set_use_block_allocator, METH_VARARGS}, {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS}, {NULL, NULL} /* sentinel */ diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c new file mode 100644 index 000000000..33ff2ce77 --- /dev/null +++ b/src/libImaging/Arrow.c @@ -0,0 +1,299 @@ + +#include "Arrow.h" +#include "Imaging.h" +#include + +/* struct ArrowSchema* */ +/* _arrow_schema_channel(char* channel, char* format) { */ + +/* } */ + +static void +ReleaseExportedSchema(struct ArrowSchema *array) { + // This should not be called on already released array + // assert(array->release != NULL); + + if (!array->release) { + return; + } + if (array->format) { + free((void *)array->format); + array->format = NULL; + } + if (array->name) { + free((void *)array->name); + array->name = NULL; + } + if (array->metadata) { + free((void *)array->metadata); + array->metadata = NULL; + } + + // Release children + for (int64_t i = 0; i < array->n_children; ++i) { + struct ArrowSchema *child = array->children[i]; + if (child->release != NULL) { + child->release(child); + child->release = NULL; + } + // UNDONE -- should I be releasing the children? + } + + // Release dictionary + struct ArrowSchema *dict = array->dictionary; + if (dict != NULL && dict->release != NULL) { + dict->release(dict); + dict->release = NULL; + } + + // TODO here: release and/or deallocate all data directly owned by + // the ArrowArray struct, such as the private_data. + + // Mark array released + array->release = NULL; +} + +int +export_named_type(struct ArrowSchema *schema, char *format, char *name) { + char *formatp; + char *namep; + size_t format_len = strlen(format) + 1; + size_t name_len = strlen(name) + 1; + + formatp = calloc(format_len, 1); + + if (!formatp) { + return IMAGING_CODEC_MEMORY; + } + + namep = calloc(name_len, 1); + if (!namep) { + free(formatp); + return IMAGING_CODEC_MEMORY; + } + + strncpy(formatp, format, format_len); + strncpy(namep, name, name_len); + + *schema = (struct ArrowSchema){// Type description + .format = formatp, + .name = namep, + .metadata = NULL, + .flags = 0, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &ReleaseExportedSchema + }; + return 0; +} + +int +export_imaging_schema(Imaging im, struct ArrowSchema *schema) { + int retval = 0; + + if (strcmp(im->arrow_band_format, "") == 0) { + return IMAGING_ARROW_INCOMPATIBLE_MODE; + } + + /* for now, single block images */ + if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->bands == 1) { + return export_named_type(schema, im->arrow_band_format, im->band_names[0]); + } + + retval = export_named_type(schema, "+w:4", ""); + if (retval != 0) { + return retval; + } + // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands. + schema->n_children = 1; + schema->children = calloc(1, sizeof(struct ArrowSchema *)); + schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); + retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); + if (retval != 0) { + free(schema->children[0]); + schema->release(schema); + return retval; + } + return 0; +} + +static void +release_const_array(struct ArrowArray *array) { + Imaging im = (Imaging)array->private_data; + + if (array->n_children == 0) { + ImagingDelete(im); + } + + // Free the buffers and the buffers array + if (array->buffers) { + free(array->buffers); + array->buffers = NULL; + } + if (array->children) { + // undone -- does arrow release all the children recursively? + for (int i = 0; i < array->n_children; i++) { + if (array->children[i]->release) { + array->children[i]->release(array->children[i]); + array->children[i]->release = NULL; + free(array->children[i]); + } + } + free(array->children); + array->children = NULL; + } + // Mark released + array->release = NULL; +} + +int +export_single_channel_array(Imaging im, struct ArrowArray *array) { + int length = im->xsize * im->ysize; + + /* for now, single block images */ + if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->lines_per_block && im->lines_per_block < im->ysize) { + length = im->xsize * im->lines_per_block; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + // Initialize primitive fields + *array = (struct ArrowArray){// Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + // Allocate list of buffers + array->buffers = (const void **)malloc(sizeof(void *) * array->n_buffers); + // assert(array->buffers != NULL); + array->buffers[0] = NULL; // no nulls, null bitmap can be omitted + + if (im->block) { + array->buffers[1] = im->block; + } else { + array->buffers[1] = im->blocks[0].ptr; + } + return 0; +} + +int +export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { + int length = im->xsize * im->ysize; + + /* for now, single block images */ + if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + return IMAGING_ARROW_MEMORY_LAYOUT; + } + + if (im->lines_per_block && im->lines_per_block < im->ysize) { + length = im->xsize * im->lines_per_block; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + // Initialize primitive fields + // Fixed length arrays are 1 buffer of validity, and the length in pixels. + // Data is in a child array. + *array = (struct ArrowArray){// Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 1, + .n_children = 1, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + // Allocate list of buffers + array->buffers = (const void **)calloc(1, sizeof(void *) * array->n_buffers); + if (!array->buffers) { + goto err; + } + // assert(array->buffers != NULL); + array->buffers[0] = NULL; // no nulls, null bitmap can be omitted + + // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands. + array->n_children = 1; + array->children = calloc(1, sizeof(struct ArrowArray *)); + if (!array->children) { + goto err; + } + array->children[0] = (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); + if (!array->children[0]) { + goto err; + } + + MUTEX_LOCK(&im->mutex); + im->refcount++; + MUTEX_UNLOCK(&im->mutex); + *array->children[0] = (struct ArrowArray){// Data description + .length = length * 4, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im + }; + + array->children[0]->buffers = + (const void **)calloc(2, sizeof(void *) * array->n_buffers); + + if (im->block) { + array->children[0]->buffers[1] = im->block; + } else { + array->children[0]->buffers[1] = im->blocks[0].ptr; + } + return 0; + +err: + if (array->children[0]) { + free(array->children[0]); + } + if (array->children) { + free(array->children); + } + if (array->buffers) { + free(array->buffers); + } + return IMAGING_CODEC_MEMORY; +} + +int +export_imaging_array(Imaging im, struct ArrowArray *array) { + if (strcmp(im->arrow_band_format, "") == 0) { + return IMAGING_ARROW_INCOMPATIBLE_MODE; + } + + if (im->bands == 1) { + return export_single_channel_array(im, array); + } + + return export_fixed_pixel_array(im, array); +} diff --git a/src/libImaging/Arrow.h b/src/libImaging/Arrow.h new file mode 100644 index 000000000..0b285fe80 --- /dev/null +++ b/src/libImaging/Arrow.h @@ -0,0 +1,48 @@ +#include +#include + +// Apache License 2.0. +// Source apache arrow project +// https://arrow.apache.org/docs/format/CDataInterface.html + +#ifndef ARROW_C_DATA_INTERFACE +#define ARROW_C_DATA_INTERFACE + +#define ARROW_FLAG_DICTIONARY_ORDERED 1 +#define ARROW_FLAG_NULLABLE 2 +#define ARROW_FLAG_MAP_KEYS_SORTED 4 + +struct ArrowSchema { + // Array type description + const char *format; + const char *name; + const char *metadata; + int64_t flags; + int64_t n_children; + struct ArrowSchema **children; + struct ArrowSchema *dictionary; + + // Release callback + void (*release)(struct ArrowSchema *); + // Opaque producer-specific data + void *private_data; +}; + +struct ArrowArray { + // Array data description + int64_t length; + int64_t null_count; + int64_t offset; + int64_t n_buffers; + int64_t n_children; + const void **buffers; + struct ArrowArray **children; + struct ArrowArray *dictionary; + + // Release callback + void (*release)(struct ArrowArray *); + // Opaque producer-specific data + void *private_data; +}; + +#endif // ARROW_C_DATA_INTERFACE diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 0fc191d15..234f9943c 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -20,6 +20,8 @@ extern "C" { #define M_PI 3.1415926535897932384626433832795 #endif +#include "Arrow.h" + /* -------------------------------------------------------------------- */ /* @@ -104,6 +106,21 @@ struct ImagingMemoryInstance { /* Virtual methods */ void (*destroy)(Imaging im); + + /* arrow */ + int refcount; /* Number of arrow arrays that have been allocated */ + char band_names[4][3]; /* names of bands, max 2 char + null terminator */ + char arrow_band_format[2]; /* single character + null terminator */ + + int read_only; /* flag for read-only. set for arrow borrowed arrays */ + PyObject *arrow_array_capsule; /* upstream arrow array source */ + + int blocks_count; /* Number of blocks that have been allocated */ + int lines_per_block; /* Number of lines in a block have been allocated */ + +#ifdef Py_GIL_DISABLED + PyMutex mutex; +#endif }; #define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)]) @@ -161,6 +178,7 @@ typedef struct ImagingMemoryArena { int stats_reallocated_blocks; /* Number of blocks which were actually reallocated after retrieving */ int stats_freed_blocks; /* Number of freed blocks */ + int use_block_allocator; /* don't use arena, use block allocator */ #ifdef Py_GIL_DISABLED PyMutex mutex; #endif @@ -174,6 +192,8 @@ extern int ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max); extern void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); +extern void +ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator); extern Imaging ImagingNew(const char *mode, int xsize, int ysize); @@ -187,6 +207,15 @@ ImagingDelete(Imaging im); extern Imaging ImagingNewBlock(const char *mode, int xsize, int ysize); +extern Imaging +ImagingNewArrow( + const char *mode, + int xsize, + int ysize, + PyObject *schema_capsule, + PyObject *array_capsule +); + extern Imaging ImagingNewPrologue(const char *mode, int xsize, int ysize); extern Imaging @@ -700,6 +729,13 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence); extern Py_ssize_t _imaging_tell_pyFd(PyObject *fd); +/* Arrow */ + +extern int +export_imaging_array(Imaging im, struct ArrowArray *array); +extern int +export_imaging_schema(Imaging im, struct ArrowSchema *schema); + /* Errcodes */ #define IMAGING_CODEC_END 1 #define IMAGING_CODEC_OVERRUN -1 @@ -707,6 +743,8 @@ _imaging_tell_pyFd(PyObject *fd); #define IMAGING_CODEC_UNKNOWN -3 #define IMAGING_CODEC_CONFIG -8 #define IMAGING_CODEC_MEMORY -9 +#define IMAGING_ARROW_INCOMPATIBLE_MODE -10 +#define IMAGING_ARROW_MEMORY_LAYOUT -11 #include "ImagingUtils.h" extern UINT8 *clip8_lookups; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 522e9f375..4fa4ecd1c 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -58,19 +58,22 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { /* Setup image descriptor */ im->xsize = xsize; im->ysize = ysize; - + im->refcount = 1; im->type = IMAGING_TYPE_UINT8; + strcpy(im->arrow_band_format, "C"); if (strcmp(mode, "1") == 0) { /* 1-bit images */ im->bands = im->pixelsize = 1; im->linesize = xsize; + strcpy(im->band_names[0], "1"); } else if (strcmp(mode, "P") == 0) { /* 8-bit palette mapped images */ im->bands = im->pixelsize = 1; im->linesize = xsize; im->palette = ImagingPaletteNew("RGB"); + strcpy(im->band_names[0], "P"); } else if (strcmp(mode, "PA") == 0) { /* 8-bit palette with alpha */ @@ -78,23 +81,36 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; im->palette = ImagingPaletteNew("RGB"); + strcpy(im->band_names[0], "P"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "A"); } else if (strcmp(mode, "L") == 0) { /* 8-bit grayscale (luminance) images */ im->bands = im->pixelsize = 1; im->linesize = xsize; + strcpy(im->band_names[0], "L"); } else if (strcmp(mode, "LA") == 0) { /* 8-bit grayscale (luminance) with alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "A"); } else if (strcmp(mode, "La") == 0) { /* 8-bit grayscale (luminance) with premultiplied alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "X"); + strcpy(im->band_names[2], "X"); + strcpy(im->band_names[3], "a"); } else if (strcmp(mode, "F") == 0) { /* 32-bit floating point images */ @@ -102,6 +118,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; im->linesize = xsize * 4; im->type = IMAGING_TYPE_FLOAT32; + strcpy(im->arrow_band_format, "f"); + strcpy(im->band_names[0], "F"); } else if (strcmp(mode, "I") == 0) { /* 32-bit integer images */ @@ -109,6 +127,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; im->linesize = xsize * 4; im->type = IMAGING_TYPE_INT32; + strcpy(im->arrow_band_format, "i"); + strcpy(im->band_names[0], "I"); } else if (strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 || strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) { @@ -118,12 +138,18 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = xsize * 2; im->type = IMAGING_TYPE_SPECIAL; + strcpy(im->arrow_band_format, "s"); + strcpy(im->band_names[0], "I"); } else if (strcmp(mode, "RGB") == 0) { /* 24-bit true colour images */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ @@ -132,6 +158,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "BGR;16") == 0) { /* EXPERIMENTAL */ @@ -140,6 +168,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "BGR;24") == 0) { /* EXPERIMENTAL */ @@ -148,32 +178,54 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 3; im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; + /* not allowing arrow due to line length packing */ + strcpy(im->arrow_band_format, ""); } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "RGBA") == 0) { /* 32-bit true colour images with alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "A"); } else if (strcmp(mode, "RGBa") == 0) { /* 32-bit true colour images with premultiplied alpha */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "R"); + strcpy(im->band_names[1], "G"); + strcpy(im->band_names[2], "B"); + strcpy(im->band_names[3], "a"); } else if (strcmp(mode, "CMYK") == 0) { /* 32-bit colour separation */ im->bands = im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "C"); + strcpy(im->band_names[1], "M"); + strcpy(im->band_names[2], "Y"); + strcpy(im->band_names[3], "K"); } else if (strcmp(mode, "YCbCr") == 0) { /* 24-bit video format */ im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "Y"); + strcpy(im->band_names[1], "Cb"); + strcpy(im->band_names[2], "Cr"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "LAB") == 0) { /* 24-bit color, luminance, + 2 color channels */ @@ -181,6 +233,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "L"); + strcpy(im->band_names[1], "a"); + strcpy(im->band_names[2], "b"); + strcpy(im->band_names[3], "X"); } else if (strcmp(mode, "HSV") == 0) { /* 24-bit color, luminance, + 2 color channels */ @@ -188,6 +244,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->bands = 3; im->pixelsize = 4; im->linesize = xsize * 4; + strcpy(im->band_names[0], "H"); + strcpy(im->band_names[1], "S"); + strcpy(im->band_names[2], "V"); + strcpy(im->band_names[3], "X"); } else { free(im); @@ -218,6 +278,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { break; } + // UNDONE -- not accurate for arrow MUTEX_LOCK(&ImagingDefaultArena.mutex); ImagingDefaultArena.stats_new_count += 1; MUTEX_UNLOCK(&ImagingDefaultArena.mutex); @@ -238,8 +299,18 @@ ImagingDelete(Imaging im) { return; } + MUTEX_LOCK(&im->mutex); + im->refcount--; + + if (im->refcount > 0) { + MUTEX_UNLOCK(&im->mutex); + return; + } + MUTEX_UNLOCK(&im->mutex); + if (im->palette) { ImagingPaletteDelete(im->palette); + im->palette = NULL; } if (im->destroy) { @@ -270,6 +341,7 @@ struct ImagingMemoryArena ImagingDefaultArena = { 0, 0, 0, // Stats + 0, // use_block_allocator #ifdef Py_GIL_DISABLED {0}, #endif @@ -302,6 +374,11 @@ ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) { return 1; } +void +ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator) { + arena->use_block_allocator = use_block_allocator; +} + void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) { while (arena->blocks_cached > new_size) { @@ -396,11 +473,13 @@ ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_ if (lines_per_block == 0) { lines_per_block = 1; } + im->lines_per_block = lines_per_block; blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block; // printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n", // im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count); /* One extra pointer is always NULL */ + im->blocks_count = blocks_count; im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1); if (!im->blocks) { return (Imaging)ImagingError_MemoryError(); @@ -487,6 +566,58 @@ ImagingAllocateBlock(Imaging im) { return im; } +/* Borrowed Arrow Storage Type */ +/* --------------------------- */ +/* Don't allocate the image. */ + +static void +ImagingDestroyArrow(Imaging im) { + // Rely on the internal Python destructor for the array capsule. + if (im->arrow_array_capsule) { + Py_DECREF(im->arrow_array_capsule); + im->arrow_array_capsule = NULL; + } +} + +Imaging +ImagingBorrowArrow( + Imaging im, + struct ArrowArray *external_array, + int offset_width, + PyObject *arrow_capsule +) { + // offset_width is the number of char* for a single offset from arrow + Py_ssize_t y, i; + + char *borrowed_buffer = NULL; + struct ArrowArray *arr = external_array; + + if (arr->n_children == 1) { + arr = arr->children[0]; + } + if (arr->n_buffers == 2) { + // buffer 0 is the null list + // buffer 1 is the data + borrowed_buffer = (char *)arr->buffers[1] + (offset_width * arr->offset); + } + + if (!borrowed_buffer) { + return (Imaging + )ImagingError_ValueError("Arrow Array, exactly 2 buffers required"); + } + + for (y = i = 0; y < im->ysize; y++) { + im->image[y] = borrowed_buffer + i; + i += im->linesize; + } + im->read_only = 1; + Py_INCREF(arrow_capsule); + im->arrow_array_capsule = arrow_capsule; + im->destroy = ImagingDestroyArrow; + + return im; +} + /* -------------------------------------------------------------------- * Create a new, internally allocated, image. */ @@ -529,11 +660,17 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { Imaging ImagingNew(const char *mode, int xsize, int ysize) { + if (ImagingDefaultArena.use_block_allocator) { + return ImagingNewBlock(mode, xsize, ysize); + } return ImagingNewInternal(mode, xsize, ysize, 0); } Imaging ImagingNewDirty(const char *mode, int xsize, int ysize) { + if (ImagingDefaultArena.use_block_allocator) { + return ImagingNewBlock(mode, xsize, ysize); + } return ImagingNewInternal(mode, xsize, ysize, 1); } @@ -558,6 +695,66 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) { return NULL; } +Imaging +ImagingNewArrow( + const char *mode, + int xsize, + int ysize, + PyObject *schema_capsule, + PyObject *array_capsule +) { + /* A borrowed arrow array */ + Imaging im; + struct ArrowSchema *schema = + (struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema"); + + struct ArrowArray *external_array = + (struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array"); + + if (xsize < 0 || ysize < 0) { + return (Imaging)ImagingError_ValueError("bad image size"); + } + + im = ImagingNewPrologue(mode, xsize, ysize); + if (!im) { + return NULL; + } + + int64_t pixels = (int64_t)xsize * (int64_t)ysize; + + // fmt:off // don't reformat this + if (((strcmp(schema->format, "I") == 0 // int32 + && im->pixelsize == 4 // 4xchar* storage + && im->bands >= 2) // INT32 into any INT32 Storage mode + || // (()||()) && + (strcmp(schema->format, im->arrow_band_format) == 0 // same mode + && im->bands == 1)) // Single band match + && pixels == external_array->length) { + // one arrow element per, and it matches a pixelsize*char + if (ImagingBorrowArrow(im, external_array, im->pixelsize, array_capsule)) { + return im; + } + } + if (strcmp(schema->format, "+w:4") == 0 // 4 up array + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children > 0 // make sure schema is well formed. + && schema->children // make sure schema is well formed + && strcmp(schema->children[0]->format, "C") == 0 // Expected format + && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && pixels == external_array->length // expected length + && external_array->n_children == 1 // array is well formed + && external_array->children // array is well formed + && 4 * pixels == external_array->children[0]->length) { + // 4 up element of char into pixelsize == 4 + if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { + return im; + } + } + // fmt: on + ImagingDelete(im); + return NULL; +} + Imaging ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { /* allocate or validate output image */ From a7537b1b06490ef3dfbf0bf1c48a0b8c9aa36940 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Mar 2025 07:31:17 +1100 Subject: [PATCH 424/628] Only change readonly if saved filename matches opened filename --- Tests/test_image.py | 9 +++++++++ src/PIL/Image.py | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index c2e850c36..7e6118d52 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -258,6 +258,15 @@ class TestImage: assert im.readonly im.save(temp_file) + def test_save_without_changing_readonly(self, tmp_path: Path) -> None: + temp_file = tmp_path / "temp.bmp" + + with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: + assert im.readonly + + im.save(temp_file) + assert im.readonly + def test_dump(self, tmp_path: Path) -> None: im = Image.new("L", (10, 10)) im._dump(str(tmp_path / "temp_L.ppm")) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 233df592c..c62d7a8a3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2540,8 +2540,13 @@ class Image: msg = f"unknown file extension: {ext}" raise ValueError(msg) from e + from . import ImageFile + # may mutate self! - self._ensure_mutable() + if isinstance(self, ImageFile.ImageFile) and filename == self.filename: + self._ensure_mutable() + else: + self.load() save_all = params.pop("save_all", None) self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} From f205a45f44b237374533d5f41db65d75da474a45 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 19:10:11 +1100 Subject: [PATCH 425/628] Added release notes for #8330 --- docs/releasenotes/11.2.0.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index 2c1c761e5..de3db3c84 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -81,6 +81,28 @@ DXT5, BC2, BC3 and BC5 are supported:: Other Changes ============= +Arrow support +^^^^^^^^^^^^^ + +`Arrow `__ is an in-memory data exchange format that is the +spiritual successor to the NumPy array interface. It provides for zero-copy access to +columnar data, which in our case is ``Image`` data. + +To create an image with zero-copy shared memory from an object exporting the +arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + +Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + Reading and writing AVIF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 867c4772c22455207d6f1982bfbe130b423b53a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Apr 2025 20:19:40 +1100 Subject: [PATCH 426/628] Do not import type checking --- Tests/test_image_array.py | 3 ++- Tests/test_numpy.py | 2 +- Tests/test_qt_image_qapplication.py | 3 ++- docs/dater.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index eb2309e0f..25cb99c43 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import pytest from packaging.version import parse as parse_version @@ -13,6 +13,7 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed") im = hopper().resize((128, 100)) +TYPE_CHECKING = False if TYPE_CHECKING: import numpy.typing as npt diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index c4ad19d23..ef54deeeb 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,7 +1,6 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING import pytest @@ -9,6 +8,7 @@ from PIL import Image, _typing from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature +TYPE_CHECKING = False if TYPE_CHECKING: import numpy import numpy.typing as npt diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 0ed9fbfa5..82a3e0741 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Union +from typing import Union import pytest @@ -9,6 +9,7 @@ from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper +TYPE_CHECKING = False if TYPE_CHECKING: import PyQt6 import PySide6 diff --git a/docs/dater.py b/docs/dater.py index f9fb0c1da..c0302b55c 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -8,8 +8,8 @@ from __future__ import annotations import re import subprocess -from typing import TYPE_CHECKING +TYPE_CHECKING = False if TYPE_CHECKING: from sphinx.application import Sphinx From 1103e82d17311dac9ec2f32cb7e738fda8c64271 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Apr 2025 11:14:58 +1100 Subject: [PATCH 427/628] Include filename in state --- Tests/test_pickle.py | 1 + src/PIL/ImageFile.py | 4 ++++ src/PIL/JpegImagePlugin.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 70661ecc1..1c48cb743 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -81,6 +81,7 @@ def test_pickle_jpeg() -> None: unpickled_image = pickle.loads(pickle.dumps(image)) # Assert + assert unpickled_image.filename == "Tests/images/hopper.jpg" assert len(unpickled_image.layer) == 3 assert unpickled_image.layers == 3 diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index c5d6383a5..bcb7d462e 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -252,8 +252,12 @@ class ImageFile(Image.Image): return Image.MIME.get(self.format.upper()) return None + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.filename] + def __setstate__(self, state: list[Any]) -> None: self.tile = [] + self.filename = state[5] super().__setstate__(state) def verify(self) -> None: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index cc1d54b93..969528841 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -403,8 +403,8 @@ class JpegImageFile(ImageFile.ImageFile): return super().__getstate__() + [self.layers, self.layer] def __setstate__(self, state: list[Any]) -> None: + self.layers, self.layer = state[6:] super().__setstate__(state) - self.layers, self.layer = state[5:] def load_read(self, read_bytes: int) -> bytes: """ From 8dbbce624f7ce9ad85eb50075d9e3dfdcb0fbfd4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Apr 2025 12:16:25 +1100 Subject: [PATCH 428/628] Compare absolute path of filename --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c62d7a8a3..807814c02 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2543,7 +2543,9 @@ class Image: from . import ImageFile # may mutate self! - if isinstance(self, ImageFile.ImageFile) and filename == self.filename: + if isinstance(self, ImageFile.ImageFile) and os.path.abspath( + filename + ) == os.path.abspath(self.filename): self._ensure_mutable() else: self.load() From 7e15c54cad753c30974005230699e888e8902b6c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 2 Apr 2025 23:53:14 +1100 Subject: [PATCH 429/628] Use multibuild build_github (#8861) --- .github/workflows/wheels-dependencies.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2f2e75b6c..accd99901 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -80,11 +80,7 @@ function build_pkg_config { function build_zlib_ng { if [ -e zlib-stamp ]; then return; fi - fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz - (cd zlib-ng-$ZLIB_NG_VERSION \ - && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ - && make -j4 \ - && make install) + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat if [ -n "$IS_MACOS" ]; then # Ensure that on macOS, the library name is an absolute path, not an From 2d452c82e5f78fb25d0271a4f4534622235176da Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:17:54 +1100 Subject: [PATCH 430/628] Removed condition that is always true (#8862) --- src/_avif.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_avif.c b/src/_avif.c index eabd9958e..312286787 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -568,9 +568,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { } end: - if (&rgb) { - avifRGBImageFreePixels(&rgb); - } + avifRGBImageFreePixels(&rgb); if (!self->first_frame) { avifImageDestroy(frame); } From 8691112a2cba76b7bcf1aaa0f4824ff9063f2f7b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:23:36 +0300 Subject: [PATCH 431/628] Update scientific-python/upload-nightly-action action to v0.6.2 (#8865) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2a8594f49..3b1be9a96 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -245,7 +245,7 @@ jobs: path: dist merge-multiple: true - name: Upload wheels to scientific-python-nightly-wheels - uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 + uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} From 9f4195752d2231b34909c95fa70d716d4c664491 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:24:37 +1100 Subject: [PATCH 432/628] Added type hints (#8867) --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 807814c02..88ea6f3b5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3326,7 +3326,9 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) -def fromarrow(obj: SupportsArrowArrayInterface, mode, size) -> Image: +def fromarrow( + obj: SupportsArrowArrayInterface, mode: str, size: tuple[int, int] +) -> Image: """Creates an image with zero-copy shared memory from an object exporting the arrow_c_array interface protocol:: From 61d3dd9e83aee05fb19e28274bcc20a8fb8148f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Apr 2025 22:12:54 +1100 Subject: [PATCH 433/628] Updated xz to 5.8.1, except on Windows x86 --- .github/workflows/wheels-dependencies.sh | 16 +--------------- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index accd99901..04bce62fb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -42,7 +42,7 @@ HARFBUZZ_VERSION=11.0.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 -XZ_VERSION=5.8.0 +XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 @@ -53,20 +53,6 @@ LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 LIBAVIF_VERSION=1.2.1 -if [[ $MB_ML_VER == 2014 ]]; then - function build_xz { - if [ -e xz-stamp ]; then return; fi - yum install -y gettext-devel - fetch_unpack https://tukaani.org/xz/xz-$XZ_VERSION.tar.gz - (cd xz-$XZ_VERSION \ - && ./autogen.sh --no-po4a \ - && ./configure --prefix=$BUILD_PREFIX \ - && make -j4 \ - && make install) - touch xz-stamp - } -fi - function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi # This essentially duplicates the Homebrew recipe diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e118cd994..e8e3aacc2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -122,7 +122,7 @@ V = { "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", - "XZ": "5.6.4", + "XZ": "5.6.4" if struct.calcsize("P") == 4 else "5.8.1", "ZLIBNG": "2.2.4", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 9f654ff748919d92ac0710a573e239b18c82e806 Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Fri, 4 Apr 2025 09:41:11 -0400 Subject: [PATCH 434/628] Fixed conversion of AVIF image rotation property to EXIF orientation (#8866) --- Tests/test_file_avif.py | 3 ++- src/_avif.c | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index 392a4bbd5..bd87947c0 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -309,7 +309,7 @@ class TestFileAvif: assert exif[274] == 3 @pytest.mark.parametrize("use_bytes", [True, False]) - @pytest.mark.parametrize("orientation", [1, 2]) + @pytest.mark.parametrize("orientation", [1, 2, 3, 4, 5, 6, 7, 8]) def test_exif_save( self, tmp_path: Path, @@ -327,6 +327,7 @@ class TestFileAvif: if orientation == 1: assert "exif" not in reloaded.info else: + assert reloaded.getexif()[274] == orientation assert reloaded.info["exif"] == exif_data def test_exif_without_orientation(self, tmp_path: Path) -> None: diff --git a/src/_avif.c b/src/_avif.c index 312286787..7e7bee703 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -59,7 +59,7 @@ irot_imir_to_exif_orientation(const avifImage *image) { return axis ? 7 // 90 degrees anti-clockwise then swap left and right. : 5; // 90 degrees anti-clockwise then swap top and bottom. } - return 6; // 90 degrees anti-clockwise. + return 8; // 90 degrees anti-clockwise. } if (angle == 2) { if (imir) { @@ -75,7 +75,7 @@ irot_imir_to_exif_orientation(const avifImage *image) { ? 5 // 270 degrees anti-clockwise then swap left and right. : 7; // 270 degrees anti-clockwise then swap top and bottom. } - return 8; // 270 degrees anti-clockwise. + return 6; // 270 degrees anti-clockwise. } } if (imir) { From 1ba32fce487cba432fd56c694651f15e0b32e25f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Apr 2025 15:44:46 +1100 Subject: [PATCH 435/628] Updated harfbuzz to 11.0.1 --- .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 accd99901..06c968d67 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=11.0.0 +HARFBUZZ_VERSION=11.0.1 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e118cd994..cf6dd0661 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": "11.0.0", + "HARFBUZZ": "11.0.1", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", "LIBAVIF": "1.2.1", From 1db27be6a0a56eefb08f7f7ed5064b2af875a6fd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Apr 2025 16:09:12 +1100 Subject: [PATCH 436/628] Use same URL as wheels-dependencies.sh --- winbuild/build_prepare.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index cf6dd0661..5806d88da 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -349,8 +349,8 @@ DEPS: dict[str, dict[str, Any]] = { "libs": [r"..\target\release\imagequant_sys.lib"], }, "harfbuzz": { - "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", - "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", + "url": f"https://github.com/harfbuzz/harfbuzz/releases/download/{V['HARFBUZZ']}/FILENAME", + "filename": f"harfbuzz-{V['HARFBUZZ']}.tar.xz", "license": "COPYING", "build": [ *cmds_cmake( @@ -514,8 +514,8 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: msg = "Attempted Path Traversal in Zip File" raise RuntimeError(msg) zf.extractall(sources_dir) - elif filename.endswith((".tar.gz", ".tgz")): - with tarfile.open(file, "r:gz") as tgz: + elif filename.endswith((".tar.gz", ".tar.xz")): + with tarfile.open(file, "r:xz" if filename.endswith(".xz") else "r:gz") as tgz: for member in tgz.getnames(): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) @@ -776,7 +776,7 @@ def main() -> None: for k, v in DEPS.items(): if "dir" not in v: - v["dir"] = re.sub(r"\.(tar\.gz|zip)", "", v["filename"]) + v["dir"] = re.sub(r"\.(tar\.gz|tar\.xz|zip)", "", v["filename"]) prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) print() From 82bccf70a0113617492b163f30e2bf31294c0a09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Apr 2025 11:10:05 +1000 Subject: [PATCH 437/628] Added XZ_CLMUL_CRC:BOOL=OFF to allow Windows x86 to use xz 5.8.1 --- winbuild/build_prepare.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e8e3aacc2..f40312506 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -122,7 +122,7 @@ V = { "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", - "XZ": "5.6.4" if struct.calcsize("P") == 4 else "5.8.1", + "XZ": "5.8.1", "ZLIBNG": "2.2.4", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) @@ -181,7 +181,11 @@ DEPS: dict[str, dict[str, Any]] = { "filename": f"xz-{V['XZ']}.tar.gz", "license": "COPYING", "build": [ - *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), + *cmds_cmake( + "liblzma", + "-DBUILD_SHARED_LIBS:BOOL=OFF" + + (" -DXZ_CLMUL_CRC:BOOL=OFF" if struct.calcsize("P") == 4 else ""), + ), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], From f6eb2e7fa50fc707aca36efd3d0dccd64edf1979 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:17:00 +0000 Subject: [PATCH 438/628] [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.9.9 → v0.11.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.9...v0.11.4) - [github.com/pre-commit/mirrors-clang-format: v19.1.7 → v20.1.0](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.7...v20.1.0) - [github.com/python-jsonschema/check-jsonschema: 0.31.2 → 0.32.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.2...0.32.1) - [github.com/woodruffw/zizmor-pre-commit: v1.4.1 → v1.5.2](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.4.1...v1.5.2) - [github.com/abravalheri/validate-pyproject: v0.23 → v0.24.1](https://github.com/abravalheri/validate-pyproject/compare/v0.23...v0.24.1) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ff947d41..66cf6d118 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.9.9 + rev: v0.11.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.7 + rev: v20.1.0 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.31.2 + rev: 0.32.1 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.4.1 + rev: v1.5.2 hooks: - id: zizmor @@ -72,7 +72,7 @@ repos: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.23 + rev: v0.24.1 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] From a5a8ece5d2211e3121ba0508d9ca78d4afd90e90 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:17:33 +0000 Subject: [PATCH 439/628] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/libImaging/QuantHash.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h index 0462cfd49..d75d55ce0 100644 --- a/src/libImaging/QuantHash.h +++ b/src/libImaging/QuantHash.h @@ -20,8 +20,12 @@ typedef uint32_t HashVal_t; typedef uint32_t (*HashFunc)(const HashTable *, const HashKey_t); typedef int (*HashCmpFunc)(const HashTable *, const HashKey_t, const HashKey_t); -typedef void (*IteratorFunc)(const HashTable *, const HashKey_t, const HashVal_t, void *); -typedef void (*IteratorUpdateFunc)(const HashTable *, const HashKey_t, HashVal_t *, void *); +typedef void (*IteratorFunc)( + const HashTable *, const HashKey_t, const HashVal_t, void * +); +typedef void (*IteratorUpdateFunc)( + const HashTable *, const HashKey_t, HashVal_t *, void * +); typedef void (*ComputeFunc)(const HashTable *, const HashKey_t, HashVal_t *); typedef void (*CollisionFunc)( const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t From 8c4510cb236597df034ab3cec97d4ec6ca3ba9a1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:25:12 +0300 Subject: [PATCH 440/628] Fix clang-format: Configuration file(s) do(es) not support C --- .clang-format | 1 - src/_imagingcms.c | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.clang-format b/.clang-format index 143dde82c..f01a1151a 100644 --- a/.clang-format +++ b/.clang-format @@ -11,7 +11,6 @@ ColumnLimit: 88 DerivePointerAlignment: false IndentGotoLabels: false IndentWidth: 4 -Language: Cpp PointerAlignment: Right ReflowComments: true SortIncludes: false diff --git a/src/_imagingcms.c b/src/_imagingcms.c index ea2f70186..f93c1613b 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1402,8 +1402,8 @@ static struct PyGetSetDef cms_profile_getsetters[] = { {"colorant_table_out", (getter)cms_profile_getattr_colorant_table_out}, {"intent_supported", (getter)cms_profile_getattr_is_intent_supported}, {"clut", (getter)cms_profile_getattr_is_clut}, - {"icc_measurement_condition", (getter)cms_profile_getattr_icc_measurement_condition - }, + {"icc_measurement_condition", + (getter)cms_profile_getattr_icc_measurement_condition}, {"icc_viewing_condition", (getter)cms_profile_getattr_icc_viewing_condition}, {NULL} From 8b7d72440e5d8a1acbe3f4692003d6c6eabd2205 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Apr 2025 20:15:45 +1000 Subject: [PATCH 441/628] Specify both C and Cpp --- .clang-format | 21 +++++++++++++++++++++ .pre-commit-config.yaml | 1 + 2 files changed, 22 insertions(+) diff --git a/.clang-format b/.clang-format index f01a1151a..1871d1f7a 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,26 @@ # A clang-format style that approximates Python's PEP 7 # Useful for IDE integration +Language: C +BasedOnStyle: Google +AlwaysBreakAfterReturnType: All +AllowShortIfStatementsOnASingleLine: false +AlignAfterOpenBracket: BlockIndent +BinPackArguments: false +BinPackParameters: false +BreakBeforeBraces: Attach +ColumnLimit: 88 +DerivePointerAlignment: false +IndentGotoLabels: false +IndentWidth: 4 +PointerAlignment: Right +ReflowComments: true +SortIncludes: false +SpaceBeforeParens: ControlStatements +SpacesInParentheses: false +TabWidth: 4 +UseTab: Never +--- +Language: Cpp BasedOnStyle: Google AlwaysBreakAfterReturnType: All AllowShortIfStatementsOnASingleLine: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66cf6d118..140ce33be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,6 +44,7 @@ repos: - id: check-json - id: check-toml - id: check-yaml + args: [--allow-multiple-documents] - id: end-of-file-fixer exclude: ^Tests/images/ - id: trailing-whitespace From 179ae9d395d5ad87e7d9f5db6e93a2cdd69af9ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Apr 2025 22:05:29 +1000 Subject: [PATCH 442/628] Disable building harfbuzz tests --- .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 06c968d67..7f1f22a3e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -107,7 +107,7 @@ function build_harfbuzz { local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) (cd $out_dir/build \ && meson install) touch harfbuzz-stamp From 6b5f8d768d2d388f2c9475f6ae9ca3780dafb8c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 13:55:02 +1000 Subject: [PATCH 443/628] Do not include libavif in wheels --- .github/workflows/wheels-dependencies.sh | 41 --- .github/workflows/wheels.yml | 7 +- Tests/check_wheel.py | 8 +- docs/releasenotes/11.2.0.rst | 5 +- wheels/dependency_licenses/AOM.txt | 26 -- wheels/dependency_licenses/DAV1D.txt | 23 -- wheels/dependency_licenses/LIBAVIF.txt | 387 ----------------------- wheels/dependency_licenses/LIBYUV.txt | 29 -- wheels/dependency_licenses/RAV1E.txt | 25 -- wheels/dependency_licenses/SVT-AV1.txt | 26 -- 10 files changed, 5 insertions(+), 572 deletions(-) delete mode 100644 wheels/dependency_licenses/AOM.txt delete mode 100644 wheels/dependency_licenses/DAV1D.txt delete mode 100644 wheels/dependency_licenses/LIBAVIF.txt delete mode 100644 wheels/dependency_licenses/LIBYUV.txt delete mode 100644 wheels/dependency_licenses/RAV1E.txt delete mode 100644 wheels/dependency_licenses/SVT-AV1.txt diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index accd99901..395db86b6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -51,7 +51,6 @@ LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 -LIBAVIF_VERSION=1.2.1 if [[ $MB_ML_VER == 2014 ]]; then function build_xz { @@ -113,45 +112,6 @@ function build_harfbuzz { touch harfbuzz-stamp } -function build_libavif { - if [ -e libavif-stamp ]; then return; fi - - python3 -m pip install meson ninja - - if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then - build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 - fi - - # For rav1e - curl https://sh.rustup.rs -sSf | sh -s -- -y - . "$HOME/.cargo/env" - if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then - yum install -y perl - if [[ "$MB_ML_VER" == 2014 ]]; then - yum install -y perl-IPC-Cmd - fi - fi - - local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) - (cd $out_dir \ - && CMAKE_POLICY_VERSION_MINIMUM=3.5 cmake \ - -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ - -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_SHARED_LIBS=OFF \ - -DAVIF_LIBSHARPYUV=LOCAL \ - -DAVIF_LIBYUV=LOCAL \ - -DAVIF_CODEC_AOM=LOCAL \ - -DAVIF_CODEC_DAV1D=LOCAL \ - -DAVIF_CODEC_RAV1E=LOCAL \ - -DAVIF_CODEC_SVT=LOCAL \ - -DENABLE_NASM=ON \ - -DCMAKE_MODULE_PATH=/tmp/cmake/Modules \ - . \ - && make install) - touch libavif-stamp -} - function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then @@ -186,7 +146,6 @@ function build { build_tiff fi - build_libavif build_libpng build_lcms2 build_openjpeg diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3b1be9a96..40d3dc7e8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -157,14 +157,9 @@ jobs: # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} + & python.exe winbuild\build_prepare.py -v --no-imagequant --no-avif --architecture=${{ matrix.cibw_arch }} shell: pwsh - - name: Update rust - if: matrix.cibw_arch == 'AMD64' - run: | - rustup update - - name: Build wheels run: | setlocal EnableDelayedExpansion diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 582fc92c2..8ba40ba3f 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,7 +1,6 @@ from __future__ import annotations import platform -import struct import sys from PIL import features @@ -10,7 +9,7 @@ from .helper import is_pypy def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} # tkinter is not available in cibuildwheel installed CPython on Windows try: @@ -20,11 +19,6 @@ def test_wheel_modules() -> None: except ImportError: expected_modules.remove("tkinter") - # libavif is not available on Windows for x86 and ARM64 architectures - if sys.platform == "win32": - if platform.machine() == "ARM64" or struct.calcsize("P") == 4: - expected_modules.remove("avif") - assert set(features.get_supported_modules()) == expected_modules diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index de3db3c84..ed41c2116 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -106,5 +106,6 @@ Pillow images can also be converted to Arrow objects:: Reading and writing AVIF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Pillow can now read and write AVIF images. If you are building Pillow from source, this -will require libavif 1.0.0 or later. +Pillow can now read and write AVIF images. However, due to concern over size, this +functionality is not included in our prebuilt wheels. You will need to build Pillow +from source with libavif 1.0.0 or later. diff --git a/wheels/dependency_licenses/AOM.txt b/wheels/dependency_licenses/AOM.txt deleted file mode 100644 index 3a2e46c26..000000000 --- a/wheels/dependency_licenses/AOM.txt +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2016, Alliance for Open Media. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt deleted file mode 100644 index 875b138ec..000000000 --- a/wheels/dependency_licenses/DAV1D.txt +++ /dev/null @@ -1,23 +0,0 @@ -Copyright © 2018-2019, VideoLAN and dav1d authors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt deleted file mode 100644 index 350eb9d15..000000000 --- a/wheels/dependency_licenses/LIBAVIF.txt +++ /dev/null @@ -1,387 +0,0 @@ -Copyright 2019 Joe Drago. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this -list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------------------------------------------------------------------------------- - -Files: src/obu.c - -Copyright © 2018-2019, VideoLAN and dav1d authors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------------------------------------------------------------------------------- - -Files: third_party/iccjpeg/* - -In plain English: - -1. We don't promise that this software works. (But if you find any bugs, - please let us know!) -2. You can use this software for whatever you want. You don't have to pay us. -3. You may not pretend that you wrote this software. If you use it in a - program, you must acknowledge somewhere in your documentation that - you've used the IJG code. - -In legalese: - -The authors make NO WARRANTY or representation, either express or implied, -with respect to this software, its quality, accuracy, merchantability, or -fitness for a particular purpose. This software is provided "AS IS", and you, -its user, assume the entire risk as to its quality and accuracy. - -This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. -All Rights Reserved except as specified below. - -Permission is hereby granted to use, copy, modify, and distribute this -software (or portions thereof) for any purpose, without fee, subject to these -conditions: -(1) If any part of the source code for this software is distributed, then this -README file must be included, with this copyright and no-warranty notice -unaltered; and any additions, deletions, or changes to the original files -must be clearly indicated in accompanying documentation. -(2) If only executable code is distributed, then the accompanying -documentation must state that "this software is based in part on the work of -the Independent JPEG Group". -(3) Permission for use of this software is granted only if the user accepts -full responsibility for any undesirable consequences; the authors accept -NO LIABILITY for damages of any kind. - -These conditions apply to any software derived from or based on the IJG code, -not just to the unmodified library. If you use our work, you ought to -acknowledge us. - -Permission is NOT granted for the use of any IJG author's name or company name -in advertising or publicity relating to this software or products derived from -it. This software may be referred to only as "the Independent JPEG Group's -software". - -We specifically permit and encourage the use of this software as the basis of -commercial products, provided that all warranty or liability claims are -assumed by the product vendor. - - -The Unix configuration script "configure" was produced with GNU Autoconf. -It is copyright by the Free Software Foundation but is freely distributable. -The same holds for its supporting scripts (config.guess, config.sub, -ltmain.sh). Another support script, install-sh, is copyright by X Consortium -but is also freely distributable. - -The IJG distribution formerly included code to read and write GIF files. -To avoid entanglement with the Unisys LZW patent, GIF reading support has -been removed altogether, and the GIF writer has been simplified to produce -"uncompressed GIFs". This technique does not use the LZW algorithm; the -resulting GIF files are larger than usual, but are readable by all standard -GIF decoders. - -We are required to state that - "The Graphics Interchange Format(c) is the Copyright property of - CompuServe Incorporated. GIF(sm) is a Service Mark property of - CompuServe Incorporated." - ------------------------------------------------------------------------------- - -Files: contrib/gdk-pixbuf/* - -Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this -list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------------------------------------------------------------------------------- - -Files: android_jni/gradlew* - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ------------------------------------------------------------------------------- - -Files: third_party/libyuv/* - -Copyright 2011 The LibYuv Project Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - - * Neither the name of Google nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBYUV.txt b/wheels/dependency_licenses/LIBYUV.txt deleted file mode 100644 index c911747a6..000000000 --- a/wheels/dependency_licenses/LIBYUV.txt +++ /dev/null @@ -1,29 +0,0 @@ -Copyright 2011 The LibYuv Project Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - - * Neither the name of Google nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/RAV1E.txt b/wheels/dependency_licenses/RAV1E.txt deleted file mode 100644 index 3d6c825c4..000000000 --- a/wheels/dependency_licenses/RAV1E.txt +++ /dev/null @@ -1,25 +0,0 @@ -BSD 2-Clause License - -Copyright (c) 2017-2023, the rav1e contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/SVT-AV1.txt b/wheels/dependency_licenses/SVT-AV1.txt deleted file mode 100644 index 532a982b3..000000000 --- a/wheels/dependency_licenses/SVT-AV1.txt +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2019, Alliance for Open Media. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. From c8d98d56a02e0729f794546d6f270b3cea5baecf Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:21:48 +1000 Subject: [PATCH 444/628] Added avif to config settings (#8875) --- docs/installation/building-from-source.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 9f953e718..9ba389b66 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -284,14 +284,16 @@ Build Options * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, ``-C lcms=disable``, ``-C webp=disable``, - ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. + ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``, + ``-C avif=disable``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, ``-C lcms=enable``, ``-C webp=enable``, - ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. + ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``, + ``-C avif=enable``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Tcl and Tk must be used together. From 75d3f1d3bdaee8b4e44ac1c437866e63ff577069 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 18:41:12 +1000 Subject: [PATCH 445/628] Assert palette is not None --- Tests/test_file_gif.py | 2 ++ Tests/test_image.py | 1 + Tests/test_image_quantize.py | 1 + Tests/test_image_transform.py | 1 + 4 files changed, 5 insertions(+) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 20d58a9dd..7ce6b7c8c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -224,6 +224,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None: out = BytesIO() im.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: + assert reloaded.palette is not None assert len(reloaded.palette.palette) // 3 == colors @@ -1359,6 +1360,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None: # Assert that the frames are correct, and each frame has the same palette assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) assert im.palette is not None + assert im.global_palette is not None assert im.palette.palette == im.global_palette.palette im.seek(1) diff --git a/Tests/test_image.py b/Tests/test_image.py index 7e6118d52..2a1911517 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -673,6 +673,7 @@ class TestImage: im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) assert im.palette is not None + assert im_remapped.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 0ca7ad86e..6d313cb8c 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -70,6 +70,7 @@ def test_quantize_no_dither() -> None: converted = image.quantize(dither=Image.Dither.NONE, palette=palette) assert converted.mode == "P" assert converted.palette is not None + assert palette.palette is not None assert converted.palette.palette == palette.palette.palette diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 77916929b..0429eb99d 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -48,6 +48,7 @@ class TestImageTransform: im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] ) assert im.palette is not None + assert transformed.palette is not None assert im.palette.palette == transformed.palette.palette def test_extent(self) -> None: From 7b459a8524a1aa7b5180cd868df3a78c6fc8ff4b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Apr 2025 19:33:17 +1000 Subject: [PATCH 446/628] Improved reading XPM images --- Tests/test_file_xpm.py | 2 +- src/PIL/XpmImagePlugin.py | 42 ++++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 73c62a44d..b604f07f5 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -17,7 +17,7 @@ def test_sanity() -> None: assert im.format == "XPM" # large error due to quantization->44 colors. - assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) + assert_image_similar(im.convert("RGB"), hopper("RGB"), 23) def test_invalid_file() -> None: diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 3c932c41b..304f58361 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -53,24 +53,20 @@ class XpmImageFile(ImageFile.ImageFile): self._size = int(m.group(1)), int(m.group(2)) - pal = int(m.group(3)) + palette_length = int(m.group(3)) bpp = int(m.group(4)) - if pal > 256 or bpp != 1: + if palette_length > 256 or bpp != 1: msg = "cannot read this XPM file" raise ValueError(msg) # # load palette description - palette = [b"\0\0\0"] * 256 + palette = {} - for _ in range(pal): - s = self.fp.readline() - if s.endswith(b"\r\n"): - s = s[:-2] - elif s.endswith((b"\r", b"\n")): - s = s[:-1] + for _ in range(palette_length): + s = self.fp.readline().rstrip() c = s[1] s = s[2:-2].split() @@ -82,7 +78,6 @@ class XpmImageFile(ImageFile.ImageFile): if rgb == b"None": self.info["transparency"] = c elif rgb.startswith(b"#"): - # FIXME: handle colour names (see ImagePalette.py) rgb = int(rgb[1:], 16) palette[c] = ( o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) @@ -99,9 +94,12 @@ class XpmImageFile(ImageFile.ImageFile): raise ValueError(msg) self._mode = "P" - self.palette = ImagePalette.raw("RGB", b"".join(palette)) + self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), "P")] + palette_keys = tuple(palette.keys()) + self.tile = [ + ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), (palette_keys,)) + ] def load_read(self, read_bytes: int) -> bytes: # @@ -114,11 +112,31 @@ class XpmImageFile(ImageFile.ImageFile): return b"".join(s) +class XpmDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: + assert self.fd is not None + self.fd.readline() # Read '/* pixels */' + + data = bytearray() + palette_keys = self.args[0] + dest_length = self.state.xsize * self.state.ysize + while len(data) < dest_length: + s = self.fd.readline().rstrip()[1:] + s = s[: -1 if s.endswith(b'"') else -2] + for key in s: + data += o8(palette_keys.index(key)) + self.set_as_raw(bytes(data)) + return -1, 0 + + # # Registry Image.register_open(XpmImageFile.format, XpmImageFile, _accept) +Image.register_decoder("xpm", XpmDecoder) Image.register_extension(XpmImageFile.format, ".xpm") From 89ac20d2b9690bed2a10195b8acaf405aeaf2fbc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Apr 2025 19:38:13 +1000 Subject: [PATCH 447/628] Allow more than 1 character per pixel --- Tests/images/hopper_bpp2.xpm | 390 +++++++++++++++++++++++++++++++++++ Tests/test_file_xpm.py | 7 +- src/PIL/XpmImagePlugin.py | 15 +- 3 files changed, 405 insertions(+), 7 deletions(-) create mode 100644 Tests/images/hopper_bpp2.xpm diff --git a/Tests/images/hopper_bpp2.xpm b/Tests/images/hopper_bpp2.xpm new file mode 100644 index 000000000..fe97b83ba --- /dev/null +++ b/Tests/images/hopper_bpp2.xpm @@ -0,0 +1,390 @@ +/* XPM */ +static const char *hopper[] = { +/* columns rows colors chars-per-pixel */ +"128 128 256 2 ", +" c #0C0C0D", +". c #0A0708", +"X c #1C0A04", +"o c #120B0C", +"O c #170808", +"+ c #0B110D", +"@ c #16120C", +"# c #0D0D12", +"$ c #0D0D1A", +"% c #070A16", +"& c #120D13", +"* c #120E1A", +"= c #1A0C16", +"- c #0D1114", +"; c #0D121B", +": c #091518", +"> c #131215", +", c #14131B", +"< c #1A141C", +"1 c #1B191D", +"2 c #191517", +"3 c #250906", +"4 c #390904", +"5 c #27150A", +"6 c #250A18", +"7 c #251719", +"8 c #361410", +"9 c #342215", +"0 c #0C0C24", +"q c #0C0D2B", +"w c #060927", +"e c #130D24", +"r c #150D2A", +"t c #0C1225", +"y c #0C122C", +"u c #061227", +"i c #151422", +"p c #1A1522", +"a c #1C1B23", +"s c #13132C", +"d c #19172A", +"f c #0C0D35", +"g c #130E37", +"h c #0D1436", +"j c #131333", +"k c #13143C", +"l c #191838", +"z c #241926", +"x c #231B38", +"c c #2E1226", +"v c #372628", +"b c #292538", +"n c #362B37", +"m c #2F2A2F", +"M c #1A2233", +"N c #4C150D", +"B c #740F10", +"V c #512916", +"C c #793419", +"Z c #6D2C13", +"A c #4E1524", +"S c #741624", +"D c #4E332E", +"F c #6F3629", +"G c #574438", +"H c #744831", +"J c #775A2E", +"K c #0E1444", +"L c #141443", +"P c #1B1A44", +"I c #14144B", +"U c #1A1B4C", +"Y c #181747", +"T c #1B1B53", +"R c #181955", +"E c #0F0E44", +"W c #231C46", +"Q c #231C56", +"! c #1C234E", +"~ c #272547", +"^ c #2E2F52", +"/ c #2E3765", +"( c #483947", +") c #742D4A", +"_ c #364970", +"` c #534A51", +"' c #6E534D", +"] c #756654", +"[ c #53556D", +"{ c #6B5B69", +"} c #746B71", +"| c #5E616A", +" . c #880C15", +".. c #881217", +"X. c #8D0D0F", +"o. c #8B3218", +"O. c #8C3828", +"+. c #AC2F30", +"@. c #9A1825", +"#. c #CE202B", +"$. c #8A452A", +"%. c #974A2B", +"&. c #884934", +"*. c #954B35", +"=. c #995539", +"-. c #895736", +";. c #A75738", +":. c #A84E30", +">. c #996839", +",. c #B6683B", +"<. c #AE6835", +"1. c #A35419", +"2. c #D26D19", +"3. c #CC712E", +"4. c #CD6922", +"5. c #A83152", +"6. c #985845", +"7. c #8A5748", +"8. c #AE5A46", +"9. c #916A4F", +"0. c #A96647", +"q. c #B76947", +"w. c #BA744A", +"e. c #B97757", +"r. c #AB6F53", +"t. c #8D736D", +"y. c #B27669", +"u. c #91566F", +"i. c #C56B4A", +"p. c #C8764B", +"a. c #C87856", +"s. c #D47A59", +"d. c #C96E53", +"f. c #C77C64", +"g. c #D17969", +"h. c #D45D68", +"j. c #C52A46", +"k. c #D58932", +"l. c #B38355", +"z. c #968775", +"x. c #BA8667", +"c. c #B38C74", +"v. c #AB9C73", +"b. c #C9845A", +"n. c #D7855B", +"m. c #D39454", +"M. c #E28C5B", +"N. c #F7B251", +"B. c #C78867", +"V. c #D98866", +"C. c #D8956A", +"Z. c #C79878", +"A. c #D89876", +"S. c #CD8C70", +"D. c #E38A68", +"F. c #E5956A", +"G. c #E79776", +"H. c #ED9176", +"J. c #D6A371", +"K. c #E8A379", +"L. c #F3A677", +"P. c #D8A05D", +"I. c #3D65AB", +"U. c #3F67B2", +"Y. c #3B5C9C", +"T. c #506796", +"R. c #72748D", +"E. c #446AAE", +"W. c #4869A9", +"Q. c #4166B2", +"!. c #436BB3", +"~. c #496EB4", +"^. c #476DB9", +"/. c #4A71B6", +"(. c #4C73BA", +"). c #4772B6", +"_. c #5176BC", +"`. c #547BBD", +"'. c #577BB7", +"]. c #5572A9", +"[. c #6B7CAA", +"{. c #505B8C", +"}. c #557CC1", +"|. c #4C73C2", +" X c #897987", +".X c #9F7593", +"XX c #C46B87", +"oX c #5981BF", +"OX c #5884BD", +"+X c #768AB9", +"@X c #7288B5", +"#X c #5C83C3", +"$X c #5D8AC5", +"%X c #6186C5", +"&X c #648AC6", +"*X c #6B8DC6", +"=X c #668BC9", +"-X c #6B8ECA", +";X c #6586C6", +":X c #738DC7", +">X c #6D91CB", +",X c #6C94C6", +" b R.DXPXLXHXHXHXHXCX~ / T.Y.T.T.W.T.W.E.Q.I.E.I.I.E.E.I.I.I.I.I.Y.I.Q.^.Q.E.E.E.E.Q.Q.~.U.U.U.U.U.U.Q.Q.U.U.U.U.U.U.Q.Q.Q.Q.U.U.U.Q.~.~.Q.U.Q.~.^._._._._._._._._.(.(.(.", +"L k f L L k y h T R I L U U L U R R T L E E E R I R I U l XuX' fXV v [ / P h z V Z.G a y l [ 7XCXHXJXHXHXCXb ! {.{.T.{._ _ {.T.W.W.T.T.W.I.I.U.U.I.I.E.W.I.I.Q.Q.E.E.E.Q.Q.~.~.~.U.U.U.U.Q.Q.Q.Q.U.U.U.U.U.Q.~.~.~.U.U.U.U.U.Q.~.Q.Q.Q.~.~._._._.'.`._._._._._.", +"L k f L L k 0 h T T I E U U L T T T U L h h E U R R E U W R.{ D pXF z l L U ^ p F fXD i P W Y ~ n CXHXHXHXFX8Xl W ~ ~ l ^ b b ^ ^ / [ T.W._.U.^.U.U.Q.E.W.W.~.^.E.E.E.~.~.Q.~.~.~.~.~.~.~.~.Q.Q.Q.Q.U.U.U.U.U.U.~.Q.U.U.U.U.U.U.^.~.~.Q.~.~._._.`.`.`.`._._.|.|.", +"k k f L L k 0 h U T L h I U I T T T U k h k E U Q I E U k ` m ' hXV z k U I Q d V fX( j L U W W z VXLX8X XuX( z b x d ` X X` n n b b ! {.W.~.I.Q.Q.Q.E.W.].~.I.~.~.~.~.~.~.~.Q.~.~.~.~.~.~.~.~.~.Q.Q.Q.U.U.U.U.U.Q.U.U.~.~.Q.Q.Q.Q.Q.Q.Q.~._._._._._._._._.|.|.", +"k k f L L k q j U T L h U U I T R T U k h h E I E R I I k b p ' Z.V z k ! T U p H Z.c k U L U W n CXCXn z = c c v 7X8X` 8XPX} c R.tX` n b / {.].W.~.~.E.W.E.~.^.~.~.~.~.^.~.~.~.~.~.E.E.~.~.~.~.~.~.~.~.~.~.U.U.~.U.~.~.~.~.~.Q.U.Q.~.~.E.~.~./././.(.(.(.(.(.(.", +"h k h P L k q j U T L h U U I T R T U h h h E E I R I E k d p ' dXV x P U L L z J fXv l L L P j n IX` m = = 7 ' HXLXKXCX7XKXtXrXLXKXJXqXv n ^ {.T.T.T.W.].E.Q.^.~.~.~.~.^.^.~.~.E.E.E.E.E.~.~.~.~.~.~.~.~.~.U.U.~.~.~.~.U.U.U.U.Q.Q.~.~.E.~.~.E.~.~.~./././.(.(.", +"j k k P Y k q h U U k h U R I R R T U h h h E E R R E I Y d d ' Z.V p L ! ! Y = H Z.v j h ! l b n iXtX7Xa p t.LXZX0XHXPXKXKXPXLXLXCXbXAXVXn m 8XVXeX[.T.W.W.^.^.~.~.~.^.^.^.^.~.E.E.E.E.E.~.U.U.~.~.~.~.~.~.~.~.~.~.~.U.I.I.I.~.~.Q.Q.~.~.~.~.E.~.~.~./.(.(.(.(.", +"j k k U P k 0 y L U k h U R I R T Q T E f E E E E I I I f k z ` Z.V d U T L P z >.B.c j l l l } IXKXKX8Xp ` t.` t.' G ] tXIXIXwX] ' z.t.` { c n PXKXLXUX+X].E.~.~.~.~.~.^.^.^.~.Q.E.E.E.E.U.U.U.~.~.~.~.~.~.~.~.~.~.I.I.I.I.~.~.~.Q.Q.Q.~.~.~.~.~.~.~./.(.(.(.(.", +"j k R.~ k k q q Y T k f Q T I I I Q I L E L E E R I I E f d x ` dXV d T T T U z -.Z.c b s b CXLXKXLXKX} z 7 7 z.hXSXSXz.AXbXmXbX0XJXmXmX` 1 n b iXKXLXLXLXDX@XT.].W.E.(.U.|.^.^.~.~.W.E.~.~.~.~.~.~.U.~.~.~.~.~.~.U.I.I.I.U.~.~.Q.~.~.~.~.E.~.~.~.~.~.~.~.~.^.^.", +"j ~ DX[ W q s q Q I f h Q L R R R R I I L f E I I I I L P d x ` dXV d R R T Y z -.Z.7 0 ` GXLXKXJXLXJX` 7 7 7 t.hXMXmXJXLXJXJXJXJXSXSXmX' n z b 7XKXKXPXKXLXPXBX].T.W./.^.^.U.|.~.~.~.~.~.~.~.^.~.U.U.~.~.~.~.~.~.~.U.U.I.U.~.~.~.~.~.~.E.E.~.~.~.~.~.~.~.~.^.^.", +"r x DXIX~ s $ b L U L L Y Q L I I I T L f L U E T T L k k d x ` dXV x T R U L p 9.Z.7 | KXKXLXLXJXSXZXD v 7 7 D mXMXmXSXZXZXCXZXAXZXdXmXG v n n 7XKXLXKXKXKXLXKXDX[.T.]./.I.}.U.~.~.~.~.~.~.~.U.~.U.U.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.~.~.~.~.^.^.^.^.", +"r b CXPX X[ iX[ Y U L k P [.~ k U T U L f f f L I U U k f d x ` dXV z T T U L z 9.x.D LXHXJXJXZXqXqXmXD @ 7 7 9 ] mXbXJXKXKXKXLXJXJXMXv.9 7 7 7 } HXKXKXHXLXJXLXKXDX[.T.W.(.~.^.Q.Q.E.E.E.~.U.U.~.U.U.U.~.U.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.~.~.~.~.~.^.^.^.", +"d x VXKXPXGXCX` P U L Y ~ BX| l k P k k k P w k h L L P j d d ( dXZ z P ! L k z 9.B.mXJXJXSX9Xz.t.D 5 5 5 7 7 9 hXmXv.mXLXKXKXKXJXSXv.mX] v c v D t.xXZXJXJXJXLXPXKXUXT.W.E.~.~.Q.Q.E.E.E.E.Q.Q.~.I.I.~.~.E.E.~.~.~.~.~.~.~.~.~.~.~.~.I.~.~.U.U.~.~.~.~.^.^.^.(.", +"x 8XGXPXHXHXtXb k U U k l CXtXd b ~ | {.j q k f P / h k k d d ( dXF < k ! L k z 7.zXSXJXSXt.] V 3 3 X 5 @ 2 c 7 z.v.bXSXAXKXLXLXZXmXhXMX' 7 n 7 9 3 8 ] qXZXJXLXLXKXPX@X].W.I.^.~.~.~.E.E.~.~.~.E.I.E.~.~.E.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.U.U.~.~.~.~.^.^.(.(.", +"uXGXHXLXJXAX} & W Q g g ~ DXCX` [ VXDX[ s j s y ^ eX~ j j d l ( pXF 7 k ! L L z 7.nXJXAX] D 3 3 3 X ' ] 7 1 = 9 t.SXSXMX9XZXJXJXxXmXSXSXJ v v 7 9 ] 9 9 5 ' 9XxXJXHXGXDX{.'.).~.~.~.E.E.E.E.~.~.~.~.~.~.~.E.I.~.~.~.~.~.~.I.I.~.~.~.~.~.~.~.U.~.U.U.~.~.^.(.(.(.", +"iXFXPXLXLXLXyX( k W k ~ b CXGXFXPXGXtXl l s 0 j ^ DX` d d d x D pXF z P T L P z ' AXAXz.5 X 3 9.] 9 5 v.5 G ` 9 J hXhXhXmX9X' ] qXhXhXMX] 9 D 9 G z.5 ] t.8 8 G wXHXPXIX[.T.W.].~.~.~.~.E.E.~.~.~.~.~.~.~.E.E.~.I.~.~.~.~.I.I.I.~.~.~.~.U.U.U.~.U.U.U.~.^.(.(.(.", +"d n } LXLXCXVX[ W W d ` tXHXAXAXHXIX^ x j l w s ` GX7X7 n } ~ D dXH p k ! P k l ` xX8X2 @ 5 7 gXbXhXhXv.hXmXSXmXMX9.J 5 5 V 9 9 9 V G dXhXSXSXdXhXl.MXMXdXV J V 9 wXLXFXtXR.T.T.W.~.^.~./.E.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.).~.~.~.^.^.^.^.~.^.(.(.(.", +"d d [ LXVX( ^ ~ k ^ 7XFXLXHXJXHXAX} x l k w l i ` GXCX8XbX Xx v Z.H z k ! P d d i . & @ . 2 7 v z.v.V dXmXdXZ.mXSXSXSXbXt.` 7 D ] bXJXSXSXSXMXSXSXl.hXMXhXmXMXV 5 v xXxX} ^ ! {.W.~.^.U.).E.E.E.~.~.E.E.~.~.~.E.~.~.~.~.~.~.~.~.~.~.).).).).(.(.(.(.(.(.(.(.(.(.", +"f ~ ` PXR.l l j Y ~ { uXFXLXJXHXFXuX~ W f f d a } HXZXxXyXn d n Z.H 7 j P l j r p & o @ @ @ o 7 X 9 D ] V 5 hXbXqXv.] G D ` n ` G ' 0X9XmXmXz.G 9.9.3 8 ] hXgX9XwXv D z > $ l ! W.~.^.U.).E.~.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.^.~.)./.(././.(.(.(.(.(.(.(.(.(.(.", +"g W ^ DX( l l q L W r b ` CXLXLXPXPXR.k k ^ | 8XCXHXbXxX{ < d v Z.J 7 j l d r * . > 2 o . @ 2 = 7 X X 5 D 5 5 5 9 9 9 @ 7 7 2 v 7 7 v 9 v V D G qXgXxXD 3 3 ' z.D 9 2 > 1 # d u Y.W.~.~.).E.W.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.^.~.)././././.(.(.(.(.(.(.(.(.(.(.", +"U U / eXP P l f L L d r b CXPXR. XUXDX/ k ~ ` 7XCXZXZXyXv p k v B.-.o d l i * * & o o 2 @ . & . < & o 7 o 7 2 @ @ @ 7 2 v < z < z 7 2 7 9 7 5 9 ] z.ZXCX` 7 5 X @ @ & . o # % u _ W.E.E.E.E.E././././././.~.~.~.~.~.~.~.~.~.~.~././.(.(./.(.(.(.(.(.(.(.(.(.(.(.", +"f U ~ / L U k f U Y k x d DXVX~ x W {.[ f d 2 7 t.ZXZXxX} x k z x.-.3 d a $ & & . 2 o . @ . # p # , & . > & o z 7 2 2 2 o & < & < . > 7 > 7 7 7 v ' m 7 @ 2 . @ . + . > . > % y _ W.E.E.E.E.~./././.(.(././.~.~.(.(.~./.~.~.~.~.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"I T P P ^ ~ k k L U q l ~ DX{.W W Q Q ~ d * * o { HXxXVXCX^ q z x.>.5 i , # & & > o > . + + > . # . * * . < & . o o 2 7 & 2 2 2 > > < > 2 7 o o @ . @ . o . . + . . # & . . ; u / ].W././././.(././.(.(././././.(.(.(.(.(.(././.(.(.(.(.(.(.(.(.(.(.(.(.(.(.}.}.", +"E Q L P Q Q P f f L k k ^ BXU ~ P W T Y j i * * XFX` b 7XR.l 7 l.>.7 , # # < o o . > . - - - $ # # . & , . & . o o o . . o . o o . . . . . o o o @ . . . . 2 . + . - . > > . w _ ].W./././.~.(./.(.(.(.(././.(.(.(.(.(.(.(.(.(.(./.(.(.).).(.(.(.(.(.(.(.(.(.}.", +"I U U U T Q k h L L h P Q / T L U T T U j 0 0 r 7XuXd r d ^ l < r.0.5 ; - - - & . > # # - + % # . . + . # # . . . . . . . . . . & # . . o o o o . o o . . . . . + . # # . > ; w _ ].]./.E.(.(.}.(.(.(.(./././././.(.(.(.(.(.(.(././././.(.(.(.(._.(.(.(.(.(.(.(.", +"L U U U T Q k q k L k P U Q U U U T T U k q q r X( j d 0 t y 7 r.0.@ ; + - - & . > & . . - - , - + + + . . + > . . . . . . . . . . . . o o o o o o o . . . . . . . % . . . - t / ].].]./.`.(.|.(.(._._.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"L U U T T Q k q k k k P I I I T R R T U L f q f / L W q w s s 2 0.r.X ; % - # # > > . # > + . . . . . @ @ o . o o o o o o o o o o o o o o o X X o o o o o . # # - . # > o . # w ^ ].].'./.`.(.}.(._.`.`.`._._._.`.`.`.`._.(.(.(._._.(.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"L U I T R Q j q k k h L I I R T R R T U L f q f E T U f j j 0 7 0.l.X ; % - # . . # . . . . o @ X X X X X X X 3 3 3 3 3 o o 3 3 o o 3 3 3 3 3 3 3 3 X X o X o o . . . & o o - % ! ].].'./.(.(.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.(.`.`._.(.(.(.(.(.(.(.(.(.(.(.(.(.", +"k U U T R Q k q l k f L T I T T R R T U k f 0 q I R E L q q q 7 >.l.X , % + . . . & = o @ X X X 3 4 N N N V V V V N N N N N N N N N V V F F H H H H F V 8 3 3 3 o . & o . . > % P ].].'./.'._.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.(.(.(._._.`.(.(.(.(.(.(._._.", +"k P U T R Q k q k k k L T I T T R R T U k q 0 q I I T f w j j 6 >.r.3 - . + + . > . X @ X X 3 N F 6.r.y.y.y.y.y.y.r.r.0.6.7.6.7.7.6.6.0.r.y.B.B.y.y.x.x.y.7.V 3 X . & & @ . # % h ].].'.'.'.`.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`._._._._.(._._.}.", +"k P U T Q Q k q f l k L T E T T R R R U j 0 0 y k E I L k j q 7 0.<.3 # . + . . o o X X 3 9 ' c.Z.A.aXaXaXaXjXjXjXA.A.A.S.B.S.B.S.S.A.A.G.K.K.G.A.C.C.Z.A.Z.r.' 9 o X o o @ - ; u ].].'.'.'.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.`.`.`.`.`._._._.}.}.", +"d W U R R Q k q f k g L T I Q R T R R U j 0 0 y k L I I f f f 6 r.>.3 # . . . . X 7 7 8 G t.pXaXaXA.A.fXzXzXzXlXzXjXzXlXzXzXzXzXkXzXkXzXlXlXzXjXK.K.K.A.A.K.Z.c.t.G 5 3 X o . t u '.'.'.'.'.'.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.`.}.}.`.`.`.`.`.`.}.}.}.}.", +"` b U Q R Q k q q l L L L T T R T T R U k 0 0 t j U I E L f f 7 r.r.X . . @ o o v ' F H y.Z.fXfXK.jXjXzXzXzXzXlXcXlXzXlXzXzXnXcXzXlXlXlXzXnXnXcXlXjXzXA.jXA.J.B.c.t.-.D 3 X & % K '.'.].'.'.oX`.oX`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.`.}.}.}.#X#X", +"xXz W Q I _ ~ y j j f U I I U U U R T U k q 0 t k U I L L f f = 0.l.X @ @ . . 9 ' ' H 0.B.A.fXfXjXjXzXzXzXlXcXcXzXzXlXnXcXzXzXzXcXcXlXcXzXzXzXzXzXcXjXA.G.A.C.B.Z.y.e.-.D o @ % K '.'./.'.'.'.oXoXoX`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.}.}.}.}.}.}.}.}.", +"HX7Xx ^ eXBXM $ l x Y U R R I R I I T U k q 0 y k U I I k j j = 0.l.X > o o 7 D ' H 7.y.B.A.fXfXjXzXzXzXzXzXjXjXzXzXlXnXlXzXzXzXjXzXzXsXsXD.B.e.x.x.S.A.B.B.S.Z.Z.c.l.e.' 7 @ , ! [.'.`.'.'.oX#XoXoXoXoXoXoXoXoXoXoXoXoXoX`.`.`.`.}.}.#X#X}.#X#X}.}.}.#X}.#X}.}.", +"AXZX{ CXPX| d 0 ` R.d Y U R I U E L R U k q q y L U U U k h l o r.l.X > . o v D H H r.x.l.B.A.A.B.B.B.e.e.e.B.S.jXzXzXlXzXzXcXzXfXjXjXD.D.a.q.e.r.e.Z.A.jXA.Z.Z.Z.c.e.e.9.G 5 # ^ [.'.`.'.'.'.'.oXoXoXoX#X#XoXoX#XoXoXoXoXoXoXoX#X#X#X#X#X#X#X#X}.}.#X#X#X#X#X#X", +"ZXHXHXGXVXb i i ` FX^ W Y g ~ P k L U I k q q q L U I U k q y @ >.l.X $ & 7 9 F ' 7.r.e.x.Z.A.A.A.V.a.a.a.f.B.A.A.jXzXzXzXzXlXzXzXfXA.D.D.8.*.=.*.6.r.B.fXaXfXc.c.Z.B.9.t.` v 7 ^ @XoX'.'.#X%X}.#XoXoXoX#X#X#XoXoXoXoXoX#X#X#XoX#X#X#X#X#X#X}.}.#X#X}.}.#X#X$X$X", +"HXAXZXFX{ x b # { FXrXx x {.eX^ k L U L k q 0 q f L E E f 0 t @ >.<.X , & 7 G 7.7.9.r.x.Z.Z.S.S.B.e.0.6.6.0.0.0.B.A.jXK.A.jXlXzXjXfXA.g.i.:.%.*.O.Z F Z Z F F F H ' -.7.t.` v c [ @X@X'.oX=X#X#XoXoXoX#X#X#X#XoX#XoXoX#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X;X;X", +"ZXAXZXGX` b & & ` ZXHX} uXGX8Xl k L U L j r r 0 r W Y f q 0 i 5 w.>.5 , . 9 7.9.-.-.9.c.c.x.B.B.x.r.9.7.7.6.r.y.r.B.A.jXG.jXlXlXzXK.B.e.0.=.C N Z &.6.*.&.H N V t.' V F ' { v v [ @XoX'.oX#X`.#X#X#XoX#X#X#X#XoX#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X$X", +"AXJXHXFXIX^ x = ] ZXAXAXCXVXb j k L U Y d i & & x ^ ~ f q q i X 0.<.5 & 7 D 9.9.-.J H ' G V V N 4 8 N N N N V Z $.r.G.lXlXzXlXnXlXK.b.0.C C 6.B.r.y.y.S.r.c.SX9.8 V A D ` ' D D R.eX+X%X$XoXoX$X#X#X#X#X#X#X#XoX&X#X#X#X#X#X#X#X#X#X#X#X%X%X#X#X#X#X#X#X#X#X#X#X", +"gXAXCXFXPXuXz D bXAXSXZXCX{ b k L L Y W r ` n v 7XtXx j w r r 3 0.>.8 2 9 ] y.9.H F 8 D D V N 0.cXaXy.r.r.B.S.*.O.Z <.n.kXkXL.L.F.p.;.0.jXy.9.V N N F F r.r.c.MXD V ' F D ' u.' XR.+X%X$X$XoX#X#X#X#X#X#X#X#X#X#X#X%X&X&X%X#X#X#X#X#X%X%X%X#X#X%X#X#X#X#X#X;X;X", +"z.ZX` ( { eX{ n CXZXZXZXxXm x k L U Y W r ` } z.iX` r w r r 0 3 0.>.8 D G 9.r.-.F V 3 8 3 N r.y.y.7.F V V N &.B.a.w.p.p.F.F.F.b.p.,.,.q.0.6.V H N V V N C $.H C H F V N V H r.9XgXrX[.*X&XOX&X=X%X%X%X#X#X#X#X#X&X&X&X&X&X&X%X%X%X%X%X%X%X%X%X#X#X#X#X%X%X;X=X=X", +"z.xXx ~ f x x z t.ZXAXAXCX} x Y U T U k p v ] ] } z r r 0 r s 6 r.r.8 D 9.r.6.C V V D D 8 7.9.r.' V N N N N Z F 0.;.;.s.n.p.a.p.3.1.p.p.;.Z O.V 4 N N N B o.o.%.0.-.H H &.r.f.6.y.yX@X:X'.oX-X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X%X%X%X%X#X%X%X%X&X=X=X=X", +"} } k W U k 0 & v CXAXCXGXCX~ f L T U k z D t.] 9 o p d w r d = >.l.N D c.e.0.6.F H V F V H F 6.V N V 4 4 N &.6.C O.%.s.i.F.zXkXF.n.M.s.;.i.8.=.&.O.F O.%.%.;.q.B.e.b.w.;.;.q.o.Z 0XeX:X[.@X-X'.&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X%X=X=X=X=X=X%X%X", +"[ ( Q L I U r * 7 CX8Xv } iXR.j L T U k r ( } t.{ . r r s 0 r 6 >.w.N ' y.$.$.=.6.6.6.r.6.$.O.O.O.C O.O.=.=.=.w.d.n.n.M.:.G.lXlXL.,.H.H.D.d.q.g.a.q.0.,.q.d.s.p.a.b.C.w.<.;.d.s.Z t. X[.:X;X;X=X&X&X&X&X&X&X&X&X=X=X=X=X&X&X&X&X&X&X&X=X=X=X=X=X&X=X=X=X=X;X;X%X", +"~ k L T T U 0 $ b CX` x x x ^ k L L U P d ( ' } 7X` r r s s 0 o 0.w.4 7.6.O.8.8.0.6.B.B.0.;.o.o.o.o.:.s.G.V.s.V.s.H.kXF.%.G.lXlXlXF.H.L.D.s.H.G.kXzXsXD.s.D.F.n.V.V.V.w.<.n.V.a.O.y. X[.:X;X;X2X=X=X=X=X=X=X=X=X=X=X=X=X=X&X&X&X&X&X=X=X=X=X=X=X%X=X=X=X=X=X=X=X", +"f k Y U L k 0 $ b 7Xj W Q L k q L U U Y r p 2 7 [ } d d 0 s t o r.r.4 F &.o.q.8.=.;.a.C.w.w.:.O.O.;.d.sXsXkXH.s.i.L.lXs.q.kXlXzXlXK.a.kXkXV.a.G.L.H.D.M.p.D.F.V.A.A.A.B.q.D.kXV.%.y.eXoX@X*X$X=X=X=X=X=X=X=X=X-X=X=X=X=X=X=X&X&X&X&X=X=X=X=X=X=X=X=X=X=X=X=X=X=X", +"k j L k P P q 0 x ^ Y Q R R L k h U U k j * # * p x k l q 0 t @ >.w.8 V 6.C a.g.q.%.w.n.p.p.d.q.:.i.i.V.s.i.q.d.lXkXH.,.V.lXlXlXzXlXs.G.K.lXkXn.n.i.p.i.p.p.G.C.B.V.B.B.a.q.V.A.=..X@X*XoX-X=X$X=X=X=X=X>X-X-X>X-X-X-X-X-X=X=X&X-X-X-X=X=X=X&X&X=X=X=X=X=X=X=X=X", +"j k L U U k q 0 j j Y U U L L L Y W L k y w ; $ 0 q h k q y y o >.w.4 8 r.O.V.s.8.%.d.s.n.p.n.q.%.i.p.a.a.f.sXlXlXzXF.q.kXlXnXcXnXnXjXB.zXlXlXnXlXkXG.V.V.G.L.F.V.n.b.b.8.o.i.D.r.t.@X+X@X*X-X>X>X-X*X*X*X-X*X*X*X*X-X>X-X*X&X*X-X-X-X-X=X=X&X&X=X=X=X=X=X=X=X=X", +"j k L U U k q 0 j f k Y Y k k L U W L k y u t 0 w j f f k y s 3 0.l.3 V r.=.D.s.:.%.i.i.n.n.V.q.,.s.G.kXlXnXnXcXlXnXb.C.zXlXcXnXnXlXnXS.G.zXlXlXlXlXzXK.K.K.F.V.V.n.,.C.d.o.;.S.c.8XeX:X@X;X-X=X>X-X*X*X*X-X*X*X>X-X-X-X-X*X*X*X-X-X-X-X=X=X=X=X=X=X=X=X=X-X-X=X", +"j k L L P j 0 0 j k L P Y k k L U U L h y 0 r r f f f f k w i 5 >.w.V 9XcXe.V.V.%.q.s.i.p.n.b.w.:.,.L.nXnXnXnXnXnXlXq.jXlXlXcXnXnXlXlXjXq.sXzXzXlXlXK.F.G.F.V.n.V.p.w.C.sX8.q.f.9X8X+X*X*X-X2X-X>X-X-X-X-X-X-X-X>X-X-X-X-X-X*X>X-X-X-X-X-X=X=X=X=X=X-X-X-X-X-X-X", +"h k L L P j 0 0 j k Y U P Y k Y Y U k h y w r r j r j f y t r X C C.c.hXcXA.K.p.w.A.C.q.<.p.a.b.p.i.F.lXcXnXlXzXjXq.q.G.kXlXcXlXnXnXnXzX0.:.s.H.G.G.G.K.kXkXF.n.a.e.b.C.kXD.q.y.0XeX+X*X*X-X-X=X>X>X-X-X-X-X-X-X>X-X-X-X>X-X-X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"h k L L k s $ $ y j L Y Y L L Y Y U k h y 0 r r r f k r r d & 9 w.C.c.hXcXA.G.n.C.jXG.q.<.p.w.b.D.i.p.D.kXkXV.i.;.:.D.H.kXlXnXcXcXlXlXlXsXi.8.8.:.;.a.G.G.G.F.a.a.a.l.C.kXs.q.S.8X@X:X>X-X-X>X=X>X-X-X-X-X>X-X-X-X-X*X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"k k L L l t $ $ y k L Y L L L Y Y P k h y 0 r r r f f s r # 7 t.J.P.c.hXMXfXC.G.C.K.K.a.q.i.w.p.F.M.i.:.%.;.;.:.o.s.kXH.kXzXcXlXlXlXzXzXsXH.8.H.H.H.sXkXD.V.V.a.b.e.B.A.G.q.V.B.8X@X*X-X-X-X2X2X>X>X-X-X>X>X>X-X-X-X-X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"k L L L l t % $ y k L I P P L L L Y h h y y s q r r r s * & ' hXK.J.x.hXcXx.B.K.C.A.J.b.i.i.p.b.a.F.D.s.V.H.V.i.:.H.D.V.G.sXzXzXjXG.sXV.s.s.:.q.H.kXkXH.D.n.n.V.b.e.B.A.b.V.G.c.eX:X>X-X&X*X*X&X>X>X-X-X>X>X>X>X>X-X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"k k L k j t # ; y k L I L L L k g L f h y y s 0 q r s r * D wXgXJ.P.x.cXMXl.b.C.jXA.C.e.q.i.a.b.p.n.V.H.sXkXV.%.o.;.O.%.q.q.f.B.B.f.8.:.Z O.B O.s.G.H.D.H.V.V.C.B.e.B.0.C.K.s.pXeX&X=X-X,X,XX>X-X-X>X>X>X>X>X>X>X>X>X-X-X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"f P k f ~ 0 $ ; j w I T L L f L Y P k y y y y % 0 w y i ` 0XgXhXP.P.x.cXhXl.x.A.b.b.%.w.p.i.a.n.p.n.V.D.G.V.:.o.V.q.Z Z C o.$.$.$.O.=.o.%.g.:.B :.s.g.s.V.n.V.C.V.B.e.q.q.w.A.9X+X-X=X>X*X*X*X,X*X>X>X>X>X-X>X>XX>X-X-X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X2X3X3X", +"d j f W s s $ % s k g L L k h g g k f y t t t t w y t | tXwXgXfXP.P.B.cXhXc.x.A.C.B.C.K.V.i.p.n.n.a.F.V.V.i.o.i.G.G.V.V.V.0.o.N C 8.g.V.g.D.i.Z B ;.d.s.s.s.V.C.B.C.kXG.K.C.fX0X+X=X=X-X*X*X*X:X*X>X>X>X>X-X>X>X>X>X>X-X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X>X", +"p d W / d $ i i 0 k P U P k L k g f q q i i $ i $ d 8XiXrXgXgXfXP.P.B.cXhXZ.x.S.jXA.jXkXsXa.p.n.F.n.n.V.i.O.:.D.H.D.H.V.H.H.g.q.a.V.s.V.s.a.V.q.B o.:.,.i.s.V.C.V.C.jXzXC.fXgX8X+X*X-X>X,X*X:X,X*X>X>X>X-X-X>X>X>X-X-X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X>X", +"( m X[ d s $ $ l q j k k k h f g k j 0 i , & * x 7XiXtXyXqXgXdXP.P.B.cXhXZ.r.e.jXzXjXlXjXa.b.V.C.n.a.p.%.o.s.D.H.H.G.H.G.G.sXH.sXH.V.V.D.V.H.s.O.B O.;.a.V.s.b.b.A.zXjXK.dX0X8X+X*X-X>X,X:X:X,X*X>X>X>X-X-X-X>X-X-X-X*X*X*X*X*X-X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"qX0X8Xx r r * $ 0 s j j j f g k g j j r * & 6 D XrXuXyXyXgXgXfXP.P.B.zXhXZ.9.=.A.jXjXzXr.e.V.b.F.b.n.<.o.:.s.D.G.sXsXkXsXkXlXzXzXsXsXD.H.G.G.V.s.O.o.:.i.s.V.C.B.B.zXjXB.c.7XeX+X*X-X>X>X*X*X-X-X>X>X>X-X-X-X>X-X-X*X*X*X*X-X-X-X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X", +"mXqX} z p z $ * b ` b j l w k k h f s i = 6 A u.rXrXyXxXqXqXxXfXJ.P.B.zXhXZ.-.H >.B.A.0.N -.e.C.C.C.n.:.o.p.d.H.G.sXG.kXsXkXlXzXzXkXsXsXsXG.G.V.V.i.o.d.n.V.F.b.B.*.r.B.e.9XeX@X+X*X-X-X-X*X*X-X-X-X>X>X-X-X-X>X-X-X-X-X-X-X-X-X*X*X*X*X-X-X*X*X-X-X-X-X-X-X-X-X", +"hXqXD z z e * ( R.[ d $ d s q q h h j r 6 c A u..XvXxXyXqXqXxXdXP.P.x.hXzXZ.7.H -.0.0.Z 4 H w.V.V.G.V.,.:.s.s.D.s.a.q.d.i.d.H.H.g.s.i.s.s.a.s.D.s.V.;.s.H.F.G.V.e.C F 6.fXwX+X+X+X+X-X-X-X*X-X-X-X-X>X-X-X*X-X>X>X>X-X-X-X-X>X>X*X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X", +"bXqX( z = p & ` } p d d 0 s q k q h j $ 6 c A &.y.gXxXxXyXqXgXhXP.P.B.fXzXZ.9.H H =.x.9.V V e.C.C.F.kXs.i.d.s.d.%.o.o.o.o.o.:.:.o.o.o.o.o.o.o.:.q.d.D.F.kXF.n.B.B.C *.c.c.z.eX+X:X:X-X5X-X-X>X>X-X-X>X-X-X-X-X>X>X>X>X-X-X>X>X>X-X*X*X*X*X*X-X*X*X-X-X-X-X-X-X-X", +"ZXxX.Xz z = . ` n x d s l q q h f j s r = A S S r.9XgXyXxXqXgXfXC.F.m.fXfXe.6.H F V D D V N 0.b.V.G.L.L.i.i.:.O.o.%.:.;.8.8.+.+.:.d.H.D.s.D.n.a.i.s.n.L.L.F.S.Z.e.H -.y.qXtX@X,X*X-X5X5X5X>X>X>X>X,X,X,X,X,X-X-XX,X*X>X>X*X*X-X-X-X>X-X-X-X*X*X-X-X-X-X-X-X2X", +"0XyXuX( = p . { ^ * d j j h y s r j s r 6 A S B 6.pXgXyXgXgXgXfXF.F.m.jXZ.0.>.;.Z N 3 9 v V =.b.b.n.G.L.V.p.a.p.s.s.g.H.sXsXH.sXsXH.G.sXH.D.s.n.V.p.F.L.F.n.A.B.9.H 9.c.yXtX+X+X-X2X5X5X5X-X>X>X>X>X,X,X>X>X-X-X>X,X,X*X*X*X*X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X", +"` = { } z & < 7X{ * d r q j 0 s s r y r 6 A S B ..g.gXgXgXgXgXfXF.P.B.fXZ.>.;.,.<.Z N N N N F w.p.n.L.F.n.p.M.n.s.n.n.V.H.sXsXH.H.D.H.V.d.p.n.F.F.p.F.L.n.n.B.x.-.H 9.0XtXeX+XX>X>X*X,X>X>X>X>X>X>X>X-X-X-X*X*X,X*X*X*X*X,X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X", +"z p r z r * & } Xx d r r s 0 s s s t * 6 N S B B r.gXgXgXgXhXfXP.P.B.J.Z.0.,.<.3.3.<.Z N N Z 0.a.b.V.F.M.3.n.M.s.p.p.p.i.q.8.;.:.:.8.q.p.D.F.V.V.a.M.M.M.a.B.r.J -.t.8X7X@X:XX>X,X,X,X>X>XX>X>X>X>X-X-X*X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"i d r s r * $ < } b d r d r 0 d y s i & 3 N B B B O.aXgXgXgXhXA.P.P.l.J.Z.0.,.<.3.2.k.k.<.Z N *.w.p.p.F.n.p.n.n.F.D.n.d.,.;.;.;.;.:.;.<.p.n.n.p.V.V.n.n.s.a.y.7.7.9.} 8XeX:XX>X>X>X>XX>X>X>X>X>XX*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"y q j q $ r * * b x d r d q i i t i = 6 N B B B B ..h.aXgXgXhXZ.P.m.b.J.A.>.<.<.3.2.2.2.3.1.B *.q.n.p.p.,.n.D.s.p.s.s.a.V.G.G.G.D.V.V.a.p.p.p.n.C.V.s.D.i.a.y.H 9.t.} eX+X1XX>X>X,X>X>XX>X>X>X>XX>X*X>X>X>X*X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"l w h l s $ . & $ i s q j q y 0 t * 6 A B B B B B .+.aXaXaXgXZ.P.m.b.K.A.<.,.3.4.2.2.2.4.1...O.d.w.q.a.n.n.D.F.F.D.V.H.zXzXzXzXzXL.kXkXL.L.L.L.C.a.n.d.s.q.B.' } 8XeX+X+X,X,X,X,X,X,X>X2X3X3X3XXX>X>X>X>X>X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X", +"y l h s $ $ * & i 0 j f h q t ; , o 4 A S .B B B .. .g.aXaXgXZ.m.P.m.G.C.q.3.4.4.2.2.2.4.4.o.o.s.q.p.p.V.a.n.V.s.D.D.D.L.lXlXlXzXH.kXkXL.L.F.L.B.b.a.;.a.e.c.t.} eXeX+X>X>X,X,X,X,X,X2X2X3X3X3XX>XX>X>X>XX>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X", +"y h j s r $ # # i t q y h q t , = c A S . .B B .X...+.aXpXpXx.k.N.m.n.n.,.p.4.4.2.2.2.4.3.Z B e.b.q.w.p.b.p.n.n.M.n.n.F.F.L.kXH.G.kXzXkXL.C.C.e.0.;.q.q.r.y.t. XeX:X1X3X2X1X,X,X,X,X>X2X2X3X3XX>XX>X>X>XX>X>X>X-X-X-X-X-X-X-X-X-X-X-X", +"y s s s r $ # # $ d l q q q i = c A S ..X.X.B B .X...@.f.S.aXe.k.N.k.p.p.<.3.4.3.2.2.2.3.,.Z F A.b.;.;.w.p.,.p.n.n.n.p.n.n.F.V.V.D.G.A.F.F.n.e.0.$.%.o.%.x.y.D 7X[.+X5X3X3X-X>X,X,X,X,X>X>XX>X>X>XX>X>X-X-X-X-X-X-X-X-X-X-X-X-X", +"r s s i r $ # # ; d j q r d = 6 A S S ..X.X. .X. .....B :.S.aXe.k.N.k.<.<.<.3.<.2.2.2.k.<.$.8 gXaXB.r.%.$.%.%.;.a.n.b.a.s.e.e.q.e.g.B.f.a.a.q.=.F C Z C r.Z.t.o ^ R.+X5X3X6X6X2X>X,X,X,X>X>XX>X>XX>X-X-X-X-X-X-X-X-X-X-X-X-X", +"r s s r r $ # - ; i q r r 6 6 A S S ....X.X. . . ..... .o.g.S.b.k.N.k.<.<.1.3.3.k.2.2.3.;.4 9 AXc.S.f.*.Z N Z C =.0.q.0.=.$.&.=.6.6.6.=.=.%.%.o.Z N Z r.B.Z.t.X < M T.:X5X3X3X4X3XX>X>X>XX>X-X-X-X-X-X-X-X-X-X-X-X-X", +"s s s r r $ # - ; i r r 6 6 N S S .X.X.X.X. .B . . . .B i.V.b.k.N.k.<.<.:.3.3.3.4.k.<.4 X t.bX9.S.f.e.$.N N N Z F F Z V N V F F V N N Z Z C o.Z Z 0.B.B.Z.t.@ > $ y {.NX3X3X-X3X3XX>X>X>X>XX>X>X-X-X-X-X-X-X-X-X-X-X-X", +"j j j r 0 $ # ; i p p 6 8 A S .. .X.X.X.X. . .B B B ..B X.:.D.,.k.N.k.1.:.%.p.<.,.<.-.8 X X wXgXH S.f.f.r.C C Z Z Z N 4 4 8 8 4 4 4 4 4 N C $.$.F 6.f.a.Z.Z.t.o . - $ y [ +X1X6X3X4X4XXX>X>XX>X>X>X-X>X>X>X>X2X2X2X2X", +"j j j q 0 $ # ; , * c c A S .. . .X.X.X.X. .B B B B .. .X.+.D.;.k.N.N.1.1.%.w.%.0.V 3 3 = & xXgXZ A.B.e.r.*.&.&.&.H V N 8 8 3 3 3 4 N V F =.6.*.*.0.e.B.B.pXz.X < # # ; % ^ @X,X>X3X4X3X1XX>XX>XX>X>X>X>X>X>X>X2X2X2X2X", +"j j j q 0 $ # - , z ( ) S S ..X. .X.X.X.X.X.B B B S B ..X.X.g.;.m.N.P.1.%.$.r.H 3 3 @ o * . CXmXV C.B.e.e.;.$.6.=.7.F V N 8 8 8 4 N V F &.6.6.*.6.f.e.B.B.mXt.o # , & . - + h _ @X:X1XNX1XX>XX>X>X>X>X2X2X2X", +"s s d w r $ # . 7 } vXXXS ..X.X.X.X.X.X.X.B B B B B B X.....f.;.m.L.P.-.Z N 3 X o o # # # & CXZXF e.B.e.e.;.;.=.6.6.&.F Z V N 8 N N Z C =.6.*.*.0.f.e.B.hXZX} ; # # & & # # ; u w ! {.+XNX,X,X4X1XXX3X4X4X1XX*X*X*X-X.Z 3 X X . . # # # # > @ CXZXF 0.A.e.0.q.;.=.;.=.O.O.C Z V V F C O.%.6.;.%.;.e.e.B.Z.SXAX| % # # # # # # # # ; t u h _ +XNX+XX>X4X4XX , i t t y h h ^ T.+X+X+X1X1XX>X>X # # # # # # # # & # # i d i r t h w h {.eXNX1X1X,X,X1X1X1XX4XX,X,X & o XFXHXt.&.f.a.,.w.<.q.0.=.&.*.*.=.8.;.:.;.0.q.;.;.e.0.pXAXZXPXVXo & # # # # # # # # & & < < , * , a i d y y l / T.+XNX1X1X1X,X,X1X4XX>X>X-X5X > > + + + . . . & # # | CXHXZXF 0.s.,.<.w.w.;.=.&.=.*.;.8.;.:.;.w.q.;.0.r.r.ZXAXGXGX} = o # # # # # # # # & & & < < < , < , i i i t y h h ^ _ ].+X1XNX1X,X1XX2X>X-X-X5X5X5X5X", +"s q q 0 , o 9 9XgXgXxXyXxXCXgXD 3 o o @ + + + . - . # - > # , . > . . . @ . . . . . . # # , | IXHXHXt.F e.a.,.<.w.;.=.&.=.%.;.8.;.;.;.q.;.=.=.-.hXAXHXGXGX` o & . # # . . . # # & < < * , < < > < < , , i d s y h l h h ! [ @XNX1X1X1X1XX-X3X2X>X-X5X-X5X-X", +"j q s 0 * o 9 9XgXxXxXyXtX X7 o o o . + . . . . & # # - - - - + + . . . o o . . > . # . # # ` IXFXLXZXF =.e.p.<.w.q.=.*.*.o.;.;.;.=.,.q.;.*.F c.ZXJXHXGXFX< & . . . # . . # # # # # * , i i , , < < < p i i i i s d l l y q l ! T.NX1X,X,X1X*XX>X2X2X-X-X5X5X", +"y y r r = o 5 9XqXbXwX7 2 2 . 2 & & & > # # & o & & # + + . . . . . . . o & & & # # , > - # n GXFXHXJXmX&.r.w.p.w.e.=.$.=.C $.$.$.=.%.w.;.C 9.ZXHXHXLXGX7X& $ # . . # . . . # # * , , , i i i i < < < < < p p p a i i d d s s j h ! T.NX1X,X,X*XX2X2X-X-X5X5X", +"0 s r r p & @ qXbXyX7 2 o & > & & = & & & # & # . . . . o o o o . . . # # & * * * * * , # & z FXPXHXLXAXZX6.a.s.e.e.6.*.=.C $.*.$.O.0.0.$.7.AXHXKXPXPXGXD . $ # . . . # # # # # * ; ; ; i i i i , < p p < < < < z < < p a p i r y h L _ @XeX1X,XX-X-X-X-X", +"i i $ r * & o wXxX` o < . # # # = = & & & & # - . . . . o o o o # # # # $ $ $ * * * * & . & 2 tXFXLXPXLXJXcX6.g.A.e.r.0.%.C *.&.$.&.e.0.H SXHXKXKXLXPXCX2 . # # & # # # . # # - # $ ; ; i t i i i i i i < < < < 7 < < < < < < p d s y h h ^ [.1X:X5X5X5X-X-X-X-X", +"p = * * * & o 0XyXo 7 . $ 1 # > & & & & & > - - . . . . o o o o # # # $ $ $ $ $ $ # * * & & & ` FXGXKXKXLXJXpX6.B.B.b.e.=.$.r.&.&.y.r.H SXLXLXKXKXKXLX X& & & & > # # # # - - - # - ; i i i i i i i i i , , < < < < < < < < p a a x i t y h / NX:X5X5X5X-X-X-X-X", +"v = * i # & = 0X' o . 1 , . & > - - & & & > & - . . . . . . . . # # # # # # # # # # & & , & < < xXFXPXKXKXLXJXc.r.B.A.B.e.0.y.0.r.y.7.AXLXLXKXKXKXKXPXn , > & > > > # # - - > > > , , i i i i i i i i i i , p < p , < , p i , , & & z i t y u [.:X > . + - & & & & & & # # # # . . . . . o o . . . . + + + + . - # p * t.GXPXKXKXKXJXJXx.e.B.B.x.e.y.e.e.c.AXLXLXKXKXKXKXKXtX# , > > > , ; # # - > > > > > > , , , i i i i i i i i i p i i , i i i t $ 7 o 7 > $ s u _ :X5X5X5X5X-X-X-X", +"yXD & > # # o } o X > > - # o @ + + o o & & & & . # # # # # # . o o o o . . . . + . . . - # , p ` GXGXKXKXLXLXJXAXx.Z.fXMXnXcXcXcX9XAXKXKXKXKXKXKXLX| , , # < & , ; # - - > > > > > > > , , , i i i i i i i i i , i i i * t i i = 2 & > a % y K :X5X5X5X-X-X-X-X", +"xX8X3 o o o 2 n o o > > > > & & > - # & o . & # . . . # # # # . # o & o . . . . . . # # # & , , m IXGXLXKXKXKXKXLXJXHXAXAXZXxXwXD 5 D FXKXKXKXKXLXCX, , 1 > . > > > > > > > , , , , , , , , , , i i i i i i , , , , , , , , , , # > , , > - $ q @X:X5X3X-X>X*X>X", +"xXqXG X o o = z & & o > > & & # & # # . # # # # . . # # # # # . . # # - # # # # # # # # # # & & < tXPXPXLXKXKXKXKX8X` ` n v z < < < & m CXKXKXKXKX} , - > > > > > > > > , , , , , , , i , , , , i i i i i i , , i i i i , , * $ $ ; , , > & # 0 {.:X3X-X>X,X*X # # # . . . - - - # # # . . . . # # # # # # # # # # # # # & < } FXPXKXLXKXLX} < . a > & < > & > z > m IXKXLXGXb 1 , > < > > , , , , , , , , , , i i i i , , p i p p p p i i p p i , , * $ $ $ , , , & & - t ! 1X3X*X>X*X>X>X", +"gXgXgXt.3 7 7 & & & o & & & # # # - # # # # # . > > > # . . . # . . # # # # # # . # # # # # # & > ` FXKXKXKXLX{ > & 1 1 < a < & 1 o . . a n GXKX8X< , 1 > < > > , , , , , , , , p p p p i , , , i i i p p p i i i i , , * * * $ $ , * * & & - t h +X*X > # # . . . . . . - - # # # # . # # # # # # & , b CXPXLXKX| # & , , . > , > 1 > 2 < < > & ` GXn , > , > > < > , , , , , , , , a a p p p , , , , i i i i i i i i i , , , , * $ , * * * & # # $ q {.+XX3X-X", +"hXpXgX0X' 7 X o o o o & & & & & # # # - > # # . > > # # # # . . . # - # # # . . # # # # # # # # & a R.PXKX| , , , # & & & > & > & & & & . z # ` 1 # 1 > > > 1 > < < , , , , , , p p p p p i , , , i i i i i i i i i i i , , , * * * * * & # # $ q ! 1X:X,X > > > . . # . . . - # # . . . # # # # # # # # # # # # # & # a ` PX} , # # & . > > . > > # > 2 < & a . , 1 a > z , > , < > , , , , , , , , p p p p i , , , i i i i i i i p i i i , i , , * $ $ * * & # # $ t u [.+X,X > > . . . . . . - - # . . . # . # # # # . # # # # # > , , # z | & , > . # # > # . # . > & & < > # a , 1 , , , , & 1 > > , , , , , , , , p p p p i , , , , , , , , , , i , * , * , , , , $ $ * & & # # # % u _ +X,X-X3X3X", +"fXpXpX9Xt.9 X o o . . # # # & > # . . . . # # - > # . # # # . . . . . . # # # # # # # # # # # > , , a # - 1 . > 1 > > > < . > m 7 z m , , , , < , , , , , , , > , , , , , , i p p i , i p p , , , , , * * , , i , , , , , , , * $ $ $ # & & & - ; u P +X1X5X3X2X", +"fXpXpX9Xt.5 X o o . # # # # # # # - . . # # > > # . . # # # # . . . . . # # # # # # # # # # # & , , 1 > # # . > # # # 8XtXCXCXCXCXCXiX7Xa > , > , , , , , , , > , > > > , , , , , , , i p p < , , , , * * , , i i , , , , , * * $ $ $ # # # # # # % y [.:X > > - # . . . # # # # # . . # # # # # # # # # # # # # ; , , & , 1 & 1 , | CXPXKXKXKXKXKXPXIX| 1 > # > > > , , , , > , , > > & > , , , , , p p p , , , , , * * , , , i , , , * * * $ , , ; - # # # # # % % [ +X1X3X3X", +"fXdXpX9Xt.3 o o o # # # # # # . - - - - # > > > # . . . . # - # # # # # # # # # # & # # # # # # # ; ; , > , 1 # > 1 uXIXKXKXLXKXKXKXPXIX} 1 # , > > > , , , > > , , > > , , , , , , , p p i , , , , , * * * , , , , , * * * * $ , # # # # # # # # # % ^ :X1X2X3X", +"fXdX9X9Xt.X o o o # . . # # # # . . . # # # # # - # . . . # # # # - - - # # # # # # # # # # # # , # # , , > < a < { CXLXKXKXKXKXKXKXPXGX( > , ; , , , , , , , < , , , , , , , , , i p p i , & # , , , * $ $ * , , , * * * * * $ # # # # # $ $ # o . $ P NX1X3X2X", +"fXdXpX9X' X o o # # . . # # # & . . # # # # # # > > # . . # # # - - # - # # # # # # # # # # # # # ; , , a 1 > > n tXFXKXKXKXKXKXKXPXGXtX, , a # , , , , , , , < , , , , , , , , p p p i , * # # , , * * $ $ * , , * * * * * * # # # . # # $ * # o . ; u +X1X3X2X", +"fXpXpX9XG X o o # # # # # # # # . . . . - # # # > > - . # . . . . . # # # # # # # # # # # # # # # , , # , , & m 7XCXLXKXKXKXKXKXKXPXCX` , , 1 , , , 1 1 , , , , , < , , , , i i p p i , * $ & & , , , , * $ * , , , * * * * $ # # # . . # $ * & o . # w [.1X @ - . > > - # # . . . . . # # # # # # # # # # # # # & , # , a a # m 7XCXGXKXKXLXKXKXKXPXCXuX< 1 a , i 1 1 1 1 , > > , 1 1 1 < 1 , p a p p < , $ # & * , , , , , ; , , , , , * & & # # # # . # # $ $ # & & # y {.1X & # . . # # # . . > . 1 @ . @ > . . > . . . . . 2 & . # # > . > > & # # # # & , # a , a , 7XCXGXKXLXKXKXKXKXLXCXVX( z a 1 , i a 1 , , > > > > 1 - 1 , , 1 , , , & = o , # * = < & , $ , , ; , , - ; & & & # # $ # & # # # $ $ ; # $ t [ NX4X3X", +"aXpXpX9X9 X & # & & & o . . # # > . . 1 . . 1 . @ @ o 2 . o > > . . . & , < . 1 & & & # # # # # # , , , i M R.CXFXKXKXKXKXKXPXIXrX} z < < a , 1 1 1 , < , > > > < > < a 1 , a < < = = = . & = 6 7 & a # - % ; , , # # # & & & - # # # & # # $ $ $ $ $ 0 _ NX4X3X", +"fXZ.9X9X5 X & # # # & o . . # # . o 8X8X8X} } | ` ` n m 2 > o . 2 & > & . < . # # & # # # # # # # 1 # , , a ` iXIXPXLXKXKXLXCX7X} n , , , a , , < < < < < > > , , a < < , , < = 6 6 6 c 6 c 6 6 6 6 , ; t : & & = * & & & & . # # # # & # # $ $ $ ; * 0 ^ NX4X4X", +"fXZ.9X9XX o & # # . o o # . # # 7 . 8XxXKXKXLX7X7X7XrXuXiXxXtXrX8X X{ ` & . 1 > # & # # # # # # # , # , , , a 7XVXHXKXLXLXFXrX{ ` i i a i a i , i < p < < , , , * x p p p < 1 ` D ) ) ) ) ) ) ) ) u.2 + + ; o = & & o > * # # - # # # o # # $ $ $ ; * 0 ! NX3X4X", +"dXZ.9Xz.X o & # . # o o & # # # . z 8XCXLXKXCXt.8X7X7XrXtXiXxXxXiXiXtXtX{ > . . & & > # # # # # & # , a i i , | iXFXLXHXZX7X} } n t i i a $ i p i p p p p , , , d r * < z & o 8X5.5.5.5.5.5.5.5.5.vX7 + ; , = = 2 > @ > $ % . . # # # & # # $ $ # ; # $ l +X & & & & & # # - > , # , , i a , m uXVXZXFXtX X8X8Xi t i i a * p p p p p i i 1 , , ; i x = < < 6 0X5.5.5.5.5.5.5.5.5.vXv @ = = 8 3 3 3 X 2 , % # . # # # & & & ; ; ; - , $ h [.1X4X", +"pXpXpX' o o . # & & o o . . # # < D yXCXCXVXFX7X8XuXVXCXCXCXVXCXCXVXxXiXVX7X` . & & & & # & > > , , , , , i a & 8XCXFXxX8XrXVX` , i p , p p p p i a i i , 1 , , 1 > 6 7 { rXu.u.u.) .Xu.u.u.XXu..X7X' t.u.7.O.@.O.7.9.] ( r o o # # & & > > ; ; ; , < , y {.1X4X", +"pXpX9XD o o . & > o o o . . # # & ` XuXFXFX7Xt.8XrXrXrXtXuXuXiXiXuXyXtXuXtX7Xz & & & & # # > > # , # a i * a < ` VXxXrX8X8X7X= < * < * * p , , i a i 1 , , , > @ 5 A ' yXPXvXu..X} uX X.Xu.vX{ BX} ] cXXXh.+.j.+.h.pX9X' = o = & - & > > > ; ; - # , $ y [ 1X4X", +"dX9X9X9 o > # # . . o o . . # # & n { } } } X7X7X7X7XrXtXtXuXuXtXtXtXtXrXrX7Xz # & # & # # # # > , ; ; , , , < & uXyX} ` 8Xv & < * * * * , , , i , , , < < < 7 4 V O.u.vXPXvXu..X{ VX X7Xt.vX} BXR.9.aXg.h.j.#.#.h.g.pX' z & > & > > > > $ ; ; ; > , $ y / 1X4X", +"dXpX9X9 @ > . # # . o o o o & > . & & # . . < z m n n ` ` { } } 8X7X8X7X7X7X7Xz # # # # # # # # > > , , , * , * < ' 8X` D { & = , , , , , , , , ; ; , < = 6 6 8 Z *.8.8.5.h.5.u. XrX8X X X7Xu.t.8X} ' 8.5.j.#.#.#.#.+.r.A & . > # > > > , ; ; ; ; > > , y ^ NXNX", +"pXpXz.5 @ > . - . . o o o . # # # # . # & & # # & & & # . . # & & & 2 z m n n & # . # # # # # > > & # # $ * * & = 7 ` D ( c & & # - - - , , 1 1 1 1 , 2 6 c A F 0.G.G.D.+.j.5.XXrXVX` 7XiXGX( { uXrXu.5.j.#.#.#.#.#.+.h.c * # > # & > > , , ; ; ; - > i t h NX4X", +"pXpXt.X @ > . > . . . o . . # # . . . # # - # # # # # # # # # & & . o & . . # & # # . # # # # & > & # # $ $ # & & & 7 ( D . < & # - - - - ; 1 1 1 1 , = 6 c D F e.K.A.B.+.+.5.u.rXVX| 7XrXCX' t.uX.Xu.5.j.+.+.+.+.+.+.y.v ; . . # , > > , ; ; ; ; > > i t u NX4X", +"pX9X] X o & . # # . o o o . # # - > > . . . . # . . # # # # # # o o 2 . # < & . # # # # # # # # & & # # # # # # & & > m < > # , - - - - - - , , 1 , > 2 7 c A A F H F D A A A A ( ( a M b ` c v n m c 4 N N 4 4 N 4 N N O . - , ; , > & & $ ; ; ; , , , t u NX1X", +"pX9X' X o & . . . . o o o o . # # + - > > > > # # > > > > > > & > & < . . & . # # # # # # # # # & & & # # # # - > & > & . < # - - - - - > , < 1 , ; - > < 7 6 6 6 7 o 7 6 6 7 z z < i a z < , 1 < < 6 c o 2 @ @ 7 3 3 X = . - - ; , * & * $ ; ; ; , , * t u @X1X", +"pX9XD X o . . . . . . . . . . . > . . . . . # & # & # # . . . . . . > & , . . > # # # # # # # # & # # # # - - , & , > > # - , # - - , * < < p < , ; ; - , > < < p < z z a < = 7 z z a , p x d - , < < z < , , p = < z = = o . # , * * * * $ ; ; ; - , # t u {.NX", +"pXc.D X o . # # . . o o o o # & # . @ > - > & & & & & & & & & & > . > . > # , # # # # . # # # # & . # # # ; ; , , . # > . - , , , , , < z 6 = * # ; : : : , 1 1 i i p p ; a 7 7 < < < z z = i i , 1 < & , p i r r r * * o & & > $ * , , * , ; ; ; # , # i q _ NX" +}; diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index b604f07f5..7b0429301 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -17,7 +17,12 @@ def test_sanity() -> None: assert im.format == "XPM" # large error due to quantization->44 colors. - assert_image_similar(im.convert("RGB"), hopper("RGB"), 23) + assert_image_similar(im.convert("RGB"), hopper(), 23) + + +def test_read_bpp2() -> None: + with Image.open("Tests/images/hopper_bpp2.xpm") as im: + assert_image_similar(im.convert("RGB"), hopper(), 11) def test_invalid_file() -> None: diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 304f58361..fcee142e3 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -56,7 +56,7 @@ class XpmImageFile(ImageFile.ImageFile): palette_length = int(m.group(3)) bpp = int(m.group(4)) - if palette_length > 256 or bpp != 1: + if palette_length > 256: msg = "cannot read this XPM file" raise ValueError(msg) @@ -68,8 +68,8 @@ class XpmImageFile(ImageFile.ImageFile): for _ in range(palette_length): s = self.fp.readline().rstrip() - c = s[1] - s = s[2:-2].split() + c = s[1 : bpp + 1] + s = s[bpp + 1 : -2].split() for i in range(0, len(s), 2): if s[i] == b"c": @@ -98,7 +98,9 @@ class XpmImageFile(ImageFile.ImageFile): palette_keys = tuple(palette.keys()) self.tile = [ - ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), (palette_keys,)) + ImageFile._Tile( + "xpm", (0, 0) + self.size, self.fp.tell(), (bpp, palette_keys) + ) ] def load_read(self, read_bytes: int) -> bytes: @@ -120,12 +122,13 @@ class XpmDecoder(ImageFile.PyDecoder): self.fd.readline() # Read '/* pixels */' data = bytearray() - palette_keys = self.args[0] + bpp, palette_keys = self.args dest_length = self.state.xsize * self.state.ysize while len(data) < dest_length: s = self.fd.readline().rstrip()[1:] s = s[: -1 if s.endswith(b'"') else -2] - for key in s: + for i in range(0, len(s), bpp): + key = s[i : i + bpp] data += o8(palette_keys.index(key)) self.set_as_raw(bytes(data)) return -1, 0 From 395bd6bd12138ec5b6b97ff172758446895cc9c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 18:56:15 +1000 Subject: [PATCH 448/628] Allow more than 256 colours --- Tests/images/hopper_rgb.xpm | 11174 +++++++++++++++++++++++++ Tests/test_file_xpm.py | 8 +- docs/handbook/image-file-formats.rst | 3 +- src/PIL/XpmImagePlugin.py | 41 +- 4 files changed, 11207 insertions(+), 19 deletions(-) create mode 100644 Tests/images/hopper_rgb.xpm diff --git a/Tests/images/hopper_rgb.xpm b/Tests/images/hopper_rgb.xpm new file mode 100644 index 000000000..063833b3a --- /dev/null +++ b/Tests/images/hopper_rgb.xpm @@ -0,0 +1,11174 @@ +/* XPM */ +static char *dummy[]={ +"128 128 11043 3", +".F0 c #000000", +".Jj c #000001", +"#94 c #000002", +".F5 c #000003", +"ax0 c #000004", +".Gy c #000005", +".HF c #000006", +".Mc c #000007", +"#G5 c #000008", +".F2 c #00000b", +".HC c #00000d", +".Dj c #000010", +"#g# c #000012", +".7l c #00001d", +"#aa c #00001e", +".qa c #000028", +".LB c #00002d", +".KU c #000100", +"aPY c #000102", +".KG c #000103", +".KV c #000104", +".KW c #000106", +"abf c #000107", +"acJ c #000108", +".EX c #00010e", +"#ve c #000114", +"#D2 c #000121", +".LC c #000127", +".Hv c #000200", +".Jo c #000201", +"aD6 c #000204", +"abc c #000205", +".SU c #000206", +".QD c #00020b", +"axP c #00020e", +".Hp c #000210", +"#7j c #000214", +"#Fs c #00021e", +"#eC c #000220", +"a.O c #000223", +".4f c #00022e", +".2z c #000230", +".Jn c #000300", +".Ky c #000304", +".Jk c #000305", +".Rr c #000307", +".IN c #000308", +"a#G c #00030a", +"aua c #000313", +".BL c #000314", +"#hQ c #000321", +".N5 c #000332", +".Kz c #000405", +".KX c #000407", +"aIk c #000408", +".KS c #00040a", +"aP7 c #00040c", +"#92 c #00040d", +"aCO c #00040f", +".Di c #000412", +"avW c #000417", +"#hF c #00041b", +"#kY c #000421", +"#mm c #000422", +".4e c #000436", +".T5 c #000437", +"ab# c #000507", +"aJL c #000508", +".KR c #00050a", +"az2 c #00050b", +"az4 c #00050f", +"#tr c #000512", +"#nM c #000522", +"aN2 c #000609", +"#uZ c #000616", +"#3T c #000617", +"atm c #00061f", +"#A5 c #00062b", +"#hW c #00062d", +"a#D c #000701", +"aPD c #000709", +".Jl c #00070a", +".IX c #00070f", +"#2x c #000715", +"#tb c #000716", +"#Fx c #000717", +"avo c #00071d", +".BN c #00071f", +"axg c #000720", +"#86 c #000722", +"#On c #000728", +"#Cy c #000729", +"arw c #000730", +"abe c #000809", +"#2w c #00080c", +".Jm c #00080d", +"#5D c #000818", +".A# c #00081d", +"axQ c #000820", +".IW c #00090f", +".IY c #000911", +"#ts c #00091b", +"asO c #000927", +"asc c #00092b", +"az3 c #000a11", +"awR c #000a25", +"asN c #000a26", +"#Rr c #000a2b", +"aOL c #000a2c", +"acE c #000b00", +"#tt c #000b1c", +"akR c #000b24", +"akS c #000b27", +"#87 c #000b29", +"aPH c #000b2d", +"aM1 c #000c2e", +"#D4 c #000c2f", +"#Cx c #000c30", +"atn c #000d28", +"aCB c #000e0a", +".Sa c #000f33", +"#7m c #000f35", +"#tq c #001020", +".TG c #001032", +"aiF c #001034", +"adG c #001232", +".QE c #001439", +".Vi c #00143a", +"#2y c #001c36", +"aAn c #010000", +".H0 c #010001", +"azp c #010002", +"adp c #010003", +".I. c #010008", +"#k3 c #010018", +".2A c #010020", +".lR c #010037", +".l2 c #01003f", +".IZ c #010107", +"awn c #01010a", +"#wx c #010124", +".DB c #01012f", +"acM c #010202", +"asx c #010204", +".Uv c #01020b", +"#x4 c #01020d", +".FX c #010301", +"as7 c #010304", +"aQD c #01030a", +"#7h c #010311", +"Qta c #010332", +"aK0 c #010405", +".Kq c #010406", +".Kk c #010409", +".Mh c #01040b", +".Mi c #010410", +"#5C c #010414", +"#K. c #010420", +".SH c #010424", +".ht c #010428", +".fZ c #010429", +"aPV c #010507", +".Kr c #010509", +"#8n c #01050e", +"ayT c #010513", +"#kL c #010518", +"#jl c #01051a", +"#p# c #010523", +".IU c #010609", +"#7i c #010615", +"#mc c #010616", +"awo c #010618", +".Xt c #010622", +"#qx c #010623", +"#rX c #010624", +"aM. c #01070a", +"#rP c #010714", +"#qo c #010715", +"#VG c #010718", +"akQ c #01071d", +"ab6 c #010720", +"aIb c #010723", +".qc c #010731", +"#Rs c #010823", +"aGQ c #010824", +".IV c #01090d", +"#7k c #01091f", +"#Rp c #010929", +"acF c #010b10", +"#vc c #010d1c", +"#5F c #010d25", +".Go c #020000", +"agL c #020002", +"aai c #020003", +"aj6 c #020004", +"#Ls c #02000a", +"#7R c #020015", +"#eW c #020022", +".oS c #020032", +"#95 c #020102", +"aup c #020109", +"#x3 c #020113", +"#Fr c #020124", +".Oh c #02012f", +".oR c #020132", +".Ei c #020200", +".Kn c #020203", +".QC c #020207", +".I5 c #020209", +"ay4 c #020210", +"#PV c #020218", +"acm c #02021b", +"#M3 c #020225", +"#hT c #02022d", +"a#J c #020303", +"asA c #020304", +".KB c #020305", +"aL3 c #020306", +"aeQ c #020307", +"abg c #020309", +".Gz c #02030a", +"#3i c #02030c", +"avV c #02030e", +"#c5 c #020317", +".#Y c #020332", +"as4 c #020404", +"#93 c #020406", +".Jp c #020408", +"aMj c #02040b", +"#8m c #02040d", +"#ZI c #02040f", +".Ju c #020412", +"#wL c #020413", +"#Cs c #02041b", +"#Ly c #020420", +".Re c #020425", +".PJ c #02042a", +".5O c #020430", +"#2v c #020506", +".Kx c #020507", +".KQ c #020509", +".Jq c #02050a", +".SI c #020523", +".Lw c #020525", +"#nR c #020528", +".ew c #020529", +".bq c #02052a", +".Ip c #020534", +".Jr c #020608", +"a#H c #02060c", +"axf c #020613", +".Y2 c #020616", +"#o2 c #020617", +"#nB c #02061a", +"#SS c #020626", +"a#C c #020703", +".SV c #02070c", +"aD7 c #020710", +"#nC c #020713", +"#He c #020716", +"#jx c #020733", +"#o3 c #020810", +"ayR c #020818", +"a.M c #020820", +"#Ro c #020822", +"#hU c #020835", +"aP8 c #02090f", +"adF c #020924", +"#to c #020926", +"#SR c #02092b", +"ahw c #020a2a", +".eJ c #020a4b", +".iX c #020a4c", +"#vb c #020b20", +"#Rq c #020b2f", +"#D5 c #020c2d", +"#A4 c #020c32", +"ajL c #020d2a", +".Gq c #030000", +"azg c #030001", +".HO c #030002", +".H2 c #030003", +".I6 c #030005", +"ayY c #030006", +".F1 c #030010", +".HB c #030013", +".SM c #030037", +"aJP c #030103", +"abb c #030105", +"ami c #030108", +"axq c #030109", +"aAF c #03010b", +"az6 c #03010e", +"ay5 c #030111", +".nt c #03013b", +".QB c #030202", +"a#K c #030203", +"arQ c #030204", +"asF c #030205", +"aj7 c #030207", +"awP c #030208", +"avC c #03020a", +"#wN c #030214", +"#wu c #03021d", +"#c4 c #030221", +"#Lx c #030225", +".km c #030244", +".KF c #030305", +"aN9 c #030306", +"#8p c #030307", +"aeP c #030308", +".I3 c #030309", +".I4 c #03030a", +"#1l c #030312", +".FR c #030316", +"#P1 c #030326", +"#A2 c #030327", +".LS c #030400", +".FY c #030402", +"acL c #030404", +".KD c #030406", +"a.E c #030408", +"ahj c #030409", +".2e c #03040a", +"#43 c #03040c", +"#Yg c #03040f", +"aAb c #030412", +".bn c #030433", +"a#I c #030504", +"aOV c #030508", +"a#F c #03050d", +"avn c #030512", +"ae2 c #03051e", +".Lx c #030523", +".J9 c #030526", +".IA c #03052b", +".MH c #03052c", +".Sy c #030535", +".GY c #030539", +"aOY c #030608", +".Oo c #03060a", +".M2 c #03060b", +"#mb c #03061e", +"#kK c #030621", +"#jk c #030623", +"#hE c #030625", +".#1 c #03062a", +"Qte c #03062b", +".Lj c #03062c", +"aPW c #030709", +"aCJ c #03070a", +".PV c #03070c", +"aNh c #03070d", +"#1. c #03070e", +"aL4 c #03070f", +"aAa c #030714", +"am7 c #030718", +"#eD c #030719", +"#Oz c #03071d", +".Im c #03072d", +".Lk c #030731", +".5N c #030738", +".Op c #03080d", +"#SO c #03081c", +"aa9 c #030900", +"#qp c #03090e", +"#91 c #030912", +"#85 c #03091f", +"ajK c #030922", +".hE c #03093b", +"acD c #030a00", +"#rQ c #030a0d", +"aiD c #030a27", +"aav c #030b28", +"#jy c #030b36", +"aPw c #030c0e", +"ayP c #030c16", +"alW c #030d23", +"aiE c #030f30", +"#tp c #031026", +"aBf c #031114", +"#vd c #03111d", +"adI c #031539", +".xN c #040000", +".ET c #040001", +".KH c #040002", +".Oq c #040003", +".HU c #040004", +".Wj c #040005", +".HJ c #040007", +".HI c #040008", +"ayJ c #04000f", +"#MX c #040014", +".vH c #04001e", +"#jw c #040027", +"#IE c #040028", +".Gu c #040101", +".HP c #040102", +"aAD c #040105", +"aei c #040108", +"acn c #040111", +".Me c #040201", +".Ji c #040202", +".FD c #040203", +"aJN c #040204", +".F8 c #040205", +"ax9 c #040206", +"avU c #040208", +"ai3 c #040209", +"#8l c #04020a", +"avm c #04020b", +"aAE c #04020c", +"axM c #04020f", +".5K c #040212", +".0O c #040225", +".LM c #040304", +"arR c #040305", +"#8q c #040306", +".H7 c #040307", +"aqZ c #040308", +"av5 c #04030b", +"#Ub c #040312", +".Hq c #040314", +"aFs c #040315", +"aaQ c #04031d", +"#Ox c #040326", +".Q7 c #04032a", +".Px c #04032b", +".nw c #04033c", +".LR c #040400", +"acN c #040402", +"ann c #040405", +"anm c #040406", +".KP c #040407", +"aq0 c #040408", +"aBp c #040409", +".Mb c #04040a", +".EV c #04040b", +"aCM c #04040c", +"auN c #04040d", +"aCN c #04040e", +"#Rl c #04040f", +"#SM c #040413", +"aD8 c #040416", +".N2 c #04042b", +".DI c #040432", +".hH c #040447", +".KT c #040500", +"abk c #040505", +".KE c #040507", +"#3S c #040509", +"aBq c #04050a", +"aA# c #04050b", +"atl c #04050c", +"aeR c #04050d", +"#9Z c #04050f", +".Ea c #04051c", +"#tn c #04052a", +".cY c #040534", +".MT c #040535", +".df c #040547", +"abl c #040604", +"abj c #040607", +"aJK c #040608", +"aOX c #040609", +".Vh c #04060b", +"aI. c #04060d", +"#81 c #04060f", +"alS c #040618", +".MP c #040624", +".IB c #040627", +".Lv c #04062c", +".Ht c #040704", +".Hz c #040705", +".IT c #040709", +"aCK c #04070c", +".NA c #040712", +"#wv c #040717", +"aQE c #04071d", +"#Ha c #040722", +".Sx c #040732", +".Kw c #040809", +".IO c #04080d", +"aO# c #04080e", +"awQ c #040817", +".FQ c #040818", +"aiC c #040822", +".Q8 c #040833", +".2y c #04083c", +"abd c #04090b", +"aBo c #04090d", +"aKS c #040911", +".VT c #040933", +".g. c #04093b", +"ayy c #040a0a", +".Ks c #040a0e", +"aFt c #040a27", +".eG c #040a3c", +"ab. c #040b0d", +"aBh c #040b10", +"#IK c #040b21", +"aOd c #040c12", +"ab7 c #040c27", +".gb c #040c4e", +"ahx c #040d2f", +"aqL c #040e3e", +"aCC c #04100d", +"#88 c #041032", +"ajM c #041434", +"adH c #041738", +".EP c #050000", +"#2O c #050001", +".KI c #050002", +"aDl c #050004", +"#pf c #050009", +"#6e c #05000b", +"#yc c #050020", +".87 c #050024", +"amJ c #050106", +"al9 c #050107", +"#zt c #05010f", +"aaR c #050112", +".Ay c #050123", +"#u7 c #05013e", +"ail c #050206", +".J# c #050207", +"afv c #050209", +"#ZG c #05020a", +"#09 c #05020b", +"axN c #050211", +"#2L c #050221", +".Sw c #05022a", +"aN3 c #050304", +".Gw c #050305", +".Mf c #050307", +"ale c #050309", +"awI c #05030a", +"av6 c #05030b", +".3X c #05030c", +"awN c #05030e", +"a#E c #05030f", +"axd c #050310", +"aAc c #050313", +".5J c #050318", +"#qz c #05031d", +".2s c #050323", +".0V c #050326", +".Ed c #050403", +"alc c #050404", +"anl c #050406", +"arf c #050407", +"#6I c #050408", +"#44 c #050409", +".HH c #05040a", +".H9 c #05040b", +".I# c #05040c", +"awl c #05040d", +"awM c #05040e", +"#W8 c #05040f", +"aDB c #050411", +"#Rk c #050413", +"#Z0 c #050418", +".uj c #05041d", +"aaO c #05041f", +".4g c #050422", +".rw c #050426", +"#zz c #05042d", +"#u2 c #050430", +".kn c #050446", +".Ej c #050503", +"aee c #050504", +".O8 c #050506", +".KC c #050507", +"apj c #050508", +"apk c #050509", +"ajw c #05050a", +"aFp c #05050b", +".I2 c #05050c", +"#Rm c #05050d", +"#SN c #05050e", +"#YA c #05050f", +"#Z1 c #050510", +"aGP c #050515", +"#ST c #05051f", +".Fn c #050538", +"Qtx c #050548", +"asB c #050607", +"ay0 c #050609", +"ahi c #05060a", +"ay1 c #05060b", +"aqK c #05060c", +"asa c #05060e", +"#hX c #050624", +"#x6 c #050626", +"#Cv c #050628", +"#o4 c #05062d", +"#rR c #05062e", +"#eL c #05062f", +".Rg c #05063e", +".FV c #050705", +".IR c #050706", +"as6 c #050708", +"aOT c #050709", +"ayZ c #05070b", +"aGO c #05070e", +"aeS c #05070f", +"#zu c #05071f", +"#IG c #050723", +".K. c #050725", +".2v c #05072c", +".at c #050730", +".et c #050736", +".iR c #05073f", +"aA. c #05080c", +"aMk c #05080e", +"aJy c #050814", +"aJz c #050815", +"aPG c #05081e", +"ab5 c #050820", +".PK c #050826", +"#ms c #050827", +"aNi c #05090f", +"aFq c #050911", +"#90 c #050912", +"#Rt c #05091b", +"aOK c #05091f", +".iV c #05093f", +".iW c #050943", +"aD5 c #050a08", +"ayO c #050a13", +"#Hb c #050a20", +"#mr c #050a30", +"#zC c #050a34", +".da c #050a3c", +".g# c #050a3f", +".hF c #050a43", +".Kt c #050b11", +"#hP c #050b2d", +".bC c #050b3d", +"ayB c #050c11", +"#Uc c #050c1e", +"am8 c #050c20", +"#k1 c #050c35", +"#vf c #050d1e", +".a# c #050d4f", +"#5E c #050e22", +"QtH c #050e37", +"aD0 c #050f09", +"#tc c #050f16", +".BM c #050f25", +"#Hd c #050f27", +"aQB c #051112", +".W6 c #05193e", +".A9 c #060000", +".KJ c #060002", +".Nv c #060004", +"aMd c #060005", +".HT c #060008", +"#7S c #060009", +"#bK c #06000e", +"##9 c #06000f", +"#AW c #060010", +".7m c #06001c", +".xt c #060026", +".HV c #060103", +"ax4 c #060104", +"aNb c #060105", +"#8k c #060106", +".lM c #060125", +"#3k c #060205", +"ahh c #060206", +"aqj c #060207", +".HL c #060208", +"ak4 c #060209", +"aj0 c #06020a", +".k. c #060229", +".CE c #060303", +".O7 c #060304", +"#45 c #060305", +"awm c #060307", +".I8 c #060308", +"aef c #06030a", +".Zf c #060320", +".Ze c #060329", +".Wb c #06032d", +".Uo c #06032f", +".Gv c #060404", +"ai2 c #060405", +"aK2 c #060406", +"aPP c #060407", +"atd c #060409", +"aeg c #06040b", +"atA c #06040c", +"awk c #06040d", +"ay# c #06040e", +"a.G c #060410", +"avk c #060411", +"a.H c #060413", +"#Yz c #060418", +"#W7 c #060419", +".4# c #06041a", +".2t c #06041c", +".0P c #06041e", +".sU c #060420", +".oT c #060436", +".lS c #06043d", +".l1 c #060443", +"acP c #060500", +"agO c #060505", +".LP c #060506", +"api c #060507", +"ano c #060508", +"amg c #060509", +"anp c #06050a", +"avl c #06050b", +".I0 c #06050c", +"avB c #06050d", +"aAG c #06050e", +"awL c #060510", +"asM c #060513", +"aCR c #06051a", +".4. c #060523", +".MU c #06052f", +".nu c #06053e", +".nz c #060543", +".l3 c #060544", +".ko c #060547", +".LU c #060601", +"acO c #060603", +"ank c #060607", +"aqY c #060608", +".TE c #060609", +".S. c #06060a", +"aim c #06060b", +"aGM c #06060c", +".Kl c #06060d", +"aqJ c #06060e", +"#VF c #06060f", +"aog c #060610", +"ay2 c #060611", +"#IL c #060612", +"asb c #060614", +"aIa c #060616", +"a#b c #060620", +"#Ka c #060626", +"#Cw c #06062a", +".Ri c #060641", +"acR c #060705", +"abm c #060706", +"abh c #06070b", +"aH8 c #06070c", +"art c #06070e", +"af7 c #06070f", +".S# c #060710", +"aBr c #060713", +"#82 c #060714", +"#hR c #060722", +"#nD c #06072e", +"#qq c #06072f", +".0U c #060738", +".GR c #06073c", +".PO c #06073f", +".hy c #060740", +".f4 c #060741", +".c6 c #060742", +".U. c #060747", +".FU c #060806", +".Ny c #06080a", +"abi c #06080b", +".Nz c #06080f", +"af8 c #060810", +".P. c #060812", +"amY c #06081a", +"amZ c #06081b", +"ajJ c #06081d", +"#mn c #060823", +".IG c #060830", +".hU c #060831", +".PI c #060834", +"#qv c #060836", +".GT c #06083d", +".eA c #060841", +".8X c #060845", +".Js c #06090d", +"aMl c #06090f", +"aMm c #060910", +".KY c #060912", +"ay3 c #060917", +"aM0 c #06091f", +".IC c #060927", +"Qtf c #06092d", +".Oc c #060935", +".PA c #060938", +".Kv c #060a0b", +"aPZ c #060a0c", +"aNT c #060a0d", +"aCL c #060a0f", +"aL5 c #060a1f", +"#M4 c #060a20", +"#ta c #060a26", +"#g. c #060a27", +"#IJ c #060a29", +".br c #060a2d", +".eH c #060a44", +"aGN c #060b11", +"aJA c #060b12", +"#rW c #060b33", +"Qtr c #060b3d", +".db c #060b40", +".Ku c #060c12", +"aKi c #060c14", +"#qw c #060c34", +"aO7 c #060d13", +"aNm c #060d14", +"#ZJ c #060d1e", +"alV c #060d21", +"#PT c #060d2d", +"#7l c #06102c", +"a.N c #06102f", +"aOc c #061117", +"#tu c #06111f", +"#Fw c #06122c", +"aBg c #061719", +".Jf c #070000", +".EC c #070002", +".Eo c #070004", +".El c #070009", +"#1W c #07000c", +"ahK c #070017", +"aD3 c #070100", +".M5 c #070101", +".KK c #070102", +"abp c #070103", +"aq# c #070104", +"aqi c #070106", +"aK8 c #070107", +"aK7 c #070108", +"#Ow c #070127", +".Er c #070200", +".Gs c #070202", +".sF c #070203", +".M4 c #070204", +"aNc c #070206", +"aM# c #070208", +".Wh c #07020a", +".Uu c #07020b", +"aER c #070212", +"#Or c #070213", +"##0 c #070228", +"#mp c #07022a", +".Je c #070303", +".Jd c #070304", +"amf c #070306", +"af6 c #070307", +".HK c #070309", +"#2N c #070311", +"#mt c #070318", +".XG c #070323", +".sJ c #070326", +"#.E c #070327", +"#wH c #07032c", +".PW c #070401", +".LZ c #070402", +".KO c #070405", +"auq c #070406", +".H4 c #070407", +"aec c #070408", +".I7 c #070409", +"ar0 c #07040a", +"ai4 c #07040b", +"aCi c #070410", +"avT c #070411", +"aqH c #070414", +"#6d c #07041a", +"#2M c #07041d", +".Fq c #070433", +".GZ c #070437", +"#y. c #070438", +"#wD c #07043b", +"aJM c #070505", +"aed c #070506", +"aK1 c #070507", +"azn c #070508", +"ax8 c #070509", +"ark c #07050a", +"#3j c #07050b", +"auM c #07050c", +"aun c #07050d", +"auL c #07050e", +"#9Y c #07050f", +"auK c #070510", +"au# c #070511", +"au. c #070512", +"aCQ c #070513", +".HD c #070514", +".4a c #070515", +"#bt c #070518", +"#VE c #070519", +"#Ua c #07051a", +"#VI c #070520", +"#9w c #070521", +"#7Q c #070522", +".XF c #07052d", +"#x9 c #070537", +".kd c #070541", +".Ef c #070605", +"abn c #070606", +".LL c #070608", +"arX c #070609", +".Mg c #07060b", +".XR c #07060c", +".IP c #07060d", +"acH c #07060e", +"azs c #07060f", +"awj c #070611", +"asL c #070612", +"as# c #070613", +"apQ c #070614", +"atk c #070615", +"#pe c #07061e", +"#LA c #070621", +".sV c #070622", +".5I c #070624", +"#II c #07062c", +".LV c #070701", +"abX c #070706", +"awx c #070708", +".H6 c #070709", +"apl c #07070b", +"aky c #07070c", +"apN c #07070e", +"aoc c #07070f", +"#YB c #070710", +"aH9 c #070711", +"aru c #070712", +"arv c #070713", +"ao5 c #070714", +"aoh c #070715", +"#nE c #070732", +".iL c #07073a", +".T9 c #070740", +".bv c #070742", +".Ra c #070743", +"aD4 c #070804", +"acK c #070809", +"#8o c #07080c", +".O9 c #07080e", +"aLn c #07080f", +"af9 c #070812", +"aCP c #070813", +"#nN c #070823", +"#nS c #070824", +"#pd c #070828", +".fW c #070837", +".ho c #07083a", +".Of c #07083d", +".#5 c #070843", +"Qtj c #070844", +".bH c #07084a", +".FZ c #070907", +".IS c #070909", +"as5 c #07090a", +".Kp c #07090b", +"aOI c #070910", +"aeT c #070912", +".EW c #070914", +"#W9 c #070919", +"#D0 c #070922", +"#qy c #070923", +"#rY c #070924", +"#MU c #070926", +"#gg c #070932", +".rB c #070935", +"#o9 c #070937", +".Sz c #07093c", +".Ul c #070946", +".Um c #070948", +".Hy c #070a07", +"aNj c #070a10", +"aPm c #070a12", +".Jt c #070a13", +"am0 c #070a1d", +"#rO c #070a25", +"#P2 c #070a26", +"#qn c #070a27", +".c2 c #070a2d", +".ex c #070a2e", +"#zD c #070a31", +".Iz c #070a35", +".R. c #070a39", +"aOG c #070b0f", +"aO8 c #070b11", +"am1 c #070b1f", +"#Rn c #070b20", +"#Ft c #070b22", +".JY c #070b35", +".2x c #070b3d", +".bD c #070b45", +"aOH c #070c14", +"ayD c #070c15", +"a.L c #070c23", +"#va c #070c28", +"#Oo c #070c2a", +"Qts c #070c41", +"#p. c #070d34", +".iY c #070d4f", +".Dk c #070f22", +"aO6 c #071017", +"ayQ c #07101c", +"#hV c #07103c", +"ayS c #07111e", +"aNl c #071319", +"#89 c #071437", +".P# c #071d45", +".M6 c #080000", +".zu c #080001", +"abq c #080003", +"aps c #080004", +"aFW c #080005", +".SW c #080006", +"ayN c #08000a", +"#Ix c #08000e", +".ml c #080010", +".7e c #08001c", +"#s7 c #080025", +"am# c #080101", +".HS c #08010c", +"#Ot c #08010d", +"#J2 c #08010f", +".9. c #080110", +".ES c #080201", +".A5 c #080202", +"al# c #080203", +"a#M c #080204", +"aEe c #080206", +"aGX c #080207", +"aKR c #080208", +"agy c #08021d", +"#ab c #08021e", +".HX c #080302", +".Jc c #080305", +"azl c #080306", +"aMa c #080309", +"aiS c #08030e", +"#zs c #080315", +".n# c #080321", +".Oi c #080327", +"#.F c #080328", +".L0 c #080402", +"aPO c #080403", +"awO c #080406", +"atI c #080407", +".HM c #080408", +"aeb c #080409", +"aMb c #08040a", +".Hb c #080410", +"aHt c #080415", +".XH c #08041f", +".V7 c #080426", +".lO c #080428", +"#hS c #08042c", +"#u6 c #08043f", +".Km c #080502", +".LY c #080503", +".TD c #080504", +"a#L c #080506", +".HN c #080507", +".HY c #080508", +"aea c #080509", +".J. c #08050a", +"ald c #08050c", +"aBe c #08050e", +".3W c #080512", +"at9 c #080513", +"apM c #080515", +".HA c #08051a", +".V8 c #080521", +"#wI c #080527", +"acQ c #080600", +".R9 c #080605", +"aJO c #080608", +"azm c #080609", +"aPS c #08060a", +"aoR c #08060b", +"afy c #08060c", +"afx c #08060d", +"auo c #08060e", +"avj c #080611", +"avS c #080612", +"aap c #080613", +"apL c #080615", +"#Rj c #08061a", +"#r2 c #080624", +".B8 c #08062e", +".MQ c #080631", +"#u3 c #080636", +".l7 c #080648", +"aa8 c #080700", +".Eg c #080706", +".Nx c #080708", +".LN c #080709", +"aow c #08070b", +"anq c #08070c", +"aEB c #08070d", +"aHj c #08070e", +"atB c #08070f", +"azr c #080710", +"avi c #080711", +"awK c #080712", +"apO c #080713", +"aof c #080714", +"as. c #080715", +"aqI c #080716", +"aBs c #08071d", +".5P c #080724", +"#v# c #08072b", +".2r c #080730", +".0N c #080733", +"#eB c #080735", +".LQ c #080803", +"acS c #080806", +"aw1 c #08080a", +"aO. c #08080b", +"amM c #08080d", +".I1 c #08080e", +"aO0 c #080810", +".Wi c #080811", +"aod c #080813", +"apP c #080816", +"#Kb c #08081b", +"#wK c #08081c", +"#M5 c #08081d", +"#ew c #080831", +"asz c #08090b", +"alG c #08090d", +"avN c #08090f", +"ahk c #080911", +"acj c #080923", +"aaP c #080925", +"acl c #080926", +"#jn c #080934", +".hp c #080938", +".fV c #08093b", +"#dc c #080943", +".PD c #080944", +"aOF c #080a0e", +"aJB c #080a11", +"ain c #080a12", +"aio c #080a13", +"ag. c #080a14", +"#wM c #080a16", +".4b c #080a2d", +".iM c #080a39", +".N6 c #080a3e", +".T4 c #080a41", +"##1 c #080a47", +".Hw c #080b08", +".Hu c #080b09", +"aLo c #080b12", +"aQt c #080b14", +".KZ c #080b18", +"aas c #080b24", +".CA c #080b26", +"#o1 c #080b29", +".f0 c #080b2e", +".hu c #080b2f", +".vU c #080b32", +".SG c #080b36", +".J8 c #080b37", +".KA c #080c0d", +"aKj c #080c13", +"#84 c #080c1e", +"Qtt c #080c46", +"#Hc c #080d30", +".Z# c #080d45", +"#tv c #080e18", +"adE c #080e28", +"#nL c #080e35", +"#SQ c #080f2e", +"aCA c #081011", +"#Om c #081133", +".bR c #08123a", +"agl c #081339", +"aPx c #081717", +"#5G c #081736", +"a#N c #090003", +"#D7 c #090013", +".Eb c #090016", +".Rl c #09001b", +"#VJ c #09001e", +"aLc c #090107", +"aBl c #09010a", +"#zF c #09011e", +"#4x c #090203", +"arU c #090207", +"#9W c #09020d", +"ayH c #090214", +"##R c #09021a", +".CB c #09021d", +"#nP c #090229", +".Gl c #090301", +".Eq c #090302", +"aCG c #090305", +"arV c #090308", +"aK6 c #090309", +"aFn c #09030b", +".Up c #09031e", +"#bL c #09031f", +"adX c #090324", +".bY c #090325", +"#r1 c #090326", +".QA c #090404", +"aAo c #090406", +".Gb c #090407", +".Jb c #090409", +".Ja c #09040a", +".Uw c #09040c", +"#9X c #09040e", +"adY c #09041e", +"#qC c #090428", +".IQ c #090502", +".LW c #090503", +"abo c #090506", +".M3 c #090507", +"aq8 c #090508", +"ae# c #090509", +"afu c #09050a", +".BJ c #09050b", +"#4w c #090511", +"ayU c #090514", +"#PW c #090517", +"#Ln c #09051d", +".Ui c #090523", +".Uh c #090528", +".V6 c #090530", +"#wE c #09053e", +".L1 c #090603", +".LX c #090604", +"acT c #090607", +"#6J c #090608", +".Nw c #09060a", +".Dg c #09060b", +"arl c #09060c", +"afz c #09060d", +"aLl c #09060e", +"aLm c #09060f", +".5u c #090612", +"#SU c #090616", +"#9x c #090619", +".Zg c #09061e", +"#1k c #090621", +"#yb c #09062c", +".Ug c #090631", +".T6 c #090632", +".VW c #090636", +".oH c #09063a", +"acC c #090700", +".Md c #090701", +"aM8 c #090709", +".F7 c #09070a", +"aba c #09070b", +"anO c #09070c", +"aj9 c #09070e", +"atE c #09070f", +"aJx c #090710", +"aEC c #090711", +"awi c #090712", +"avR c #090713", +"auI c #090714", +"ao4 c #090716", +".2u c #09071a", +"#ZZ c #090721", +"a#. c #090722", +".XL c #09072f", +"#Fu c #090733", +".nk c #09073d", +"aw0 c #09080a", +"ay. c #09080b", +"aQA c #09080d", +"#ZH c #090810", +"aDn c #090811", +"aFr c #090813", +"aOJ c #090814", +"aoe c #090816", +"ay6 c #09081f", +"a## c #090824", +"#9v c #090826", +"#J9 c #09082b", +"#x8 c #090833", +"#zA c #090834", +"#y# c #090836", +"axp c #09090b", +"atb c #09090c", +"alb c #09090f", +".GB c #090914", +"aF6 c #090918", +"#OA c #09091b", +"adB c #09091e", +".39 c #090930", +"#qk c #090936", +"#js c #09093a", +".gv c #09093b", +".Fm c #09093c", +".Ko c #090a0b", +"aaj c #090a0c", +"ayv c #090a0d", +"avd c #090a0f", +"auB c #090a10", +"aOa c #090a11", +".TF c #090a12", +"aip c #090a15", +"ahl c #090a16", +"amQ c #090a17", +"#83 c #090a1a", +"ae1 c #090a22", +"ack c #090a26", +"#Cu c #090a29", +".Kd c #090a2d", +".hM c #090a30", +"#mq c #090a33", +"#hH c #090a35", +"#oY c #090a36", +".W# c #090a49", +".gd c #090a4c", +"aCI c #090b0d", +"akz c #090b13", +"amP c #090b16", +"#Ru c #090b18", +".A. c #090b19", +"#nA c #090b2d", +"#nK c #090b39", +".iQ c #090b41", +"#VH c #090c21", +"#Oy c #090c28", +".xH c #090c31", +".Lu c #090c38", +".J0 c #090c3b", +".V0 c #090c52", +"aPX c #090d0f", +".EY c #090d1b", +"#PU c #090d28", +".XA c #090d48", +"aQa c #090e0c", +"aNU c #090e13", +"aPE c #090e15", +"#IH c #090e24", +"aau c #090e29", +"aP9 c #090f14", +"#SP c #090f28", +"#ml c #090f37", +".5D c #090f3e", +".0K c #090f44", +"#vg c #09111f", +"ab8 c #09122f", +"ayx c #091311", +"aLr c #09131a", +".Aa c #091731", +".M. c #0a0000", +"#8j c #0a0003", +"#ye c #0a000c", +"aco c #0a0104", +"aBi c #0a010c", +".zy c #0a0203", +"aiT c #0a0206", +".On c #0a020a", +"ayK c #0a0214", +"#IB c #0a0216", +"aak c #0a030d", +".HR c #0a0310", +"adZ c #0a0313", +".za c #0a0329", +"#kZ c #0a032a", +".0b c #0a0400", +".ER c #0a0402", +".A8 c #0a0404", +"acU c #0a0407", +".F9 c #0a0408", +"as9 c #0a0409", +"aK5 c #0a040a", +".89 c #0a0420", +"afi c #0a0423", +"#ID c #0a042a", +".Jh c #0a0504", +"aN1 c #0a0505", +"ame c #0a0506", +"ahT c #0a0507", +"azi c #0a0508", +"am. c #0a0509", +"aM9 c #0a050a", +"aMc c #0a050b", +".86 c #0a0520", +"#Rf c #0a052b", +".k# c #0a052c", +"#v. c #0a0530", +".ks c #0a0545", +"axe c #0a0605", +".HW c #0a0606", +"ar# c #0a0609", +"agN c #0a060a", +"agM c #0a060b", +"aJw c #0a060f", +"a.F c #0a0612", +".Ar c #0a0628", +"#dl c #0a062a", +"#H. c #0a062e", +".CH c #0a0706", +"#96 c #0a0709", +"ahU c #0a070b", +".I9 c #0a070c", +"agP c #0a070e", +"aao c #0a0713", +"aob c #0a0717", +"a#c c #0a0719", +"#r3 c #0a071a", +".0Q c #0a071c", +"#Yy c #0a0721", +"#W6 c #0a0722", +"#4u c #0a0726", +".sZ c #0a0734", +".H1 c #0a080a", +"aPR c #0a080b", +"aOU c #0a080c", +"anN c #0a080d", +"aN5 c #0a080e", +"ai6 c #0a080f", +"atD c #0a0810", +"aDm c #0a0812", +"atj c #0a0813", +"at5 c #0a0814", +"at6 c #0a0815", +"aoa c #0a0817", +"apK c #0a0818", +"#9u c #0a0824", +"#6c c #0a0825", +".ry c #0a082a", +".Zl c #0a082d", +"#zB c #0a0836", +".G2 c #0a0838", +"#f7 c #0a0849", +".kk c #0a084b", +"adq c #0a0907", +".Ee c #0a0908", +"arS c #0a090b", +".Gx c #0a090c", +"ai1 c #0a090e", +"avh c #0a0910", +"avA c #0a0911", +"avQ c #0a0913", +"auH c #0a0914", +"akL c #0a0919", +"#P3 c #0a091a", +"#MW c #0a0920", +".sT c #0a0925", +"#u0 c #0a0930", +".IF c #0a0933", +"#ex c #0a093a", +".dx c #0a0941", +".H5 c #0a0a0a", +"atT c #0a0a10", +"#Yf c #0a0a11", +"acG c #0a0a12", +"aDD c #0a0a17", +"akD c #0a0a19", +"a.I c #0a0a1b", +"#jA c #0a0a26", +"a#a c #0a0a27", +"#H# c #0a0a2d", +"#nw c #0a0a35", +".Uj c #0a0a3a", +".es c #0a0a3d", +".gw c #0a0a3e", +".XK c #0a0a41", +".VY c #0a0a46", +".LT c #0a0b05", +".FW c #0a0b09", +"asw c #0a0b0c", +"ayw c #0a0b0d", +"azM c #0a0b0f", +"ave c #0a0b10", +"a.D c #0a0b11", +".Dh c #0a0b15", +".0A c #0a0b16", +"ag# c #0a0b17", +"amR c #0a0b18", +"akC c #0a0b1a", +"amX c #0a0b1b", +"alR c #0a0b1d", +"#zw c #0a0b28", +"#zy c #0a0b2f", +".do c #0a0b31", +"#l8 c #0a0b35", +"Qt# c #0a0b3c", +".Zk c #0a0b3f", +".c5 c #0a0b46", +".bu c #0a0b47", +"aOW c #0a0c0e", +"acI c #0a0c13", +"alH c #0a0c14", +"alI c #0a0c15", +"aDy c #0a0c18", +"#uY c #0a0c28", +".MJ c #0a0c40", +".hx c #0a0c43", +".W. c #0a0c48", +"#gh c #0a0c49", +".Py c #0a0d38", +".Ll c #0a0d39", +".kp c #0a0d50", +"aP0 c #0a0e10", +"ayA c #0a0e12", +"aQC c #0a0e16", +".MI c #0a0e38", +".i7 c #0a0e3d", +".hG c #0a0e4b", +".qd c #0a0f3c", +"#Yh c #0a1023", +"aD9 c #0a102f", +"#kX c #0a1038", +".35 c #0a1040", +".2n c #0a1042", +"#PS c #0a1437", +"aQd c #0a171a", +".Os c #0b0000", +".PZ c #0b0001", +".M# c #0b0003", +".Nu c #0b0004", +"aLj c #0b0005", +"#Cn c #0b0013", +"#0B c #0b0107", +"aFo c #0b010c", +".Jg c #0b0200", +"aCF c #0b0203", +".M1 c #0b020a", +"aaS c #0b0306", +".PU c #0b030b", +"aiQ c #0b0314", +"aIS c #0b040e", +"akN c #0b0410", +"#P0 c #0b0417", +".oY c #0b0433", +".kt c #0b0437", +".Df c #0b0505", +".O6 c #0b0508", +"aK4 c #0b050b", +"az9 c #0b050e", +"##S c #0b0515", +"ayG c #0b0517", +".Wc c #0b051f", +"#bO c #0b0521", +".FB c #0b0524", +"#G9 c #0b052a", +"#wG c #0b0534", +"axO c #0b0605", +".Gn c #0b0606", +"ax6 c #0b0609", +"ak5 c #0b060a", +"al8 c #0b060d", +"#MY c #0b0617", +"ahL c #0b0618", +"#Lp c #0b061c", +".yR c #0b0622", +".nf c #0b0624", +".lH c #0b062a", +"#bM c #0b062b", +"#VA c #0b062c", +"#t. c #0b0634", +".Gr c #0b0706", +"aq9 c #0b070a", +"af5 c #0b070b", +"ajv c #0b070c", +"asr c #0b070d", +".F3 c #0b0714", +"#bs c #0b0724", +"#J8 c #0b072f", +".nA c #0b0742", +".ge c #0b0743", +"#ti c #0b0745", +".CI c #0b0808", +"alF c #0b080c", +"a.C c #0b080d", +"aoT c #0b080e", +"aeh c #0b080f", +"aEZ c #0b0812", +"axL c #0b0815", +"alO c #0b0817", +"ao# c #0b0818", +"#LB c #0b081f", +"#VD c #0b0822", +"#U# c #0b0823", +"#k0 c #0b0831", +".HZ c #0b090b", +"#2u c #0b090c", +"ai0 c #0b090d", +"aoS c #0b090e", +"ahV c #0b090f", +"atQ c #0b0910", +"au2 c #0b0911", +"avP c #0b0912", +"auG c #0b0913", +"an1 c #0b0914", +"an0 c #0b0915", +"at7 c #0b0916", +"ao. c #0b0919", +"#uX c #0b092f", +".p8 c #0b0933", +"#ya c #0b0934", +".Zd c #0b0938", +".Uf c #0b093d", +".lU c #0b0948", +"aCH c #0b0a0b", +"abW c #0b0a0c", +".H8 c #0b0a0f", +"avg c #0b0a10", +"auF c #0b0a11", +".HG c #0b0a12", +"anS c #0b0a13", +"anT c #0b0a14", +"anU c #0b0a15", +"aDC c #0b0a17", +"alP c #0b0a19", +"ab3 c #0b0a1e", +".Kb c #0b0a35", +"#kS c #0b0a3b", +"#jr c #0b0a3f", +".kc c #0b0a42", +"#f6 c #0b0a47", +".l4 c #0b0a49", +"anQ c #0b0b0f", +"anR c #0b0b10", +"aj8 c #0b0b11", +"aP1 c #0b0b12", +".GA c #0b0b14", +"aEO c #0b0b19", +"alK c #0b0b1a", +"ajz c #0b0b1b", +"ab4 c #0b0b21", +".Ke c #0b0b22", +".5H c #0b0b31", +"#kN c #0b0b36", +".V9 c #0b0b3a", +"#jo c #0b0b3b", +".Wa c #0b0b43", +".f6 c #0b0b49", +"asy c #0b0c0d", +"azN c #0b0c0e", +"aA0 c #0b0c10", +"amh c #0b0c11", +"awf c #0b0c12", +"aKg c #0b0c13", +"akA c #0b0c16", +"ajx c #0b0c17", +"alJ c #0b0c18", +"ahm c #0b0c19", +"ajy c #0b0c1a", +"#MV c #0b0c26", +"#ma c #0b0c30", +"#jm c #0b0c33", +"#hy c #0b0c34", +"#kG c #0b0c35", +".cX c #0b0c3e", +".Rf c #0b0c42", +".ez c #0b0c46", +".ML c #0b0c47", +".eC c #0b0c48", +".#4 c #0b0c49", +".hA c #0b0c4b", +".Hx c #0b0d0b", +"as3 c #0b0d0e", +"aKh c #0b0d14", +"aQn c #0b0d15", +"aCd c #0b0d18", +"#D1 c #0b0d2a", +".Li c #0b0d3a", +"#mk c #0b0d3b", +".PB c #0b0d40", +".f3 c #0b0d44", +".7a c #0b0d4a", +"aLp c #0b0e15", +".0B c #0b0e16", +"ae3 c #0b0e28", +".iO c #0b0e34", +".hs c #0b0e35", +".JZ c #0b0e3a", +".Io c #0b0e3b", +".ga c #0b0e4b", +"aMn c #0b0f15", +"aat c #0b0f29", +".Ih c #0b0f3b", +".eI c #0b0f4c", +"azO c #0b1012", +"aLs c #0b1017", +"#ww c #0b1019", +".E# c #0b1024", +"#Ue c #0b102c", +"#k2 c #0b1033", +"#Fv c #0b1035", +"#jv c #0b1038", +".uw c #0b1043", +".s7 c #0b1044", +".V2 c #0b104c", +".de c #0b1053", +"aQ# c #0b1110", +"#hO c #0b1139", +"aD1 c #0b120a", +"auO c #0b122a", +"#jz c #0b1237", +"aL6 c #0b183a", +".Qx c #0c0000", +"a#x c #0c0001", +".R7 c #0c0004", +"ad0 c #0c0005", +"aH6 c #0c0009", +"#1V c #0c0011", +"#nO c #0c0026", +"aiU c #0c0100", +".Rt c #0c0106", +"#Kc c #0c010c", +".Dd c #0c0200", +"abZ c #0c020a", +"ayV c #0c0211", +".FS c #0c0214", +"aHw c #0c0215", +"amc c #0c0301", +".PY c #0c0307", +".LJ c #0c030b", +"akM c #0c030d", +"#9V c #0c030e", +"#zr c #0c031c", +"#bF c #0c0320", +".z# c #0c032f", +".YA c #0c0400", +"agI c #0c0403", +".KN c #0c0404", +".XS c #0c040a", +".Rq c #0c040c", +"#Os c #0c0410", +"#MZ c #0c0411", +"#J1 c #0c0413", +".7n c #0c0414", +"#eG c #0c0427", +"adW c #0c042a", +"#uV c #0c042e", +".an c #0c0435", +"aC2 c #0c0507", +"aqf c #0c0509", +"apw c #0c050a", +"aBk c #0c050d", +"aHv c #0c0517", +"#6a c #0c0522", +".B9 c #0c0533", +"a#B c #0c0605", +".B. c #0c0606", +"#6K c #0c0607", +".Ep c #0c0608", +"aAA c #0c0609", +".Ma c #0c060a", +"aBC c #0c060b", +"aK3 c #0c060c", +"aA3 c #0c060e", +"aKf c #0c0610", +"aJv c #0c0611", +".Ca c #0c0629", +".sI c #0c0635", +".Gt c #0c0706", +"agJ c #0c0707", +"ae. c #0c0709", +"azj c #0c070a", +".G. c #0c070b", +"am5 c #0c0710", +"aiR c #0c0715", +"#eX c #0c0723", +".A3 c #0c0726", +"#dm c #0c072c", +"#Yw c #0c072d", +".Gp c #0c0807", +"#8r c #0c080a", +"aNa c #0c080c", +"amI c #0c080d", +"ap5 c #0c080e", +"aBn c #0c080f", +"aIT c #0c0811", +"QtO c #0c081b", +"#Rg c #0c082b", +"#VB c #0c082c", +"#pc c #0c082d", +".0H c #0c082f", +".p2 c #0c083a", +"ax3 c #0c090a", +"aN6 c #0c090d", +"aqp c #0c090f", +"afw c #0c0910", +"aO1 c #0c0912", +".FC c #0c0916", +"an9 c #0c0919", +"#4v c #0c0921", +"#Ri c #0c0923", +"#7P c #0c0927", +"#ZY c #0c0928", +"ax1 c #0c0a0b", +"aIl c #0c0a0c", +"aPQ c #0c0a0d", +"ala c #0c0a0e", +".F6 c #0c0a0f", +"ai5 c #0c0a11", +"aPF c #0c0a12", +"aoZ c #0c0a13", +"ao0 c #0c0a14", +"anZ c #0c0a15", +"apI c #0c0a16", +"at4 c #0c0a17", +"ao2 c #0c0a19", +"an8 c #0c0a1a", +"ahv c #0c0a1f", +"#zE c #0c0a2d", +".Pw c #0c0a39", +".oU c #0c0a3c", +".V5 c #0c0a3d", +".nj c #0c0a40", +".LK c #0c0b0d", +"aoV c #0c0b0f", +"aoW c #0c0b10", +"aoX c #0c0b11", +"aoY c #0c0b12", +"apH c #0c0b13", +"azG c #0c0b15", +"aqD c #0c0b16", +"aNV c #0c0b17", +"aaq c #0c0b19", +"aF7 c #0c0b1a", +"#wJ c #0c0b26", +"#qD c #0c0b2a", +".rv c #0c0b2d", +".N1 c #0c0b39", +"#nx c #0c0b3a", +".XE c #0c0b3c", +"#ny c #0c0b3e", +"#l9 c #0c0b3f", +"aN7 c #0c0c0f", +"auE c #0c0c10", +"avO c #0c0c11", +"atS c #0c0c12", +"aGg c #0c0c14", +"ayE c #0c0c17", +"aga c #0c0c1a", +"amS c #0c0c1b", +"#zx c #0c0c2d", +"#D3 c #0c0c2e", +"#hz c #0c0c39", +".MG c #0c0c3a", +"#kT c #0c0c3b", +"#hI c #0c0c3c", +"#kH c #0c0c3f", +".N8 c #0c0c48", +".Iu c #0c0c4c", +"aA1 c #0c0d10", +"azP c #0c0d11", +"avf c #0c0d12", +"auC c #0c0d13", +"aEY c #0c0d14", +"amO c #0c0d16", +"akB c #0c0d18", +"aeU c #0c0d19", +"aeV c #0c0d1b", +"aiq c #0c0d1c", +"agj c #0c0d24", +"#hG c #0c0d34", +"#kM c #0c0d35", +".MB c #0c0d3a", +"#f5 c #0c0d40", +".PM c #0c0d43", +".Lm c #0c0d45", +".Ln c #0c0d49", +"Qti c #0c0d4b", +".kq c #0c0d52", +"amN c #0c0e16", +"aCc c #0c0e19", +"#kW c #0c0e3c", +"Qtm c #0c0e42", +".SJ c #0c0e47", +".SK c #0c0e4b", +".VZ c #0c0e50", +"aOZ c #0c0f11", +".fY c #0c0f35", +".2w c #0c0f3a", +".N4 c #0c0f3b", +".dq c #0c0f3e", +".dc c #0c0f4c", +"aPU c #0c1011", +"#DX c #0c101d", +"a.K c #0c1026", +".go c #0c103f", +".bE c #0c104d", +".Ub c #0c105d", +".5E c #0c1141", +".rK c #0c1147", +".oV c #0c1244", +"ab9 c #0c1633", +"aaw c #0c1635", +"a.P c #0c183a", +"ahy c #0c183d", +".L8 c #0d0000", +".Nt c #0d0003", +".Qy c #0d0004", +"azX c #0d000c", +".FK c #0d0011", +"aJJ c #0d0100", +"afk c #0d0107", +"aze c #0d0201", +".xQ c #0d0204", +"alT c #0d0208", +"abY c #0d020b", +".M7 c #0d0300", +".O4 c #0d0302", +"az7 c #0d0310", +"acB c #0d0400", +"al. c #0d0403", +".2H c #0d0406", +".EA c #0d0407", +".O5 c #0d0408", +"ahN c #0d0409", +"aph c #0d040a", +".Rs c #0d040b", +".ST c #0d040c", +"aIR c #0d040e", +".H# c #0d0434", +"#6L c #0d0504", +"#97 c #0d0506", +"aK9 c #0d050b", +".Kj c #0d050d", +"ayX c #0d050f", +".Gf c #0d0510", +"ajY c #0d0511", +"#dn c #0d0515", +"#hY c #0d0519", +"aAB c #0d060a", +"aqh c #0d060b", +".En c #0d060d", +".HQ c #0d0613", +"#PX c #0d0614", +"#bP c #0d0615", +"ayL c #0d0618", +".MW c #0d0619", +".R8 c #0d0707", +".5R c #0d0709", +"aAm c #0d070b", +"arc c #0d070c", +"akG c #0d0714", +"#bE c #0d0732", +"#wF c #0d0738", +"aIj c #0d0808", +"ajE c #0d0817", +"agz c #0d081e", +"afj c #0d0820", +".lN c #0d082c", +"#bN c #0d082d", +".j9 c #0d082f", +".VQ c #0d083c", +"#tj c #0d0847", +"apz c #0d090d", +"aq1 c #0d090e", +"ak3 c #0d0911", +"aiy c #0d0919", +"#Yx c #0d0928", +"#VC c #0d0929", +"#ZX c #0d092c", +".rn c #0d0931", +".VU c #0d0932", +".Sv c #0d0938", +".Sr c #0d0939", +".Fo c #0d093b", +".oX c #0d0940", +".CD c #0d0a0a", +"atG c #0d0a0c", +"aO9 c #0d0a0d", +"are c #0d0a0e", +"azE c #0d0a14", +"alN c #0d0a17", +"an3 c #0d0a1a", +"#Rh c #0d0a29", +"#c0 c #0d0a35", +".Q6 c #0d0a38", +".Q1 c #0d0a39", +"#wC c #0d0a3e", +"#u5 c #0d0a42", +"aN4 c #0d0b0d", +".EU c #0d0b0f", +"aoU c #0d0b10", +"aqw c #0d0b13", +"aqx c #0d0b14", +"aqy c #0d0b15", +".Kf c #0d0b16", +"arp c #0d0b17", +"at3 c #0d0b18", +"amU c #0d0b19", +"ao3 c #0d0b1a", +"an7 c #0d0b1b", +"#A6 c #0d0b2c", +"#nQ c #0d0b32", +".q# c #0d0b34", +".uo c #0d0b36", +".Pr c #0d0b39", +".G1 c #0d0b3c", +".oQ c #0d0b3d", +".nh c #0d0b40", +".lZ c #0d0b4a", +".Hc c #0d0c0a", +".LO c #0d0c0e", +"aqt c #0d0c10", +"aqu c #0d0c11", +"aqv c #0d0c12", +"arm c #0d0c13", +"arn c #0d0c14", +"aro c #0d0c15", +"awh c #0d0c16", +"ao1 c #0d0c17", +"aGf c #0d0c18", +"aEP c #0d0c1a", +"akK c #0d0c1c", +"#Oq c #0d0c21", +"#Cz c #0d0c2a", +"#A3 c #0d0c34", +"#u1 c #0d0c35", +"#eK c #0d0c3c", +"#ql c #0d0c3e", +"#hA c #0d0c3f", +"#kR c #0d0c40", +".kl c #0d0c4e", +"aOB c #0d0d07", +"atU c #0d0d13", +"aEX c #0d0d17", +"aGe c #0d0d1a", +"aeW c #0d0d1c", +"#wy c #0d0d31", +"#rT c #0d0d3c", +"#kO c #0d0d3d", +"#mg c #0d0d3e", +".bm c #0d0d40", +".SL c #0d0d4b", +".Lo c #0d0d4c", +".iT c #0d0d4d", +"aCh c #0d0e12", +"atW c #0d0e13", +"auD c #0d0e14", +"aIU c #0d0e15", +"aDH c #0d0e16", +"aDz c #0d0e1a", +"agb c #0d0e1c", +"air c #0d0e1d", +"akP c #0d0e1f", +".zo c #0d0e30", +"#jj c #0d0e36", +"#rN c #0d0e37", +"#hD c #0d0e38", +".qe c #0d0e40", +".c8 c #0d0e47", +".ab c #0d0e50", +"aFm c #0d0f0b", +"as8 c #0d0f10", +"aNk c #0d0f16", +"aQk c #0d0f17", +"aEN c #0d0f1c", +"adC c #0d0f26", +"#zv c #0d0f29", +"#A0 c #0d0f2a", +".eT c #0d0f35", +".ev c #0d0f36", +"#f4 c #0d0f3b", +".ux c #0d0f3d", +".2q c #0d0f3e", +".#7 c #0d0f44", +".XJ c #0d0f4c", +".l6 c #0d0f51", +"a.J c #0d1023", +"aiB c #0d1028", +".s9 c #0d103f", +".PH c #0d1042", +".0S c #0d1045", +".a. c #0d104d", +"Qtu c #0d104e", +"aQb c #0d110d", +"aLq c #0d1117", +"#DY c #0d1121", +".4d c #0d1141", +".qn c #0d114a", +"#Lz c #0d1228", +"adD c #0d122a", +".A2 c #0d1232", +".36 c #0d1243", +".bG c #0d1255", +"aQ. c #0d1315", +"Qtw c #0d1355", +"aMq c #0d141a", +"Qtv c #0d1657", +".dp c #0d173f", +".Hs c #0e0000", +"aDL c #0e0008", +"azW c #0e000e", +".L7 c #0e0100", +"#nU c #0e0105", +"#0A c #0e010a", +"aC1 c #0e0202", +".KL c #0e0301", +"au1 c #0e0305", +"aGK c #0e030d", +"amb c #0e0401", +".KM c #0e0403", +"a#w c #0e0404", +".xO c #0e0406", +"aGY c #0e0408", +"az8 c #0e040f", +"aiP c #0e0415", +"#pb c #0e042a", +"#8t c #0e0506", +".Qz c #0e0508", +"apv c #0e0509", +"acV c #0e050a", +"#eZ c #0e050c", +"#Z2 c #0e0516", +"#mo c #0e052c", +".Rk c #0e0531", +".SN c #0e0532", +".CQ c #0e0600", +"#46 c #0e0604", +".Ex c #0e0606", +".zx c #0e0607", +"arb c #0e060c", +"#qE c #0e060d", +".IM c #0e060e", +"#gs c #0e0616", +"afh c #0e0629", +".TC c #0e0707", +"aAz c #0e070b", +"aal c #0e0712", +".Wd c #0e0716", +"#yd c #0e0720", +".PP c #0e072f", +".qg c #0e0734", +".W5 c #0e0805", +"afs c #0e0808", +"#8s c #0e0809", +".Or c #0e080b", +"asD c #0e080d", +"ahM c #0e0814", +"ahq c #0e0819", +"#gj c #0e0833", +".t9 c #0e0836", +"#rU c #0e083f", +".Vg c #0e090a", +"aBV c #0e090e", +"#2t c #0e0913", +"ajZ c #0e0915", +"ajD c #0e0917", +".hT c #0e0922", +"#4t c #0e0928", +".b1 c #0e0935", +"#eJ c #0e0942", +"aq6 c #0e0a0d", +"amL c #0e0a0e", +"aP2 c #0e0a13", +".5Q c #0e0a17", +"#6b c #0e0a28", +"#th c #0e0a44", +".kg c #0e0a50", +".kh c #0e0a52", +"ar. c #0e0b0d", +"alE c #0e0b0f", +"arj c #0e0b11", +"ahW c #0e0b12", +"aLt c #0e0b13", +"auJ c #0e0b18", +"an4 c #0e0b1b", +"adA c #0e0b1c", +"agf c #0e0b1f", +"ahu c #0e0b20", +".oI c #0e0b3c", +".Rj c #0e0b40", +".nn c #0e0b42", +".YB c #0e0c08", +"apG c #0e0c11", +"awe c #0e0c12", +"atP c #0e0c13", +"ar5 c #0e0c14", +"ar6 c #0e0c15", +"ar7 c #0e0c16", +"anY c #0e0c17", +".HE c #0e0c18", +"at8 c #0e0c19", +"an2 c #0e0c1b", +"apJ c #0e0c1c", +".uk c #0e0c25", +".oM c #0e0c36", +".T8 c #0e0c40", +".ns c #0e0c46", +"ar2 c #0e0d11", +"ar3 c #0e0d12", +"ar4 c #0e0d13", +"asJ c #0e0d14", +"asK c #0e0d15", +"aBX c #0e0d16", +"atg c #0e0d17", +"anX c #0e0d18", +"aEW c #0e0d19", +"aDE c #0e0d1b", +"amV c #0e0d1c", +"amW c #0e0d1d", +"#X. c #0e0d21", +"agi c #0e0d24", +".ui c #0e0d25", +".LD c #0e0d27", +".sS c #0e0d29", +"#te c #0e0d3b", +"#md c #0e0d3d", +"#nG c #0e0d3e", +"#oZ c #0e0d40", +"#hL c #0e0d41", +".lQ c #0e0d42", +".e3 c #0e0d46", +".Eh c #0e0e0d", +"atf c #0e0e12", +"atX c #0e0e13", +"atV c #0e0e14", +"aCg c #0e0e15", +"azK c #0e0e16", +"aAZ c #0e0e17", +"aAY c #0e0e18", +"aCf c #0e0e19", +"aDG c #0e0e1a", +"agc c #0e0e1e", +"#D6 c #0e0e2b", +"#IF c #0e0e31", +"#qm c #0e0e38", +"#nH c #0e0e3c", +"#mh c #0e0e3d", +".#X c #0e0e41", +".Un c #0e0e48", +".SC c #0e0e4f", +"awg c #0e0f15", +"ayt c #0e0f16", +"aAV c #0e0f19", +"alQ c #0e0f20", +".38 c #0e0f3c", +"Qt. c #0e0f41", +".GX c #0e0f44", +".by c #0e0f46", +".MK c #0e0f47", +"#eN c #0e0f49", +".Rh c #0e0f4b", +".iZ c #0e0f51", +"azR c #0e1016", +".c1 c #0e1037", +".5G c #0e103c", +"#hN c #0e103e", +".XI c #0e104a", +"aIW c #0e1119", +".bp c #0e1138", +".Q9 c #0e113d", +".Pz c #0e113e", +".Ob c #0e1143", +".Iy c #0e1144", +".0T c #0e1148", +".l5 c #0e1151", +"aMo c #0e1219", +".N3 c #0e123d", +".Za c #0e124a", +".XB c #0e124d", +".o5 c #0e124e", +".Uc c #0e1250", +".2o c #0e1345", +".0L c #0e1348", +"#Ud c #0e142b", +"awp c #0e1a37", +".FT c #0f0000", +".Ru c #0f0001", +"a#A c #0f0002", +"#Cm c #0f0017", +"#rZ c #0f0022", +"au0 c #0f0101", +"#8i c #0f0102", +".L6 c #0f0200", +"#gm c #0f0201", +"acA c #0f0300", +"axZ c #0f0303", +"aCE c #0f0305", +"aHC c #0f0308", +"adr c #0f030b", +"ayM c #0f030d", +"ayW c #0f0310", +"#r0 c #0f0327", +"aj4 c #0f0401", +".CL c #0f0402", +".CK c #0f0404", +"atz c #0f0406", +".01 c #0f0407", +"ap4 c #0f0408", +"agB c #0f0409", +".Hr c #0f0414", +"agw c #0f0423", +"#qB c #0f0429", +".f# c #0f050b", +"ab0 c #0f050d", +"aPv c #0f050e", +".EQ c #0f0600", +"aj1 c #0f0605", +".zv c #0f0608", +"ady c #0f0609", +"aMe c #0f060c", +"#PY c #0f0612", +"#Fk c #0f0613", +"#M6 c #0f061a", +".Oj c #0f061e", +"#6# c #0f0620", +"#eP c #0f0623", +".AB c #0f062a", +".DV c #0f0635", +"ak6 c #0f0707", +".Ey c #0f0709", +"al7 c #0f070d", +"aBm c #0f0710", +"aE0 c #0f0711", +"#4s c #0f0724", +"agx c #0f0725", +"#bp c #0f073e", +".02 c #0f080b", +".Gi c #0f080d", +".PX c #0f080e", +".LF c #0f080f", +"aKe c #0f0812", +"#SV c #0f0813", +".XN c #0f0815", +".Ho c #0f0817", +"ayI c #0f081a", +".A6 c #0f0909", +".A7 c #0f090a", +"aq. c #0f090c", +".0X c #0f0910", +"azD c #0f0914", +"akF c #0f0915", +"#Lr c #0f091b", +"#7O c #0f0922", +"#a. c #0f0925", +"#Uf c #0f0927", +"#2K c #0f0928", +"#J7 c #0f092f", +".s0 c #0f0933", +"#qt c #0f0940", +".Gm c #0f0a06", +"ax5 c #0f0a0d", +".G# c #0f0a0e", +"aNd c #0f0a0f", +"aQj c #0f0a15", +"akH c #0f0a18", +"aHu c #0f0a1b", +".xv c #0f0a22", +"#a# c #0f0a2e", +"#tm c #0f0a36", +"#o8 c #0f0a40", +".l8 c #0f0a48", +"aAC c #0f0b0f", +"arO c #0f0b10", +"arP c #0f0b11", +"aO2 c #0f0b14", +"aan c #0f0b16", +".4h c #0f0b19", +".2B c #0f0b1a", +"aiz c #0f0b1c", +".zr c #0f0b2f", +".VV c #0f0b36", +"amH c #0f0c10", +"aqs c #0f0c12", +"auA c #0f0c13", +".F4 c #0f0c17", +"alM c #0f0c19", +"amT c #0f0c1a", +"ajF c #0f0c1b", +"an6 c #0f0c1c", +"#rV c #0f0c40", +"#tg c #0f0c42", +"ax7 c #0f0d11", +"anP c #0f0d12", +"axc c #0f0d17", +"anW c #0f0d18", +"anV c #0f0d19", +"aqG c #0f0d1c", +"an5 c #0f0d1d", +".7c c #0f0d42", +".nl c #0f0d43", +".nq c #0f0d47", +".ki c #0f0d50", +"aN8 c #0f0e11", +"atc c #0f0e12", +"atY c #0f0e14", +"atZ c #0f0e15", +"at0 c #0f0e16", +"at1 c #0f0e17", +"aAT c #0f0e18", +"ati c #0f0e19", +"aCe c #0f0e1a", +"aDF c #0f0e1b", +"aGb c #0f0e1d", +"aHA c #0f0e1e", +"#nF c #0f0e42", +"#m. c #0f0e43", +".Ue c #0f0e49", +".l0 c #0f0e4d", +"awJ c #0f0f15", +"azL c #0f0f16", +"azJ c #0f0f18", +"aEV c #0f0f1c", +"ahn c #0f0f1e", +"#o0 c #0f0f3a", +".DF c #0f0f3d", +".MS c #0f0f40", +".f5 c #0f0f4c", +".MM c #0f0f4e", +"aPc c #0f1014", +"axa c #0f1015", +"ayu c #0f1016", +"aAW c #0f101a", +".BK c #0f101c", +"agk c #0f1028", +"#Ct c #0f102b", +".gm c #0f1036", +"Qtb c #0f103d", +".0M c #0f1042", +".8Y c #0f104a", +".eB c #0f104b", +".hz c #0f104d", +"az1 c #0f1119", +".hr c #0f113b", +".#Z c #0f113d", +".0R c #0f113e", +".rM c #0f1142", +"#eM c #0f114f", +"ayz c #0f1214", +"#MT c #0f1230", +".#0 c #0f1238", +"Qtd c #0f1239", +".c0 c #0f123b", +".5F c #0f1241", +".37 c #0f1243", +".SF c #0f1245", +".V3 c #0f124f", +"#K# c #0f132a", +".nH c #0f1351", +".V1 c #0f135b", +".f9 c #0f1449", +"aPb c #0f1719", +"ayC c #0f171e", +"am9 c #0f192e", +"aMp c #0f1b21", +".N# c #100000", +".Wk c #100001", +"a#y c #100002", +"aGL c #10000e", +"#P4 c #100010", +"#Ye c #100100", +"azV c #100110", +"#gn c #100200", +".L5 c #100201", +"avz c #100202", +".Ec c #100205", +"aQr c #10020f", +"#OB c #100214", +"#DU c #100217", +".SO c #100224", +"aM7 c #100300", +"aot c #100306", +".CC c #10030b", +"#CA c #10031c", +"#2s c #100402", +".Zs c #100409", +"aJu c #10040c", +"#mu c #10040d", +"ajX c #100410", +"#gl c #100411", +"ahI c #10041d", +".xP c #100507", +"aIQ c #100511", +".A4 c #100512", +"#LC c #100516", +"#zq c #100520", +".CN c #100600", +"#JZ c #100617", +"ahJ c #10061e", +"#X# c #100620", +"afg c #10062a", +".i0 c #100635", +".EN c #100702", +".De c #100704", +"apu c #10070b", +"aMi c #10070d", +"aA2 c #100711", +"#k4 c #100715", +"aG. c #100719", +"#jB c #10071a", +"#.z c #100723", +"#3l c #100806", +"amd c #100807", +"#.H c #10080f", +"aOA c #100907", +"aP. c #10090b", +"aDZ c #10090d", +"a#d c #10090e", +".XQ c #100910", +"aPq c #100915", +"#.G c #100918", +"#s9 c #100937", +"#u9 c #100939", +"aMZ c #100a0a", +".B# c #100a0b", +".4i c #100a0d", +".2C c #100a0e", +"aix c #100a1a", +".vI c #100a25", +".yY c #100a26", +".Y5 c #100a29", +"#Fp c #100a30", +"##3 c #100a35", +"#nI c #100a41", +".Gc c #100b0e", +"aN. c #100b10", +"#08 c #100b18", +"aES c #100b1a", +"aF8 c #100b1c", +".bI c #100b47", +"agK c #100c0e", +"alD c #100c10", +"aPr c #100c14", +"alL c #100c1a", +"ais c #100c1b", +"#9t c #100c26", +".88 c #100c30", +"#M2 c #100c34", +".oF c #100c43", +".ac c #100c48", +".IJ c #100d0e", +"au6 c #100d0f", +"aik c #100d11", +"avc c #100d14", +"aOb c #100d15", +"#YC c #100d1d", +".MV c #100d2c", +"#bB c #100d35", +".Lz c #100d38", +"#qu c #100d40", +".kC c #100d47", +"anM c #100e13", +"ax# c #100e15", +"ar9 c #100e19", +"aqC c #100e1a", +"aqF c #100e1d", +"aqE c #100e1e", +".xK c #100e35", +".B7 c #100e36", +".Y8 c #100e3f", +"#rS c #100e44", +".mg c #100e46", +".kr c #100e53", +"aJQ c #100f11", +"asG c #100f13", +"axb c #100f14", +"azt c #100f18", +"axK c #100f19", +"ar8 c #100f1a", +"aEU c #100f1d", +"#x7 c #100f36", +"#td c #100f38", +".DH c #100f3d", +"#kI c #100f44", +"#jh c #100f45", +".MD c #100f4a", +"axE c #101016", +"ays c #101018", +"aPl c #101019", +"aAX c #10101b", +"aGd c #10101e", +"#db c #101038", +"#nz c #10103d", +"#o7 c #10103e", +"#qs c #10103f", +"#o6 c #101041", +".bZ c #101042", +".PL c #101043", +".Le c #10104a", +".iS c #10104e", +"aCb c #10111c", +".Kc c #10113b", +"Qtl c #101148", +"Qtk c #10114a", +".bw c #10114b", +"#Op c #10122c", +"Qtc c #10123b", +".bo c #10123e", +".qp c #101246", +".c7 c #10124b", +".Ud c #10124f", +".Xz c #101253", +"aPn c #10131b", +".bS c #101342", +".J7 c #101345", +".2p c #101346", +".SD c #101353", +".md c #101354", +".hC c #101451", +".d# c #10154a", +".Cz c #10172f", +".rz c #101839", +"asd c #101f42", +".Na c #110000", +"a#z c #110002", +".Ot c #110200", +"#pa c #110227", +"aa7 c #110300", +".5V c #110302", +".L4 c #110303", +"aA5 c #11030b", +"#jC c #11030f", +"#zG c #110319", +"ahO c #110402", +"aL2 c #110404", +".TB c #110407", +".gE c #110409", +"aLk c #11040b", +"aGi c #11040d", +"#G3 c #110410", +"azT c #110413", +"#.l c #110428", +".O3 c #110500", +"aPy c #11050c", +"#yf c #11050f", +"adu c #11060c", +"#2I c #110720", +"#u8 c #11073b", +"#ad c #11080f", +"#gt c #110810", +".zs c #11081a", +"#4r c #110821", +".80 c #110825", +"adV c #11082f", +".EF c #110906", +"ani c #11090f", +"#PZ c #110916", +".ku c #110929", +"#bq c #11093f", +".Ew c #110a09", +"arT c #110a0f", +"apx c #110a10", +"aIX c #110a11", +".Em c #110a13", +".Zn c #110a15", +"#eY c #110a1a", +"#Ou c #110a1d", +"#Ov c #110a28", +".Ha c #110a29", +".Ba c #110b0c", +"aam c #110b16", +"aET c #110b1b", +"ayF c #110b1c", +".rD c #110b36", +"aNg c #110c11", +"ahr c #110c1e", +"#eV c #110c31", +"#mi c #110c42", +".Q3 c #110c48", +".eL c #110c49", +"aft c #110d10", +"akx c #110d11", +"amK c #110d12", +"apm c #110d13", +"aOs c #110d17", +".pS c #110d1b", +".LE c #110d1c", +".DX c #110d1d", +"age c #110d20", +".ox c #110d23", +".Ps c #110d49", +".CF c #110e0d", +".CG c #110e0e", +"atK c #110e10", +".H3 c #110e11", +"aqr c #110e14", +"akI c #110e1c", +"aeX c #110e23", +".DN c #110e32", +".G0 c #110e41", +"#u4 c #110e42", +".no c #110e45", +".NX c #110e49", +"anL c #110f14", +"axA c #110f16", +"axJ c #110f18", +"ars c #110f1a", +"at2 c #110f1b", +"akJ c #110f1e", +"agg c #110f24", +"aeY c #110f25", +".B2 c #110f37", +".vX c #110f38", +".o8 c #110f43", +"#.x c #110f44", +"#hB c #110f46", +"av8 c #111018", +"ayb c #111019", +"aqB c #11101b", +"aGc c #11101f", +".Od c #11103f", +".ka c #111043", +"#jp c #111044", +"#jq c #111045", +"#o5 c #111046", +".np c #111049", +".dy c #11104a", +"axF c #111117", +"aI# c #11111a", +"aC. c #11111c", +".kI c #111132", +"#m# c #111140", +".Fi c #111144", +".f7 c #11114f", +"azH c #11121b", +"aC# c #11121d", +".Zc c #111247", +".#6 c #111249", +".bx c #11124a", +".PN c #11124b", +".Y1 c #111327", +".cZ c #11133f", +".iP c #111346", +".Zi c #11134b", +".kA c #111355", +".Lt c #111446", +".f8 c #111451", +".Lq c #111454", +".cb c #111539", +".In c #11153f", +".eE c #111552", +".MO c #111555", +".#9 c #11164b", +"Qtq c #11164c", +".aa c #111659", +".zq c #11183c", +".bF c #11195b", +".vW c #111a44", +"adJ c #112650", +".NB c #112752", +"#41 c #120000", +".Ns c #120105", +"aKb c #12020a", +"#Z3 c #120217", +".Bc c #120300", +".Dc c #120400", +".L3 c #120405", +".L2 c #120406", +".eX c #120423", +".r2 c #120500", +"#42 c #120505", +"aHy c #120519", +"aOS c #120602", +"ak9 c #120806", +"#6H c #120808", +"adv c #12080c", +".dL c #12081c", +".CS c #120900", +"aOk c #120907", +".ED c #12090b", +".Zr c #12090e", +"#go c #120910", +"aKd c #120913", +"aQe c #120915", +".8W c #12092d", +".AL c #120936", +"Qtz c #120937", +"#J6 c #120a28", +".C. c #120a37", +".Ev c #120b09", +"aEA c #120b10", +".Gg c #120b14", +"aP3 c #120b15", +"akE c #120b17", +"ajC c #120b18", +"agA c #120b19", +"aiw c #120b1a", +"#IC c #120b28", +"ab2 c #120c13", +"aO3 c #120c15", +".up c #120c36", +"#.y c #120c37", +"#kU c #120c43", +".Gd c #120d0f", +".jb c #120d20", +".XM c #120d24", +".Y6 c #120d32", +"aiZ c #120e11", +"as1 c #120e14", +"aPf c #120e17", +"aQh c #120e19", +".0W c #120e20", +"#Lw c #120e36", +"#c1 c #120e44", +".lW c #120e50", +"aq7 c #120f11", +"ate c #120f15", +"amj c #120f16", +"aAS c #120f1a", +"aht c #120f23", +".T7 c #120f3e", +"#nJ c #120f43", +".ni c #120f45", +"#ey c #120f4a", +"azo c #121013", +"aPT c #121014", +"aoQ c #121015", +"axI c #121018", +"arr c #12101b", +"aqA c #12101c", +"agh c #121026", +".qb c #121039", +".qq c #121041", +"#dd c #121044", +"axH c #121117", +"av7 c #121119", +"aFX c #12111a", +"ath c #12111c", +"aar c #12111f", +"aHB c #121120", +".A0 c #12112f", +".rN c #121140", +"#qr c #121143", +"#kP c #121145", +"#me c #121146", +"#kQ c #121147", +"azI c #12121b", +"ae0 c #121229", +".Og c #121247", +".XD c #121249", +".e2 c #12124a", +"##2 c #12124d", +".bs c #121250", +".hB c #121252", +".o7 c #12134a", +".N7 c #12134b", +".eD c #12134e", +".c3 c #12134f", +".V4 c #121350", +".eu c #121440", +".Zh c #121444", +"Qtn c #121447", +".Iq c #121448", +".Zj c #12144e", +".jc c #121537", +".oW c #12154a", +".d. c #121552", +".Rc c #121555", +"aQc c #121611", +".eV c #121645", +".bA c #121653", +".J5 c #121656", +"aQm c #12171e", +"aIV c #12171f", +".xJ c #121a42", +".A1 c #121b37", +".gn c #121b44", +".zp c #121c3e", +".xI c #121e42", +".vV c #121f45", +"agm c #121f46", +".YC c #122549", +".Ow c #130000", +"aQy c #13000b", +"aH7 c #13000f", +"arJ c #130100", +"#DS c #130114", +".Vf c #130200", +"aIH c #13020b", +"#Fy c #13030f", +"QtK c #130424", +"azU c #130514", +"#AU c #13051d", +".pk c #130600", +"aqX c #130608", +"acz c #130700", +"aPN c #130702", +"aP6 c #130712", +".PQ c #130723", +".xS c #13080a", +".CU c #130903", +".gD c #130910", +".Az c #130936", +".eM c #130938", +".CR c #130a00", +"am4 c #130a0f", +".7o c #130a11", +".9# c #130a12", +".MX c #130a16", +".rF c #130a1f", +"#2J c #130a26", +".s1 c #130a2c", +".gy c #130a2f", +".gf c #130a38", +"afr c #130b09", +"#bQ c #130b12", +"aOq c #130b16", +"ajB c #130b18", +"aiv c #130b19", +".Uq c #130b1b", +"#df c #130b27", +"#G8 c #130b29", +".gp c #130b3b", +".Gj c #130c0e", +"aoJ c #130c12", +"aPs c #130c15", +"aO4 c #130c16", +"aQs c #130c18", +"#Lt c #130c1a", +"ahp c #130c1c", +"#Fo c #130c2a", +"#ge c #130c4f", +"asC c #130d12", +"aDk c #130d13", +".FP c #130d1d", +".zb c #130d29", +"#jt c #130d44", +".Ge c #130e10", +"aN# c #130e13", +"aQo c #130e16", +"#wt c #130e32", +".p1 c #130e44", +".dg c #130e4a", +"atH c #130f12", +"aeO c #130f13", +"ayp c #130f19", +"aiA c #130f21", +"#x2 c #130f29", +"#gr c #130f33", +".j6 c #130f36", +"ax2 c #131011", +"atF c #131012", +"ajG c #131020", +".rC c #13103f", +".oG c #131045", +"#eA c #13104c", +".lV c #131051", +"apF c #131116", +"atR c #131117", +"axD c #131118", +"atC c #131119", +"aya c #13111b", +"aqz c #13111c", +".q. c #13113b", +".MR c #131140", +"#mj c #131144", +"#gi c #131146", +"#mf c #131147", +".nv c #13114b", +"axG c #131217", +"awy c #13121a", +".t. c #13123e", +"#kJ c #131243", +"#hJ c #131246", +"#hK c #131248", +".Ek c #131312", +"ayr c #13131c", +"aCa c #13131e", +"#wz c #13133a", +".uy c #13133e", +".Oe c #131345", +".Z. c #13134a", +"#bC c #13134e", +".#2 c #131353", +".iU c #131354", +".Ld c #131447", +".Xy c #131453", +".fX c #131541", +".JW c #131548", +".hv c #131549", +".PC c #13154c", +".Zb c #13154d", +".XC c #13154f", +"#Cr c #131629", +".am c #131645", +".Ii c #131648", +".PG c #13164e", +".Oa c #13164f", +"Qto c #131653", +".O. c #131656", +".PF c #131657", +".SE c #13174f", +".Rd c #131754", +".Ua c #131760", +".eK c #13185b", +"aBj c #14000e", +"axo c #140100", +".Nb c #140200", +"#8h c #140201", +"aIO c #14030c", +"#DT c #14041a", +"#6G c #140500", +"aHz c #140519", +".Bd c #140600", +"aLu c #14060f", +"aQz c #140610", +"#Fj c #140611", +".AV c #140618", +".ds c #140625", +"#eR c #140706", +".2G c #140708", +"ap3 c #14070a", +"#1m c #140719", +"#Ug c #14071e", +"ads c #140810", +"aIP c #140813", +"#eQ c #140815", +"aPh c #140816", +"aHx c #14081b", +"#tk c #14083d", +"aH5 c #14090d", +"aCm c #140912", +"aP5 c #140914", +"aA4 c #140a11", +".zz c #140b0d", +"aou c #140b11", +"ab1 c #140b13", +"aP4 c #140b15", +"aO5 c #140b16", +"ajA c #140b17", +"#YD c #140b1f", +"#gk c #140b27", +"##N c #140b2e", +".vF c #140b36", +".FA c #140b3b", +".Gh c #140c14", +".oZ c #140c2c", +".dw c #140c35", +"aqe c #140d11", +".vl c #140d22", +".DW c #140d2f", +".rk c #140d47", +"ayo c #140e18", +"#Iy c #140e1c", +"#gq c #140e2a", +".dz c #140e40", +"azk c #140f12", +".z9 c #140f15", +"aQf c #140f1a", +"#nT c #140f1e", +"Qty c #140f4b", +"aBW c #141014", +"#Lo c #141027", +".Ax c #141032", +".j5 c #141036", +"#gf c #14104e", +"ar1 c #141116", +"ak. c #141118", +"ajH c #141122", +"au3 c #14121a", +"arq c #14121d", +"aeZ c #141229", +".NW c #141246", +"ayq c #14131c", +"ajI c #141325", +"#ji c #141345", +"#hC c #141346", +".Y9 c #141348", +"aB9 c #14141f", +"aDA c #141420", +".DC c #141442", +".MC c #141447", +".nJ c #14144e", +".7b c #14144f", +".N9 c #141452", +".J4 c #141454", +".bQ c #14153b", +".GU c #14154a", +".Uk c #14154b", +".J2 c #14154d", +".c9 c #14154e", +".c4 c #141550", +".bt c #141552", +".MN c #141555", +".hq c #141642", +".J1 c #141649", +".#8 c #14164a", +".f1 c #14164c", +".ny c #141653", +".Ix c #141750", +".Lr c #141754", +".O# c #141755", +".Iv c #141757", +".U# c #14175d", +".J6 c #141855", +"aQg c #141c23", +"aub c #141c35", +".dd c #141c5e", +".P0 c #150000", +"aC0 c #150100", +"aQv c #150110", +".N. c #150200", +"aQw c #150210", +".Nc c #150300", +"#Vh c #150303", +"#mv c #150407", +"anh c #150505", +"aPB c #15050e", +"aMr c #15050f", +"azY c #150514", +"#qA c #150529", +".r1 c #150600", +"#ZF c #150706", +"apf c #150708", +".PR c #15071c", +"aFh c #150806", +"aLd c #15080f", +"aNn c #150811", +"aiX c #150905", +"adt c #150910", +"aPt c #150a14", +"#JY c #150a1b", +"aG# c #150a1d", +"aP# c #150b0b", +"az5 c #150b1d", +"adx c #150c0f", +"#G4 c #150c19", +"#uU c #150c2c", +".uq c #150c2e", +"#ac c #150d1d", +".xL c #150d22", +".5C c #150d26", +".rE c #150d2e", +"#gc c #150d34", +"#6f c #150e0d", +"aF9 c #150f20", +"#Lq c #150f23", +".yW c #150f2b", +"at# c #151015", +"aPd c #151017", +"adz c #151021", +".zc c #151025", +".Xw c #151039", +".DK c #15103f", +".DJ c #151041", +".Ss c #151045", +"ahs c #151123", +".Q2 c #151145", +"azq c #151216", +".Aw c #151233", +"#Fq c #151239", +"#kV c #151246", +"#ez c #151252", +"arZ c #151318", +".II c #15131c", +"aHk c #15131d", +"#wB c #151343", +"#bD c #151348", +".nr c #15134d", +"au4 c #15141c", +"aED c #15141d", +".ug c #15142d", +"#f8 c #151450", +"asH c #151518", +".mf c #151551", +".#3 c #151555", +"Qtg c #151556", +".iN c #151643", +"#f9 c #151644", +".hw c #15164b", +".bz c #15164c", +".R# c #15164e", +".ey c #151750", +".5L c #15183a", +".Ls c #151850", +".Iw c #151856", +".nx c #151953", +".gc c #151b5d", +".hN c #151f47", +".Uy c #160000", +"#Xa c #16001b", +".Rv c #160100", +".Ut c #160300", +".L9 c #160303", +"aDM c #16030b", +".rZ c #160400", +".R6 c #160404", +"#Hf c #16040e", +".SP c #16041e", +"azZ c #160515", +"aKc c #16060f", +"aPC c #160610", +".Ux c #160709", +".qE c #160800", +"aDJ c #160814", +".4l c #160908", +"aCl c #160914", +".Ct c #16091a", +".vv c #160924", +"aIY c #160a0f", +"aGa c #160a1d", +"agH c #160b09", +".EB c #160c12", +"#wp c #160c22", +".zw c #160d0f", +"aiu c #160d1a", +".qi c #160d21", +"##4 c #160d2a", +".Cj c #160d3c", +".t7 c #160d49", +"aOj c #160e09", +".Ez c #160e10", +"aqg c #160e14", +"aGh c #160e15", +".8V c #160e25", +".m. c #160e2e", +".qh c #160e2f", +"#c9 c #160e35", +".eW c #160e3f", +"aqa c #160f13", +".Xu c #160f31", +"#.w c #160f34", +"aqc c #161014", +"at. c #161015", +".C# c #161038", +"#eO c #16103b", +".gx c #16103c", +"aOu c #16111a", +"aPg c #16111c", +".E. c #161125", +".Zm c #161126", +".T2 c #16114d", +"ara c #161215", +"aPk c #16121c", +".p3 c #16123e", +"au5 c #161315", +"axB c #16131a", +".Cy c #16132c", +".Xx c #161341", +".oJ c #161342", +".qf c #161346", +"#ju c #161347", +"aqq c #161419", +"awH c #16141b", +"axs c #16141c", +"azF c #16141d", +".je c #161430", +".oN c #161446", +"#hM c #161447", +"ayc c #16151e", +".rx c #161536", +"#wA c #161540", +".kb c #16154a", +".VX c #16154b", +".Fj c #161649", +".kB c #161652", +".It c #161655", +".i6 c #16173d", +".au c #161741", +".f2 c #16174e", +".me c #161751", +".Ij c #161756", +"#DZ c #16182d", +".hD c #161a54", +"aCS c #161c3c", +".P1 c #170000", +".Ng c #170100", +".Nh c #170200", +"aGj c #17020e", +"#A7 c #170217", +"#Uh c #170311", +"aBB c #170400", +"#It c #170511", +"aos c #170605", +"aKk c #17060d", +"ap2 c #170706", +"#k5 c #170810", +".i5 c #170911", +".uv c #170a17", +".s6 c #170a18", +".hI c #170a26", +".SX c #170b11", +"aQq c #170b16", +"aCk c #170b18", +".r4 c #170c02", +".1O c #170c0a", +".jk c #170c11", +"#Iw c #170c19", +".H. c #170c46", +"adw c #170d11", +"#wq c #170d28", +"##Q c #170e33", +"aCj c #170f1c", +".e1 c #170f38", +".xs c #170f3a", +".Eu c #17100d", +"anF c #171015", +".x# c #171028", +".34 c #17102d", +"azS c #17111b", +".x. c #171128", +".7k c #17112d", +".rm c #171141", +"aFl c #17120b", +".Ga c #171215", +".as c #17122b", +".ne c #171230", +"#de c #17123c", +".Kg c #171316", +"aOr c #17131d", +"aqk c #171418", +"aEQ c #171423", +".ro c #171432", +".Y7 c #17143f", +"#c3 c #171442", +"axC c #17151c", +"axr c #17151d", +".B6 c #17153d", +"#tf c #171547", +".lX c #171554", +".ME c #171555", +".uh c #17162e", +".Lg c #171654", +".Lf c #171655", +"am6 c #171724", +"#wO c #171728", +".p7 c #171731", +".Ik c #171755", +".JX c #171756", +"Qth c #171757", +"#A1 c #171838", +".nI c #171850", +".PE c #171856", +".o6 c #17194e", +".SA c #171950", +".eF c #171b55", +".eU c #172049", +"#40 c #180000", +"#LD c #180008", +".tj c #180200", +"aQx c #180211", +"aQu c #180212", +"aOR c #180300", +"#VK c #18031f", +".Ov c #180400", +"#zH c #180414", +".Wg c #180500", +"aCD c #18050b", +"#8g c #180600", +"#3h c #180602", +"aE2 c #180713", +".eO c #180912", +".k7 c #180a11", +".vT c #180a16", +"#hZ c #180a18", +".bU c #180a29", +"aOi c #180b07", +"aPz c #180b12", +".Cs c #180b1a", +".to c #180c03", +".Ok c #180c1c", +"#JX c #180c1e", +"#AV c #180c23", +".yZ c #180c36", +".M8 c #180d0a", +".xR c #180d0f", +"apg c #180d11", +".7# c #180e31", +"ahS c #180f0d", +".dh c #180f3d", +".m# c #18101f", +"alU c #18111a", +".vY c #181128", +".nB c #181141", +"aPj c #18121d", +".p0 c #18124b", +".St c #181252", +".Q4 c #181253", +"aNf c #181318", +"agd c #181326", +".Q5 c #181352", +".Pu c #181353", +".Pt c #181354", +"azf c #181415", +"aox c #18141a", +".hW c #181437", +".Fp c #181445", +".NZ c #181453", +".NY c #181454", +"akO c #181523", +".0J c #181542", +".DY c #18161a", +".c. c #181623", +".G5 c #181642", +".lY c #181656", +".oP c #181749", +"aPo c #181821", +"aAU c #181822", +".b0 c #18184b", +".Lp c #181858", +".rr c #181924", +".Xs c #181a3b", +"#AZ c #181b32", +".qo c #181b4d", +".bB c #181c56", +"ac. c #182d51", +"aiG c #182d55", +"a#v c #190000", +".Om c #190100", +"#1U c #190108", +".Nf c #190300", +".Nr c #190407", +"aCz c #190411", +"as0 c #190502", +".zA c #190600", +"asq c #190603", +"aqW c #190604", +".Ou c #190702", +"aIZ c #19080d", +"aHs c #19080f", +"aE3 c #190813", +".Bo c #19090c", +"aOe c #190910", +".03 c #190a0d", +"agC c #190b08", +"am2 c #190b0c", +".gh c #190b14", +".rJ c #190b1b", +".nX c #190c03", +"am3 c #190c0f", +".AU c #190c1b", +".5W c #190d0d", +".CW c #190d0e", +".v3 c #190d10", +"aPa c #190e0e", +".dK c #190f23", +".VO c #190f32", +"ait c #19101d", +".s2 c #191026", +".EI c #191108", +".G7 c #19113a", +".i8 c #191142", +"#eH c #191145", +".sG c #19114c", +"aC3 c #191214", +".dI c #19131c", +".8Z c #19133e", +".e4 c #191346", +".7d c #19143e", +".DL c #191440", +"#d# c #191444", +"avD c #191518", +".DM c #19153e", +"atJ c #191619", +"azQ c #19171d", +"aDI c #191723", +"aAH c #191821", +".G6 c #191843", +".LA c #191846", +"#x5 c #191a37", +".Fh c #191a4c", +".jd c #191b3f", +"ae4 c #191c37", +".4c c #191c44", +".rL c #191c4c", +"Qtp c #191d57", +".0c c #192a4d", +".Mj c #192f5c", +"#XI c #1a0000", +"aOy c #1a0008", +"aKl c #1a0109", +".M0 c #1a0200", +"aFg c #1a0309", +".Ne c #1a0400", +"#IM c #1a040b", +"ap1 c #1a0501", +"#YE c #1a051f", +"aFA c #1a0600", +"aE4 c #1a0712", +"aPi c #1a0718", +".kD c #1a0923", +"aJ7 c #1a0a12", +".mh c #1a0a23", +".xG c #1a0b16", +".h3 c #1a0c0f", +".dn c #1a0c14", +".D5 c #1a0c1e", +".qm c #1a0c1f", +".ao c #1a0c2b", +".xg c #1a0c36", +".C1 c #1a0e0a", +".v2 c #1a0e10", +"aOv c #1a0e18", +".CJ c #1a0f10", +".xM c #1a0f11", +".T0 c #1a0f3a", +"##P c #1a0f3c", +".G9 c #1a0f47", +"aiY c #1a100d", +"apr c #1a1015", +"#tl c #1a1042", +".BI c #1a1111", +"#s8 c #1a113b", +"ayn c #1a121d", +".kv c #1a1220", +"#x1 c #1a1232", +".t8 c #1a1249", +".gu c #1a1336", +".xr c #1a1337", +"ad9 c #1a1416", +"aqd c #1a1417", +".yX c #1a1430", +".rl c #1a144a", +".Es c #1a1510", +"apy c #1a151a", +"aPe c #1a151d", +"ass c #1a161c", +".lL c #1a163a", +".kf c #1a175a", +"ata c #1a181b", +"aBY c #1a1822", +".B3 c #1a1840", +".oO c #1a184a", +"azu c #1a1922", +".Rb c #1a1a59", +"QtP c #1a1d3f", +".s8 c #1a1d4a", +".hO c #1a1e4d", +".Np c #1b0000", +".ti c #1b0400", +".Zq c #1b0600", +"aD2 c #1b0702", +"aGW c #1b0801", +"auZ c #1b0804", +".M9 c #1b0900", +"aOz c #1b090d", +".nK c #1b0b22", +".Rm c #1b0b23", +".Db c #1b0d00", +".hJ c #1b0d16", +".zj c #1b0d1f", +".Hk c #1b0e21", +".v4 c #1b1012", +"aoI c #1b1015", +".Fy c #1b1145", +".CP c #1b1206", +".zt c #1b1314", +"#J5 c #1b1327", +".vG c #1b133c", +".hP c #1b1344", +"aj5 c #1b1413", +"aqb c #1b1418", +".vE c #1b1436", +".l9 c #1b1446", +"ard c #1b151a", +".0G c #1b1536", +".2c c #1b1625", +"#c2 c #1b1650", +"arK c #1b181d", +"atM c #1b181f", +".Fr c #1b1846", +"aJR c #1b191b", +".Fv c #1b193f", +".lP c #1b1a4e", +".SB c #1b1b5a", +".J3 c #1b1c58", +".sY c #1b1d46", +".Il c #1b1d53", +".c# c #1b1f43", +".dU c #1b264a", +"apR c #1b265d", +".yt c #1b2c49", +".tg c #1c0000", +".Nq c #1c0304", +"aLv c #1c030d", +"#M7 c #1c0314", +".LI c #1c0400", +".2F c #1c0500", +"aOC c #1c0506", +".00 c #1c0600", +".Nd c #1c0601", +".uE c #1c0700", +"aI0 c #1c070b", +".rY c #1c0902", +".zn c #1c0c14", +".o9 c #1c0c20", +".vt c #1c0d35", +".qD c #1c0e05", +".o4 c #1c0e24", +".gg c #1c0f2a", +".0a c #1c1006", +".CY c #1c1014", +"aDK c #1c1019", +".uD c #1c1100", +".C4 c #1c1104", +".CT c #1c1209", +".CM c #1c120d", +"#bu c #1c1212", +"aMf c #1c1219", +"aOp c #1c121e", +"#.n c #1c1224", +".ad c #1c1241", +".CO c #1c1309", +".EO c #1c130d", +"apt c #1c1318", +"aNC c #1c131f", +".2j c #1c132b", +".AK c #1c133e", +".EM c #1c140e", +"aov c #1c151c", +"aQp c #1c151e", +"aho c #1c1525", +".DR c #1c1633", +".ng c #1c1835", +"aPp c #1c1923", +"arW c #1c1a1e", +".N0 c #1c1a52", +".p9 c #1c1b44", +".MF c #1c1b52", +".T3 c #1c1b56", +".Lh c #1c1c53", +".Fl c #1c1d4f", +".GV c #1c1d52", +"QtI c #1c204f", +"aQl c #1c2128", +".Ni c #1d0000", +"#CB c #1d0316", +"#0z c #1d070a", +"#3g c #1d0800", +".Cx c #1d0907", +".rX c #1d0a03", +"#Fi c #1d0a14", +".AZ c #1d0b0e", +"#Ck c #1d0b17", +".Br c #1d0d04", +".qr c #1d0d1f", +".pl c #1d0f06", +".D6 c #1d0f23", +".qG c #1d1007", +".C0 c #1d110f", +".h2 c #1d1218", +"aOt c #1d121f", +"#J0 c #1d1323", +"##O c #1d133f", +"#.o c #1d141e", +".EH c #1d150d", +".nC c #1d1525", +".0F c #1d1530", +".sH c #1d154c", +"#gd c #1d154f", +"#Fn c #1d1629", +"#.v c #1d1631", +"#eI c #1d1652", +"aBD c #1d1719", +"#br c #1d1742", +".Su c #1d1851", +".As c #1d193b", +".Pv c #1d1951", +"asI c #1d1b20", +".2d c #1d1b26", +".sK c #1d1b33", +".rs c #1d1b3d", +".Fs c #1d1b46", +".hV c #1d1e48", +".GS c #1d1e53", +".VS c #1d1f4d", +".cd c #1d2245", +"aKT c #1d2b4d", +".Ab c #1d2e4b", +"#A8 c #1e000f", +"#VL c #1e0013", +".Nj c #1e0100", +".Rw c #1e0300", +"aOD c #1e0608", +"aJt c #1e0708", +"aOE c #1e070a", +"aI1 c #1e070b", +".D9 c #1e0902", +"aOw c #1e0914", +"#DR c #1e0b14", +"aQi c #1e0d1d", +".Bb c #1e0f07", +".gF c #1e0f12", +".i1 c #1e0f18", +".rO c #1e0f1d", +".nG c #1e0f28", +"aPA c #1e1018", +".i9 c #1e1030", +".tn c #1e1108", +"#zp c #1e1129", +".mu c #1e1208", +".v5 c #1e1214", +".C5 c #1e1304", +"aoH c #1e1318", +".vM c #1e1319", +".EE c #1e1515", +"aJ6 c #1e151b", +"#gp c #1e1726", +".xu c #1e1737", +".Fw c #1e173c", +".Gk c #1e1817", +".na c #1e1937", +"azh c #1e1a1c", +".pT c #1e1a28", +"asE c #1e1b1f", +"agQ c #1e1b22", +".0I c #1e1b46", +".lT c #1e1c58", +".DG c #1e1e4c", +".aE c #1e1f2e", +"#AX c #1e2134", +"#WM c #1e2234", +".ce c #1e2246", +".fn c #1e2345", +"#3U c #1e283d", +".O1 c #1f0000", +".O2 c #1f0100", +"aOf c #1f060e", +"aE5 c #1f0613", +".Rp c #1f0700", +"asZ c #1f0701", +"arI c #1f0a06", +"aHD c #1f0c14", +".pA c #1f0d0c", +".SY c #1f0e10", +".TA c #1f0e12", +".r0 c #1f0f07", +"#Cl c #1f0f23", +"#pg c #1f100f", +"aCn c #1f1017", +".t# c #1f101c", +".mc c #1f102b", +".kR c #1f1109", +"aLe c #1f1219", +".bP c #1f121a", +".Hj c #1f1224", +"#.m c #1f1230", +".Xo c #1f152a", +".bJ c #1f1544", +"#7T c #1f1613", +".o0 c #1f1627", +".ur c #1f162d", +"#M1 c #1f172b", +".z. c #1f173e", +"QtJ c #1f1747", +"aL. c #1f181d", +".3V c #1f1829", +"#d. c #1f1848", +".Xv c #1f193d", +"#ga c #1f1a1d", +".lI c #1f1a3e", +"aq2 c #1f1b21", +".p4 c #1f1c42", +"#cZ c #1f1d40", +".oL c #1f1d47", +".nm c #1f1d54", +".ke c #1f1d5c", +"#t# c #1f1e46", +".GW c #1f2055", +".Ir c #1f2058", +".ca c #1f2346", +"aKm c #200109", +".Nk c #200400", +".yn c #200800", +"#Ui c #200810", +"aBd c #200817", +"#1n c #200b21", +".kz c #20102c", +"aE1 c #20121e", +".eS c #20131b", +"aPu c #20131d", +".C2 c #20140d", +"ak2 c #20141b", +".kH c #201630", +".e5 c #201641", +".EG c #201812", +".VP c #201845", +".dr c #201849", +".yS c #201a36", +"#uW c #201a45", +".5t c #201b2b", +".xq c #201c2f", +".kj c #201f61", +"#9. c #203057", +".No c #210100", +"aA7 c #21020e", +"#D8 c #210416", +".SS c #210900", +".FO c #210a00", +"aKZ c #210b03", +".pB c #210d17", +"aum c #210e09", +".Bq c #211109", +"afl c #21110d", +".Bn c #211111", +"#AT c #211122", +".uL c #21120a", +".Be c #211305", +".CZ c #211413", +".62 c #21141b", +"#9y c #211610", +".dB c #21161c", +".dA c #211642", +".2i c #211728", +"#.D c #211929", +".AC c #211935", +".rc c #211d23", +".j8 c #211d44", +"#Kd c #220107", +".Ve c #220300", +"aOg c #22040b", +"#zI c #220915", +".5U c #220a00", +"asp c #220b06", +"aAl c #220f08", +".gq c #221433", +".CX c #221619", +"#wr c #22183b", +".AA c #221842", +".f. c #221920", +"azC c #221a26", +".Et c #221b18", +".oy c #221d33", +".DD c #22214f", +".Fk c #222255", +"#1# c #22364e", +"aFk c #230000", +"aB. c #23000d", +".Nl c #230600", +"ang c #230a02", +".Hn c #230b00", +".BD c #230f00", +".ja c #231108", +".2I c #231112", +"az0 c #23121e", +".kS c #23130b", +".FL c #231529", +".Bg c #231602", +".Bl c #231606", +".D4 c #231624", +"afq c #231814", +".G8 c #231949", +"aIG c #231a20", +".DS c #231c40", +"atN c #232027", +".K0 c #233867", +"#Rv c #24000b", +"a#u c #240100", +"#6F c #240200", +"#0y c #240300", +".P2 c #240600", +"aMs c #240612", +"aOo c #240615", +"aNo c #240a14", +"aty c #24110d", +".mw c #24140c", +".fi c #24141b", +"aNH c #241423", +".tm c #24150d", +".Bm c #241510", +".vu c #241539", +"aNG c #241624", +".qF c #24170e", +"aLi c #24171e", +".Bh c #241802", +".r3 c #24180e", +"a#e c #24180f", +".jC c #24181a", +"aoG c #24191e", +".fa c #24191f", +".TZ c #241937", +"aHi c #241b21", +".7. c #241b30", +".b2 c #241b40", +".hX c #241c38", +"aNe c #241f24", +".u. c #241f41", +".Cb c #24203c", +"atL c #242123", +".G3 c #242250", +".kJ c #242642", +".dT c #242a42", +".cg c #242c4d", +"aa6 c #250000", +"aB# c #25000f", +"aBc c #250013", +"#YF c #25001b", +"#1T c #250100", +"aOh c #25020a", +".7s c #250500", +"aA6 c #250613", +".Nn c #250900", +".Nm c #250901", +".tK c #250e00", +".mM c #251524", +"#eS c #25170e", +".xC c #251729", +".BH c #251814", +".kQ c #251a10", +"##8 c #251c24", +"aL# c #251e24", +"aJS c #251e25", +".xf c #251e36", +"aw2 c #25232b", +".dS c #252536", +".cf c #25294d", +"#9U c #260000", +"#Xb c #260019", +"#OC c #260112", +".9m c #260200", +".uF c #260300", +"aKn c #26040c", +"aA9 c #260412", +"aOl c #260610", +"aA8 c #260613", +"aGk c #260f1c", +".e0 c #26111e", +"ad1 c #261611", +".k9 c #261628", +"#.k c #26193a", +".Bi c #261a02", +".Hg c #261a21", +".CV c #261b18", +".VN c #261b30", +".y0 c #261b41", +".Fz c #261c53", +".T1 c #261d54", +"#c6 c #261e1f", +".gC c #261e27", +"#bo c #261f4d", +"aNE c #26202b", +".rj c #262128", +".2m c #262145", +".0z c #262333", +".dR c #262430", +".aD c #262634", +"ajN c #26395c", +".O0 c #270503", +".3t c #27151e", +".Bp c #27161c", +".nY c #271810", +".AW c #27192d", +"agG c #271a16", +".gl c #271a22", +".Bj c #271b05", +"#Lv c #271f33", +".gT c #272034", +".y9 c #27223b", +"aEE c #27262f", +"aBt c #272d4e", +".sW c #27304c", +".EZ c #273f6d", +"#VM c #280009", +"aBb c #280211", +".9k c #280300", +"#Z4 c #280923", +".z0 c #280f00", +".Da c #281a0d", +".mv c #281a11", +".xD c #281a2e", +"#yg c #281b2e", +".eN c #281b37", +"#bx c #281f37", +".bT c #282050", +".AM c #282146", +"aAy c #282226", +"aoK c #282429", +".nd c #282441", +"apE c #28262b", +".DQ c #282643", +".hY c #282939", +".GC c #283e69", +".Ox c #290000", +"#8f c #290700", +"aJI c #29140c", +".mN c #291929", +".Bt c #291a0c", +".qC c #291a11", +".Cu c #291c2f", +".C3 c #291d13", +"#.j c #291e33", +".Fx c #29204c", +".5B c #292138", +"aND c #29222d", +"apA c #29262a", +"atO c #29262d", +".u# c #29273c", +".fm c #292b40", +".cc c #292d51", +".fo c #29315f", +".Ia c #293d66", +"aOn c #2a0616", +".v. c #2a0d00", +".u7 c #2a1200", +".wh c #2a1408", +".qU c #2a141a", +".BC c #2a1601", +".zB c #2a1705", +"#2P c #2a1719", +".Yz c #2a1809", +".fb c #2a1a21", +".dj c #2a1b24", +"aaT c #2a1d12", +".tp c #2a1f15", +"aEf c #2a2124", +".e9 c #2a222a", +"QtZ c #2a2b3b", +"avX c #2a3857", +".1P c #2a395a", +"#2z c #2a4a6c", +"#0T c #2b0000", +".6A c #2b0600", +".nN c #2b1508", +".XT c #2b1718", +".Bk c #2b1f0a", +"#xY c #2b1f2f", +".C6 c #2b2111", +"#M0 c #2b2330", +".gU c #2b2543", +"axt c #2b2931", +".rA c #2b3158", +".ul c #2b354d", +".ys c #2b364d", +"aJC c #2b385a", +".Jv c #2b4070", +"#0U c #2c0000", +"#M8 c #2c000b", +"#P5 c #2c0011", +"#jF c #2c0100", +".th c #2c0300", +"aFi c #2c0502", +"#CC c #2c0513", +"aOm c #2c0514", +"aBa c #2c0615", +"aLw c #2c0813", +".sj c #2c1100", +".u4 c #2c1212", +".kG c #2c1319", +".x8 c #2c1519", +".uJ c #2c1710", +".4m c #2c1717", +"#nV c #2c1915", +"acp c #2c1e12", +".uO c #2c2218", +"#4y c #2c231d", +"aJT c #2c252c", +".Ck c #2c2548", +".e6 c #2c262f", +"aDx c #2c2e3a", +"#7n c #2c3f69", +"#9T c #2d0000", +"aOx c #2d111d", +".qV c #2d171e", +"##T c #2d201f", +"##Z c #2d2748", +".gz c #2d2b38", +".IH c #2d2d43", +"ahz c #2d3f66", +"#2b c #2e0303", +"#4Z c #2e0d00", +".SZ c #2e1613", +".sh c #2e161a", +".zF c #2e1d00", +".ob c #2e1e20", +".nW c #2e2218", +".ii c #2e2232", +".dJ c #2e2438", +".EJ c #2e2620", +".Is c #2e2f6a", +".5M c #2e3158", +"aax c #2e3a5b", +"aFj c #2f0000", +"#LE c #2f0005", +"#0S c #2f0100", +".tF c #2f1617", +".mk c #2f1715", +"aIi c #2f1911", +".gt c #2f1a21", +".qT c #2f1b17", +".R5 c #2f1c17", +"akw c #2f2b2f", +"ato c #2f4465", +".Dl c #2f4a7c", +"aa5 c #300002", +"aNJ c #300314", +".Oy c #300400", +".9l c #300e00", +".tI c #301600", +".sk c #301707", +".sf c #301a13", +".wg c #301c06", +".hS c #301d1b", +".k8 c #302131", +".pm c #302219", +".qH c #30231a", +"ajW c #30232d", +".Xp c #302848", +"#eE c #302a2c", +".gV c #302a4f", +"aAR c #302b37", +"awz c #302d30", +".al c #303a62", +".ch c #303b60", +".aJ c #304475", +"#HA c #310000", +"#1S c #310400", +"aI2 c #310a00", +"#Z. c #310e00", +".tG c #311600", +".z1 c #311800", +"ak8 c #312623", +"aLa c #312a2f", +".8U c #312b2e", +"#2a c #320604", +"#2c c #320909", +".7t c #321700", +"aNI c #321827", +".wk c #321921", +".Zt c #321a1a", +".1N c #32221c", +".zk c #322438", +".Cr c #322630", +".EL c #322923", +"aIm c #323033", +"ari c #323035", +".B4 c #323058", +"#da c #32305c", +"aql c #323134", +".Wl c #331b0d", +".3s c #331d21", +"azd c #331f18", +"aIN c #33222b", +".xT c #33282a", +".vm c #332b40", +"anj c #332d34", +".vD c #332f40", +"Qt9 c #335e9d", +".#g c #3365a8", +".#f c #3367a8", +".#e c #3368a8", +"#2d c #340806", +"aNp c #340d19", +".7z c #341304", +".v# c #341608", +".wl c #341f08", +"#.A c #342835", +"Qt0 c #343748", +"aQF c #344d7b", +".#h c #3463a8", +"#8e c #350000", +"#Zo c #350100", +"a#t c #350103", +"aDY c #350a1e", +"#h2 c #350b00", +".7r c #350c00", +"#XH c #350d00", +"aMY c #351717", +".si c #351806", +".vP c #352739", +".xh c #35284c", +"anE c #352e33", +"#Vi c #35374b", +".d6 c #355d9e", +".aW c #3565aa", +"#Zn c #360000", +"#9S c #360005", +"#Xc c #360011", +"#SW c #360213", +"#0R c #360703", +".Rx c #361609", +"#3f c #361c0c", +".Bu c #362716", +".zi c #362938", +".0E c #362c41", +"aLb c #362f35", +".Ly c #36325a", +"#Zt c #370000", +"#Zp c #370500", +"#0V c #370600", +".9c c #370c00", +".wj c #371e22", +".yo c #37231c", +".Bv c #372916", +".aj c #372931", +".7h c #372a20", +"al6 c #372b2f", +"aoF c #372c31", +"#Fm c #372f3c", +"aAx c #373134", +"aJU c #373137", +"afA c #37343b", +".DE c #373664", +"ac# c #374d71", +".#i c #3763ac", +".aX c #3766ab", +"#9R c #38040a", +"#Zq c #380700", +".OM c #380800", +"#9Q c #38080c", +"#Wf c #380900", +"#h3 c #380b00", +"aL1 c #380e0e", +".04 c #381b1a", +".48 c #381d2d", +".tD c #382017", +".pz c #382a14", +".mL c #382a2f", +".ih c #382b37", +".us c #382e3e", +".dQ c #382e42", +".dH c #38313a", +".VR c #38376a", +"#YG c #390018", +"#Zs c #390300", +"#Zr c #390400", +"aa4 c #390608", +"#HB c #39100c", +".OZ c #39130f", +"aHE c #39202b", +".49 c #392236", +"#TV c #392626", +".z7 c #392822", +".Bs c #392a1f", +".FJ c #392c3b", +".jD c #392f31", +".Ci c #393060", +"#ws c #393157", +"#80 c #39353b", +"aAd c #393e60", +"#5H c #394b71", +".aK c #394e7f", +".#a c #395ea5", +".## c #3960a4", +".yy c #3966b3", +".wL c #3966b4", +".Oz c #3a0e02", +".P3 c #3a1a0c", +".tE c #3a2121", +".tJ c #3a220e", +".uI c #3a231d", +".wi c #3a2320", +".7g c #3a2d2c", +".vN c #3a2d38", +"aMh c #3a3137", +"alX c #3a465d", +"Qt3 c #3a5a93", +".lk c #3a5fab", +".aT c #3a62ad", +".m2 c #3a64b8", +".aV c #3a6bb0", +"#e2 c #3b0200", +"#0Q c #3b0a03", +".OK c #3b0b03", +"#D9 c #3b101d", +".nZ c #3b2b23", +".xe c #3b344b", +".cq c #3b62ad", +".a1 c #3b63a9", +".su c #3b64a9", +".cm c #3b64ac", +".oq c #3b67bc", +"#dq c #3c0400", +"aNR c #3c0408", +".OL c #3c0c04", +"#2e c #3c0d0a", +"#2# c #3c0e0b", +"#.K c #3c0f00", +"aMt c #3c121f", +".sg c #3c2427", +"#Cj c #3c2a28", +".od c #3c2a37", +".oc c #3c2b37", +"anG c #3c363b", +"#bn c #3c3759", +".At c #3c385a", +"adK c #3c517e", +"Qt2 c #3c5b95", +".ct c #3c64a6", +".cs c #3c64a9", +".cr c #3c64ac", +".#j c #3c65b0", +".#B c #3c66b0", +".cJ c #3c66b1", +"##j c #3d0000", +"#Rw c #3d0113", +".58 c #3d0900", +".OJ c #3d0d04", +"#VN c #3d0d12", +"#Wg c #3d180b", +".u2 c #3d2419", +".zO c #3d292c", +".zE c #3d2c12", +".b3 c #3d2d30", +".z8 c #3d3130", +".8O c #3d3336", +"aFB c #3d3437", +".xd c #3d364e", +".Av c #3d395b", +".aS c #3d62aa", +".#A c #3d65b2", +".#C c #3d66b1", +".vj c #3d67ae", +".#F c #3d67b1", +".a9 c #3d67b2", +".r# c #3d69b8", +"#Zu c #3e0000", +"aNO c #3e0001", +"aNK c #3e0012", +"#gw c #3e0300", +"#Zm c #3e0600", +"#Z5 c #3e0b28", +"aNS c #3e181b", +"#FW c #3e1d1d", +"#dg c #3e3340", +"#gb c #3e3749", +".t6 c #3e3936", +"ahg c #3e3b3f", +"aIc c #3e507a", +".dV c #3e507f", +".BO c #3e5d93", +".oo c #3e64af", +".a8 c #3e66b2", +".#z c #3e66b3", +".a0 c #3e67ad", +".#G c #3e67b2", +".lx c #3e68af", +".lw c #3e68b0", +".m9 c #3e68b1", +".#D c #3e68b2", +".#E c #3e68b3", +".ly c #3e69b0", +".ln c #3e69c1", +".Af c #3e6ab4", +".Dp c #3e6cab", +"#CZ c #3f0200", +"aa3 c #3f0b0d", +"#ag c #3f0f01", +"#A9 c #3f1823", +"axY c #3f2b25", +".8K c #3f3147", +".63 c #3f3333", +"#G7 c #3f3744", +".Ka c #3f3b64", +".un c #3f4268", +".g3 c #3f63af", +".jX c #3f64b1", +".jY c #3f65b0", +".a2 c #3f66ad", +".#o c #3f66ae", +".cp c #3f66b3", +".#n c #3f67ad", +".#y c #3f67b3", +".#x c #3f67b4", +".#H c #3f68b3", +".lv c #3f69b0", +".m6 c #3f69b1", +".lq c #3f69b2", +".#I c #3f69b3", +".lu c #3f6ab1", +".wN c #3f6bae", +".E4 c #3f6dab", +".Uz c #401615", +"#HC c #401a14", +"aym c #403541", +".5s c #40374b", +".dY c #4061a7", +".d2 c #4061ae", +".#d c #4062b0", +".fJ c #4065b3", +".a4 c #4067ae", +".cy c #4067af", +".#p c #4067b0", +".#k c #4067b4", +".#m c #4068ae", +".cI c #4068b3", +".a7 c #4068b4", +".#w c #4068b5", +".cu c #4069a9", +".jS c #4069af", +".wU c #4069b0", +".eg c #4069b3", +".cH c #4069b4", +".jT c #406aaf", +".iy c #406ab0", +".os c #406ab1", +".m5 c #406ab2", +".ls c #406ab3", +".eh c #406ab4", +".fI c #406ab5", +".or c #406bb2", +".lp c #406bb3", +".yA c #406caf", +".pO c #406cbe", +"aNQ c #410003", +"#UN c #410900", +"#bT c #410d01", +".6C c #411912", +".wB c #41281c", +".wm c #412b13", +"aNA c #41313f", +"#Co c #413647", +".3u c #414e6d", +".#b c #4165af", +".#L c #4166b2", +".ba c #4166b3", +".#K c #4166b4", +".#t c #4167b5", +".a3 c #4168ae", +".cx c #4168af", +".cz c #4168b1", +".#q c #4168b3", +".#v c #4168b5", +".d9 c #4169b0", +".ef c #4169b3", +".cG c #4169b4", +".a6 c #4169b5", +".#u c #4169b6", +".#. c #416aab", +".jU c #416aaf", +".iz c #416ab0", +".yD c #416ab1", +".ha c #416ab2", +".fH c #416ab3", +".ee c #416ab4", +".cF c #416ab5", +".aU c #416ab7", +".lz c #416bb2", +".pP c #416bb3", +".lr c #416bb4", +".hb c #416bb5", +".lC c #416bb6", +"Qt8 c #416caa", +".m7 c #416cb3", +".ou c #416cb4", +".aY c #416eb3", +".Do c #416fb0", +"aNN c #420004", +"#Hg c #420c17", +"#0W c #420d05", +"#Fz c #420d1a", +".59 c #420e00", +".7D c #420e02", +".ON c #42130a", +"aNq c #421320", +".OA c #42160a", +"aKQ c #421714", +".ym c #422604", +".x9 c #422b32", +".qB c #42332b", +".pj c #42352c", +"##5 c #423643", +"arY c #424245", +"#AY c #424559", +".d1 c #4261b1", +".jZ c #4267b1", +".cM c #4267b2", +".cL c #4267b3", +".fK c #4267b4", +".b# c #4267b5", +".a5 c #4268b3", +".#s c #4268b5", +".#l c #4268b6", +".cw c #4269b0", +".e. c #4269b2", +".cA c #4269b4", +".#r c #4269b5", +".cC c #4269b6", +".d8 c #426ab0", +".wW c #426ab2", +".fG c #426ab3", +".ed c #426ab4", +".cE c #426ab5", +".cD c #426ab6", +".wS c #426bb1", +".wV c #426bb2", +".fF c #426bb3", +".m4 c #426bb4", +".ec c #426bb5", +".lD c #426bb6", +".yE c #426cb2", +".lA c #426cb3", +".m8 c #426cb4", +".lt c #426cb5", +".hc c #426cb6", +".ov c #426cb7", +".lm c #426cc2", +".Dq c #426daf", +".wX c #426db2", +".ot c #426db4", +".sw c #426db5", +".E3 c #426fae", +"#9P c #430103", +"#OD c #430513", +"aLx c #431622", +".6B c #431d14", +".rW c #432215", +".eR c #432409", +".7A c #432418", +"ape c #432c26", +".bX c #432e35", +".tf c #433121", +".Bf c #433624", +"aLg c #43363d", +"aMg c #433940", +"#r4 c #433a3f", +".qj c #433a44", +"aGJ c #433b39", +"ado c #433f48", +"awd c #434047", +".ej c #4368b2", +".ei c #4368b3", +".hd c #4368b4", +".cK c #4368b5", +".#J c #4368b6", +".yw c #4369ae", +".iA c #4369b2", +".eb c #4369b6", +".fA c #436ab2", +".fB c #436ab3", +".e# c #436ab5", +".cB c #436ab6", +".h# c #436ab7", +".d5 c #436bac", +".d7 c #436bb1", +".yF c #436bb3", +".lo c #436bb5", +".yB c #436cb0", +".wR c #436cb2", +".wT c #436cb3", +".yG c #436cb4", +".jV c #436cb7", +".lB c #436db6", +".lE c #436db7", +".vk c #436db8", +".wY c #436eb4", +".E5 c #436fb0", +".BS c #4370b1", +".Ah c #4370b2", +".tV c #4370ba", +".wM c #4372b9", +"#Zl c #440a00", +"#IN c #440a12", +".OI c #44140c", +".9n c #44180a", +".44 c #441c0f", +".qW c #443124", +"aLh c #44373f", +"#bG c #443946", +".rG c #443a47", +".s3 c #443a49", +"ast c #444046", +"ay7 c #44496c", +".aP c #44629b", +".aQ c #4464a2", +".Dn c #4468ab", +".yC c #4469b1", +".cN c #4469b3", +".#M c #4469b4", +".fL c #4469b5", +".b. c #4469b7", +".j0 c #446ab1", +".iB c #446ab2", +".he c #446ab3", +".ea c #446ab6", +".fE c #446ab7", +".jR c #446ab8", +".fz c #446bb2", +".g6 c #446bb3", +".g7 c #446bb4", +".fC c #446bb6", +".h. c #446bb8", +".yH c #446cb4", +".tU c #446daf", +".wQ c #446db3", +".Aj c #446db4", +".BX c #446db5", +".ra c #446db8", +".tW c #446eb6", +".pQ c #446eb7", +".lF c #446eb8", +".n. c #446eb9", +".yx c #446eba", +".aZ c #446fb4", +".yI c #446fb5", +".vi c #4471b9", +".yz c #4472b9", +"#ay c #450000", +"aNP c #450003", +"#M9 c #450109", +"#I9 c #451200", +".OO c #45150d", +"aFf c #451528", +".9Z c #451809", +"#HD c #451a09", +".41 c #451b02", +"#6O c #451c01", +"#FX c #451e0c", +"aK# c #45353d", +".vQ c #45374b", +".Bw c #453822", +"aNB c #453845", +".d0 c #4562b5", +"Qt4 c #45649e", +"Qt6 c #45659e", +".jW c #4569b7", +".iE c #456baf", +".iD c #456bb1", +".iC c #456bb2", +".fM c #456bb3", +".fD c #456bb7", +".ix c #456bb8", +".j1 c #456caf", +".Jz c #456cb2", +".fy c #456cb3", +".it c #456cb4", +".iu c #456cb5", +".g8 c #456cb7", +".g9 c #456cb8", +".jQ c #456cb9", +".co c #456cba", +".E2 c #456db0", +".wO c #456db1", +".g5 c #456db3", +".Dr c #456db4", +".Ai c #456eb2", +".BW c #456eb4", +".Ak c #456eb5", +".Al c #456eb6", +".BT c #456fb2", +".wZ c #456fb5", +".lG c #456fb9", +".w0 c #4570b5", +".m3 c #4571c8", +".GH c #4573af", +".9O c #460500", +".6. c #461200", +"#3D c #461803", +"#zJ c #462b34", +".zM c #46342b", +".BG c #463730", +".nV c #463b31", +"#ZK c #465470", +".ci c #465580", +".aL c #465b8c", +".dZ c #4662b7", +".E1 c #4667a7", +".aR c #4669ac", +".fw c #4669b2", +".#c c #4669b5", +".hf c #466cb3", +".ek c #466cb4", +".bb c #466cb5", +".hh c #466db0", +".BU c #466db4", +".jN c #466db5", +".jO c #466db6", +".iv c #466db7", +".jP c #466db8", +".iw c #466db9", +".m1 c #466dbc", +".g4 c #466eb4", +".Am c #466eb6", +".BV c #466fb5", +".BY c #466fb6", +".BZ c #466fb7", +".r. c #466fb8", +".yJ c #4670b6", +".w6 c #4670b8", +".ow c #4670ba", +".wK c #4670be", +".w1 c #4671b6", +".w5 c #4671b8", +".sv c #4673bf", +".BR c #4674b7", +".If c #4675b1", +"#Hz c #470b00", +".OH c #47170f", +".40 c #471901", +".90 c #47190a", +".8a c #47231c", +".5X c #473131", +".di c #473a56", +"aJV c #474047", +"#bm c #47435c", +"ao6 c #47518e", +"arx c #475680", +".aI c #475b8c", +".aF c #475b8d", +".ck c #47659f", +".fv c #4766b1", +".Ac c #4769a3", +".q9 c #476aa9", +".d3 c #476ab3", +".jJ c #476bb0", +".wP c #476bb4", +".fx c #476cb5", +".iF c #476db1", +".hg c #476db3", +".BQ c #476db4", +".ir c #476dba", +".j2 c #476eb2", +".jM c #476eb5", +".is c #476fb5", +".Ae c #476fb9", +".cv c #4770af", +".Du c #4770b6", +".Dv c #4770b7", +".Dx c #4770b8", +".tX c #4770bb", +".yL c #4771b7", +".w4 c #4771b8", +".pR c #4771bb", +".yK c #4772b7", +".Dy c #4772b8", +".w3 c #4772b9", +".cn c #4772bd", +".GG c #4773b1", +".GI c #4774b4", +".Ag c #4775ba", +"a#s c #480209", +".57 c #480400", +"#KC c #480700", +"#Fl c #484453", +".li c #4866a7", +"Qt5 c #4867a1", +".g2 c #4868b0", +".st c #486aa4", +".iq c #486bb5", +".yv c #486ca9", +".wJ c #486eac", +".hi c #486eb2", +".fN c #486eb5", +".cO c #486eb6", +".iG c #486fb4", +".jL c #4870b6", +".Dw c #4870b8", +".Ds c #4871b7", +".Dt c #4871b8", +".GN c #4871b9", +".w2 c #4872b8", +".yO c #4872ba", +".rb c #4872bc", +".tY c #4872bd", +".yM c #4873b8", +".An c #4873b9", +"aNL c #490010", +"#E. c #49131d", +".OG c #491910", +".6H c #492f4b", +"#xX c #493d47", +".vJ c #493f3c", +"aPI c #496190", +".Ad c #496daf", +".#N c #496eb8", +".j3 c #496fb5", +".iH c #496fb6", +".K4 c #4970b4", +".hj c #4970b5", +".Fa c #4971b9", +".E9 c #4972b8", +".E8 c #4972b9", +".F# c #4972ba", +".Dz c #4973b9", +".w7 c #4973ba", +".Ap c #4973bb", +".sx c #4973bd", +".Fb c #4974b9", +".yN c #4974ba", +".yP c #4974bb", +".Ie c #4975b2", +"#Ke c #4a0a0f", +".6# c #4a1500", +"#J# c #4a1603", +"#0P c #4a160e", +".OB c #4a1e12", +".xW c #4a3519", +".dv c #4a3542", +".Y3 c #4a415a", +"#tw c #4a4348", +"aAI c #4a4852", +".yu c #4a5e7f", +".tS c #4a6085", +".dW c #4a639d", +".Ic c #4a69a5", +".vg c #4a6a9d", +".GE c #4a6aa7", +".hk c #4a70b6", +".iI c #4a70b7", +".hl c #4a70b8", +".hm c #4a70b9", +".JS c #4a71b8", +".K6 c #4a71b9", +".Lc c #4a72b8", +".MA c #4a72b9", +".GM c #4a72ba", +".E6 c #4a73b9", +".F. c #4a73bb", +".JK c #4a74b8", +".Ao c #4a74ba", +".Aq c #4a74bb", +".tZ c #4a74be", +".Fc c #4a75ba", +".Fe c #4a75bb", +".w8 c #4a75bc", +"aNM c #4b020d", +"#SX c #4b0617", +"#.0 c #4b0c00", +"#3C c #4b1804", +"#CD c #4b1b25", +".OP c #4b1c12", +"#1o c #4b2741", +".gk c #4b2c11", +"#G2 c #4b3a44", +".uM c #4b3e35", +".o1 c #4b424a", +"aNF c #4b424e", +"aAw c #4b4549", +"ax. c #4b494f", +".Dm c #4b69a3", +".m0 c #4b6eb7", +".iJ c #4b70ba", +".fP c #4b71b5", +".fO c #4b71b8", +".bc c #4b71b9", +".JB c #4b71ba", +".Mp c #4b72b8", +".Mn c #4b73b5", +".JR c #4b73ba", +".GL c #4b73bb", +".vh c #4b74b5", +".JL c #4b74b9", +".E7 c #4b74ba", +".GJ c #4b74bb", +".JJ c #4b75b8", +".My c #4b75b9", +".Ff c #4b75bb", +".B0 c #4b75bc", +".yQ c #4b75bd", +".Fd c #4b76bb", +".B1 c #4b76bd", +"#.2 c #4c0a00", +"#I8 c #4c1100", +"#XY c #4c1200", +"aNr c #4c1826", +"#2f c #4c1913", +"#.R c #4c1c08", +".9d c #4c2a1d", +"#FV c #4c2a2e", +".W4 c #4c2e1f", +".u3 c #4c3231", +".pa c #4c3720", +".zC c #4c3a25", +".bM c #4c3d3a", +".8J c #4c414a", +".nD c #4c4449", +".pZ c #4c4856", +"aej c #4c4a51", +"a.Q c #4c5a7e", +".aG c #4c6192", +".aN c #4c679b", +".fu c #4c67b3", +".q8 c #4c689a", +".BP c #4c6dac", +".Jy c #4c6fb5", +".d4 c #4c71b6", +".el c #4c71b9", +".hn c #4c71bc", +".jK c #4c71bd", +".GF c #4c72b3", +".fQ c #4c72b6", +".j4 c #4c72bb", +".NF c #4c73b3", +".JT c #4c73ba", +".NT c #4c74ba", +".JV c #4c74bb", +".JI c #4c75b8", +".JM c #4c75b9", +".JN c #4c75ba", +".GK c #4c75bb", +".JF c #4c76b7", +".JE c #4c76b8", +".JH c #4c76b9", +".JO c #4c76ba", +".GO c #4c76bc", +".w9 c #4c76bd", +".DA c #4c77be", +"#ax c #4d0700", +"#Bs c #4d0d00", +"#KB c #4d0e00", +".4T c #4d0f00", +"#J. c #4d1c0c", +"#0x c #4d2004", +".Qw c #4d2113", +".ma c #4d4549", +".AD c #4d475b", +".fl c #4d4c55", +".jq c #4d4e45", +".gZ c #4d5e9a", +".ip c #4d6dae", +".fT c #4d72ba", +".#V c #4d72bc", +".Id c #4d73b3", +".fR c #4d73b9", +".fS c #4d73ba", +".#W c #4d73be", +".Mz c #4d74bb", +".NH c #4d75b8", +".JU c #4d75bb", +".JQ c #4d75bc", +".ll c #4d75c6", +".L# c #4d76b8", +".JG c #4d76b9", +".JP c #4d76ba", +".Mx c #4d76bb", +".JD c #4d77b8", +".L. c #4d77b9", +".Lb c #4d77ba", +".NS c #4d77bb", +".Fg c #4d77be", +"#Xd c #4e0912", +"aNz c #4e1721", +".OF c #4e1e16", +"QtL c #4e2400", +".7C c #4e2419", +".OY c #4e241f", +".wA c #4e2f0b", +".jl c #4e3f41", +"aBU c #4e4a4e", +".av c #4e4a6d", +".sR c #4e4c68", +".mm c #4e4e65", +".fp c #4e588f", +"avp c #4e5c7e", +".fr c #4e60af", +".pM c #4e6ca7", +".pN c #4e73b8", +".#O c #4e73bc", +".fU c #4e73be", +".#U c #4e74bc", +".iK c #4e74be", +".NU c #4e75bc", +".Pd c #4e76b3", +".NV c #4e76bc", +".K9 c #4e77b9", +".La c #4e77ba", +".Mw c #4e77bc", +".K8 c #4e78b9", +".Mr c #4e78ba", +".Mu c #4e78bb", +".Mv c #4e78bc", +".GP c #4e78bf", +".GQ c #4e79c0", +"#Z6 c #4f031d", +"#Uj c #4f0b1c", +"#aq c #4f1f07", +"a.# c #4f2513", +"#6E c #4f2615", +".ky c #4f2813", +"#.T c #4f2814", +".wy c #4f300c", +".6G c #4f3148", +".TY c #4f4359", +"aoE c #4f444a", +".dP c #4f4458", +".yT c #4f4965", +"aGR c #4f608b", +".E0 c #4f6aa1", +".aO c #4f6ba1", +".dX c #4f6daf", +"Qt1 c #4f6fa8", +".on c #4f70b3", +".bj c #4f75be", +".bk c #4f75bf", +".QY c #4f76bd", +".Pp c #4f77bd", +".Mt c #4f78bb", +".NR c #4f78bc", +".NQ c #4f78bd", +".Mq c #4f79ba", +".NM c #4f79bb", +".Ms c #4f79bc", +".Pm c #4f79bd", +".op c #4f79ca", +"#9O c #500409", +"#.Z c #501200", +"#3E c #501500", +"#.L c #50291e", +".mb c #502a14", +".7B c #502e23", +"#DQ c #503d36", +"aHl c #503f46", +".oa c #504330", +".81 c #504451", +"#x0 c #504665", +"aEz c #50494e", +"ap6 c #504c52", +"ai7 c #504d54", +"#Yi c #505b79", +".gY c #505e96", +".aH c #506596", +".cj c #506699", +".Jx c #506dae", +".ep c #5075bf", +".bl c #5075c0", +".K7 c #5075c1", +".bd c #5076bd", +".#P c #5076be", +".Pf c #5077b8", +".em c #5077ba", +".QX c #5077be", +".Pq c #5078be", +".NG c #5079b9", +".NP c #5079bc", +".QR c #5079bd", +".Pn c #5079be", +".NL c #507abb", +".NK c #507abc", +".NO c #507abd", +".Po c #507abe", +".Ig c #507ac1", +".5d c #507dbb", +"#XQ c #510900", +"#.1 c #511000", +".9v c #511305", +"acy c #512320", +".OC c #512519", +".dm c #513217", +"aPM c #513327", +".BB c #513d28", +".Bx c #51442d", +"#DV c #514456", +".5. c #515d7b", +".aM c #516596", +".fs c #5165b3", +".ft c #5167b5", +"Qt7 c #5171aa", +".K3 c #5174b8", +".eq c #5176c0", +".#T c #5177bd", +".#Q c #5177be", +".cU c #5177c0", +".cV c #5177c1", +".#R c #5178bb", +".en c #5178bc", +".QZ c #5178bf", +".QK c #5179b7", +".So c #5179bf", +".TV c #5179c0", +".Pe c #517ab7", +".VF c #517aba", +".NJ c #517abc", +".NN c #517abd", +".QS c #517abe", +".QT c #517abf", +".Pj c #517bbc", +".Pi c #517bbd", +".Pl c #517bbe", +".Sm c #517bbf", +"#L4 c #520300", +".73 c #52130a", +"#.Q c #521d09", +".6a c #521e07", +"#jG c #522512", +".6D c #522928", +".ai c #523318", +".jA c #524140", +"aK. c #52424a", +".uN c #52473d", +".vw c #524756", +"aGZ c #52484c", +"#c8 c #524a64", +".85 c #524b5b", +".sO c #52506c", +"#3V c #525f79", +"#9# c #52648b", +".mX c #526ba1", +".io c #526ca3", +".K2 c #526fb0", +".mZ c #5272b4", +".Vl c #5275b6", +".cl c #5276b8", +".er c #5277c2", +".#S c #5278bd", +".eo c #5278be", +".be c #5278bf", +".cT c #5278c0", +".cW c #5278c2", +".JA c #5279bf", +".Sn c #5279c0", +".VE c #527abb", +".Mo c #527abd", +".K5 c #527abf", +".Q0 c #527ac0", +".Sp c #527ac1", +".VD c #527bbb", +".Pk c #527bbe", +".QU c #527bbf", +".QV c #527bc0", +".QJ c #527cb7", +".Ph c #527cbd", +".QP c #527cbe", +".QQ c #527cbf", +".QW c #527cc0", +"#P6 c #530a1a", +"#Et c #531705", +"#0X c #53180d", +"#0O c #531d13", +".nF c #532d15", +".i4 c #533419", +"axn c #533a30", +".xw c #534846", +".7f c #534855", +"arg c #535256", +"#Cq c #535666", +".mY c #536eaa", +".lj c #5375bb", +".JC c #5377c4", +".bf c #5379bd", +".bi c #5379bf", +".cP c #537abd", +".TT c #537ac1", +".VC c #537bbc", +".TU c #537bc2", +".TK c #537cb3", +".QI c #537cb6", +".Sg c #537cb8", +".Vw c #537cbb", +".TS c #537cc0", +".TR c #537cc1", +".TL c #537db4", +".Sf c #537db6", +".QO c #537dbe", +".QN c #537dbf", +".TQ c #537dc1", +".6O c #5380bf", +".5e c #5383c1", +"#8d c #540007", +".OE c #54271c", +".OD c #54281c", +".Zu c #543424", +".Tz c #544040", +".zQ c #544233", +".xa c #544d65", +".ak c #54567c", +"axR c #54668b", +".in c #546995", +".pL c #546c9b", +".ol c #546c9d", +".lh c #5470ac", +".W9 c #5477b8", +".Mm c #5477ba", +".X# c #5478b3", +".bg c #547abe", +".bh c #547abf", +".cS c #547ac0", +".QL c #547bbd", +".cQ c #547bbf", +".TW c #547bc2", +".Xh c #547cbb", +".VB c #547cbc", +".Sq c #547cc3", +".Se c #547db6", +".Vv c #547dbc", +".VA c #547dbd", +".Sl c #547dbf", +".QM c #547ebf", +".Sk c #547ec0", +"#X3 c #551506", +"#XX c #551904", +".9t c #551b0c", +"aLy c #552330", +".OQ c #55261b", +"#ah c #55281f", +"aLN c #553d4e", +"aIL c #55434c", +"ahR c #554843", +"aLf c #55484f", +".Hf c #554a4c", +".dM c #554b5f", +"#dk c #554e5e", +".sN c #555758", +".lg c #5570aa", +".Ml c #5572b0", +".TI c #5574a7", +".NE c #5578b8", +".Vn c #557ab4", +".NI c #557ac2", +".cR c #557bc0", +".Sh c #557cbc", +".Vz c #557dbe", +".VH c #557dbf", +".Vs c #557ebb", +".Xg c #557ebc", +".Vu c #557ebd", +".Vy c #557ebe", +".VG c #557ebf", +".Sj c #557ec0", +".Xd c #557fbc", +".Si c #557fc0", +".TP c #557fc1", +".8o c #5587bc", +"aNw c #560a13", +"#LF c #560b0f", +"aNy c #560b1b", +".74 c #56140b", +".9u c #561a0b", +".55 c #561d0c", +"#5l c #56200c", +"#.S c #562d18", +"aeo c #563726", +"aNu c #56392a", +".XU c #563b2c", +".ar c #564341", +"QtN c #56443c", +".zX c #564628", +".fe c #56464d", +".DU c #564c80", +"aAv c #564f53", +".Cl c #565165", +"aB8 c #565561", +"agn c #566790", +".wI c #566b8e", +".g0 c #566cac", +".GD c #5670a3", +".g1 c #5671b6", +".ND c #5674af", +".TJ c #567bb2", +".0h c #567cb9", +".Pg c #567cc0", +".TX c #567dc4", +".Xb c #567eb5", +".0o c #567ebd", +".Xj c #567ebe", +".Xi c #567ebf", +".VI c #567ec0", +".TM c #567fb9", +".Vr c #567fbc", +".Vt c #567fbd", +".Vx c #567fbe", +".TO c #567fc1", +".0j c #5681ba", +".8n c #5689bb", +"##k c #571002", +".9Q c #571105", +"#X4 c #571401", +"#XZ c #571f0d", +".dt c #572806", +"#FU c #572b27", +".o3 c #573116", +".8# c #57342c", +".zP c #574349", +".zL c #574636", +"aoD c #574c51", +"amk c #574c58", +"#7g c #575359", +".oC c #575369", +".Pb c #5775ae", +".Sc c #5776aa", +".tT c #5778ad", +".0f c #577aba", +".Pc c #577bb8", +".Vm c #577bba", +".X. c #577cbb", +".0g c #577cbc", +".5c c #577dbe", +".Vp c #577eb4", +".YN c #577fbe", +".Xk c #577fc1", +".0i c #5780ba", +".Vq c #5780bd", +".Xc c #5780be", +".Xf c #5780bf", +".VJ c #5780c2", +".0m c #5781be", +".1W c #5784bf", +".8l c #5787bf", +"#YH c #58081a", +"##l c #580d00", +"#WG c #581000", +"#Zv c #581502", +"#3e c #58371d", +"aOQ c #583b2f", +"aF5 c #58484f", +".i2 c #584946", +".0C c #584d5b", +"aFV c #584f55", +".kw c #585053", +".nO c #585151", +"aJW c #585158", +"avb c #58565c", +".q7 c #586b90", +".QG c #5876ad", +".1S c #587bba", +".YH c #587db9", +".1T c #587dbd", +".Xa c #587fb6", +".TN c #587fbe", +".YO c #5880c0", +".YP c #5880c1", +".Xl c #5880c2", +".Xe c #5881be", +".YK c #5881bf", +".YM c #5881c0", +".YQ c #5881c1", +".0r c #5881c3", +"a#r c #59040f", +"#XR c #590e00", +"#av c #591400", +".9M c #591803", +"#gx c #591a16", +".9Y c #592e1e", +".9j c #592f1d", +".0# c #594534", +"aLJ c #594555", +"aCo c #59464d", +".D# c #594b3e", +".gB c #59545e", +"ae5 c #59678f", +".K1 c #5971a8", +".om c #5975af", +".Vk c #597ab4", +".Sd c #597eb6", +".1Z c #5981c0", +".YR c #5981c2", +".YT c #5981c3", +".0k c #5982bf", +".0l c #5982c0", +".YL c #5982c1", +".YS c #5982c2", +".VK c #5982c3", +".0s c #5982c4", +".3B c #5988c4", +"#8# c #5a0009", +".9R c #5a1205", +"#au c #5a1501", +".56 c #5a1b0c", +"#3B c #5a200c", +".6b c #5a260f", +"aMu c #5a2635", +"#2. c #5a2a24", +"#k8 c #5a2f10", +".bO c #5a3b20", +"#Is c #5a4753", +".mt c #5a5147", +".Ib c #5a72a4", +".Mk c #5a72a7", +".Jw c #5a72aa", +".W8 c #5a7ab4", +".5b c #5a7cbb", +".QH c #5a7eb8", +".15 c #5a82c2", +".0p c #5a82c3", +".YU c #5a82c4", +".1X c #5a83c0", +".3F c #5a83c1", +".0n c #5a83c2", +".0q c #5a83c3", +".0t c #5a83c4", +".VL c #5a83c5", +".1V c #5a84bf", +".1Y c #5a84c1", +".6P c #5a8ac9", +"#Wn c #5b1000", +"aI3 c #5b1814", +"#Eu c #5b1e06", +".72 c #5b2611", +"#5m c #5b2915", +".ap c #5b3000", +".7y c #5b3323", +".ql c #5b3618", +"aor c #5b4139", +".x7 c #5b4542", +".lK c #5b577a", +"#vh c #5b5e68", +".lf c #5b6f7b", +".mW c #5b72a6", +".ss c #5b739e", +".NC c #5b74a7", +".YF c #5b7ebe", +".Vo c #5b81b8", +".3y c #5b81c1", +".YI c #5b82bb", +".1U c #5b83c0", +".3G c #5b83c2", +".3M c #5b83c3", +".14 c #5b83c4", +".3N c #5b83c5", +".YJ c #5b84bc", +".3C c #5b84c1", +".10 c #5b84c2", +".3E c #5b84c3", +".13 c #5b84c4", +".3D c #5b85c2", +".3A c #5b87c3", +".8m c #5b8ec1", +"#R0 c #5c0d00", +"aGV c #5c4032", +".zN c #5c4847", +".zR c #5c4a3a", +"#eT c #5c535a", +"amG c #5c595d", +".p6 c #5c5b77", +".jp c #5c5d55", +".Pa c #5c75a5", +".QF c #5c76a3", +".3x c #5c7fbe", +".YG c #5c81c0", +".5h c #5c84c3", +".5k c #5c84c4", +".12 c #5c84c5", +".16 c #5c84c6", +".5f c #5c85c2", +".3H c #5c85c3", +".3J c #5c85c4", +".11 c #5c85c5", +".5l c #5c85c6", +".6V c #5c85c7", +".6N c #5c86c5", +"#OE c #5d0812", +"#24 c #5d0a00", +"#az c #5d1703", +"#.Y c #5d200e", +"#1Z c #5d2700", +"#ao c #5d2c13", +".1M c #5d4539", +".3U c #5d5265", +".DT c #5d5483", +".rt c #5d5c7d", +".gX c #5d69a0", +".wH c #5d708f", +".Sb c #5d77a2", +".8p c #5d85c1", +".3I c #5d85c4", +".3L c #5d85c6", +".17 c #5d85c7", +".6Q c #5d86c3", +".5i c #5d86c4", +".5j c #5d86c5", +".3K c #5d86c6", +".9P c #5e1a10", +".Vd c #5e3325", +"#8w c #5e3822", +".dk c #5e4e4b", +".gi c #5e4f4c", +".vZ c #5e5254", +".vs c #5e576c", +"aAu c #5e595b", +"aay c #5e6c8e", +"aca c #5e749b", +".TH c #5e79a3", +".5g c #5e86c5", +".6T c #5e86c6", +".6U c #5e86c7", +".18 c #5e86c8", +".6S c #5e87c6", +".5m c #5e87c9", +".99 c #5e8abf", +"#ht c #5e91d1", +"aNx c #5f0517", +"#Uk c #5f0c1c", +"#X2 c #5f2115", +".9s c #5f2616", +"aNv c #5f312b", +".rI c #5f3a19", +".2J c #5f3e3c", +".S0 c #5f4139", +".hK c #5f504d", +"acW c #5f545a", +".oE c #5f5b71", +"aIn c #5f5d5f", +"#bd c #5f7fbf", +".8G c #5f85c5", +".8t c #5f87c3", +".3O c #5f87c9", +".3z c #5f88c6", +"#.. c #5f8bc0", +"##m c #601202", +"#XS c #601401", +"##i c #601905", +"#Zk c #602312", +".3k c #603016", +".OR c #603225", +"#49 c #603415", +"awZ c #60463c", +".u5 c #604a2c", +"#qF c #60504b", +".mK c #605545", +".k6 c #605647", +"#WN c #606986", +".fq c #606ca8", +".0e c #607eb7", +".8F c #6085c5", +".8u c #6086c4", +".8E c #6086c5", +"#.f c #6086c6", +"#.d c #6087c4", +".6M c #6087c7", +".8q c #6088c4", +".6R c #6089c7", +".3P c #6089ca", +"#Rx c #61091a", +"aKo c #611d08", +"#3F c #612209", +"#5k c #612411", +".OX c #61342e", +".8c c #613b34", +".tL c #614b3c", +".eP c #61524f", +"#dh c #615453", +".EK c #615953", +".gW c #615b84", +".wG c #616d85", +".YE c #6180b9", +".8k c #6186bf", +".8v c #6186c5", +".8D c #6186c6", +".8w c #6187c5", +"#.e c #6187c6", +".8C c #6187c7", +"#.a c #6188c4", +".8r c #6189c5", +"#hs c #618eca", +"#vR c #621400", +".6E c #623c42", +".s5 c #623d1b", +"aHF c #624554", +".wn c #624d33", +"aH4 c #625251", +".gG c #625254", +".Y0 c #626078", +".W7 c #627baa", +".Vj c #627cab", +"##D c #6282be", +".8x c #6287c6", +".8B c #6287c7", +".8y c #6288c6", +".8A c #6288c7", +".8z c #6288c8", +".8s c #6289c5", +"#.b c #6289c6", +"#.c c #628ac6", +"#rD c #628cc6", +"#X5 c #631d05", +"#X0 c #632b1a", +"#FT c #632c18", +"#ap c #63361c", +".8d c #633c36", +".uu c #633f1b", +".va c #634437", +"aJ8 c #63535c", +".8N c #635763", +"aDj c #635d62", +".AN c #635e76", +".pK c #63759a", +"asP c #63799c", +"##E c #6386bf", +"#bj c #6388c7", +"#.g c #6388c8", +"##J c #6389c7", +"##I c #6389c8", +"##K c #6389c9", +"#nu c #638aca", +"##H c #638bc7", +"#fW c #638bc8", +"#rC c #6391cf", +"#fX c #6391d2", +"#s1 c #6393d2", +"#9N c #640711", +"##n c #641603", +"#Zj c #642715", +"#a8 c #642722", +".45 c #643c34", +"#4Y c #643d1a", +".05 c #643f2e", +"aMK c #645160", +"#Iu c #645360", +".2b c #645b6b", +"#IA c #645d6a", +".2k c #645d7b", +"arN c #646066", +"aDo c #64636c", +".ru c #646384", +"axh c #64769c", +"#ba c #647cb9", +"#em c #6485c3", +".6L c #6486c4", +"#bi c #6489c8", +"#bl c #6489c9", +"#bh c #648ac8", +"#cY c #648ac9", +"#bk c #648aca", +"#rG c #648bc6", +"#be c #648bc7", +"#bf c #648cc8", +".98 c #648ec7", +"#N. c #650c0f", +"#KD c #65210f", +"#.3 c #652212", +"#XW c #652710", +".3i c #652712", +".9X c #653a2a", +".9e c #65462d", +".jn c #65504e", +"aj3 c #655a56", +".y1 c #655b7a", +"#5B c #656168", +".Fu c #65638a", +"#7o c #6579a6", +"aiH c #657da8", +".1R c #6582ba", +"#cU c #658ac9", +"#f1 c #658aca", +"#cV c #658bc9", +"#cX c #658bca", +"#cW c #658bcb", +"#bg c #658cc8", +"#cR c #658dc9", +"#kA c #658dce", +"#GG c #6593c4", +"a#q c #660714", +"#WF c #662209", +"#CY c #662c18", +".vS c #664531", +".6I c #66728f", +".im c #667696", +"aOM c #667fad", +".5a c #6681b7", +"#hq c #668abd", +"##G c #668bc7", +"#jb c #668bc8", +"#jc c #668bc9", +"#jd c #668bca", +"#f2 c #668bcb", +"apV c #668cc4", +"#er c #668cca", +"#et c #668ccb", +"#eu c #668ccc", +"#cS c #668dca", +"#oV c #668dcb", +"aoo c #668ec6", +"#cT c #668eca", +"#s0 c #6690cc", +"#38 c #6691d0", +"#8. c #670209", +"#IO c #670b16", +"#VO c #670f1e", +"#Wm c #671f08", +".75 c #67231a", +"#2g c #672d25", +"aN0 c #67493d", +"aJX c #676067", +".uV c #676250", +"aBT c #676267", +"#G6 c #676271", +"aq5 c #676368", +"aFu c #6778a3", +".0d c #677eaa", +"#ky c #6788c3", +"#Ol c #678ac8", +"#rE c #678bc1", +"#ja c #678bc8", +"#kF c #678bc9", +"#je c #678bca", +"#j# c #678cc9", +"#jf c #678cca", +"#es c #678ccb", +"#ev c #678ccc", +"#hw c #678dcb", +"#f3 c #678dcd", +"#eq c #678eca", +"#qd c #678fca", +"#eo c #678fcb", +"anc c #6790ca", +"and c #6791c8", +"#qc c #6791ce", +"#s2 c #6792cc", +"#.# c #6794cc", +"#Hh c #680d1b", +".3f c #682612", +"#e3 c #682b27", +"afX c #683115", +".3j c #68311a", +"aNs c #683326", +".UB c #68362e", +"#3o c #683a1a", +".3r c #684c48", +"aKa c #685760", +".bL c #685962", +".AT c #685c67", +".YD c #6880ae", +"#cP c #6884c3", +"#fU c #6887bd", +"#2A c #6888bc", +"#iX c #6889c5", +"#MS c #688bc9", +"#rF c #688cc4", +"#j. c #688cc9", +"#kz c #688cca", +"#jg c #688ccb", +"#i5 c #688dc7", +"#kE c #688dca", +"#l6 c #688dcb", +"#hv c #688dcc", +"#hx c #688dcd", +"aom c #688ec6", +"#kC c #688ec8", +"al3 c #688fca", +"#ep c #688fcb", +"#GC c #688fce", +"#fY c #6890cc", +"al4 c #6891c8", +"#GF c #6892c5", +"#Im c #6894c7", +"#23 c #690800", +"#SY c #690b1b", +"#cJ c #692219", +"#62 c #692d19", +".9i c #693a28", +"aeH c #693e29", +".xF c #694832", +".ag c #695957", +".8L c #695a75", +".69 c #696160", +".AJ c #696283", +"au7 c #696668", +".xp c #696768", +"#bb c #6984c0", +"#kx c #6988c3", +"#cQ c #6989cb", +"#l0 c #698ac5", +"#GV c #698bc9", +"#GU c #698cc8", +"#GT c #698cc9", +"#Ca c #698cca", +"#zk c #698dc7", +"#AI c #698dc9", +"#i9 c #698dca", +"#DG c #698dcb", +"#l7 c #698dcc", +"#i2 c #698ec8", +"#i8 c #698ecb", +"#nv c #698ecc", +"al2 c #698ecd", +"#i1 c #698fc8", +"#oW c #698fc9", +"akZ c #698fca", +"#Cb c #698fce", +"ap# c #6990c9", +"#fZ c #6990cc", +"#f0 c #6990cd", +"ak0 c #6991c8", +"#hu c #6991cd", +"#iZ c #6991d4", +"#T5 c #6994c8", +"#SE c #6996c8", +"#FA c #6a0f20", +"#Xe c #6a1d20", +"#X9 c #6a270e", +"#X1 c #6a2d23", +".OS c #6a3c2e", +".bV c #6a3d13", +".jx c #6a4e4b", +"arH c #6a5149", +".tC c #6a5637", +"aMG c #6a5b69", +"#bJ c #6a6269", +"adL c #6a80b0", +".3w c #6a87bd", +"#Ym c #6a87d4", +"#iW c #6a88c3", +"#DM c #6a8bc9", +"#DN c #6a8bca", +"#uH c #6a8cc9", +"#Cg c #6a8cca", +"#xU c #6a8dc8", +"#GS c #6a8dc9", +"#GW c #6a8dca", +"#GY c #6a8dcb", +"#GN c #6a8ec5", +"#ut c #6a8ec6", +"##F c #6a8ec7", +"#wm c #6a8ec8", +"#nt c #6a8eca", +"#i7 c #6a8ecb", +"#kD c #6a8ecc", +"#iY c #6a8ecd", +"#i3 c #6a8fc9", +"#i6 c #6a8fcc", +"#oX c #6a8fcd", +"#qi c #6a8fce", +"#DH c #6a8fcf", +"#kB c #6a90c9", +"#l4 c #6a90ca", +"#AJ c #6a90cd", +"ap. c #6a91c9", +"#uu c #6a91ca", +"#Ii c #6a91cf", +"#l3 c #6a91d2", +"#Il c #6a93c7", +"#SF c #6a98cb", +"#4O c #6b0702", +"#Kf c #6b0c12", +"#WH c #6b2000", +"#XT c #6b270d", +"#3A c #6b2916", +"ac1 c #6b2b16", +"#19 c #6b3831", +".Zv c #6b422e", +".se c #6b593c", +"#by c #6b6483", +".ok c #6b7eaa", +"#1a c #6b86ab", +"#uI c #6b8cc9", +"#AO c #6b8cca", +"#AP c #6b8ccb", +"#fV c #6b8dc6", +"asS c #6b8dc7", +"#uG c #6b8dc9", +"#uL c #6b8dca", +"#zl c #6b8dcb", +"ajT c #6b8dcd", +"aqP c #6b8ec3", +"#nr c #6b8ec9", +"#GR c #6b8eca", +"#Cc c #6b8ecb", +"#GX c #6b8ecc", +"#Io c #6b8fc6", +"#GM c #6b8fc7", +"#wf c #6b8fc8", +"#uE c #6b8fc9", +"ajU c #6b8fca", +"#GQ c #6b8fcb", +"#rI c #6b8fcc", +"#rJ c #6b8fcd", +"#qj c #6b8fce", +"ajV c #6b90c8", +"#xO c #6b90c9", +"#i0 c #6b90ca", +"apW c #6b90cc", +"#rK c #6b90cd", +"#rL c #6b90ce", +"#Fa c #6b90d0", +"#T2 c #6b90d2", +"#hr c #6b91c9", +"#rB c #6b91cb", +"#zg c #6b91cd", +"#wg c #6b92cc", +"#MI c #6b94cc", +"#Ob c #6b95ca", +"#T6 c #6b97cb", +"#Ra c #6b97ce", +"#R# c #6b98cc", +"#Z7 c #6c081a", +"#25 c #6c0a02", +"#8c c #6c0a15", +"#Wo c #6c1f09", +"#Vc c #6c2000", +"#aw c #6c2712", +".4U c #6c2a19", +"#61 c #6c2d18", +"#5o c #6c2e14", +"#1p c #6c314e", +"#0E c #6c3512", +".54 c #6c3925", +".3m c #6c442b", +".6F c #6c4a59", +"aLO c #6c5163", +".C7 c #6c5e51", +"abr c #6c6166", +"aB7 c #6c6468", +"av9 c #6c686b", +"ap9 c #6c686e", +".vf c #6c81a3", +"#cO c #6c84c1", +"#el c #6c88c3", +"#Cd c #6c8cc7", +"#Ce c #6c8dc7", +"#DJ c #6c8dc8", +"#uJ c #6c8dca", +"#xV c #6c8dcb", +"#xW c #6c8dcc", +"aiN c #6c8dce", +"anb c #6c8dd0", +"#uF c #6c8eca", +"#uK c #6c8ecb", +"#uM c #6c8ecc", +"#ns c #6c8fc8", +"#zj c #6c8fca", +"#AK c #6c8fcb", +"#GP c #6c8fcc", +"#DI c #6c8fcd", +"#GL c #6c90c7", +"#GK c #6c90c8", +"aaN c #6c90c9", +"#wl c #6c90ca", +"#uD c #6c90cb", +"#Iq c #6c90cc", +"#rM c #6c90cf", +"#PG c #6c91c3", +"#uv c #6c91c8", +"#4l c #6c91c9", +"#i4 c #6c91cb", +"#oS c #6c91cc", +"aqQ c #6c92ca", +"#xP c #6c92cd", +"#JR c #6c92cf", +"#JT c #6c93ca", +"#qg c #6c93cf", +"#JQ c #6c93d0", +"#Vr c #6c94cb", +"#Lh c #6c94ce", +"#T4 c #6c95c9", +"#JU c #6c95ca", +"#MJ c #6c95cc", +"#Oc c #6c96cc", +"#1i c #6c96cd", +"#37 c #6c96d5", +"#SD c #6c97c8", +"#SG c #6c97cd", +"#PK c #6c98cd", +"#8a c #6d0d18", +"#y1 c #6d0e00", +"#ub c #6d1700", +"aCp c #6d2238", +"abv c #6d3b2c", +"#8x c #6d3f29", +".3l c #6d4226", +"aGl c #6d5564", +".5v c #6d5d57", +".2f c #6d6064", +".uW c #6d695a", +".jr c #6d6e65", +".6K c #6d88bd", +"#qb c #6d8dc4", +"#AL c #6d8dc6", +"#DK c #6d8dc9", +"al1 c #6d8dd0", +"akY c #6d8dd1", +"#AM c #6d8ec7", +"#l1 c #6d8ec9", +"#Fc c #6d8eca", +"#uN c #6d8ecc", +"#uO c #6d8ecd", +"#uw c #6d8fc3", +"#ux c #6d8fc4", +"#JS c #6d8fc9", +"#Cf c #6d8fcb", +"#DL c #6d8fcc", +"#uP c #6d8fcd", +"asi c #6d8fcf", +"aiO c #6d90c9", +"#AN c #6d90ca", +"#uz c #6d90cb", +"#GO c #6d90cc", +"#Ip c #6d90cd", +"#Fb c #6d90ce", +"#qe c #6d91c8", +"#wh c #6d91c9", +"#56 c #6d91ca", +"#oU c #6d91cb", +"#Lj c #6d91cc", +"#JV c #6d91cd", +"#4k c #6d92c9", +"#9q c #6d92ca", +"#l5 c #6d92cc", +"#Li c #6d92cd", +"aaM c #6d93ca", +"#rH c #6d93cd", +"#s5 c #6d93ce", +"#en c #6d93d5", +"aug c #6d93dd", +"#Vq c #6d94cc", +"#Vt c #6d95cb", +"#MK c #6d95cd", +"#Rb c #6d95d0", +"#Vs c #6d96cc", +"#PI c #6d97cb", +"#Od c #6d97ce", +"#PJ c #6d98cc", +"#PL c #6d98cf", +"#R. c #6d99cc", +"#4P c #6e0000", +"#P7 c #6e0f19", +"#1q c #6e1933", +"#vS c #6e2203", +"#KA c #6e2d03", +"#XV c #6e2d15", +"#FS c #6e2f10", +".UA c #6e413e", +".9W c #6e4434", +"aNt c #6e4939", +"aFz c #6e5244", +".sl c #6e5a4b", +".FE c #6e6462", +"#Lu c #6e6773", +".jf c #6e677c", +"aAt c #6e6a6b", +"as2 c #6e6a70", +"#DW c #6e727e", +"auP c #6e7da0", +"akT c #6e7e9b", +".1Q c #6e83ae", +"#fT c #6e84b0", +"#C# c #6e8bc2", +"#DF c #6e8bc4", +"#bc c #6e8bc8", +"#AH c #6e8cc2", +"adU c #6e8ccc", +"agt c #6e8dce", +"ajS c #6e8dd1", +"#PE c #6e8eba", +"#nq c #6e8ec8", +"#Fd c #6e8eca", +"ahG c #6e8ecb", +"aiM c #6e8ed1", +"#wi c #6e8fc5", +"#zh c #6e8fc7", +"ahH c #6e8fc9", +"#Ik c #6e8fca", +"#Ij c #6e8fcb", +"#wn c #6e8fcd", +"#uQ c #6e8fce", +"#wj c #6e90c5", +"#4m c #6e90cb", +"#Fe c #6e90cd", +"#wo c #6e90ce", +"arC c #6e90d1", +"#uy c #6e91c7", +"#oT c #6e91c9", +"#Lk c #6e91cb", +"#uA c #6e91cc", +"#Lm c #6e91cd", +"#Ok c #6e91ce", +"#Ir c #6e91cf", +"#GH c #6e92c9", +"#In c #6e92ca", +"#xQ c #6e92cb", +"#uC c #6e92cc", +"#PR c #6e92ce", +"#l2 c #6e92cf", +"#Vu c #6e93c9", +"a.7 c #6e93ca", +"#qh c #6e93cd", +"#oR c #6e93cf", +"#4j c #6e94ca", +"#55 c #6e94cb", +"#T7 c #6e94cc", +"#SH c #6e94cd", +"#ML c #6e95ce", +"#Q7 c #6e96c2", +"#Q6 c #6e97c6", +"#4d c #6e97cb", +"#5Y c #6e97cd", +"#4e c #6e98cb", +"#5Z c #6e98cd", +"#Oe c #6e98d0", +"#1B c #6f0100", +"a#p c #6f0c19", +"#2T c #6f1719", +"aCq c #6f233a", +"#Y. c #6f290c", +".9S c #6f2918", +"#Hy c #6f2b02", +".46 c #6f4b49", +".zm c #6f4c32", +".yr c #6f7281", +"#ej c #6f7eb0", +"#lZ c #6f8ac1", +"#F# c #6f8cc5", +"#us c #6f8dbf", +"adT c #6f8dcc", +"afd c #6f8dce", +"#Ih c #6f8ebb", +"agu c #6f8ecb", +"adS c #6f8ecc", +"ahF c #6f8ed1", +"afc c #6f8ed2", +"ao9 c #6f8fc7", +"#xR c #6f90c6", +"#xS c #6f90c7", +"#GE c #6f90cb", +"#9r c #6f90cc", +"#uR c #6f90ce", +"#uS c #6f90cf", +"#zi c #6f91ca", +"#Oa c #6f91cc", +"#uT c #6f91cf", +"#wk c #6f92c9", +"#uB c #6f92cd", +"#W5 c #6f92ce", +"#JW c #6f92cf", +"#GJ c #6f93ca", +"#MR c #6f93cb", +"#Ll c #6f93cc", +"#U. c #6f93cf", +"#s3 c #6f94c9", +"#9p c #6f94cb", +"#s6 c #6f94ce", +"#MM c #6f94cf", +"#T3 c #6f94d4", +"#Yu c #6f95c7", +"a.6 c #6f95cb", +"#54 c #6f95cc", +"auT c #6f95de", +"#9l c #6f96ce", +"#PM c #6f96d1", +"#4c c #6f97cc", +"#SB c #6f97ce", +"#9m c #6f98d0", +"#50 c #6f99ce", +"#7C c #6f99d0", +"#Q9 c #6f9acc", +"#6w c #700005", +"#79 c #70030c", +"#L3 c #702202", +"#X6 c #70280c", +"aMv c #703746", +".XW c #704436", +".47 c #705158", +".BE c #705c47", +"ak1 c #706166", +".jB c #706263", +"aIq c #706e70", +"QtG c #707197", +"aoi c #707bba", +"#DD c #7086b3", +"aqN c #7087b8", +"#zf c #708dc2", +"afe c #708dcb", +"#we c #708ec0", +"#JP c #708ec7", +"aci c #708ecd", +"apU c #708fc5", +"agv c #708fc9", +"ach c #708fce", +"#1c c #7090c7", +".97 c #7090c9", +"#GD c #7090cd", +"#57 c #7091cd", +"adP c #7091cf", +"#7J c #7091d0", +"#xT c #7092ca", +"agr c #7092cc", +"#4n c #7092ce", +"avs c #7092d4", +"#MN c #7093cf", +"#Vz c #7093d0", +"#WZ c #7094c7", +"#MQ c #7094cb", +"#qf c #7094cc", +"#W4 c #7094d0", +"#PH c #7095c5", +"#PF c #7095c6", +"#WY c #7095c9", +"adR c #7095cc", +"#7y c #7095ce", +"#WX c #7096cc", +"#9o c #7096cd", +"#WV c #7096cf", +"#5Q c #7096d0", +"af# c #7096d2", +"#1h c #7097cf", +"#Of c #7097d1", +"#7B c #7098d0", +"#OF c #710c10", +"#LG c #710d10", +"#9H c #711215", +"##g c #711f07", +"#Zi c #712911", +"#5j c #712e1a", +".4S c #713013", +".OT c #714233", +"aJs c #714942", +".Yy c #71533e", +".rV c #715849", +".zK c #71614a", +".30 c #71656f", +"aHT c #716973", +"#eU c #716a79", +"aIp c #716f71", +"#Vj c #717692", +"#xN c #718ec2", +"aff c #718ec9", +"al0 c #718ed2", +"akX c #718ed3", +"#7N c #718fce", +"#9s c #718fcf", +"aiL c #718fd3", +"#7L c #7190ce", +"#7M c #7190cf", +"ahE c #7190d3", +"afb c #7190d4", +"#MP c #7191ce", +"ahD c #7192cb", +"#MO c #7192ce", +"ags c #7192cf", +"acg c #7192d0", +"afa c #7192d2", +"#33 c #7193ca", +"#7w c #7193cb", +"ash c #7193cd", +"#53 c #7193d1", +"av0 c #7193d3", +"#W0 c #7194c7", +"#7E c #7194cb", +"#SL c #7194d0", +"awV c #7194d4", +"#Sz c #7194d7", +"#PQ c #7195cc", +"#Vy c #7195cd", +"#7x c #7195ce", +"#WU c #7195d0", +"#WT c #7195d1", +"#WW c #7196ce", +"avt c #7196de", +"#Q5 c #7197c6", +"#7K c #7197cd", +"adQ c #7197ce", +"apa c #7197d1", +"#ZU c #7198cc", +"#4b c #7198ce", +"aon c #7198d0", +"#SA c #7198d4", +"#5X c #7199d0", +"#SC c #719bcc", +"#2H c #719cd5", +"#VP c #720d1c", +"#6C c #721819", +"#To c #72270a", +".3h c #72301c", +"#5n c #72361c", +"#am c #72361f", +".9V c #72371d", +".XV c #72493d", +".Ry c #724d3e", +"aDv c #72666c", +".zd c #726866", +".YX c #726870", +".te c #726a55", +"##M c #726b7f", +"#sZ c #728cbb", +"a.9 c #728fcf", +"aiJ c #7290cb", +"aue c #7290cc", +"a.8 c #7290cf", +"#6. c #7290d0", +"#MH c #7291d2", +"#GB c #7292b8", +"#58 c #7292cf", +"#O# c #7293cb", +"aaK c #7293d0", +"a.5 c #7293d1", +"aaL c #7293d2", +"#Vv c #7294ca", +"#SK c #7294cc", +"aaJ c #7294d0", +"#Og c #7294d1", +"#Rc c #7294d2", +"#Vp c #7294de", +"#s4 c #7295cc", +"#Vx c #7295cd", +"#34 c #7295cf", +"#Yn c #7295d1", +"auS c #7295d9", +"#GI c #7296cd", +"#W3 c #7296ce", +"av1 c #7296db", +"#Yt c #7297ca", +"#Q8 c #7298c1", +"#2G c #7299d3", +"#5R c #7299d4", +"#7A c #729ad2", +"#7D c #729cd3", +"#0f c #730202", +"#1E c #730307", +"#Kh c #730b07", +"a#o c #730f1c", +"#sE c #731d04", +".OW c #73433c", +".91 c #734436", +".OU c #734536", +"aJ# c #735468", +"aww c #735950", +".uP c #73602c", +"#AS c #736368", +"QtA c #736682", +".D3 c #736772", +".kP c #736a60", +"apq c #736a6f", +"aJY c #736c73", +".j7 c #736f96", +"aIo c #737173", +".96 c #737eaa", +".le c #738590", +"#9c c #738ab7", +"asf c #7390bc", +"ajR c #7390d2", +"aiK c #7391ce", +"#59 c #7391d0", +"#Lg c #7391d2", +"#Vn c #7391d9", +"a.V c #7392c2", +"#SI c #7393cf", +"#4o c #7393d0", +"asg c #7394c7", +"#ZO c #7394cf", +"adO c #7394d1", +"#PN c #7394d2", +"#W1 c #7395cd", +"#4g c #7395ce", +"#4h c #7395cf", +"#2F c #7395d0", +"#Yo c #7395d1", +"#52 c #7395d2", +"#4i c #7395d3", +"#ZV c #7396cd", +"#1j c #7396ce", +"aEa c #7396da", +"aCV c #7396db", +"az. c #7396de", +"#Oj c #7397ce", +"#W2 c #7397cf", +"aBw c #7397dd", +"aAg c #7397de", +"#4a c #7398d0", +"aqR c #7398d4", +"#7z c #7399d1", +"#36 c #739bd8", +"#1C c #74000a", +"#LH c #740205", +"#Kg c #74020a", +"#IP c #740210", +"#YI c #74131c", +"#Nx c #741903", +"#RZ c #741f00", +"#t1 c #742408", +"#gz c #742611", +"aCr c #74263d", +"#I7 c #742f00", +"#bU c #744139", +".7x c #744837", +"aGz c #745361", +".uH c #745b56", +".tk c #746059", +"#6g c #746a62", +"#.C c #746b72", +"#bA c #747097", +"QtQ c #74769a", +".sr c #74829d", +"#N9 c #748bb2", +"#C. c #748bbd", +"aok c #748cc7", +"ana c #748ed1", +"ajP c #748fcb", +"#Oi c #748fd0", +"akW c #748fd1", +"ajQ c #7490ce", +"ahB c #7491c3", +"#Oh c #7491d0", +"aaF c #7492c5", +"#2B c #7492c7", +"#4q c #7492d2", +"#1b c #7493c1", +"a.W c #7493c4", +"aol c #7493cd", +"#4p c #7493d2", +"atq c #7494c5", +"agq c #7495cd", +"#T8 c #7495cf", +"#Yp c #7495d0", +"#7I c #7495d2", +"#T1 c #7495da", +"#7v c #7496cd", +"#4f c #7496ce", +"#Yq c #7496cf", +"a.4 c #7496d0", +"#39 c #7496d1", +"#51 c #7496d2", +"a.3 c #7497cf", +"#5P c #7497d0", +"arA c #7497d1", +"arB c #7497d5", +"#ZW c #7498cf", +"#Yv c #7498d0", +"#9k c #749ad1", +"#5W c #749ad2", +"#35 c #749ad5", +"#Na c #750301", +"#4N c #750605", +"#t0 c #752307", +"aCs c #75253d", +"#b8 c #752811", +"#E3 c #753112", +"#3G c #753118", +".3g c #75321f", +"#0Y c #753325", +"#Ja c #753c23", +".52 c #754931", +"ajp c #754f40", +".AY c #755032", +".8b c #75514a", +".wz c #755732", +"#cM c #7582b8", +"ahA c #758bb7", +"#cN c #758bc7", +"#AG c #758cbd", +"#DE c #758cbf", +"#kw c #758cc3", +"#ur c #758dba", +"#ek c #758ec7", +"akU c #758ecb", +"#5K c #758fc3", +"akV c #758fce", +"#E9 c #7590c0", +"asQ c #7591bb", +"auR c #7592cd", +"#WR c #7592df", +"aaG c #7593c8", +"axS c #7593cc", +"#32 c #7594cb", +"atr c #7595d7", +"asR c #7596c7", +"axT c #7596d4", +"a.2 c #7597cf", +"#7F c #7597d0", +"#4. c #7597d1", +"#9n c #7597d2", +"#Yr c #7598cf", +"#4# c #7598d1", +"auf c #7598de", +"#Ys c #7599cd", +"a.0 c #7599cf", +"aFw c #7599db", +"a.1 c #759ad1", +"#5V c #759ad3", +"#0g c #76000b", +"#Hi c #760214", +"#OH c #760400", +"#Ul c #760c1b", +"#N# c #760d0d", +"#xx c #761500", +"#sG c #761902", +"#vZ c #762004", +"#Vb c #762d10", +"ah9 c #763213", +".6p c #763417", +"#XU c #76341a", +"#0N c #763f33", +".4Z c #76402b", +"#Ep c #76461c", +"#N4 c #764737", +"aid c #764a36", +"azB c #766c79", +"avM c #76737a", +".Ce c #767383", +"#b# c #7685be", +"aoj c #7686c4", +"awS c #7689b0", +".5# c #7689b1", +"#iV c #768bc1", +"#5I c #768cb8", +"avq c #768cba", +"#F. c #768cc0", +"an. c #768dca", +"#wd c #768ebb", +"#ze c #768ebd", +"alY c #768eca", +"an# c #768ecd", +"alZ c #768ece", +"#oQ c #768fc2", +".jI c #7691bc", +"#PO c #7692d3", +"arz c #7694c3", +"aqO c #7694c7", +"#Sy c #7694da", +"aaH c #7695cb", +"#WS c #7695e5", +"#Vw c #7696cd", +"#ZP c #7696d1", +"aws c #7696d4", +"#7H c #7697d3", +"#Vo c #7697e1", +"aaI c #7698d0", +"adN c #7698d1", +"#7G c #7698d2", +"#ZT c #7699d0", +"#9i c #769ad0", +"#5S c #769eda", +"#FB c #770317", +"#Q. c #770500", +"#6x c #77050d", +"#9M c #770a18", +"aCx c #77294b", +"#Zh c #772f17", +"#3z c #77301c", +"#as c #773420", +"#6D c #773a30", +"#Eo c #77411b", +".Wo c #77463a", +".uz c #77494a", +"aHL c #775a73", +".z2 c #775e42", +".u1 c #776141", +"aGI c #77635e", +".wf c #776545", +".qS c #77664d", +"##7 c #776a60", +"#.u c #77707c", +"#J3 c #77717f", +"aAQ c #77727e", +"asv c #777379", +".pC c #777469", +"aoP c #77747a", +"aJh c #77757b", +"#TX c #777da3", +".3v c #778ab4", +".8j c #778ab5", +"#WP c #778cc5", +"auQ c #778dbd", +"##C c #778dc7", +"ae8 c #778ebe", +"#xM c #778fbd", +"acf c #7790c3", +"#3Z c #7792c8", +"ae9 c #7793c7", +"#T0 c #7793d8", +"aaE c #7794c5", +"#2E c #7794cf", +"ats c #7795dc", +"#5N c #7796cb", +"axj c #7796d0", +"a.X c #7797c9", +"ahC c #7797ca", +"asj c #7797da", +"#5O c #7798ce", +"#9j c #779bd2", +"#5U c #779bd5", +"#6v c #780007", +"#1A c #780201", +"#LI c #780402", +"#4U c #780c0b", +"#P8 c #780c10", +"#Xf c #780d19", +"#dE c #782309", +"aCu c #78243d", +"#Vd c #782d09", +"ag5 c #782f10", +"#Zz c #78371d", +"#CU c #78421c", +"##t c #784225", +".06 c #784e36", +".Wm c #784f49", +".y. c #78634f", +"#PA c #786667", +"##L c #78727d", +".sE c #787474", +"#N7 c #78767e", +".oK c #7876a2", +"#B9 c #7885ab", +"#TY c #7885b8", +"aqM c #7887b7", +"acb c #788eb8", +"awT c #7892c5", +"awr c #7892c8", +"#Rd c #7892d5", +"#WQ c #7892d7", +"avZ c #7894cc", +"avr c #7894ce", +"#5M c #7895c8", +"#1d c #7896ce", +"#1g c #7896cf", +"awU c #7897d3", +"asT c #7897dc", +"#ZS c #7898d0", +"#9h c #789bd1", +"#YQ c #790304", +"#OG c #790b09", +"#8b c #791421", +"aCt c #79273f", +"#e5 c #792e18", +"#Zg c #79331b", +"#60 c #793620", +"#Tp c #793627", +"#5p c #79381e", +"#.P c #793e1d", +"#63 c #79402d", +"##u c #794125", +".8e c #79504b", +"aML c #796374", +".82 c #796c6a", +"aJZ c #797379", +"apn c #79757b", +"#Le c #7985ad", +"ao7 c #7989c4", +"ary c #798fba", +"#PP c #7990d3", +"#Lf c #7991c9", +"#Vm c #7992d3", +"#hp c #7993bd", +"#Yl c #7993d9", +"#Q4 c #7994bd", +"#MG c #7995d0", +"#SJ c #7995d4", +"aId c #7996ce", +"aGS c #7996cf", +"aFv c #7996d0", +"#31 c #7997cd", +"#ZR c #7997d1", +"#ZQ c #7998d2", +"a.Z c #799cd0", +"aGT c #799ddf", +"#YR c #7a010c", +"#RC c #7a0401", +"#P9 c #7a0805", +"#26 c #7a0907", +"#v0 c #7a1900", +"#t2 c #7a2c0f", +"afP c #7a2d0d", +"#X7 c #7a3011", +"#8J c #7a3620", +".P4 c #7a3b2b", +".9N c #7a3e2a", +"#an c #7a432c", +".hQ c #7a4e1c", +".W3 c #7a503d", +".43 c #7a5343", +"aM6 c #7a5c50", +"aHU c #7a6971", +".Y4 c #7a728c", +".vn c #7a7388", +"#Cp c #7a7e8c", +"#qa c #7a87af", +"#np c #7a8ab8", +"#9a c #7a8db6", +"#rA c #7a8fbc", +"#TZ c #7a8fcd", +"aud c #7a91c1", +"#9d c #7a94c2", +"#7r c #7a94c3", +"#5L c #7a96c9", +"aE# c #7a96d2", +"#O. c #7a98ca", +"#7u c #7a9acf", +"a.Y c #7a9ccf", +"#5T c #7a9dd7", +"#1D c #7b0b13", +"aCv c #7b253f", +"##o c #7b2f19", +"aGF c #7b2f2f", +"a#n c #7b3233", +"#Zw c #7b331d", +".76 c #7b372a", +"#.4 c #7b3828", +".9U c #7b3d25", +"#Pz c #7b4d40", +".Zw c #7b503b", +"#L# c #7b522d", +".3n c #7b533a", +".3o c #7b533c", +"aKC c #7b5f71", +".zD c #7b6a52", +".bK c #7b6e89", +"ama c #7b7270", +"#3R c #7b777e", +".jo c #7b7c74", +"#Vl c #7b8dc4", +"#9b c #7b90bb", +"avY c #7b90bc", +"#Sx c #7b91d4", +"apT c #7b92c8", +"axi c #7b95c6", +"atp c #7b97bf", +"#9f c #7b97c8", +"aCU c #7b97d4", +"#T9 c #7b98d4", +"aNX c #7b9bd0", +"#YU c #7c0102", +"#0e c #7c0202", +"#S3 c #7c0302", +"#22 c #7c0704", +"#0h c #7c0a12", +"#Ry c #7c0b19", +"#1r c #7c0d1f", +"#b9 c #7c2f18", +"#a7 c #7c350f", +".6o c #7c351a", +"#Dx c #7c3819", +"#E# c #7c4149", +".53 c #7c4e37", +".8g c #7c524d", +".Cw c #7c5633", +".jw c #7c605b", +".u8 c #7c624c", +"aKF c #7c6974", +"#3W c #7c8dae", +"aE. c #7c8dba", +"#JO c #7c90bf", +"#Re c #7c91d6", +"#7q c #7c94c3", +"aJD c #7c95c3", +"#30 c #7c98d0", +"#1f c #7c98d2", +"aON c #7c9cd1", +"af. c #7c9cd4", +"#9g c #7c9ed4", +"#Uu c #7d0302", +"#Xm c #7d0305", +"#O4 c #7d1700", +"aCw c #7d2640", +"#Wp c #7d2e19", +"#5i c #7d3621", +"#65 c #7d3a1f", +"#8K c #7d3a26", +".3e c #7d3c28", +".UC c #7d4638", +"#JJ c #7d4c2c", +".Wn c #7d5149", +".4n c #7d5956", +"aJi c #7d5d62", +".tH c #7d624a", +"#N5 c #7d6657", +".x6 c #7d685d", +"#Q1 c #7d6e7a", +"#MC c #7d6f68", +"#Iv c #7d6f7c", +"#GA c #7d8fab", +"#MF c #7d92c1", +"#PD c #7d93b6", +"#3Y c #7d95c5", +"aiI c #7d97c4", +"#ZM c #7d97ce", +"#2D c #7d98d1", +"#7s c #7d99ca", +"aBv c #7d99d7", +"#1e c #7d9ad3", +"aM3 c #7d9cd2", +"aIe c #7da1e2", +"#Uq c #7e0303", +"#Xn c #7e030d", +"#78 c #7e0410", +"#YS c #7e0810", +"#IQ c #7e221d", +".7O c #7e2d08", +"#b7 c #7e3119", +"##p c #7e371d", +"#Ib c #7e4426", +"a#S c #7e4938", +"aKG c #7e4956", +".tl c #7e6c65", +"aIK c #7e6c75", +"#.B c #7e7066", +"aIr c #7e7d7f", +"##B c #7e7faa", +"#lY c #7e86b0", +"#Sw c #7e8cc4", +"awq c #7e92bb", +"#ZL c #7e93bd", +"ao8 c #7e96cf", +"aNW c #7e97c5", +"ace c #7e97c8", +"aaA c #7e99c3", +"aaD c #7e9ac8", +"#2C c #7e9ad1", +"#7t c #7e9cd0", +"#RB c #7f0203", +"#VV c #7f0304", +"#0j c #7f0405", +"#6p c #7f0607", +"#6y c #7f0610", +"#Xg c #7f0b16", +"#Va c #7f3a22", +"#Dy c #7f3a28", +"aGq c #7f3a5b", +"#0M c #7f463a", +".Qv c #7f5142", +"#MA c #7f6043", +"#2Q c #7f6667", +".qA c #7f6f67", +".fh c #7f6f76", +"#bH c #7f7270", +".vO c #7f7281", +"aG5 c #7f7679", +".fj c #7f7a75", +"aAs c #7f7b7c", +"aur c #7f7c7f", +"azv c #7f7e87", +"#Ig c #7f92b5", +"a.U c #7f94bd", +"agp c #7f98c9", +"#9e c #7f9bcb", +"aAf c #7f9bda", +"aJE c #7f9ed4", +"aKV c #7f9fd5", +"#0i c #800e12", +"#EF c #80290f", +"#aC c #802a07", +"ag9 c #803314", +"#h5 c #80331f", +"#X8 c #803413", +"#e4 c #803e31", +"#Gv c #804025", +"#Bn c #804b24", +".OV c #804e46", +".eY c #80522f", +"aGn c #805f72", +".pn c #806a4c", +"alf c #80727f", +".8i c #807c8e", +"#WO c #808eb9", +"auc c #808fb4", +".6J c #8092ba", +"ae7 c #8093c0", +"ajO c #8094b9", +"ay9 c #809cdb", +"aPJ c #809fd5", +"#Q# c #810000", +"#4H c #810200", +"#S2 c #810304", +"#73 c #81030c", +"#Nb c #810403", +"#VW c #81040c", +"#OJ c #810708", +"#Xo c #81070e", +"#YK c #810810", +"#VQ c #810a17", +"#SZ c #810b19", +"#Xr c #810d07", +"#Qb c #811016", +"#EQ c #81240b", +"#aU c #81311d", +"aey c #813212", +"#c. c #81351d", +"#8I c #813c25", +".4V c #813e2d", +"#C0 c #81422a", +".gr c #81542b", +".51 c #81553c", +"#La c #815d3e", +"aDw c #81767b", +"arL c #817d83", +"#PC c #81879d", +"#DC c #818b9c", +"#Vk c #818cb5", +"#sY c #818db0", +"aaz c #8190b2", +"#7p c #8198c6", +"aKU c #8199c8", +"aaB c #819cc6", +"#ZN c #819fdd", +"#4T c #820005", +"#Ur c #820309", +"#VZ c #820707", +"#4Q c #82080c", +"#yI c #822905", +"#t3 c #823719", +"#aT c #823824", +"#1R c #823917", +"afG c #825740", +".7u c #82623d", +".u9 c #826653", +".zY c #82694d", +"#di c #82746b", +".dO c #82778b", +"aju c #827e83", +".sX c #8288aa", +"#ME c #828dac", +"#N8 c #828ea6", +"apS c #8292c8", +"a.R c #8293b8", +"aaC c #829dc9", +"#OI c #830200", +"#9L c #830a1b", +"#Z8 c #830c14", +"#YJ c #83181b", +"#dD c #832a14", +"#dC c #833117", +"#b6 c #83311d", +"ajm c #83442b", +"#dr c #834b45", +".4p c #83573d", +".3p c #835c4a", +"#xZ c #837790", +"aC8 c #837c7e", +".K# c #837da3", +"abV c #837e86", +"aBS c #837f83", +"#ho c #838197", +"#Sv c #8389b6", +"ase c #8399c0", +"#Yk c #8399d4", +"acc c #839ac7", +"aQG c #83a3d8", +"#6u c #84000a", +"#Z9 c #840104", +"#Up c #840305", +"#S4 c #840308", +"#4M c #840609", +"#9I c #840616", +"#Xp c #84080d", +"#Xq c #840909", +"#29 c #84110f", +"#t9 c #843516", +"#vQ c #843518", +"#5h c #843d25", +"#dt c #844027", +"ajj c #844528", +"#mw c #844913", +"aha c #844a2c", +"aHR c #844f70", +".50 c #84573f", +"##6 c #847776", +".Hi c #847786", +"aBR c #847f84", +".yV c #847f9b", +".oB c #848096", +"#Su c #8485aa", +"a.T c #8498c1", +"acd c #849ccc", +"#Xl c #850406", +"#RD c #850408", +"#Qa c #850709", +"#4E c #851318", +"#xy c #852508", +"#A# c #852f09", +"#dF c #853117", +"#b5 c #853a23", +"#WE c #853c13", +"#6Y c #853e24", +"#6Z c #853f28", +"#66 c #854026", +"#64 c #854329", +"#bW c #85492e", +"##v c #854c2f", +"#ar c #854c36", +"#6P c #855339", +"aHP c #855575", +".ta c #85574f", +".D8 c #855d35", +"aJf c #856377", +"#Yd c #856b61", +".mx c #85746c", +".dN c #857a8f", +"aAp c #858082", +"ay8 c #8595c6", +"ago c #859ac7", +"#27 c #860206", +"#RA c #860308", +"#0. c #860309", +"#6z c #860311", +"#1z c #860402", +"#VU c #860406", +"#VX c #86060d", +"#6q c #86070a", +"#9J c #860718", +"#Nc c #861414", +"#F8 c #862807", +"##h c #863b25", +"#US c #863c1d", +"a.l c #863e27", +"#3y c #863e28", +"ai# c #863f24", +"#FR c #864418", +"#b4 c #86462b", +".R4 c #86583b", +"av4 c #866c63", +".zZ c #866d51", +".jm c #867372", +".rU c #867966", +".b9 c #86838f", +"#iU c #8685a9", +"#oP c #8689a9", +"ae6 c #8695bf", +"#74 c #87040f", +"#6A c #870412", +"#VY c #87050b", +"#YL c #87070e", +"#YT c #871115", +"#6B c #87121b", +"aGC c #874349", +"#Wv c #87462a", +".9w c #87473a", +"#Zb c #874d31", +".Vc c #875242", +".9h c #875330", +".XX c #875745", +"ahb c #875a43", +"#3d c #875f39", +"akq c #876357", +"aKD c #87687b", +"#ZE c #876e64", +"#ph c #87736a", +".dF c #877e86", +"#E8 c #8796ab", +"#Yj c #8797c3", +"aAe c #8797c8", +"aCT c #8798c6", +"#3X c #879cc5", +"adM c #879ccd", +"aL7 c #879fce", +"#0d c #880403", +"#S1 c #880409", +"#Rz c #880711", +"#9K c #88091a", +"#Um c #880a17", +"#V0 c #881d14", +"#Au c #882c0d", +"#TR c #883c18", +"a.k c #884228", +"#XP c #88422d", +"#bX c #884325", +"aia c #88452a", +"#2h c #88463b", +".9r c #885140", +"#JI c #885530", +".rP c #885b45", +".uG c #885b5d", +".8f c #885f59", +"aEd c #886c5f", +"aJd c #886d7f", +"#.p c #887977", +".xB c #887b8a", +".Hd c #887e7b", +"#ei c #887e9d", +"#J4 c #88808d", +"ad8 c #888180", +"#fS c #88839c", +".Xq c #8883a9", +".rd c #88848a", +".Cf c #888496", +"aKE c #888993", +"#AF c #8889a4", +"#Q3 c #8893b2", +"#5J c #88a0d2", +"aM2 c #88a1cf", +"#1u c #890104", +"#Xh c #890711", +"#aV c #893520", +"ag6 c #894021", +".9T c #894631", +"#15 c #894723", +".mi c #895531", +"aHJ c #895a75", +"aGB c #895c61", +"aso c #896d65", +"ap0 c #897068", +"#XJ c #89736d", +"#bI c #897b71", +"aBE c #898486", +"#q# c #8986a0", +"#kv c #898bb1", +"#JN c #898daa", +"#77 c #8a0414", +"#4I c #8a0505", +"#Us c #8a050c", +"#YV c #8a0d09", +"#72 c #8a1219", +"#iP c #8a2f17", +"#ES c #8a3116", +"#pG c #8a3210", +"#BA c #8a330d", +"#eg c #8a3e2f", +"#t7 c #8a3f20", +".3c c #8a4328", +"#aA c #8a432f", +"#8G c #8a4428", +".1z c #8a4a32", +".4Y c #8a4d3a", +".kE c #8a5536", +"##s c #8a5537", +"#.U c #8a5847", +"#Y9 c #8a5b31", +".2L c #8a5f45", +".07 c #8a5f46", +".2K c #8a614f", +"aGH c #8a625d", +"aGo c #8a6378", +"aHN c #8a6581", +"#MB c #8a705b", +"aKz c #8a7484", +".we c #8a7952", +".h4 c #8a797a", +"#cL c #8a7ea3", +"aJ0 c #8a8288", +"#MD c #8a8894", +"#rz c #8a90b0", +"aBu c #8a9aca", +"#S0 c #8b0711", +"#6n c #8b0c13", +"aI4 c #8b2d3a", +"#ER c #8b3015", +"#E4 c #8b4836", +".Qh c #8b4939", +"#.X c #8b4e3c", +"#CV c #8b592e", +".4q c #8b5f45", +"#B. c #8b616a", +".qX c #8b796c", +"aG0 c #8b8184", +"#b. c #8b83ac", +".33 c #8b849e", +"#wP c #8b8d9b", +"#TW c #8b8dab", +"#4S c #8c0008", +"#2X c #8c0402", +"#YP c #8c0406", +"#0# c #8c0409", +"#Uo c #8c040a", +"aGD c #8c2c36", +"afQ c #8c3f1f", +"#1P c #8c4120", +"#t6 c #8c4222", +"#8H c #8c462c", +".3d c #8c472b", +"#02 c #8c4c32", +"#Ww c #8c4d33", +"aa2 c #8c5554", +"#Bm c #8c5a39", +"#Mz c #8c5d49", +"a#O c #8c8185", +".ij c #8c8193", +".DZ c #8c827f", +"aqo c #8c898f", +"#28 c #8d0005", +"#S5 c #8d040b", +"#75 c #8d0513", +"#1G c #8d0605", +"#1F c #8d1113", +"#EH c #8d3219", +"#EG c #8d341b", +".7N c #8d3d17", +"#Zx c #8d422a", +"#t4 c #8d4324", +".6c c #8d4827", +"##q c #8d482c", +"#UM c #8d491f", +"#Bt c #8d4b31", +".P5 c #8d4f3f", +"#18 c #8d5950", +"#Eq c #8d5b38", +".6z c #8d644c", +"aKw c #8d6e81", +"asY c #8d7167", +"aJb c #8d7586", +"aFY c #8d7d84", +"#N6 c #8d7e77", +".ae c #8d809b", +"#PB c #8d848d", +"#RE c #8e030a", +"#Xk c #8e0408", +"#VR c #8e0812", +"#2U c #8e141a", +"#71 c #8e2428", +"aGw c #8e315a", +"#Zy c #8e4127", +"#TP c #8e4428", +"#WD c #8e451e", +"#5q c #8e4930", +".4X c #8e4d3c", +".7M c #8e4f44", +".7w c #8e5e3a", +".5Y c #8e6965", +"#2r c #8e766c", +"aKA c #8e7889", +"#St c #8e7a7b", +"aF2 c #8e7e85", +".83 c #8e8077", +"aEK c #8e8086", +"#zd c #8e8394", +"aC4 c #8e878a", +"aAq c #8e898b", +"#Q2 c #8e899c", +"a.S c #8ea0c8", +"#6t c #8f020d", +"#VT c #8f0409", +"#YM c #8f050b", +"#6r c #8f060e", +"#21 c #8f0709", +"#Un c #8f0710", +"#0k c #8f0c0a", +"#vI c #8f3311", +"#u. c #8f3e21", +"ag8 c #8f4325", +"a.m c #8f4430", +"a.j c #8f4a2d", +"ac9 c #8f4c29", +"abE c #8f4c2a", +"ajl c #8f5032", +"aGG c #8f514e", +"#Gw c #8f543b", +"#HE c #8f5b3a", +".FN c #8f653a", +".3q c #8f6d61", +"aL9 c #8f7266", +"#Lb c #8f7361", +"#no c #8f879f", +"#1v c #900406", +"#76 c #900515", +"#4R c #900710", +"#EP c #903118", +"#BW c #903416", +"#ss c #903514", +"aGu c #903961", +"#aD c #903b17", +"#4D c #903d3e", +"a#2 c #90482d", +"#UR c #90492a", +"#aS c #904935", +"a#1 c #904c2c", +"aGp c #904d6d", +".4A c #90553f", +"ah2 c #905642", +"#17 c #905a51", +"#CT c #905e3d", +".qs c #906341", +"atx c #90776d", +".Ty c #907b77", +".C8 c #908275", +"aoC c #90858a", +"aJ5 c #90878d", +".AO c #908c9b", +"aIs c #908e90", +"aGE c #91202e", +"#6m c #912c2e", +"#EM c #912d16", +"#kq c #91361e", +"#su c #913a1b", +"afS c #913f20", +"aKp c #914235", +"a#3 c #91442d", +"#t5 c #914829", +"#Wl c #914c34", +"#0K c #914d2e", +".4B c #915640", +".4o c #916451", +".1K c #916b51", +"#.M c #916c56", +".jz c #917c7a", +".gL c #918482", +"aG6 c #91858b", +"ak7 c #918785", +".jj c #918a94", +"aAr c #918c8e", +".mO c #919898", +".il c #919eb6", +"#RF c #92050b", +"#Xi c #92050d", +"#1H c #921108", +"#S7 c #921817", +"#EI c #92341c", +"#F7 c #923913", +"aGs c #924569", +"#fQ c #924632", +"ai. c #924d2f", +"a.i c #924e2e", +".Qg c #925141", +"#Wx c #92533a", +"aeG c #925737", +"#.O c #925f3b", +".2M c #92674c", +".j. c #92682f", +".42 c #926d5d", +"#WL c #92766d", +"avy c #92786f", +".ff c #928289", +"aEM c #92848b", +".ID c #928cb1", +"aw3 c #928f91", +"#VS c #93050d", +"#1y c #930605", +"#2Y c #930606", +"#4L c #93070d", +"#Ut c #930c12", +"#1s c #930e17", +"#Qv c #932705", +"#Qw c #93281a", +"#EJ c #93331c", +"#5t c #934329", +"afR c #934625", +"#8N c #934a2f", +"#0v c #934b20", +"#0L c #934d2f", +"#TS c #935132", +"#8L c #93523e", +".Qf c #935241", +".Qi c #935544", +"#Es c #935947", +"#b1 c #935b3d", +"aep c #936149", +"#Vg c #93766e", +"aul c #937a70", +".7i c #938a92", +"#.h c #938c87", +"axu c #939199", +".B5 c #9391b9", +".oe c #939b94", +"#YO c #940407", +"#Xj c #94040a", +"#6s c #94040f", +"#0c c #940505", +"#6o c #94050f", +"#0a c #940609", +"#4J c #94070b", +"#sF c #943a22", +"#sv c #944122", +".1x c #944a30", +"#6X c #944e32", +"ah8 c #945031", +"aMO c #945067", +".4W c #945140", +".Qj c #945745", +"aCy c #945c76", +"agX c #945f47", +"afY c #946e5a", +"aGA c #94747b", +".0. c #94755e", +".8h c #94807d", +".zW c #948467", +".v1 c #94888b", +".uC c #948a77", +".aw c #948ca8", +"#Ld c #9492a6", +"#YN c #950409", +"#1t c #950507", +"#UT c #95492b", +"#5g c #954f35", +"#B3 c #955132", +"#2l c #95553b", +"#zY c #956442", +"#4X c #956940", +".1L c #957560", +".tq c #958254", +"aHp c #95848c", +"#eF c #958f9e", +"#EO c #96341c", +"#tX c #963918", +"#aX c #964228", +"#gD c #964826", +"afN c #964a29", +"#1O c #964b1e", +"#hm c #964b34", +"#WC c #964d2b", +"#8O c #964d31", +"#8M c #964d33", +".1y c #964e34", +"#zZ c #96603a", +"#Ic c #966147", +".4r c #966950", +".Hm c #966b3e", +".SR c #966f47", +"a#R c #966f61", +"#iT c #967781", +"auY c #967c72", +"#zK c #967e90", +"#lX c #968495", +"#Lc c #968686", +"#zo c #968897", +"aFI c #968a90", +".Hh c #968a95", +"#E7 c #968e85", +"aBJ c #969192", +"af4 c #969296", +"#4F c #970812", +"#S6 c #970c13", +"#O5 c #973227", +"#EN c #97341c", +"#xe c #973e1a", +"#Hj c #97453f", +"#3J c #97462d", +"#2k c #974735", +"#UU c #97492c", +"#0t c #974c24", +"#tK c #974f27", +"adi c #975328", +"a#0 c #975532", +"ac8 c #975630", +"##y c #975b40", +"#Dz c #975d49", +"#b3 c #976041", +".Wp c #97614f", +".nL c #97663e", +"#En c #976644", +".9f c #977450", +"#4z c #978484", +"#oO c #978899", +"aEL c #978990", +".vL c #978c8e", +".dE c #978e95", +"aIt c #979097", +"#Iz c #9791a0", +"#B8 c #9799a3", +"#0b c #980607", +"#1w c #980708", +"aH0 c #983237", +"#y0 c #98371a", +"#At c #98381b", +"#y2 c #983b1d", +"#aW c #98432b", +"abF c #984830", +"#TQ c #984a28", +"#0s c #984d1b", +"ag4 c #984f2f", +"ag7 c #984f30", +"#WB c #985032", +"#0Z c #98503f", +"#WA c #985239", +"#8F c #985435", +".Qe c #985646", +"abD c #985732", +"#ds c #985b4d", +"ajd c #986456", +".5Z c #986a57", +".IL c #987048", +".4k c #98733e", +".jy c #987f7c", +"aBK c #989094", +"aJ1 c #989095", +".2l c #9893b5", +"#2V c #99010c", +"#4K c #99080f", +"aMP c #99354f", +"#sH c #993b25", +"#Ny c #993d36", +"#EE c #994328", +"#L5 c #994940", +"#01 c #994b36", +"#dQ c #994c25", +"#t8 c #994c2d", +"ag3 c #995031", +"#3H c #995037", +"#67 c #995137", +"#3x c #995139", +"#gy c #995348", +".6q c #995c3e", +"#I3 c #99643a", +"#Bo c #996538", +"#cK c #996b75", +"#Q0 c #996c60", +".kT c #998880", +"aHq c #998890", +".fd c #998990", +".xz c #998d94", +"aEm c #998f94", +"#bw c #99909e", +"aJ2 c #999197", +"aIu c #999299", +".Cm c #99969f", +"#1x c #9a0807", +"#2Z c #9a080b", +"#20 c #9a080d", +"#aE c #9a4823", +"#7. c #9a4c30", +"#lc c #9a4d29", +"afO c #9a4d2c", +".Qk c #9a5d4a", +"#.V c #9a5f4e", +"aic c #9a644c", +"#CE c #9a656c", +"#eh c #9a6a6f", +"aac c #9a6f56", +"#JK c #9a7058", +"apd c #9a7e75", +"#Fh c #9a838a", +".r5 c #9a8560", +"aJ9 c #9a8a92", +".xi c #9a8eaa", +"aD# c #9a9196", +"#LJ c #9b3634", +"aKv c #9b385f", +"#fc c #9b3d25", +"#fd c #9b4024", +"ad# c #9b4a31", +"aex c #9b4c2c", +"#at c #9b513f", +".4R c #9b553a", +"#aB c #9b5541", +"#dx c #9b5638", +"#Wu c #9b583a", +"#0J c #9b593a", +".1A c #9b5a42", +".4z c #9b604a", +"#gu c #9b6634", +".xU c #9b866e", +"aIM c #9b8992", +"aLK c #9b8a99", +".kX c #9b8f69", +"#AE c #9b9294", +"aFH c #9b9295", +".8H c #9b9482", +".h1 c #9b949d", +"aBZ c #9b99a3", +".wF c #9b9eae", +"aMW c #9c3941", +"#jM c #9c4926", +"#3K c #9c4931", +"#jL c #9c4b28", +".3b c #9c5238", +".2X c #9c5a44", +"aji c #9c5d3f", +"#k6 c #9c632c", +"#ae c #9c6433", +"##r c #9c6749", +"#5. c #9c684b", +".2D c #9c6c31", +"#FY c #9c6c44", +".Ro c #9c754c", +"#xL c #9c858d", +".k2 c #9c908f", +"aC9 c #9c9398", +"aJ3 c #9c9499", +"aAP c #9c9699", +"ayd c #9c9aa4", +"aMC c #9d2e3f", +"#Ml c #9d4828", +"aGt c #9d4a70", +"a#4 c #9d4b37", +"aew c #9d4f2e", +"#ca c #9d5139", +"#c# c #9d513a", +"adg c #9d532c", +"#Wq c #9d5330", +"#jI c #9d533e", +"#V# c #9d5626", +".2W c #9d5b45", +"#We c #9d612e", +".71 c #9d634d", +".7E c #9d6356", +"#bR c #9d6635", +"ac0 c #9d6c5e", +".9g c #9d704a", +".Yx c #9d745b", +".Ki c #9d764d", +"abQ c #9d7760", +".2E c #9d7845", +".u6 c #9d866a", +"aDN c #9d868f", +".BF c #9d8974", +"aMH c #9d909e", +"#yh c #9d94a4", +"aEy c #9d969b", +"aDW c #9e315e", +"aKu c #9e3b5d", +"#cb c #9e533b", +"#5e c #9e542c", +"ad. c #9e5536", +"#2i c #9e5647", +"#ZA c #9e593b", +"#Wz c #9e5949", +"#bZ c #9e5b42", +"#CX c #9e674c", +"#0w c #9e6942", +".We c #9e6d3c", +".XO c #9e6e3a", +".92 c #9e6e60", +".2N c #9e7156", +"aHK c #9e849c", +".Xn c #9e949c", +"aD. c #9e9599", +"aFU c #9e959b", +"aBL c #9e969a", +"aIF c #9e969c", +"aIv c #9e969d", +"auz c #9e9ca3", +"#2W c #9f0004", +"aE7 c #9f2953", +"aMA c #9f2b33", +"#Gd c #9f3f2c", +"#rh c #9f432c", +"#sr c #9f4532", +"#fb c #9f482b", +"#h9 c #9f4a27", +"#aR c #9f4e38", +"#lk c #9f5125", +"#ch c #9f522b", +"#2j c #9f5242", +"#Y4 c #9f5422", +"#3v c #9f542d", +"aa# c #9f5430", +"afW c #9f5a36", +"#CW c #9f6b46", +".7q c #9f6b4b", +".Ol c #9f6c3a", +".MY c #9f6c3b", +"#Ea c #9f6f81", +"a.x c #9f7059", +"aHO c #9f7492", +".5T c #9f7a44", +".S1 c #9f7b6d", +"aCZ c #9f8375", +"#07 c #9f867c", +"aKy c #9f8798", +"aG4 c #9f9699", +"#OK c #a0363a", +"#F9 c #a04121", +"aeC c #a04925", +"aev c #a05131", +"#bY c #a05338", +"#Wy c #a05b4c", +"a.h c #a05c3b", +"#al c #a05d3f", +"#h0 c #a06730", +".9b c #a06c4c", +"ajo c #a06f5c", +".0Y c #a07038", +"#4W c #a07144", +".p. c #a07148", +"#Ss c #a08179", +".qz c #a08774", +"aJ4 c #a0989e", +"#dj c #a0989f", +".uf c #a09fb8", +"#4G c #a1010e", +"aMB c #a11e31", +"aDO c #a1274c", +"aLS c #a13149", +"#Gc c #a1402d", +"#q5 c #a14725", +"a#7 c #a14b2f", +"#ua c #a14c2f", +"#sD c #a14f35", +"#XA c #a1541a", +"#69 c #a15438", +"#5s c #a1543a", +"#8P c #a1573b", +"adh c #a15b31", +"#16 c #a15d3a", +".2Y c #a15f49", +"#Hw c #a16339", +"#b0 c #a16348", +"#FQ c #a1653c", +"akl c #a1674a", +"#Hv c #a16d46", +"#Sr c #a16f5b", +".Zx c #a1735c", +"aJ. c #a17f94", +"#TU c #a1827a", +"#ku c #a18693", +"aB6 c #a1999d", +".h7 c #a19b95", +"#0l c #a2261c", +"aE8 c #a22a55", +"#EK c #a24029", +"#oj c #a24e2b", +"#3u c #a25430", +"#00 c #a25642", +"#1N c #a25719", +"#e8 c #a25737", +"#0r c #a25815", +"#Hx c #a25c30", +"#a5 c #a25c36", +"#6W c #a25c3f", +"#5f c #a25c41", +"#3# c #a25e4c", +"ac7 c #a26139", +"#14 c #a2613d", +"#ak c #a26646", +"#do c #a26d3b", +".PS c #a26f3d", +"aMN c #a27c8e", +".Us c #a27f56", +"aKB c #a28a9b", +"aJc c #a28a9c", +".8M c #a294aa", +"aMI c #a295a2", +"aG7 c #a2969b", +".2h c #a296a2", +"aj2 c #a29793", +"#wR c #a2989a", +"#If c #a2a1b4", +"aLZ c #a3282d", +"aKs c #a33c50", +"#KV c #a34b2e", +"afT c #a34f2d", +"#oy c #a35030", +"#9G c #a35050", +"a.p c #a35338", +"a.q c #a35438", +"abN c #a35832", +".9L c #a35b42", +"#Ts c #a35e34", +"#dA c #a36540", +"#Ia c #a36747", +"akn c #a3694c", +".LG c #a3703e", +"#CF c #a3788a", +".zS c #a3917e", +".uK c #a39189", +".fc c #a39299", +".zJ c #a39478", +"aDu c #a3979d", +"aIw c #a39ca3", +"aLE c #a41e35", +"aLD c #a42633", +"aFd c #a4416c", +"#pC c #a4432a", +"aKH c #a44655", +"#ET c #a44c30", +"#yH c #a44d28", +"a#8 c #a44f32", +"afU c #a4502a", +"#oz c #a45131", +"#m7 c #a45338", +"aeA c #a45534", +"#du c #a4563c", +"#6U c #a4572f", +"a.n c #a45846", +"#Y3 c #a4591f", +"#3w c #a45d44", +"ah# c #a4603f", +".2Z c #a4634d", +".P6 c #a46556", +".Ql c #a46753", +"#jH c #a46a5c", +"#Ku c #a46c3f", +".7I c #a46c5f", +"#XG c #a46f3c", +"#iS c #a46f64", +".R3 c #a47356", +".08 c #a4765c", +"#fR c #a47776", +".PT c #a47d54", +".Z9 c #a47d60", +".XP c #a48054", +"aKY c #a4867a", +"#JL c #a4867d", +"anf c #a4877d", +"#nn c #a48890", +"#zn c #a49699", +"aG8 c #a4989e", +".IE c #a4a0c7", +".G4 c #a4a2cf", +"aDQ c #a5274e", +"aI9 c #a5497e", +"#Av c #a54d2d", +"#Mm c #a5512e", +"#dG c #a55137", +"#V9 c #a55619", +"afM c #a55838", +"#5r c #a55b42", +"aLR c #a55d73", +"#8E c #a56040", +"abC c #a5633c", +"a#Z c #a5633e", +".Qd c #a56353", +"#XL c #a56850", +"#L. c #a5755d", +".LH c #a57d55", +"#zc c #a59089", +".tM c #a59183", +"aF3 c #a5959c", +"aIE c #a59ca2", +".js c #a5a59d", +".rq c #a5a5b4", +"#Uv c #a64338", +"#pB c #a64729", +"#Aa c #a64c29", +"#Mh c #a64f32", +"abG c #a6503c", +"aeB c #a65232", +"#5u c #a6543b", +"a.o c #a6553b", +"#aY c #a65639", +"#So c #a65836", +"aeF c #a65b34", +"ag2 c #a65d3d", +"#03 c #a66244", +".20 c #a6644e", +"#Br c #a66852", +".7p c #a66d3d", +".Rn c #a67341", +"aGx c #a6738e", +"##z c #a67975", +"#.N c #a67c5a", +".7v c #a67e56", +".w. c #a6906e", +"aHr c #a6959c", +".95 c #a69aab", +"azA c #a69ca8", +"aID c #a69da3", +"#vi c #a69ea2", +"aBQ c #a69fa3", +"aDP c #a72c52", +"aI5 c #a7485f", +"#Nw c #a74b1e", +"#F6 c #a74f29", +"a#6 c #a74f34", +"#Y# c #a75e3a", +"aGr c #a75e80", +"#dB c #a76040", +"#a4 c #a7633c", +"#.W c #a76455", +"#I4 c #a76539", +".6w c #a76b5a", +"#h4 c #a76b5e", +".4C c #a76c56", +"#jD c #a76e37", +".Kh c #a77442", +".Qu c #a77565", +".Ur c #a77747", +"aGy c #a77c92", +".wu c #a78863", +".yg c #a78a69", +"anD c #a79ba1", +"aDa c #a79ea3", +".uU c #a7a088", +"aDR c #a82850", +"aLF c #a83048", +"aKO c #a8312d", +"#Ge c #a84931", +"aKq c #a84a49", +"#xf c #a84d2b", +"#Mi c #a85035", +"#Mk c #a85334", +"#5d c #a85a34", +"#3I c #a85a42", +"#sC c #a85b3f", +"#68 c #a85e43", +"ah7 c #a86345", +".Zp c #a88456", +"#tx c #a89190", +"afp c #a89b96", +"ahX c #a8a5ac", +".h0 c #a8a5b1", +".jv c #a8a8a0", +"aE9 c #a92e5a", +"aMV c #a92f39", +"aKr c #a9434d", +"#EL c #a94730", +"#xz c #a94d2e", +"#KW c #a95133", +"#Mj c #a95137", +"aez c #a95a39", +"#0n c #a95b3b", +"a.t c #a95e3c", +"#KE c #a96252", +"#Zf c #a9664d", +"#z4 c #a9664e", +".2V c #a96751", +"ajk c #a96a4c", +"#Y8 c #a96d33", +"akg c #a96f52", +".7J c #a96f62", +"ako c #a97059", +".Vb c #a97060", +".9a c #a97140", +"a.a c #a9715d", +".9o c #a97365", +".SQ c #a97644", +"#yt c #a97755", +"#h1 c #a9784a", +".Zo c #a97943", +"#s# c #a98161", +"#7Y c #a98178", +"aKx c #a98d9f", +"aHo c #a9989f", +".gO c #a99c9a", +".tu c #a99e86", +"ad7 c #a9a09e", +"aIx c #a9a2a9", +".7j c #a9a2b1", +".ri c #a9a5ab", +"aF. c #aa2b58", +"#ie c #aa4a24", +"#tW c #aa4e2d", +"abJ c #aa4f33", +"#oi c #aa5532", +".7X c #aa5b2b", +"#vT c #aa5d3e", +"aaa c #aa613b", +".Qc c #aa6858", +".ZZ c #aa6c4f", +".IK c #aa7746", +"#hn c #aa7f7b", +".MZ c #aa835a", +"akf c #aa8b81", +".xV c #aa957b", +"aiV c #aa9c97", +".qQ c #aa9d6e", +"#7U c #aa9f94", +"aHf c #aaa1a7", +".y8 c #aaa7af", +".Xr c #aaaacf", +"#uc c #ab4a2d", +"#aO c #ab4e33", +"abI c #ab4e34", +"#y3 c #ab5333", +"#Bz c #ab572f", +"#aQ c #ab5741", +"#1J c #ab5b3b", +"#UV c #ab5c3f", +"#cG c #ab5e33", +"#Y2 c #ab6118", +"a.g c #ab6744", +".6s c #ab6859", +".77 c #ab6959", +"abw c #ab6b55", +".78 c #ab6e5a", +"#e0 c #ab7644", +"#b2 c #ab7757", +".5S c #ab7b3d", +"#a9 c #ab8594", +".Wf c #ab875d", +"aEn c #aba1a6", +"aEl c #aba3a5", +".oD c #aba6bd", +"awA c #aba7aa", +"alC c #aba7ab", +"#G. c #ac4d2f", +"ada c #ac523e", +"##c c #ac5426", +"#kr c #ac583f", +"a.r c #ac5e40", +"#FC c #ac6159", +"#Y6 c #ac632b", +"#Tt c #ac643b", +"#Wt c #ac6647", +"#TO c #ac664e", +"#R3 c #ac6737", +"#OV c #ac6a36", +".Qb c #ac6a5a", +"#sT c #ac6c46", +"#No c #ac6d3b", +"alu c #ac7659", +"#Id c #ac8373", +".0Z c #ac8757", +"asn c #ac8d80", +"aBA c #ac9083", +"aME c #ac96a7", +".qJ c #ac9983", +"#zL c #ac99a8", +".zT c #ac9a85", +"aG9 c #aca0a6", +"aH. c #aca1a6", +"aHh c #aca4aa", +"aus c #aca9ab", +"#KU c #ad523b", +"abK c #ad5537", +"#C6 c #ad5631", +"a#5 c #ad5947", +"aMD c #ad5964", +"adf c #ad5d39", +"#I6 c #ad622a", +"#8Q c #ad6244", +"abz c #ad6431", +"#6V c #ad6439", +".6n c #ad6449", +"#Kz c #ad652f", +"#B4 c #ad6753", +"#LV c #ad7242", +".4y c #ad725c", +"##w c #ad7357", +"#gv c #ad7657", +".XY c #ad7963", +".4j c #ad7c40", +"akp c #ad7c69", +".Z8 c #ad8161", +"#B6 c #ad8266", +"#lW c #ad8584", +"aPL c #ad897b", +"aE6 c #ad8f9d", +"#GZ c #ad949b", +"#r5 c #ad9d95", +"#DB c #ada194", +".vx c #ada5a6", +"aEw c #ada6ac", +"aDh c #ada7ac", +"aoy c #ada9af", +"#Gz c #ada9b4", +".fk c #adaaaa", +"#O3 c #ae470f", +"#gK c #ae4f33", +"#ow c #ae573f", +"#A. c #ae5a32", +"#3t c #ae5b3a", +"#UE c #ae5d24", +"#.7 c #ae5d38", +"#8S c #ae6142", +".1w c #ae6148", +"a.s c #ae6242", +"#8R c #ae6243", +"a.u c #ae6341", +"#dw c #ae6348", +"abO c #ae663e", +"#a6 c #ae6841", +"#7a c #ae6b4b", +"#Sq c #ae6b4d", +"#0I c #ae6e4e", +"aib c #ae7056", +".9p c #ae7163", +".4D c #ae745d", +"#.I c #ae7645", +"alv c #ae7a64", +"#aj c #ae7b5d", +"#Gx c #ae7e6c", +"#6k c #ae7e79", +"#tF c #ae8266", +"#Z# c #aea09e", +"#JM c #aea0a9", +"aFC c #aea4a7", +"aEg c #aea5a8", +"aDb c #aea5aa", +"aIC c #aea5ab", +"aIB c #aea6ac", +"#yj c #aea9af", +"#bz c #aea9cd", +"awG c #aeabb2", +"aJg c #aebdc2", +"aMS c #af1d30", +"aKI c #af2a39", +"aDS c #af2b54", +"aF# c #af2c5a", +"aKt c #af4a66", +"#st c #af5534", +"##f c #af583c", +"#hj c #af6045", +"#WI c #af6641", +"#Wr c #af6643", +"#8D c #af6738", +"a#X c #af6932", +"ah6 c #af6a4b", +"aMX c #af6d71", +"#3N c #af7048", +"#8U c #af7456", +"#9D c #af9084", +"QtC c #af9f9c", +"aHe c #afa6ac", +"aIy c #afa7ae", +"aDi c #afa9ae", +"aFa c #b02959", +"#pF c #b04e3d", +"#v1 c #b05033", +"#Dq c #b05436", +"#70 c #b05c5b", +"#3L c #b06541", +".4Q c #b0674c", +"ac5 c #b06b30", +".4E c #b06c4c", +"#Ve c #b06f50", +"afH c #b07054", +".Qm c #b0735e", +"akk c #b07659", +"#dp c #b0795a", +"alo c #b0795d", +"#4C c #b07977", +"#wc c #b08f90", +"aeI c #b09081", +"#AD c #b0947b", +"aGm c #b095a5", +".z6 c #b09c93", +"#B7 c #b09d8b", +"#AR c #b0a097", +"aHa c #b0a4aa", +"aBP c #b0a9ad", +"aMR c #b1243c", +"aDU c #b12752", +"aGv c #b1567f", +"aeD c #b15830", +"#iO c #b1593d", +"#.6 c #b15e3a", +"#7# c #b16145", +"#L2 c #b16332", +"#ec c #b1633a", +"aa. c #b16341", +"aKP c #b1635e", +"#Nz c #b16737", +"ah. c #b16746", +"#Sn c #b1674a", +"#ip c #b16c49", +"#UQ c #b16d4c", +"a#Y c #b16e48", +".P7 c #b17263", +"#bS c #b17b5c", +".09 c #b18165", +".Rz c #b18a78", +"aOP c #b18d7f", +"aLQ c #b18d9e", +"arG c #b19387", +"ad2 c #b19d93", +".qy c #b19f8d", +"acq c #b1a08c", +".sm c #b1a193", +"ahP c #b1a29b", +"al5 c #b1a2a4", +".vb c #b1a38a", +"#zm c #b1a39f", +".gS c #b1aab3", +".sy c #b1adad", +"avE c #b1aeb0", +"axz c #b1afb6", +".mV c #b1b9c6", +"aDT c #b22a55", +"#RG c #b23836", +"#pE c #b2503e", +"#pD c #b2513b", +"#G# c #b25237", +"#Gb c #b2523c", +"#QW c #b25937", +"#cw c #b25a40", +"#Mg c #b25c3c", +"#h8 c #b25f3d", +".7P c #b2623d", +"#m6 c #b26448", +"#fp c #b2653e", +"#XE c #b2660f", +"#Xz c #b26627", +"#Qm c #b26d39", +"#XO c #b26e58", +"#a3 c #b27048", +"#Gu c #b27156", +"#nW c #b27740", +"als c #b27b5f", +"#yu c #b27c56", +"#FP c #b27f59", +".p# c #b28e6d", +"avx c #b29181", +"#Ba c #b294a3", +"#Bd c #b2a39b", +"aEJ c #b2a5ab", +"aH# c #b2a6ac", +"#98 c #b2a7a9", +"aFJ c #b2a7ac", +"aHg c #b2a9af", +"#wQ c #b2b2ba", +"#lT c #b35a42", +"#xA c #b35b3b", +"#dv c #b35d46", +"#Mn c #b35e39", +"#sw c #b36447", +".9K c #b36546", +"#id c #b3663b", +"#cF c #b3683c", +"#1Q c #b36949", +"#kn c #b36a44", +"#Kv c #b36b3e", +"#Ws c #b36b4a", +"#f# c #b37047", +"#.J c #b37f5f", +".W2 c #b3806b", +"#uq c #b38d8a", +"auX c #b39283", +"aAk c #b39789", +"aqV c #b3978d", +"afm c #b3a097", +"#zM c #b3a5b0", +"#9z c #b3a698", +"aIz c #b3abb2", +"#yi c #b3adb9", +"aFc c #b4295a", +"#e7 c #b45843", +"abH c #b45948", +"add c #b45a3c", +"#KT c #b45a41", +"#gQ c #b46040", +"#ld c #b46440", +"#vP c #b46448", +"a.d c #b4663f", +"#8C c #b4683d", +"a#V c #b46840", +"aby c #b4693f", +"#YY c #b46949", +"ac4 c #b46c37", +"#Px c #b46d54", +"abA c #b46e35", +".Qa c #b47262", +"#Jb c #b47557", +"ajn c #b47a63", +".7H c #b47e6f", +"ams c #b48064", +".1H c #b48969", +"awv c #b49383", +"aJH c #b4968a", +".z5 c #b49c80", +"#vj c #b49c9a", +"aMM c #b4a1b0", +"a#f c #b4a594", +".61 c #b4a5b6", +"agF c #b4a6a0", +"aiW c #b4a7a3", +".gN c #b4a7a5", +"#zN c #b4aaad", +".31 c #b4aabb", +".t0 c #b4afad", +".hZ c #b4b4c2", +"aFb c #b52b5c", +"aMU c #b52f39", +"#fe c #b55b3f", +"#xw c #b55e43", +"#n8 c #b5633c", +"#Tw c #b56640", +"#oM c #b5684d", +"a.e c #b56a3c", +"#V. c #b56d40", +"#U3 c #b5714f", +"abB c #b5734b", +"#13 c #b57752", +"ajh c #b57759", +"#af c #b58060", +"#bV c #b5806f", +"a.w c #b58267", +"#r8 c #b58366", +"#JH c #b58469", +"#kt c #b5857d", +".2O c #b5866b", +"#my c #b58869", +"aH3 c #b59591", +".wC c #b59f97", +".ph c #b5a99f", +"aEo c #b5aaaf", +"aIA c #b5aeb5", +"#pT c #b65c44", +"#KX c #b65e3e", +".9H c #b66330", +"#5c c #b66342", +"#6T c #b66542", +"a#9 c #b66545", +"#UL c #b66625", +"#UD c #b6662d", +"#V8 c #b66829", +"#fM c #b66847", +".7Y c #b66941", +"#Qy c #b66a40", +"#ll c #b66b3f", +"#cc c #b66b53", +".6d c #b67150", +"#z5 c #b67154", +"#Y7 c #b6732e", +".6t c #b67364", +"#C1 c #b67655", +".P8 c #b67768", +"#uo c #b67d4c", +"#Er c #b68167", +"#qJ c #b68468", +"#jE c #b68557", +"#B# c #b693a5", +"#E6 c #b6947e", +"auk c #b69585", +"awY c #b69586", +"azc c #b69a8c", +"aLH c #b69bad", +"#Be c #b6a290", +"agD c #b6a49c", +".pg c #b6aba1", +".y2 c #b6adc3", +"aEx c #b6aeb4", +"aEv c #b6afb4", +".t5 c #b6b2af", +"aHY c #b72a40", +"adc c #b7593d", +"#KY c #b7603d", +"#Tf c #b76330", +"#cv c #b7634a", +"abM c #b76845", +"#mW c #b76846", +"#Np c #b76936", +"#jS c #b76a3f", +"#lC c #b76c50", +"#Xy c #b76d20", +"a.v c #b76d4a", +"a#W c #b76e3d", +"ag1 c #b76e4f", +".ZY c #b77155", +"abP c #b77249", +"#5w c #b7734f", +"aHW c #b7747e", +"ac6 c #b7754c", +".ZH c #b7785c", +".UD c #b77e67", +"#N3 c #b77e6a", +".9q c #b78070", +"#k7 c #b78457", +".Zy c #b7856a", +"#TT c #b78672", +".Yw c #b7886c", +"atw c #b79687", +"asX c #b79689", +"aHG c #b798a9", +"aJe c #b798ac", +"axX c #b79b8e", +"#Ef c #b7a48d", +"aG1 c #b7adb1", +".dG c #b7b0b9", +"auy c #b7b5bc", +".l. c #b7b8bd", +".jH c #b7cce7", +"aMQ c #b83a54", +"#YW c #b84538", +"#jT c #b86339", +"#e6 c #b8634a", +"#mI c #b8643e", +"#mH c #b86540", +"#5b c #b86548", +"#lU c #b8664d", +"#mV c #b86743", +"#cu c #b86950", +"#I5 c #b86c3f", +"#sg c #b86c43", +"#LW c #b86d3c", +"a.f c #b8703e", +"#5v c #b8704d", +".ZX c #b87055", +"#Sm c #b8715a", +".70 c #b8765c", +".1B c #b8765e", +"#Ev c #b8795a", +".Q# c #b8796a", +"#0H c #b87a59", +"#2S c #b87d7d", +"#e1 c #b88162", +"#8V c #b88165", +"#3p c #b88263", +"alt c #b88266", +".Zz c #b88364", +".4s c #b88a71", +"#vr c #b88b70", +"alw c #b88d7d", +"aNZ c #b89385", +"axm c #b89788", +"#G0 c #b8a0a8", +"#G1 c #b8a3ad", +"#CK c #b8a58f", +"aaU c #b8a894", +"aLP c #b8a9b7", +".zI c #b8aa8b", +".zh c #b8acb7", +".py c #b8ad83", +"aDf c #b8b2b7", +"aAJ c #b8b6c0", +".q6 c #b8c3db", +"aMT c #b92435", +"adb c #b95a49", +"#Ki c #b9605d", +"ade c #b96443", +"#tZ c #b9654a", +"#4V c #b96a5f", +"#Sp c #b96c49", +"#sx c #b96d51", +".3a c #b96d54", +"#Tu c #b96f47", +".Z0 c #b97a5d", +".P9 c #b97a6b", +"#T# c #b97d60", +".W1 c #b9826d", +".Qt c #b98371", +"#w1 c #b98766", +"#k9 c #b98d7a", +".j# c #b99872", +"#yl c #b9a399", +".r6 c #b9a584", +"#Ci c #b9a796", +"#CJ c #b9a99e", +".af c #b9aab3", +"#zO c #b9aca7", +"#Za c #b9acaa", +".v6 c #b9aeb0", +"#KS c #ba6045", +"#aP c #ba6148", +"#LX c #ba6432", +"#RQ c #ba6435", +"#.5 c #ba6743", +"#gC c #ba6749", +"#Kw c #ba6839", +"#gV c #ba6843", +"#OW c #ba6934", +"#pu c #ba6a43", +"#n7 c #ba6a44", +"#nl c #ba6a51", +"aeu c #ba6b4a", +"#qT c #ba6d44", +"afK c #ba6d4d", +"#8T c #ba6d4e", +"#ls c #ba6e4b", +"agY c #ba7158", +"#un c #ba723f", +"aab c #ba734c", +"#l# c #ba755e", +".1f c #ba775f", +"#Dw c #ba794b", +".ZI c #ba7a5e", +".Q. c #ba7b6b", +".Qn c #ba7d66", +".Qr c #ba7e6a", +".4x c #ba7f69", +"aln c #ba8467", +"alr c #ba8468", +"amA c #ba9385", +".qt c #ba9778", +"#7X c #ba9d94", +"a#i c #baa695", +".Cq c #baaeb5", +"#yk c #baafad", +"aEs c #bab0b4", +"aHd c #bab1b7", +"aBM c #bab2b6", +".p5 c #bab8d8", +".sP c #bab9d4", +"aLY c #bb1c26", +"#Ga c #bb5b43", +"#ri c #bb5e48", +"#F5 c #bb653f", +"#Th c #bb681e", +"#ND c #bb693b", +".9J c #bb6943", +"#UG c #bb6b1e", +"#p8 c #bb6b4f", +"aet c #bb6c4b", +"#fa c #bb6f4c", +"#tL c #bb6f4e", +"#V7 c #bb7024", +"#L6 c #bb723e", +"#Gm c #bb723f", +"#R4 c #bb7344", +".Yl c #bb7b5e", +".Qs c #bb816e", +".1. c #bb8769", +"#ai c #bb8f7b", +"aHM c #bb9ab5", +"##A c #bba1b2", +"a#j c #bba493", +"#6h c #bba7a4", +"aIJ c #bba9b2", +"aaY c #bbaa97", +"aDg c #bbb5ba", +".yq c #bbb5bc", +"aHS c #bbb5c2", +"aLT c #bc2641", +"aDV c #bc305c", +"#cx c #bc6246", +"#ox c #bc634b", +"aeE c #bc663b", +"#Da c #bc674d", +"afI c #bc684b", +"#xo c #bc684d", +"#ov c #bc684f", +"afV c #bc6c45", +"#aZ c #bc6d4e", +"#UC c #bc6e2b", +"#qU c #bc6e45", +"#mE c #bc744d", +"#Mx c #bc7454", +"#N2 c #bc7458", +"#QY c #bc755d", +"#sR c #bc7a50", +"#a1 c #bc7c53", +".ZG c #bc7c61", +".Qq c #bc7e6a", +"akm c #bc8265", +"#My c #bc836a", +"#E5 c #bc8371", +"#tC c #bc8a5d", +".93 c #bc8b7d", +".1J c #bc9274", +"aoq c #bc9e93", +"a#g c #bca493", +"afn c #bcaca5", +"ad6 c #bcb0ad", +"aHc c #bcb3b9", +"aBN c #bcb5b9", +".ik c #bcc6da", +"#mN c #bd6e44", +"#gH c #bd6e48", +"#pw c #bd7045", +"##. c #bd704a", +"#U4 c #bd705e", +"#XC c #bd732f", +"#U1 c #bd7552", +"ac2 c #bd7555", +"#gE c #bd764e", +"#U2 c #bd7755", +".2U c #bd7b65", +"#Zd c #bd8065", +"##x c #bd8166", +"#12 c #bd825c", +"#QZ c #bd8574", +".1G c #bd8e70", +"aIg c #bd998a", +"aM5 c #bd998b", +"#6j c #bd9d98", +"aIh c #bd9f93", +"#Gy c #bda19c", +".uX c #bda594", +"#9C c #bda597", +"#zP c #bda89a", +"#Bb c #bda8b2", +"#0D c #bdb0aa", +"aEq c #bdb3b8", +"aDd c #bdb5b9", +"aEu c #bdb5bb", +"#lf c #be6743", +"#C5 c #be6a42", +".9z c #be6d48", +"aL0 c #be6e6f", +"#sh c #be7147", +"afL c #be7150", +"#cE c #be7347", +"#0u c #be744d", +".4P c #be745a", +"#TN c #be7843", +"#gG c #be794d", +"#p4 c #be7950", +"ah5 c #be7a5b", +"#Uy c #be7e61", +"#Ze c #be7e63", +".4u c #be8671", +"#Py c #be8674", +"#sa c #be895c", +".Yv c #be8a6e", +"#vo c #be8d60", +"#I# c #be8d6f", +".Z7 c #be8f6e", +"#qM c #be9374", +"aad c #be9784", +"aBz c #be9a86", +"aAj c #be9a87", +"#DP c #beac95", +"aLL c #beacbb", +"aaX c #bead99", +"aHb c #beb2b7", +"aDc c #beb5b9", +"aFT c #beb6bc", +".DP c #bebcd9", +"aKN c #bf1f1f", +"aes c #bf5b3a", +"aLz c #bf614a", +"#Qo c #bf662d", +".9I c #bf6b3e", +"abL c #bf6c4b", +"#Qn c #bf6e37", +"#tN c #bf6f4a", +"#pv c #bf7047", +"#8B c #bf704b", +"#8A c #bf7051", +"#O8 c #bf7145", +"#Qz c #bf7148", +"#eb c #bf7249", +"#W# c #bf7327", +"#ct c #bf735a", +"#XB c #bf7436", +"aeq c #bf785b", +"#R1 c #bf7e4c", +"ajg c #bf8062", +"#7b c #bf8160", +".Qo c #bf836b", +"#RK c #bf8468", +".Hl c #bf8553", +"#tG c #bf8d6c", +"#vs c #bf8e6c", +"#pk c #bf8e71", +"#Eb c #bf97a5", +"asW c #bf9988", +"aJG c #bf9a8c", +"aCY c #bf9b88", +"apZ c #bfa196", +"#FH c #bfa89d", +"#Ie c #bfa8a7", +"#FF c #bfaaa4", +".x0 c #bfac86", +"aEF c #bfb1b7", +"QtF c #bfb2ba", +"aEr c #bfb5ba", +".6W c #bfb89d", +".0y c #bfb8c9", +".oj c #bfd1f9", +"#Wb c #c06701", +"#cr c #c0684f", +"#cm c #c06942", +"#RS c #c06b24", +"#W. c #c0722e", +"#tM c #c07251", +"#O7 c #c07447", +"#qV c #c07449", +"#sk c #c07548", +"ag0 c #c07757", +"#04 c #c07955", +"#e9 c #c07c56", +"#rs c #c07d53", +"#Bu c #c07e5b", +"#WJ c #c08060", +"#5x c #c08560", +"#FZ c #c08952", +"alq c #c08a6e", +"aCX c #c0967d", +"aEc c #c0967e", +"aKX c #c09c8e", +"#Ee c #c0afa3", +".zG c #c0b091", +".AS c #c0b4bb", +"aDp c #c0b5ba", +".o. c #c0b78f", +"aBO c #c0b9bd", +"#vJ c #c16443", +"#Dc c #c1684e", +"#vY c #c16b4f", +"#xB c #c16d4c", +"#F4 c #c16e46", +"#vz c #c1724d", +"#aG c #c1734d", +"#O6 c #c17649", +"a#U c #c17657", +"#lQ c #c17a54", +"#Tr c #c17f54", +"#oI c #c18057", +"#XN c #c18069", +".Yn c #c18362", +".Qp c #c1856c", +".FM c #c18858", +".6x c #c18c78", +".Tx c #c1936c", +"#Bl c #c19479", +"aBy c #c1977f", +".nM c #c19a7d", +"azb c #c19d89", +"aFy c #c19d8a", +"QtE c #c1a387", +"#CH c #c1a6ae", +"#Hn c #c1a89c", +".xX c #c1ad8f", +"#Hm c #c1ada5", +"acu c #c1b4a0", +".v0 c #c1b5b8", +".zg c #c1b5bb", +"aEp c #c1b7bc", +"aFS c #c1b9be", +".84 c #c1b9c0", +".Cc c #c1bdd4", +"#Nq c #c26935", +"#Aw c #c26e4c", +"#oA c #c2704f", +"#ih c #c2714e", +"#6S c #c27153", +"#o. c #c2754a", +"#lP c #c2754e", +"a.c c #c27557", +"#5a c #c27559", +"##a c #c2764f", +"#nm c #c27a5f", +"#2m c #c27f60", +".1g c #c27f67", +".6v c #c28071", +"#dy c #c28361", +".7L c #c28478", +".7K c #c28579", +"#0G c #c28866", +".6r c #c28868", +"#K9 c #c2886c", +"#FD c #c28880", +"#11 c #c28a63", +"#Nl c #c28c4f", +"#7c c #c28c6c", +".Z6 c #c29271", +"#w0 c #c2967a", +".1I c #c29777", +".mj c #c29983", +"av3 c #c29a85", +"aM4 c #c29a8b", +"#2R c #c29d9d", +"aqT c #c29e8e", +"#Ec c #c2a4ac", +"#IU c #c2aaa2", +"#r6 c #c2ab9c", +"#7V c #c2aca5", +".By c #c2ae99", +"#FG c #c2afa8", +"#CI c #c2b0af", +"aII c #c2b0b9", +"ad3 c #c2b1aa", +"anC c #c2b5bb", +"aEt c #c2b7bc", +".D2 c #c2b7bd", +"aDe c #c2b9bd", +".5A c #c2b9cb", +"aBF c #c2bcbe", +"#EU c #c36245", +"#gM c #c3674a", +"#KR c #c36a4c", +"#xd c #c36c46", +"#Db c #c36d52", +"#oh c #c36e59", +"#3s c #c36f52", +"aH1 c #c36f70", +"#Te c #c37040", +"#n9 c #c3734a", +"#3r c #c37559", +"#Y5 c #c37946", +"#ng c #c37951", +"#U8 c #c37956", +"#ZB c #c37c56", +".1e c #c38068", +".ZF c #c38367", +"#Yb c #c38565", +"#sS c #c3875d", +".Va c #c38778", +".XZ c #c38c70", +"#wX c #c39164", +"#zX c #c3967b", +"#CS c #c3977b", +"#ZD c #c39781", +".kF c #c39989", +"awX c #c39b86", +"avw c #c39b87", +"aL8 c #c39b8b", +"axl c #c39c87", +"aNY c #c39c8c", +"axW c #c39f8c", +"#Hl c #c3aba5", +"#4A c #c3abaa", +"#7W c #c3aca4", +"ahQ c #c3b5af", +"ayl c #c3b7c4", +".VM c #c3b8c5", +"aEh c #c3bbbe", +"azw c #c3c2cb", +"aHX c #c45b6b", +"#gJ c #c4624a", +"#h# c #c46942", +"#OX c #c46b34", +"#cy c #c46c4d", +"#R7 c #c47548", +"#xp c #c47556", +".ZW c #c47960", +"#R5 c #c47a4c", +"#fu c #c47b56", +"#yA c #c47b5c", +"#6l c #c47b79", +"#U9 c #c47c53", +"#2n c #c47d59", +".6u c #c48071", +"#Wk c #c48168", +"a#T c #c4826b", +".Ym c #c48867", +"#8y c #c48871", +"#2p c #c48968", +"#HF c #c48a5d", +"akj c #c48a6d", +".Wq c #c48b72", +".D7 c #c48c5f", +"#qN c #c48c61", +"#Ej c #c49366", +"#5y c #c49574", +"aAi c #c49a82", +".S2 c #c49b8a", +"arE c #c49e8c", +".94 c #c4a6a3", +"#IV c #c4a99c", +"#wS c #c4ada7", +"#Ed c #c4afae", +"aF1 c #c4b4bb", +"#.i c #c4bcc1", +".8T c #c4bdaf", +".oz c #c4c0d6", +"QtY c #c4c2cf", +"#gI c #c56b4d", +"#KK c #c57141", +"#N1 c #c5714e", +"#D# c #c57156", +"#pt c #c57850", +"#Tv c #c57851", +"ac3 c #c57a4f", +"#sB c #c57b5e", +"#lV c #c57b61", +"#oH c #c57e55", +"#Xu c #c57e5f", +"ah3 c #c57f6c", +"#g2 c #c5825c", +".4w c #c58372", +"#6Q c #c5876d", +".4v c #c58774", +"#LS c #c59057", +".Cv c #c59068", +".R2 c #c59274", +"#CP c #c59466", +"#3O c #c5946f", +"#AC c #c59472", +"#nY c #c59679", +"aKW c #c59d8d", +"aOO c #c59e8e", +"acx c #c59e98", +".eZ c #c5a191", +"#4B c #c5a29f", +"#CM c #c5a884", +"aJa c #c5aabc", +".z4 c #c5ac90", +"acs c #c5b09b", +".Bz c #c5b19d", +".ws c #c5b28c", +"#Bc c #c5b6b7", +"aFK c #c5b9bf", +"aFO c #c5babf", +".vr c #c5bed3", +".xc c #c5bed6", +".kK c #c5c4d4", +"#Tj c #c66002", +"#q4 c #c66554", +"#Nd c #c6686a", +"#jN c #c66f4c", +"#gP c #c67051", +"#Mo c #c6734b", +"#cA c #c6734f", +"#QA c #c6754c", +"#lj c #c6794e", +"#hi c #c6795e", +"#w8 c #c67a58", +"#ii c #c67c5b", +"#sy c #c67d63", +"#ij c #c68363", +".Yk c #c68367", +".1C c #c6846c", +"#Wi c #c6866c", +"ajf c #c68769", +"#rt c #c6895f", +".2Q c #c68e74", +"#OS c #c68f50", +"#10 c #c69068", +"alp c #c69074", +"#FM c #c69164", +".Yu c #c69175", +"#Hs c #c69263", +"aHQ c #c693b3", +"#yq c #c69467", +"#Bi c #c69567", +"#Gt c #c69576", +"#ys c #c6997e", +"#Yc c #c69984", +"aza c #c69c84", +"auW c #c69e89", +"asl c #c69e8b", +"ajc c #c69e8f", +"asm c #c6a292", +"apY c #c6a496", +"#Kl c #c6a79f", +"#Eg c #c6ae8e", +"ad4 c #c6b6af", +".FH c #c6bbc1", +".pY c #c6c2d0", +"aye c #c6c4ce", +"#Di c #c7674e", +"#vH c #c76d4a", +"#ha c #c76e44", +"#jO c #c76e4a", +"#ig c #c76f4b", +"#BX c #c76f4f", +"#KZ c #c7704a", +"#iQ c #c7715a", +"#Tn c #c7723e", +"#QV c #c77250", +"#oF c #c77750", +"#oG c #c77751", +"#K4 c #c7794d", +"#cH c #c7794f", +"#si c #c77b50", +"### c #c77b54", +"#px c #c77c50", +"#U7 c #c77c5e", +"#sf c #c77d55", +"#K8 c #c77e5b", +"#2o c #c7825c", +"#E2 c #c78558", +"#XF c #c7863f", +".ZE c #c7886c", +"#B5 c #c78973", +"akh c #c78d70", +".V# c #c78f68", +".79 c #c79078", +".AX c #c79471", +".2P c #c79478", +".4t c #c7947d", +"#zU c #c79568", +".Z5 c #c79674", +"#WK c #c79984", +"axk c #c79a81", +"#Kt c #c79b6e", +"#Em c #c79b7f", +"axV c #c79d85", +"aHI c #c79eb6", +"aPK c #c79f8f", +".aq c #c7a585", +"apb c #c7a698", +"#zQ c #c7a893", +"aaV c #c7ae9b", +".wo c #c7b396", +".yp c #c7b9b8", +"#0C c #c7bcbc", +"aB0 c #c7bec3", +"aFR c #c7bec4", +".DO c #c7c4e5", +"aLU c #c81831", +"#Tm c #c8681e", +"#pA c #c86947", +"#Gf c #c86a51", +"#g9 c #c86f4c", +"#EB c #c8714c", +"#cd c #c8714e", +"#gR c #c87554", +".9y c #c87651", +"#NU c #c8774b", +"#NT c #c8774d", +"#p2 c #c87851", +"#Tx c #c87852", +".9A c #c87853", +"#H5 c #c87946", +"#JB c #c87949", +"#QU c #c87b5a", +".3# c #c87d63", +"#KF c #c87f4c", +"#nh c #c8845d", +"#Az c #c88464", +"#a2 c #c8865e", +"#v7 c #c88a5f", +"#p5 c #c88a60", +"#XM c #c88a72", +".2R c #c88b71", +".Z4 c #c89272", +"#w2 c #c8936c", +"#I0 c #c89462", +"#3c c #c89a6a", +"#up c #c89b81", +"#I2 c #c89d73", +"#06 c #c89e87", +"asV c #c89f8b", +"auj c #c8a08c", +"aJF c #c8a090", +".bW c #c8a48e", +".hR c #c8a686", +"#xK c #c8a898", +"#Km c #c8a89a", +"#9A c #c8b1a5", +".v9 c #c8b293", +"ak# c #c8b8c5", +"aFQ c #c8c0c6", +"##Y c #c8c2da", +".ld c #c8d8e2", +"#Xs c #c96253", +"#ED c #c96c4b", +"#ob c #c96f48", +"aer c #c96f50", +"#LZ c #c97024", +"#HN c #c97048", +"#3. c #c97166", +"#Mf c #c97350", +"#kp c #c97357", +"#mD c #c97547", +"#yG c #c9754d", +"#mJ c #c9774f", +"#UF c #c97834", +"#aF c #c97853", +"#Td c #c9793f", +".7W c #c97a44", +"#NB c #c97c4c", +"#w9 c #c97c52", +"#Jc c #c97f4e", +"#sW c #c98265", +"#QT c #c98364", +"#tJ c #c98553", +".RO c #c98569", +"#ix c #c98666", +"#V3 c #c98668", +"#R2 c #c98755", +"#ry c #c9896b", +".ZD c #c9896d", +"#y8 c #c98a5e", +"#JG c #c98e6e", +".W0 c #c9907b", +".ZA c #c9916e", +"#Bp c #c9926c", +"#Kr c #c9955f", +"atu c #c99c83", +"#CG c #c9a4b3", +".FI c #c9bdc8", +"#8u c #c9bfbf", +"aah c #c9c2c7", +".Cg c #c9c3df", +"aw. c #c9c5c8", +".rp c #c9c8de", +".tR c #c9d4e7", +"aKM c #ca1115", +"#gL c #ca6c50", +"#if c #ca6d48", +"#S8 c #ca6e61", +"#BB c #ca704d", +"##b c #ca7342", +"#lg c #ca744f", +"#Jh c #ca7548", +"#nf c #ca7752", +"#QX c #ca7758", +"#ef c #ca7952", +"#NC c #ca7b4c", +"#um c #ca7b60", +"#o# c #ca7f53", +"#qW c #ca8054", +"abx c #ca8162", +"#cB c #ca8255", +"#3M c #ca835d", +"#q. c #ca886a", +".Yq c #ca8969", +".9x c #ca8a7d", +"#Hk c #ca8a83", +"aje c #ca8b6d", +".ZJ c #ca8b6f", +"#Zc c #ca8f73", +".WZ c #ca936d", +".Yt c #ca9571", +"amt c #ca9579", +"#Qg c #ca9a7c", +"#Vf c #ca9a85", +"awW c #ca9d84", +"atv c #caa28d", +"aIf c #caa292", +"#zb c #caa483", +"apc c #caab9e", +"#9B c #cab3a6", +"aHn c #cab9c0", +"aF4 c #cabac1", +".ig c #cabcc3", +".dC c #cabfc5", +".YY c #cac2d5", +"aux c #cac7ce", +"#O2 c #cb6211", +"#BV c #cb6b4e", +"#v2 c #cb6e50", +"##e c #cb714e", +"#i. c #cb7350", +"#HL c #cb754c", +"#EA c #cb774f", +"#NV c #cb7b4c", +"#UW c #cb7b50", +"#Ky c #cb7c3d", +"#vD c #cb7c50", +"#tQ c #cb7d51", +"#KG c #cb804d", +"#qS c #cb8058", +"#IR c #cb807b", +"#L7 c #cb814e", +"#he c #cb8265", +"#cC c #cb8356", +"#sz c #cb8369", +"#05 c #cb845f", +"#tI c #cb8850", +"#gF c #cb885c", +".21 c #cb8968", +"#9F c #cb8a86", +"#B2 c #cb8b5e", +"#a0 c #cb8b62", +"#dz c #cb906b", +"#wa c #cb9567", +".zl c #cb9a7b", +".Tw c #cb9b74", +".hL c #cb9d7c", +"aEb c #cb9d82", +"aui c #cb9e85", +".kx c #cba085", +".nE c #cba183", +".o2 c #cba281", +"#IX c #cba68e", +".du c #cba797", +"#LN c #cba798", +"apX c #cba899", +"#ym c #cbaa99", +"#Hp c #cbac94", +"#FJ c #cbb098", +"aLI c #cbb3c4", +"act c #cbbca6", +".AE c #cbc6d2", +"aq3 c #cbc7cd", +"aut c #cbc8ca", +"aij c #cbc8cc", +"#Qu c #cc5e1e", +"aLG c #cc677c", +"#Nv c #cc6f2c", +"#BC c #cc704e", +"#oc c #cc724f", +"#Pv c #cc7450", +"#gA c #cc745c", +"#ib c #cc7750", +"#xu c #cc775a", +"#NK c #cc7954", +".9B c #cc7c57", +"#vA c #cc7d50", +"#vU c #cc7e5f", +"#L8 c #cc7f4d", +"#y6 c #cc7f5b", +"#fL c #cc805e", +"#hh c #cc8064", +"#Qx c #cc8156", +"#UZ c #cc8159", +"#sA c #cc8366", +"#Ya c #cc845f", +"#oN c #cc876b", +".4F c #cc8867", +".Yo c #cc8b6b", +".2S c #cc8d73", +"#uk c #cc8e63", +"#Gs c #cc916d", +"#8W c #cc9d85", +".eQ c #cc9f7d", +"aFx c #cc9f84", +".S3 c #cca18e", +".rH c #cca37d", +".qk c #cca37f", +".s4 c #cca47c", +"QtM c #ccab85", +"#IW c #ccab98", +"#IT c #ccaca7", +"#Ho c #ccb09d", +".qI c #ccb698", +"#qG c #ccb7aa", +"ad5 c #ccbeb8", +".AR c #ccc1c3", +".FG c #ccc1c4", +"aFF c #ccc3c6", +"aC5 c #ccc5c7", +".Ft c #cccaf2", +"aKL c #cd0a13", +"aKJ c #cd2935", +"aJm c #cd2e2a", +"#Qp c #cd6820", +"#RT c #cd6917", +"afJ c #cd6e50", +"#fl c #cd7250", +"#KP c #cd7452", +"#KQ c #cd7454", +"#ff c #cd7458", +"agZ c #cd775e", +"#cz c #cd7855", +"#xn c #cd795e", +"#O9 c #cd7c51", +"#fy c #cd7d5a", +"#dK c #cd7e62", +"#mK c #cd7f54", +"#U5 c #cd806c", +"#RN c #cd813e", +"#sj c #cd8256", +"#U0 c #cd845e", +".4O c #cd866b", +"#Uz c #cd8823", +"#3q c #cd8a6d", +".1h c #cd8a72", +".2T c #cd8b75", +"#Ay c #cd8d60", +"#C2 c #cd8d67", +".Yr c #cd906f", +".Ys c #cd9673", +"#OP c #cd9c7f", +".ah c #cda07e", +"aCW c #cda085", +"awu c #cda087", +"#Ko c #cda28a", +".ut c #cda47b", +"#yn c #cda490", +"#LM c #cda49d", +"#ty c #cda59f", +".gs c #cda993", +"#FE c #cdaaa3", +"aop c #cdaea1", +".wx c #cdaf8a", +"a#h c #cdb7a6", +"aaZ c #cdb7a7", +".BA c #cdbaa5", +"aMF c #cdbac9", +"#Ch c #cdbca1", +".qY c #cdbdb2", +".sc c #cdbe8c", +"aEG c #cdbfc5", +"aoL c #cdcace", +".sQ c #cdcce8", +"aLX c #ce1c28", +"aJo c #ce1f1a", +"#OZ c #ce6915", +"#fE c #ce744c", +"#y4 c #ce7a59", +"#ne c #ce7f57", +"#QF c #ce8053", +"#p3 c #ce8059", +"#iN c #ce805f", +"#B1 c #ce825e", +"#Tc c #ce833a", +"#UB c #ce8431", +"#V6 c #ce852a", +"a.b c #ce8871", +"#Qj c #ce9655", +".1# c #ce9676", +"#Qe c #ce9b8c", +"#ON c #ce9e8f", +"#LU c #ce9f6f", +"axU c #cea085", +"aGU c #cea186", +"auV c #cea188", +"#Ng c #cea394", +"#vk c #cea79f", +"aqU c #ceada0", +"#Eh c #ceb28d", +"#6i c #ceb6b2", +".uY c #ceb995", +"afo c #cebfb8", +".uS c #cec29c", +"aFP c #cec2c8", +".xA c #cec2cc", +"aFN c #cec3c8", +"#bv c #cec4c9", +".pU c #cec9d7", +".lJ c #cec9ed", +".aC c #cecbd7", +".ua c #cecdd6", +".pJ c #cedaf8", +"aLV c #cf0f26", +"#OY c #cf6a1f", +"#Ns c #cf6f1d", +"#cq c #cf745a", +"#Dd c #cf745b", +"#i# c #cf7552", +"#KO c #cf7651", +"#C7 c #cf7652", +"aMw c #cf7662", +"#M# c #cf7a4a", +"#cs c #cf7a61", +"#jU c #cf7c53", +"#NJ c #cf7c55", +"#oL c #cf7c61", +"#dH c #cf7c62", +"#XD c #cf7e1e", +"#yF c #cf7e54", +"#NS c #cf7e57", +"#.8 c #cf7f59", +".9F c #cf825b", +"#mL c #cf8358", +"#Pt c #cf8360", +"#rr c #cf845c", +"#iM c #cf8560", +"#j3 c #cf8561", +"#yB c #cf865e", +"#hf c #cf8669", +"#Wd c #cf873f", +".6m c #cf886d", +"#z7 c #cf895e", +".RA c #cf8d77", +"#xE c #cf9165", +"#RI c #cf9178", +"#ZC c #cf9372", +".Z3 c #cf9476", +".1E c #cf967b", +"#Ni c #cf9f83", +"#LP c #cfa087", +".bN c #cfa180", +".i3 c #cfa280", +"ask c #cfa691", +"#wb c #cfa791", +"#zR c #cfaa90", +"#Kn c #cfaa96", +"arF c #cfac9d", +"#FI c #cfb5a2", +"acr c #cfb6a1", +"aaW c #cfbaa6", +".xZ c #cfbc98", +".uQ c #cfbe8d", +"aEk c #cfc7c9", +".k1 c #cfcbbc", +"axv c #cfccce", +"QtR c #cfcde9", +"aKK c #d01721", +"#Nt c #d0711c", +"#ud c #d07154", +"#Gl c #d0744d", +"#Jw c #d07653", +"#dZ c #d0785a", +"#RR c #d07b3f", +"#qX c #d07b51", +"#Tg c #d07c3d", +"#hb c #d07c4d", +"#Mr c #d07d50", +"#NH c #d07d53", +"#u# c #d07d60", +"#Wc c #d07e24", +"#fo c #d07e5a", +"#vW c #d07e61", +"#D. c #d07e62", +"#tO c #d07f51", +"#TB c #d07f52", +"#xr c #d07f61", +"#ic c #d08056", +"#xC c #d0805e", +"#UY c #d08259", +"#ul c #d08a6a", +".1d c #d08c74", +"#vv c #d08d5c", +"aJr c #d08d80", +".RZ c #d09270", +"#Ql c #d09561", +"aH2 c #d09695", +"#0F c #d09775", +"#z0 c #d09969", +"#RJ c #d0997c", +"#n1 c #d0997e", +"#pn c #d0a083", +"avv c #d0a38a", +"arD c #d0a895", +".my c #d0bc8f", +".vq c #d0c8dd", +".kZ c #d0c9b2", +"auv c #d0cdd0", +".um c #d0d7f5", +"#RY c #d17138", +"#UJ c #d17211", +"#d6 c #d17654", +"#Dl c #d1775c", +"#Qc c #d1777b", +"#Jx c #d17852", +"#pH c #d17b59", +"#le c #d17d59", +"#v4 c #d17d5c", +"#Pw c #d17d5d", +"#M. c #d17f4f", +"#mM c #d18055", +"#Gn c #d1814d", +"#ed c #d18159", +"#y5 c #d1815e", +"#km c #d1825d", +"#R6 c #d18456", +"#rv c #d18666", +"#hg c #d1866a", +"#U6 c #d1866d", +"#sQ c #d1875f", +"#TM c #d18a58", +"#iF c #d18a64", +"#V4 c #d18c1f", +".RM c #d18d71", +".Z1 c #d18f73", +"#sX c #d19274", +"#po c #d1956b", +".xE c #d1a085", +"az# c #d1a489", +"#wU c #d1a495", +"#Hu c #d1a57e", +"#DA c #d1ab93", +"#wT c #d1aca0", +"#Bf c #d1b59b", +"aHm c #d1c0c8", +".b4 c #d1c3c6", +"aFM c #d1c6cb", +".Cn c #d1c7c4", +"#c7 c #d1c9d3", +".Ch c #d1c9f1", +".Au c #d1cdef", +"auw c #d1cfd5", +"aHZ c #d2304a", +"#EV c #d27255", +"#Nu c #d27423", +"#LY c #d27628", +"##d c #d2784f", +"#rg c #d27961", +"#V1 c #d27966", +"#HM c #d27a51", +"#v3 c #d27a5a", +"#Mw c #d27d57", +"#rf c #d27d64", +"#NI c #d27f56", +"#fk c #d28062", +"#TG c #d2806b", +"#L1 c #d28143", +"aMz c #d28177", +"#R8 c #d28255", +"#UX c #d28257", +".7Q c #d2825d", +"#xa c #d28357", +"#.9 c #d2845e", +"#NA c #d28757", +"#HG c #d2875a", +"#V2 c #d2886e", +".RN c #d28d72", +"#S9 c #d28f77", +"#Wj c #d29177", +"#Ew c #d2936e", +".ZC c #d29870", +".UE c #d29879", +"#mx c #d29d71", +".7G c #d29e8f", +"#UO c #d2a098", +"#Qf c #d2a38a", +"aBx c #d2a58a", +"aqS c #d2ad9b", +"#3P c #d2b096", +"#Bg c #d2b192", +"a#k c #d2b1a4", +".yj c #d2b594", +".v8 c #d2bb9e", +".ty c #d2bbab", +".64 c #d2c0bd", +"aFZ c #d2c2c9", +"##U c #d2c6c6", +"#1X c #d2c6c7", +"au8 c #d2ced1", +"aLW c #d30c20", +"#Tk c #d36b0c", +"#O0 c #d36b0f", +"#UI c #d3710a", +"#Dj c #d3745b", +"#sI c #d3765a", +"#h. c #d37853", +"#Dm c #d37a5f", +"#NG c #d37b4f", +"#m8 c #d37f66", +"#TA c #d38051", +"#xt c #d38063", +".9G c #d3814a", +"#NR c #d3815d", +"#NQ c #d3815e", +"#re c #d38267", +"#vV c #d38365", +"#tR c #d38459", +"#dL c #d38468", +"#cI c #d3855a", +"#lN c #d3855d", +"#vy c #d38667", +".1v c #d3866d", +"#Dv c #d38763", +".Yp c #d39071", +"#qP c #d3986e", +"#I. c #d39876", +".R0 c #d39978", +"#nX c #d39c70", +"#vt c #d39d77", +"#Nf c #d39f99", +"#IS c #d3a19c", +"aAh c #d3a68b", +"adj c #d3b09b", +"a.. c #d3b1a0", +".tN c #d3bfb2", +".x5 c #d3c0ac", +".ax c #d3c0c0", +"#XK c #d3c1c0", +".uR c #d3c397", +".6Y c #d3c7c9", +"#6M c #d3c9c7", +".e7 c #d3cdd6", +"axy c #d3d0d7", +"#RX c #d46a1d", +"#RU c #d46c15", +"#C8 c #d47957", +"#Jj c #d47b50", +"#gO c #d47d5e", +"#QC c #d47e57", +"#NZ c #d47f58", +"#BY c #d4805f", +"#Mq c #d48154", +"#P. c #d48157", +"#xb c #d48359", +"#uh c #d48462", +"#vB c #d48553", +"#E0 c #d48764", +"#HH c #d4885b", +"#sl c #d4885d", +".ZV c #d4886f", +"#Jd c #d48959", +"#Ms c #d48d66", +"#Ta c #d48e32", +"#z6 c #d48e69", +"#Ps c #d48e6d", +"#xF c #d48f6f", +".RL c #d48f74", +"#ka c #d49071", +"#xI c #d49467", +".RY c #d49470", +"#pq c #d4956c", +".WY c #d49a75", +"aki c #d49a7d", +"#OU c #d49c68", +".X0 c #d49c79", +"#Nn c #d4a16f", +".Tv c #d4a179", +".1F c #d4a184", +"#Kk c #d4aaa6", +".wt c #d4b590", +"ane c #d4b5a9", +".wp c #d4c0a0", +".kU c #d4c18d", +"#DO c #d4c2a2", +"acv c #d4c3b2", +"##W c #d4cbd2", +"aw9 c #d4d2d8", +"aJn c #d52b26", +"#O1 c #d56d0f", +"#Qq c #d56d1a", +"#HO c #d57856", +"#Dr c #d57c5d", +"#uf c #d57d5d", +"#g8 c #d57e5b", +"#yZ c #d57e62", +"#RP c #d58156", +"#Xt c #d58168", +"#lh c #d5825a", +"#xs c #d58265", +"#fP c #d58464", +"#jR c #d5855b", +"#iL c #d58560", +"#li c #d5865c", +"#Mt c #d5875e", +"#Ax c #d58865", +"#Uw c #d58872", +"#y7 c #d58965", +".1u c #d58970", +"#g7 c #d58a63", +"#E1 c #d58a66", +"#p9 c #d58a6f", +".ZU c #d58b71", +"#8z c #d58d74", +".RQ c #d59175", +".RR c #d59275", +".1i c #d5927a", +".RB c #d5937c", +"#5# c #d59478", +".S5 c #d5967b", +"#OM c #d59993", +"aMy c #d59a82", +"#zT c #d59c60", +"#3a c #d59e62", +".R1 c #d59e7f", +"#tH c #d59f79", +"#vl c #d5a499", +".vR c #d5a58b", +"#OO c #d5a78f", +"#LO c #d5ac96", +".yh c #d5b896", +"agE c #d5c6bf", +"aFL c #d5cacf", +".e8 c #d5cdd6", +".gA c #d5d2de", +"apB c #d5d4d7", +".sM c #d5d5da", +".sL c #d5d5e2", +"#HP c #d67958", +"#Gi c #d67959", +"#Dk c #d67a60", +"#q0 c #d67c5d", +"#L0 c #d68139", +"#Me c #d6815b", +"#NL c #d68260", +"#OL c #d68285", +"#q8 c #d68465", +"#By c #d6855b", +"#x. c #d68958", +"#ui c #d68965", +"#Xx c #d68e31", +".RP c #d69277", +"#Qd c #d6948f", +".S6 c #d6977c", +"#Bq c #d69c7f", +"#CO c #d69d61", +"awt c #d6a68b", +".dl c #d6a887", +"#9E c #d6a89f", +"asU c #d6ab96", +"#Ks c #d6ac7b", +".z3 c #d6bda2", +".8P c #d6c6c3", +"anK c #d6d4d9", +"#gB c #d77864", +"#yJ c #d77b59", +"#Jk c #d77c58", +"#dM c #d77d5b", +"#dY c #d77e5f", +"#vG c #d7805b", +"#Kx c #d7833d", +"#sO c #d7855f", +"#yE c #d7885d", +"#TD c #d78c62", +"#iG c #d78d66", +"#JF c #d78d67", +"#K3 c #d78f64", +"#NX c #d7916d", +"#vu c #d7965f", +"#Nk c #d79753", +"a#m c #d79895", +"#mB c #d79980", +"#yp c #d79e62", +".Tu c #d7a177", +"av2 c #d7a78c", +"#LT c #d7aa76", +"QtD c #d7aa88", +"#Nh c #d7ab94", +".y# c #d7c2ad", +".uZ c #d7c590", +"#47 c #d7ceca", +".n6 c #d7cfcd", +".jg c #d7dbec", +"#Qr c #d86d0d", +"#UH c #d8750c", +"#Dh c #d8765e", +"#aL c #d87e54", +"#ce c #d8815e", +"#Mu c #d88258", +"#jQ c #d8835c", +"#cf c #d8845f", +"#Mp c #d8855a", +"#rp c #d88760", +"#NW c #d88859", +"#rq c #d88860", +"#gW c #d88c65", +"#ks c #d88c72", +"#H9 c #d88e65", +"#H4 c #d88f5e", +"#yC c #d88f61", +".3. c #d89176", +"#vw c #d8926a", +"#j4 c #d8926f", +"#n4 c #d8946c", +"#Tq c #d8976c", +".1j c #d89979", +"#7Z c #d89c97", +".ZB c #d89e79", +"#Ei c #d89f62", +".Wr c #d89f7e", +"avu c #d8a88d", +".gj c #d8ab89", +"#mz c #d8ac99", +".zH c #d8c8a7", +"#AQ c #d8c8b7", +".5r c #d8cbe1", +"#3m c #d8ceca", +".AP c #d8cecb", +"aC7 c #d8d1d4", +".nb c #d8d4f1", +".vC c #d8d7d4", +"#Qt c #d96a12", +"#RV c #d96d11", +"aJk c #d96d70", +"#As c #d9785b", +"#lb c #d97f53", +"#KN c #d9815b", +"#jP c #d9825d", +"#Pu c #d98560", +"#yU c #d98568", +"#ro c #d98661", +"#la c #d98664", +"#Pe c #d98960", +"#xq c #d9896b", +"#lM c #d98c64", +"#v5 c #d98c68", +"#ea c #d98d63", +"#e# c #d98e65", +"#lR c #d98e6c", +"#6R c #d98e74", +"#TF c #d99068", +"#Xv c #d99325", +"#oJ c #d9946f", +"ah4 c #d99476", +"#Sl c #d9955d", +".RS c #d99678", +".1c c #d9967e", +"#sb c #d99965", +"#Hr c #d99a65", +".S4 c #d99b7f", +".1b c #d99c7b", +".V. c #d99e78", +".1a c #d99e7d", +"#wW c #d9a064", +"#pj c #d9a176", +"#tz c #d9a79e", +"auU c #d9a98e", +"#nZ c #d9ae9b", +"#CL c #d9c0a1", +".ve c #d9e2f1", +"#jK c #da7c51", +"#HQ c #da7c5e", +"#tV c #da805d", +"#Jy c #da8159", +"#yV c #da8569", +"#og c #da856f", +"#Tz c #da8655", +"#vO c #da876c", +"#z9 c #da895f", +"#sm c #da8b64", +"#rw c #da8b6f", +"#F0 c #da8f64", +"#Ne c #da9393", +".Yj c #da9478", +"#y9 c #da9576", +".RX c #da9874", +"#OR c #da9a54", +".U9 c #da9c76", +"#T. c #da9f83", +".Tt c #daa075", +"#LL c #daa2a0", +"#FO c #daac88", +"#wZ c #dab096", +"#yr c #dab097", +"#vq c #dab197", +".w# c #dac5a0", +".ye c #dac7a6", +".sd c #daca9a", +".px c #dacfa3", +".68 c #dad4bf", +"aAO c #dad4d8", +"anr c #dad6dc", +"#Qs c #db6d09", +"#EC c #db7f5d", +"#q1 c #db7f64", +"#H2 c #db805a", +"#Jl c #db805d", +"#ia c #db825d", +"#Go c #db834d", +"#QD c #db835d", +"#lO c #db8561", +"#Sa c #db8857", +"#RO c #db8a56", +"#ee c #db8a62", +"#cg c #db8a64", +"#Pd c #db8b60", +"#p1 c #db8b64", +"#fN c #db8b6b", +"#ko c #db8e6c", +"#e. c #db9167", +"#sU c #db9271", +"#ni c #db9370", +".1t c #db9479", +"#v8 c #db9575", +"#se c #db986e", +".RC c #db9882", +"#LR c #db995a", +"#Bv c #db9970", +".WV c #db9976", +"#Ux c #db997e", +"#IZ c #db9a63", +"#f. c #db9b71", +"#FL c #db9c68", +"#vn c #dba265", +"#tB c #dba266", +"#za c #dba27b", +"auh c #dbab90", +"#xJ c #dbac84", +"att c #dbac91", +"#tE c #dbb198", +".ie c #dbcac7", +".pi c #dbcfc5", +"abs c #dbcfd4", +".xy c #dbd0d2", +"aFD c #dbd1d4", +"aFE c #dbd2d5", +".n7 c #dbd5d5", +".mn c #dbdcef", +"#Nr c #dc7b2c", +"#Do c #dc7b5f", +"#aN c #dc7d5d", +"#Mv c #dc8258", +"#QE c #dc835d", +"#sq c #dc836e", +"#xv c #dc866a", +"#xc c #dc8961", +"#C4 c #dc8b61", +"#fH c #dc8e5d", +"#kk c #dc8e66", +"#LK c #dc8e8d", +"#KH c #dc8f5d", +"#n3 c #dc8f5f", +"#Ex c #dc8f62", +"#iR c #dc8f75", +"#v6 c #dc916d", +"#ru c #dc9c76", +"#qQ c #dc9d73", +"#sd c #dc9f74", +"#Qk c #dca56c", +"#Nm c #dcac76", +"#FN c #dcae87", +"#I1 c #dcb184", +"#Ek c #dcb191", +"#zW c #dcb298", +"acw c #dcc1b5", +".0D c #dcd2e2", +".k0 c #dcd7c5", +"aAK c #dcd7da", +"azz c #dcd8db", +"aJq c #dd7467", +"#Tl c #dd761d", +"#Wa c #dd8014", +"#Gk c #dd815c", +"#Jm c #dd8161", +"#ue c #dd8163", +"#Jv c #dd8363", +"#yX c #dd876b", +"#Ty c #dd8857", +"#d9 c #dd8b60", +"#Bx c #dd8f63", +"#fK c #dd916f", +"#TK c #dd926a", +"#mF c #dd926c", +"#RL c #dd9740", +"#Kq c #dd9c60", +"#Kj c #dd9d9a", +".WX c #dd9f7b", +"#ik c #dd9f81", +"#pi c #dda06a", +".rQ c #ddbba4", +".pp c #ddcab4", +".r7 c #ddccaf", +"#1Y c #ddcec6", +"ayk c #ddd0dd", +".8I c #ddd4cd", +".mJ c #ddd5b1", +"auu c #ddd9dc", +"aJp c #de4c43", +"#Ti c #de7a1e", +"#h7 c #de7d54", +"#Gh c #de8063", +"#cp c #de8064", +"#aM c #de815b", +"#nk c #de886e", +"#NE c #de895c", +"#ug c #de8968", +"#tY c #de896e", +"#QB c #de8a62", +"#vX c #de8a6e", +"#vN c #de8a6f", +"#HK c #de8b60", +"#tT c #de8b63", +"#tS c #de8d63", +"#p0 c #de8d67", +"#tP c #de8e5a", +"#QG c #de8f63", +"#vC c #de9064", +"#TC c #de9065", +"#NY c #de916c", +"#fJ c #de9371", +"#fI c #de9471", +"#TL c #de9567", +"#cD c #de9569", +".6e c #de9978", +"#Qh c #de9a4d", +".4G c #de9a79", +".RD c #de9b85", +".RE c #de9c86", +"#UP c #de9d7c", +".UF c #dea57f", +".UG c #dea77c", +"#OT c #deaa72", +"amz c #dead98", +"#Ht c #deb289", +"aHV c #debac1", +".gK c #ded1cf", +"acX c #ded1d8", +".uT c #ded4b6", +".5z c #ded4e0", +"aB5 c #ded5da", +".uc c #dedfda", +"aJl c #df5454", +"#C9 c #df8262", +"#fD c #df845f", +"#h6 c #df8464", +"#H3 c #df855d", +"#UK c #df8631", +"#iK c #df8663", +"#BT c #df876c", +"#xk c #df876d", +"#vM c #df886e", +"#K7 c #df895f", +"#Md c #df8a63", +"#rn c #df8a67", +"#vF c #df8b63", +"#fx c #df8c6a", +"#ok c #df8d6c", +"#hl c #df8f74", +"#hk c #df9075", +"aFe c #df91b3", +"#B0 c #df926e", +"#hd c #df945f", +"#TE c #df956d", +"#YZ c #df982e", +"#Sk c #df9964", +"#iE c #df9974", +"#p6 c #df9c77", +".RT c #df9c7d", +".RF c #df9d87", +"#wV c #dfa15b", +"#r7 c #dfa57a", +".X2 c #dfa67c", +".Wt c #dfa77d", +"a#l c #dfb0a8", +"#Bk c #dfb59b", +"#r9 c #dfb6a2", +"#Fg c #dfc5cb", +"aG3 c #dfd5d8", +"aEi c #dfd6d9", +".k5 c #dfd8b5", +"#.t c #dfd8d6", +".pX c #dfdbe9", +"awB c #dfdcde", +"aek c #dfdce3", +"avL c #dfdde4", +".jt c #dfe0d7", +"#Jn c #e08466", +"#xi c #e0856c", +"#Dn c #e0886d", +"#Ar c #e0896d", +"#Ma c #e08a5b", +"#S# c #e08b59", +"#P# c #e08b61", +"#Jg c #e08d60", +"#sN c #e08d68", +"#BZ c #e08f6d", +"#dJ c #e09074", +".7V c #e09258", +"#mO c #e0926b", +"#io c #e09773", +"#kb c #e0977a", +"#FK c #e09c63", +".RJ c #e09c80", +".U7 c #e09d7a", +".22 c #e09e7e", +".WW c #e09f7c", +"#Qi c #e0a159", +"#vm c #e0a15b", +".1D c #e0a188", +".S7 c #e0a286", +".Ts c #e0a376", +".ZR c #e0a384", +"#qI c #e0a77b", +"#CQ c #e0b494", +".id c #e0cdc6", +"aMJ c #e0d1df", +".tt c #e0d2b3", +".D. c #e0d2c5", +".wE c #e0d9e0", +"aeN c #e0dde1", +"#1I c #e17a64", +"#Dp c #e18164", +"#xg c #e18464", +"#EW c #e18567", +"#K6 c #e18759", +"#gN c #e1876a", +"#RH c #e1887a", +"#Mb c #e1895a", +"#KL c #e18b5c", +"#JE c #e18b5d", +"#yT c #e18c70", +"#Pc c #e19165", +"#QH c #e19167", +".9D c #e1936d", +"#TJ c #e19371", +"#yD c #e19467", +"#xD c #e19571", +"#Kp c #e19657", +"#uj c #e19672", +"#JA c #e1986a", +"#rx c #e1997d", +"#qR c #e19c73", +".25 c #e19f7f", +".RG c #e19f89", +"#w4 c #e1a172", +"#yo c #e1a25c", +"#iw c #e1a281", +"#tA c #e1a35d", +"#CR c #e1b89e", +"ah1 c #e1bba7", +".yi c #e1c5a3", +".qK c #e1d1c2", +"aDt c #e1d6db", +".xj c #e1d7e8", +".o# c #e1d8b1", +".y3 c #e1d9e5", +".nc c #e1dcfa", +"azx c #e1dee1", +"apD c #e1dee4", +"ava c #e1dfe6", +"#RW c #e2741a", +"#BU c #e28164", +"#pz c #e2845f", +"#sL c #e28a6a", +"#An c #e28a6f", +"#kl c #e28b67", +"#K2 c #e28c61", +"#q7 c #e28c6c", +"#xm c #e28c71", +"#pZ c #e28f6c", +"#NP c #e2906f", +"#TH c #e29079", +"#oB c #e2916e", +"#vE c #e29267", +"#sP c #e2926a", +"#mG c #e2926d", +"#EZ c #e2926f", +".9C c #e2936d", +"#q9 c #e29376", +".9E c #e2946e", +"#x# c #e29568", +"#LQ c #e29754", +"#dR c #e29870", +"#z# c #e29c7e", +"#n2 c #e29e77", +".RK c #e29e82", +".4I c #e29f7e", +".RH c #e2a08a", +"#CN c #e2a45e", +"amu c #e2ae92", +"#pl c #e2b8a5", +".v7 c #e2cbaf", +".sa c #e2cdbe", +".n1 c #e2ceac", +".6Z c #e2d4e4", +"#.q c #e2d5cf", +".Cp c #e2d7d9", +"ael c #e2ddde", +"agR c #e2dfe6", +"arh c #e2e2e5", +".ju c #e2e3da", +"aLC c #e37d79", +"#q3 c #e38370", +"#Gj c #e38663", +"#YX c #e3866e", +"#R9 c #e38b57", +"#jJ c #e38b6a", +"#K5 c #e38c5e", +"#fF c #e38c61", +"#yY c #e38d71", +"#d1 c #e39072", +"#L9 c #e39463", +"#jV c #e3956d", +"#w. c #e39571", +"#n# c #e39573", +"#Du c #e39672", +"#lJ c #e39873", +"#rc c #e3997c", +"#g3 c #e39a75", +"#Y1 c #e39b41", +".RW c #e3a07e", +".RU c #e3a180", +"#zS c #e3a55f", +".X9 c #e3a687", +".Ws c #e3aa83", +"#l. c #e3ad9e", +"#wY c #e3b797", +"#vp c #e3b898", +"#El c #e3b99f", +"#qK c #e3baa6", +".u0 c #e3d19d", +".sb c #e3d1b1", +".zU c #e3d2ba", +".5w c #e3d4d0", +".n4 c #e3d6c8", +".qZ c #e3d6cb", +"aG2 c #e3dadd", +".8S c #e3dbc6", +".k4 c #e3ddb8", +"anH c #e3dee3", +"avF c #e3dfe2", +"ap7 c #e3dfe5", +"amm c #e3e2e8", +".ub c #e3e4e4", +"#Jt c #e4896e", +"#od c #e48c6b", +"#S. c #e48d59", +"#KM c #e48d5e", +"#Ji c #e48d61", +"#pS c #e48d74", +"#K0 c #e48e66", +"#yR c #e48e73", +"#Ds c #e4906f", +"#sn c #e49370", +"#BE c #e49377", +"#Sc c #e49668", +"#na c #e49673", +"#F1 c #e4986e", +"aI8 c #e498c4", +"#0o c #e49b38", +"aJj c #e49da2", +"#w# c #e49f6e", +".4H c #e4a080", +".RV c #e4a17f", +".24 c #e4a282", +".WU c #e4a381", +".UI c #e4a583", +"#Bh c #e4a660", +".X1 c #e4ab84", +"#Bj c #e4b898", +"#tD c #e4b998", +"#7d c #e4ba9f", +"#qL c #e4bfaa", +".n2 c #e4d3b6", +"a#P c #e4d8dc", +"aDq c #e4d9de", +"aka c #e4dae2", +"apo c #e4e0e6", +"awc c #e4e1e8", +"ayf c #e4e2e4", +"aqm c #e4e3e7", +"#yO c #e58e74", +"#NO c #e59074", +"#Ac c #e59277", +"#KJ c #e59363", +"#oE c #e5956e", +"#kj c #e59770", +"#aH c #e59972", +"#IY c #e59b60", +"#ps c #e59d75", +"#Hq c #e59e64", +"#OQ c #e59f54", +".4N c #e5a084", +".1s c #e5a386", +".Tr c #e5a577", +".UJ c #e5a584", +".UH c #e5a684", +".Wu c #e5a887", +".X8 c #e5a889", +".Ye c #e5a98a", +".7F c #e5b1a2", +"#zV c #e5ba9a", +"aEI c #e5d8de", +".re c #e5e1e7", +".tQ c #e5e6e8", +"#pU c #e68b73", +"#yL c #e68d74", +"#EX c #e68e6e", +"#vL c #e68e75", +"#NF c #e68f63", +"#yW c #e69074", +"#dW c #e6926f", +"#dP c #e69570", +"#Dt c #e69673", +"#dV c #e69772", +"#fz c #e69874", +"#Nj c #e69e56", +"#qO c #e6a46f", +".Tq c #e6a576", +".Wv c #e6a988", +"#sc c #e6ad81", +"#s. c #e6c2ad", +"aa0 c #e6c4b9", +".wd c #e6d5ab", +"aF0 c #e6d6dd", +"anB c #e6dae0", +".D1 c #e6dbde", +".D0 c #e6dcdb", +".n5 c #e6ddd5", +".h6 c #e6e0da", +".t1 c #e6e2df", +"aeJ c #e6e3e7", +".ue c #e6e4fd", +".y7 c #e6e5e1", +".mU c #e6eefa", +"#Dg c #e7846c", +"#Gg c #e7896e", +"#yK c #e78a69", +"#H1 c #e78c68", +"#xh c #e78c73", +"#fC c #e78d6b", +"#qZ c #e78f6a", +"#sM c #e7916e", +"#NM c #e79373", +"#fj c #e79476", +"aDX c #e794b7", +"#dI c #e7967b", +"#Bw c #e79a6d", +"#lL c #e79a73", +"#1K c #e79c3f", +"#iH c #e79c74", +"#n6 c #e79c75", +"#Gr c #e79d72", +"#pp c #e7a06d", +"#yz c #e7a085", +".23 c #e7a484", +"#pr c #e7a57c", +".ZK c #e7a98a", +".WR c #e7af8f", +".n9 c #e7dbc4", +"#3n c #e7ddd7", +".Co c #e7dddc", +".nU c #e7ded4", +".b8 c #e7e3ed", +".mR c #e7eef3", +".jE c #e7f1eb", +"#0m c #e8846d", +"#Ju c #e88d6f", +"#N0 c #e88f68", +"#d7 c #e88f6a", +"#BS c #e89075", +"#tU c #e8916b", +"#m9 c #e89379", +"#TI c #e8987c", +"#Ey c #e8996d", +"#iJ c #e89a72", +"#n. c #e89b79", +"#p7 c #e89c7c", +"#mX c #e89d7c", +".7Z c #e89f7f", +".Yi c #e8a086", +".29 c #e8a488", +".RI c #e8a489", +"#AB c #e8a68c", +".UK c #e8a988", +"#w3 c #e8aa75", +"#qH c #e8ab75", +".U5 c #e8ab85", +"#3b c #e8b47c", +"#n0 c #e8bba9", +".yl c #e8cba9", +".qu c #e8d5ba", +".5q c #e8d8e8", +".66 c #e8dcc5", +".2a c #e8dce6", +".8R c #e8ddca", +".32 c #e8dff5", +"alg c #e8e0e9", +".YZ c #e8e3fb", +"af3 c #e8e4e8", +".AI c #e8e4f5", +"aeM c #e8e5e9", +"awF c #e8e5ec", +"#HR c #e98b6f", +"#Pb c #e99068", +"#yM c #e99077", +"#Pa c #e99169", +"#Aq c #e99276", +"#K1 c #e99368", +"#qY c #e9936b", +"#fG c #e99768", +"#BG c #e9977b", +"#Sb c #e99869", +"#Jf c #e9996a", +"#rd c #e99d80", +"#rb c #e9a284", +".ZT c #e9a488", +".Ww c #e9ab8a", +".X7 c #e9ad8d", +"#pm c #e9c1ad", +"afF c #e9cfbf", +".n0 c #e9d4b0", +".po c #e9d4b9", +".zV c #e9d8be", +".qR c #e9dbae", +"aEH c #e9dbe1", +".2g c #e9dce3", +".sn c #e9ddd0", +"a.B c #e9dfe1", +"aFG c #e9e0e3", +".0x c #e9e0eb", +"##X c #e9e2f0", +"afB c #e9e7ee", +"#py c #ea8c65", +"#q2 c #ea8c76", +"#Jr c #ea8c77", +"#HX c #ea8d73", +"#oa c #ea8f67", +"#Ao c #ea9277", +"#gU c #ea9370", +"#d8 c #ea956c", +"#z8 c #ea9c6f", +"#sV c #ea9d80", +"#lK c #ea9e78", +"#ci c #ea9f78", +"#r. c #ea9f84", +"#1M c #eaa051", +"#vx c #eaa07f", +"#0q c #eaa14d", +".Yh c #eaa287", +".6l c #eaa589", +"#ft c #eaa780", +"#yx c #eaaa87", +".Z2 c #eaaa8e", +"#z2 c #eaad8e", +"#yv c #eaaf7c", +".WQ c #eaaf90", +"#z1 c #eab087", +"amy c #eab69a", +".wc c #ead6ab", +".tz c #ead6b3", +"ai8 c #ead9e4", +"ahY c #eadae3", +".C9 c #eadccf", +".0u c #eaddc8", +".YW c #eadfd9", +"#48 c #eadfdb", +".zf c #eadfe1", +"aEj c #eae2e5", +".tv c #eae3d2", +"QtS c #eae3f8", +"#De c #eb8e76", +"#d5 c #eb9070", +"#Js c #eb9076", +"#JC c #eb9362", +"#aK c #eb9367", +"#xl c #eb947a", +"#Sf c #eb957e", +"#Mc c #eb966e", +"#Pf c #eb9a74", +"#oC c #eb9a75", +"#hc c #eb9d6a", +"#lD c #eb9d81", +"#Sj c #eba372", +"#kg c #eba37d", +".4J c #eba787", +".U8 c #eba985", +".26 c #eba989", +".1k c #ebab8b", +".1r c #ebad8f", +".X3 c #ebaf8f", +".Yc c #ebb192", +".WJ c #ebb294", +"#mA c #ebbaa9", +"#2q c #ebc1ab", +"ai9 c #ebe0e6", +".He c #ebe1e0", +"abU c #ebe1ea", +".rT c #ebe5cf", +".h8 c #ebe5df", +".sz c #ebe6e7", +"au9 c #ebe8eb", +".sq c #ebf1fc", +".jG c #ebfcff", +"#Ab c #ec8f6e", +"#xj c #ec9279", +"#Jz c #ec936a", +"#pW c #ec9577", +"#NN c #ec977a", +"#C3 c #ec9d71", +"#HI c #ec9e72", +"#ol c #ec9f7f", +"#w7 c #eca285", +"#iq c #ecab89", +".Th c #ecac88", +".S8 c #ecad92", +".WH c #ecb395", +"abu c #ecc8bd", +".mz c #ecd9af", +".8Q c #ecddd3", +".r9 c #ece0d2", +"afC c #ece3e6", +".67 c #ece4c7", +".yU c #ece7ff", +"ans c #ece8ed", +"axw c #ece9ec", +".Cd c #ece9fc", +"#Jo c #ed9075", +"#q6 c #ed9574", +"#Ap c #ed957a", +"#rm c #ed9675", +"aLB c #ed9988", +"#pR c #ed9b80", +"#pJ c #ed9d7d", +".7U c #ed9f79", +"#Pr c #eda06e", +"#QR c #eda06f", +"#iI c #eda078", +"#mC c #eda27d", +"aMx c #eda890", +".Tb c #edad89", +"#z3 c #edad94", +".Wx c #edb08f", +".UW c #edb192", +".WS c #edb28d", +".Y. c #edb293", +".8. c #edb99f", +".pf c #eddccb", +".mA c #edddb7", +"QtB c #eddee7", +"#8Z c #ede1df", +"anz c #ede1e6", +"#6N c #ede2df", +".mH c #ede3cf", +".vK c #ede3e2", +".mI c #ede5bf", +"aml c #ede6f0", +"aii c #ede9ed", +"aw8 c #edebf2", +".xo c #edede2", +".vB c #edeede", +"#HU c #ee8f7a", +"#co c #ee9071", +"#Ag c #ee997e", +"#Ae c #ee9a7f", +"#Sg c #ee9a81", +"#Pq c #eea070", +"#nb c #eea07c", +"#Je c #eea171", +"#pK c #eea284", +"#lI c #eea47f", +"#w5 c #eeaa85", +".U6 c #eead89", +".ZS c #eead90", +".Te c #eeae8a", +".UR c #eeb698", +"aen c #eedcd3", +".2# c #eee1df", +"anA c #eee2e8", +"#.s c #eee6dc", +"avK c #eeebf2", +".ud c #eeedff", +".lc c #eefaff", +"#BD c #ef9271", +"#sK c #ef9576", +"#H6 c #ef9763", +"#yQ c #ef997e", +"#BI c #ef9a7f", +"#mU c #ef9c78", +"#oD c #ef9f79", +"#KI c #efa06f", +"#xG c #efa289", +".UL c #efaf8e", +".1n c #efaf8f", +".U0 c #efaf92", +".X6 c #efb293", +".X4 c #efb393", +".WI c #efb798", +"a.y c #efc8b7", +".xx c #efe5e4", +".b7 c #efe8f0", +".sD c #efeaeb", +"aeL c #efebf0", +"awb c #efecf3", +".q4 c #eff1fa", +"#sJ c #f09477", +"#vK c #f0977d", +"#yP c #f0997f", +"#lS c #f09b7f", +"#ou c #f0a085", +"#F2 c #f0a279", +"#lm c #f0a87f", +"#lt c #f0a886", +"#lB c #f0aa8c", +".Tf c #f0b08c", +".Yf c #f0b193", +".WC c #f0b799", +".UU c #f0b89a", +".wa c #f0dbb4", +"#7f c #f0e1db", +".3Y c #f0e1e1", +"#99 c #f0e5e7", +".aB c #f0e9f2", +".rh c #f0ebf1", +".rg c #f0ecf2", +"#yN c #f1997f", +"#d0 c #f19b7d", +"#fh c #f19c7e", +"aLA c #f19c85", +"#d2 c #f1a083", +"#QM c #f1a27d", +"#QS c #f1a472", +"#g4 c #f1a480", +"#ki c #f1a57e", +"#kh c #f1a780", +"#n5 c #f1aa83", +".Tk c #f1ad89", +".Tj c #f1ad8a", +".Yg c #f1ad91", +"#j5 c #f1af8e", +".1l c #f1b191", +".1m c #f1b192", +"#ir c #f1b393", +".ZL c #f1b495", +".Yd c #f1b697", +".UT c #f1b99b", +"#Ff c #f1d5da", +".5n c #f1decd", +".60 c #f1e2f8", +"agT c #f1e6e7", +".oA c #f1ecff", +".rf c #f1edf3", +"af1 c #f1eef2", +".AH c #f1eff5", +"#H0 c #f29674", +"#HY c #f2967a", +"#BR c #f2987d", +"#dN c #f29a77", +"#fB c #f29b7a", +"#pV c #f29b7d", +"#fg c #f29b7e", +"#yS c #f29d81", +"#F3 c #f2a179", +"#BF c #f2a185", +"aI6 c #f2a5c0", +"#xH c #f2a786", +"#jW c #f2aa84", +"#ra c #f2ab91", +"#iD c #f2ae89", +".1q c #f2b292", +".WP c #f2b295", +".S9 c #f2b498", +".ZO c #f2b596", +".Y# c #f2b798", +".WD c #f2b99b", +".WE c #f2ba9b", +".US c #f2ba9c", +"amx c #f2bea2", +".U3 c #f2c09e", +"aa1 c #f2c3be", +"aaf c #f2dcda", +".x1 c #f2dfb8", +"adm c #f2e3ec", +".3T c #f2e3f0", +".gH c #f2e4e2", +"#8v c #f2e6e6", +".0w c #f2e7e7", +"adn c #f2eaf6", +".vA c #f2edd5", +".y6 c #f2eee4", +"asu c #f2eef4", +".pW c #f2eefb", +"axx c #f2eff6", +".pD c #f2f0e6", +"#HT c #f3947d", +"#Df c #f3967e", +"#H8 c #f39c6c", +"#oK c #f3a486", +"#fq c #f3aa82", +".Tl c #f3af8a", +".Ti c #f3af8d", +".28 c #f3b191", +"#mT c #f3b296", +".Tc c #f3b38f", +"#yw c #f3b68a", +".Wy c #f3b695", +".ZM c #f3b697", +".X5 c #f3b797", +"#g0 c #f3ba92", +"amv c #f3bfa3", +".3Q c #f3e2ce", +"#5A c #f3e3da", +".if c #f3e3e4", +".n3 c #f3e4ce", +".2. c #f3e4d6", +"aag c #f3e5e9", +".b5 c #f3e7eb", +"aDs c #f3e8ed", +".xb c #f3ecff", +".vz c #f3edda", +"ahe c #f3eff3", +".pc c #f3f2e8", +"alh c #f3f2f6", +".og c #f3f9f6", +"#rj c #f4997c", +"#Al c #f49a7f", +"#so c #f4a083", +"#Af c #f4a084", +"#Ad c #f4a185", +"#HJ c #f4a479", +"#Pl c #f4a480", +"#Po c #f4a579", +"#kf c #f4ad88", +".Ta c #f4b48f", +".Tg c #f4b490", +".1o c #f4b495", +".T. c #f4b69a", +".Ya c #f4b99a", +"abR c #f4d3c3", +".ww c #f4d5b0", +"alx c #f4d9d2", +"#3Q c #f4e2d8", +"abT c #f4e2e7", +".3S c #f4e3e5", +".AQ c #f4e9e8", +".kY c #f4eacc", +"aB1 c #f4ebf0", +".xl c #f4ede7", +".h9 c #f4efe8", +".sA c #f4f0f0", +"af0 c #f4f0f4", +"aoA c #f4f0f5", +"aoz c #f4f0f6", +"av# c #f4f1f8", +".vd c #f4f3ef", +".la c #f4f9ff", +".lb c #f4fdff", +"#pY c #f5a17f", +"#nj c #f5a386", +"#Pn c #f5a67d", +"#fv c #f5a684", +"#nd c #f5a780", +"#QQ c #f5a879", +"#r# c #f5ad92", +".4M c #f5b296", +".UM c #f5b594", +"#j6 c #f5b797", +".T# c #f5b79b", +".ZQ c #f5b899", +".tb c #f5d2c6", +"agW c #f5d5c0", +".wv c #f5d6b2", +"#8Y c #f5ddd3", +"akr c #f5ded8", +"aLM c #f5e0f0", +".65 c #f5e6da", +"any c #f5e9ee", +".YV c #f5eadc", +"#.r c #f5eae0", +"QtW c #f5eaef", +"afD c #f5ebea", +".b6 c #f5ebf1", +".6X c #f5ecdd", +".vy c #f5efe4", +".xn c #f5f1de", +".y5 c #f5f1eb", +"ahf c #f5f1f5", +"akb c #f5f3f5", +"awE c #f5f3fa", +".jF c #f5ffff", +"#Am c #f69d82", +"#Gq c #f69f6d", +"#pX c #f6a080", +"#Pp c #f6a87a", +".7T c #f6a882", +"#v9 c #f6a88e", +"#Sd c #f6a97e", +"#mY c #f6af8f", +".ZN c #f6b99a", +".U4 c #f6bb95", +".WG c #f6bd9e", +".UQ c #f6bea0", +"#j0 c #f6c1a1", +".xY c #f6e2c1", +"a.A c #f6e4e1", +".r8 c #f6e7d1", +".ze c #f6ebeb", +"##V c #f6ecee", +"QtX c #f6eff9", +"aAL c #f6f0f3", +".pG c #f6f1ef", +"aeK c #f6f2f6", +"arM c #f6f2f8", +".oi c #f6f3fe", +"apC c #f6f5f9", +".q5 c #f6fbff", +".mT c #f6ffff", +"#HS c #f7997f", +"#HW c #f79a81", +"#HZ c #f79b7c", +"#cn c #f79c79", +"#fm c #f79e7b", +"#Pm c #f7a781", +"#QN c #f7a982", +"#nc c #f7a984", +"#Si c #f7ac81", +"#om c #f7ad8e", +"#pL c #f7ae92", +"#w6 c #f7af91", +"#AA c #f7af9a", +".6h c #f7b291", +".6f c #f7b292", +".UZ c #f7b699", +".Td c #f7b793", +"ajq c #f7dbd0", +".wq c #f7e3c1", +".yd c #f7e4c4", +".n8 c #f7e8e4", +".3Z c #f7eaee", +"agS c #f7eaf0", +".Xm c #f7ecec", +".qx c #f7efdc", +".tP c #f7f0e5", +"aBI c #f7f2f4", +".t2 c #f7f3f0", +"app c #f7f3f9", +"aw5 c #f7f4f7", +"alz c #f7f5f9", +".mP c #f7fefe", +"#HV c #f89985", +"#Jq c #f89a84", +"#BP c #f89c82", +"#d4 c #f8a082", +"#BJ c #f8a287", +"#QI c #f8a883", +"#g6 c #f8ab86", +"#Se c #f8ac81", +"#z. c #f8ae96", +"#RM c #f8af60", +"#aJ c #f8af87", +"#m5 c #f8b092", +".6g c #f8b392", +".Tm c #f8b48e", +".Tp c #f8b58b", +".Wz c #f8ba99", +"#lp c #f8c4a4", +".3R c #f8e7dc", +".ts c #f8e8c2", +".kW c #f8e9bd", +"alm c #f8eae3", +".5y c #f8ebf2", +".FF c #f8eded", +"aDr c #f8edf2", +".vc c #f8eedd", +".nT c #f8f0e6", +".gR c #f8f1f0", +".t4 c #f8f3f0", +".sC c #f8f3f4", +"aAM c #f8f3f6", +".kL c #f8f3fa", +"avI c #f8f4f7", +".AF c #f8f4fa", +"amF c #f8f5f9", +"ap8 c #f8f5fa", +".oh c #f8f7fe", +"aqn c #f8f8fb", +"amp c #f8fbf6", +"amD c #f8fefe", +"amC c #f8ffff", +"#fi c #f9a688", +"#fO c #f9a988", +"#pQ c #f9aa8f", +"#lu c #f9b494", +"#pM c #f9b499", +".WO c #f9b69a", +"#fs c #f9b991", +".UN c #f9b998", +"#m1 c #f9b99d", +".UX c #f9bb9d", +".WT c #f9bc98", +"#il c #f9bda0", +"amw c #f9c5a9", +"afZ c #f9dcce", +".5p c #f9e7eb", +".fg c #f9e8ef", +".aA c #f9eef4", +".td c #f9efd9", +".dD c #f9eff5", +"aB4 c #f9f1f5", +"ahZ c #f9f2ed", +".vo c #f9f2ff", +"aAN c #f9f4f7", +"azy c #f9f5f8", +"af2 c #f9f5f9", +"avJ c #f9f6fd", +"alj c #f9fcf7", +"amE c #f9fcf8", +"amo c #f9fefb", +".mQ c #f9ffff", +"#JD c #fa9f6d", +"#sp c #faa38a", +"#cl c #faa67d", +"#pI c #faa686", +"#Pi c #faa889", +"#in c #faaf89", +"#pP c #fab093", +"#iy c #fab395", +"#V5 c #fab44d", +".To c #fab68d", +"#yy c #fab69b", +".Tn c #fab78f", +".WA c #fabd9c", +".Yb c #fabfa1", +"#it c #fac0a1", +".UP c #fac2a4", +"#5z c #fadac3", +"abS c #fae0dc", +".5x c #faecec", +".0v c #faede0", +".vp c #faf3ff", +"ant c #faf6fc", +".AG c #faf7f9", +"anJ c #faf7fd", +"aoM c #faf9fc", +"aoN c #fafafd", +"alB c #fafcfb", +"amn c #fafeff", +".of c #fafffc", +"#BO c #fb9e84", +"#Jp c #fb9e85", +"#H7 c #fb9f6b", +"#oe c #fba487", +"#of c #fba58c", +"#fA c #fba685", +"#Ah c #fba78c", +"#BH c #fba88d", +".7R c #fbac86", +"#Sh c #fbac88", +"#QP c #fbad81", +"#kc c #fbaf92", +".6k c #fbb89c", +".27 c #fbb999", +".UO c #fbbb9a", +".WL c #fbbd9f", +".WB c #fbbe9d", +".ZP c #fbbe9f", +".WK c #fbbfa0", +"#iu c #fbc19f", +".WF c #fbc2a3", +".UV c #fbc3a5", +"ahc c #fbdbca", +"adk c #fbddd0", +".yk c #fbdebc", +".uB c #fbe6d9", +"anx c #fbeef4", +"aem c #fbf2f0", +".xk c #fbf3f8", +".ia c #fbf6ef", +".t3 c #fbf6f4", +"aq4 c #fbf7fd", +"aw7 c #fbf8fa", +"ayh c #fbf9fb", +".jh c #fbfdff", +".mS c #fbffff", +"#BQ c #fca187", +"#rl c #fca384", +"#dX c #fca484", +"#fw c #fcaa88", +"#Ez c #fcab81", +"#QO c #fcae85", +"#1L c #fcb258", +"#gX c #fcb48c", +"#dS c #fcb58c", +"#os c #fcb597", +"#mS c #fcba9d", +"#op c #fcba9e", +"#jX c #fcbb97", +"#g1 c #fcbf98", +"#j7 c #fcc0a1", +".tA c #fcebb7", +".x2 c #fcecc5", +"QtV c #fcecee", +".so c #fcf3e7", +".nS c #fcf5ea", +"akt c #fcf5f2", +"aC6 c #fcf5f7", +".pu c #fcf5fc", +".nQ c #fcf6f2", +".xm c #fcf7e8", +"awC c #fcf8fb", +"aih c #fcf8fc", +".pV c #fcf8ff", +"avG c #fcf9fb", +"awD c #fcf9fc", +"akv c #fcf9fd", +"ayi c #fcfafc", +"aj# c #fcfbf2", +"anI c #fcfcff", +"akc c #fcfef9", +"ali c #fcffff", +"#rk c #fda385", +"#QK c #fdac8d", +"#Tb c #fdb660", +"#Xw c #fdb74e", +".4L c #fdb999", +".WM c #fdbb9e", +".UY c #fdbc9f", +".1p c #fdbd9e", +"#lz c #fdbe9d", +"#k. c #fdc09f", +"#is c #fdc2a3", +"aie c #fdddcc", +"a.z c #fde0d7", +"adl c #fde5e4", +".tc c #fde8d6", +".19 c #fdeed8", +"afE c #fdeee6", +".az c #fdeff2", +".s. c #fdf5f3", +"aB3 c #fdf5f9", +".mD c #fdf6e5", +".tx c #fdf8ef", +".mq c #fdf9ed", +"aw# c #fdf9fc", +"ajt c #fdf9fd", +"anv c #fdf9ff", +"aw4 c #fdfafc", +"alk c #fdfbf4", +"ayj c #fdfbfd", +"aoO c #fdfbff", +"alA c #fdffff", +"#d3 c #feaa8d", +"#Pj c #feac8e", +"#UA c #feb758", +"#on c #feb89b", +"#ke c #feb995", +"#or c #feb99a", +".4K c #feba9a", +"#mZ c #feba9c", +"#iC c #febb96", +"#ly c #fec09f", +"#lw c #fec0a2", +"#iv c #fec2a0", +".yf c #fee2c0", +"acY c #fee5de", +".wb c #fee9c0", +".yb c #feebd0", +".tB c #feedba", +".k3 c #fef5e1", +".tw c #fef8ec", +".i# c #fef8f1", +".y4 c #fef9fa", +".sB c #fefafa", +"aw6 c #fefafd", +"anu c #fefaff", +"av. c #fefbfd", +"ayg c #fefcfe", +"awa c #fefcff", +".nP c #fefeff", +"#BN c #ffa288", +"#Ak c #ffa68b", +"#Gp c #ffa872", +"#BL c #ffa88e", +"#BM c #ffa88f", +"#gT c #ffaa88", +"#dO c #ffab87", +"#EY c #ffad8c", +"#Aj c #ffad92", +"#lF c #ffad93", +"#fn c #ffae8b", +"#QJ c #ffae8d", +"#Pk c #ffaf8c", +"#gS c #ffaf8d", +"#lE c #ffaf95", +"#lr c #ffb08c", +"#Pg c #ffb08d", +"#Ph c #ffb08f", +"#BK c #ffb096", +"#QL c #ffb395", +".7S c #ffb48e", +"#iA c #ffb499", +"#j1 c #ffb58f", +"#Ai c #ffb59a", +"#aI c #ffb68e", +"#lH c #ffb693", +"#iz c #ffb699", +"#dU c #ffb790", +"#im c #ffb892", +"#mP c #ffb994", +"#g5 c #ffb995", +"#0p c #ffba5c", +"#pO c #ffba9c", +".6j c #ffbb9b", +"#pN c #ffbb9c", +"#ot c #ffbb9e", +"#kd c #ffbba0", +"#ck c #ffbc94", +"#lG c #ffbc99", +".WN c #ffbca0", +"#iB c #ffbca1", +"#Y0 c #ffbd58", +".6i c #ffbd9d", +"#gZ c #ffbe94", +"#cj c #ffbe96", +"#lv c #ffbe9f", +"#m0 c #ffbea1", +"#fr c #ffbf96", +"#lq c #ffbf9a", +"#mQ c #ffbf9d", +"#lA c #ffbf9f", +"#oq c #ffc0a1", +"#dT c #ffc197", +"#mR c #ffc1a2", +"#m4 c #ffc2a3", +"#gY c #ffc39a", +"#j2 c #ffc39e", +"#m3 c #ffc3a3", +"#k# c #ffc4a4", +".U1 c #ffc4a5", +"#oo c #ffc4a7", +"#m2 c #ffc5a5", +"#j9 c #ffc6a4", +"#lo c #ffc7a4", +"#lx c #ffc7aa", +"#jY c #ffc8a6", +"#j8 c #ffc9aa", +"#ln c #ffcaa3", +".U2 c #ffcaaa", +"#jZ c #ffcdac", +"aI7 c #ffd2f3", +".6y c #ffdbc4", +"amr c #ffdcc0", +".uA c #ffddd8", +"#8X c #ffdfcd", +"#Wh c #ffdfdd", +"aae c #ffe0d6", +"#7e c #ffe6d5", +"abt c #ffe7de", +"aHH c #ffe7fb", +"a#Q c #ffe8db", +"ahd c #ffebde", +"acZ c #ffebe1", +".wr c #ffecc8", +".rR c #ffecd4", +"amB c #ffece7", +".tr c #ffedc3", +"agV c #ffeddf", +".x3 c #ffeecb", +".yc c #ffeed1", +"aif c #ffeee1", +".qP c #ffefd2", +".ic c #ffefe5", +".ay c #ffeff0", +"QtU c #fff0ef", +"ake c #fff1e7", +".5o c #fff2e9", +"ajb c #fff3e3", +".wD c #fff3f2", +"aly c #fff3f3", +".kV c #fff4c2", +".pw c #fff4da", +".pq c #fff4e4", +"QtT c #fff4f2", +".nR c #fff5eb", +"ajs c #fff5ec", +".pr c #fff5ed", +".ps c #fff5f4", +".mB c #fff6d7", +".pe c #fff6e8", +".qO c #fff6eb", +".pt c #fff6fa", +"aoB c #fff6fc", +".x4 c #fff7db", +".rS c #fff7df", +"ah0 c #fff7e5", +"all c #fff7ef", +".gJ c #fff7f6", +".mC c #fff8e1", +"ajr c #fff8ef", +".q0 c #fff8f0", +".mG c #fff8f6", +".h5 c #fff8f7", +".mp c #fff8f8", +".tO c #fff9e8", +".ms c #fff9ee", +".pv c #fff9f1", +"aks c #fff9f6", +".gI c #fff9f7", +"aBH c #fff9fb", +".qM c #fff9fd", +"anw c #fff9ff", +".ya c #fffae2", +".qv c #fffae3", +"aja c #fffaed", +".q1 c #fffaf4", +".q2 c #fffaf5", +".qL c #fffaf9", +".pI c #fffafd", +"aB2 c #fffafe", +".kO c #fffbef", +"akd c #fffbf0", +".gQ c #fffbf3", +".ib c #fffbf5", +".gM c #fffbf9", +".s# c #fffbfb", +".pH c #fffbfc", +"aBG c #fffbfd", +".qN c #fffbff", +".qw c #fffce8", +".pb c #fffcee", +".mE c #fffcf0", +".sp c #fffcf6", +"avH c #fffcfe", +".mo c #fffcff", +".kN c #fffdf0", +".gP c #fffdf1", +".pE c #fffdf5", +"aig c #fffdff", +".mr c #fffef3", +".pd c #fffef4", +"amq c #fffef8", +".pF c #fffef9", +"aj. c #fffefc", +".ji c #fffeff", +".kM c #fffff4", +".mF c #fffff7", +"agU c #fffff8", +"aku c #fffffa", +".q3 c #fffffb", +".i. c #fffffc", +".l# c #ffffff", +"Qt.Qt#QtaQtbQtcQtdQteQtfQtgQthQtiQtjQtkQtlQtmQtnQtoQtpQtqQtrQtrQtsQttQtuQtvQtwQtxQtyQtzQtAQtBQtCQtDQtEQtFQtGQtHQtIQtJQtKQtLQtMQtNQtOQtPQtQQtRQtSQtTQtUQtVQtWQtXQtYQtZQt0Qt1Qt2Qt3Qt4Qt5Qt6Qt7Qt5Qt8Qt9.#..##.#a.#b.#c.#d.#e.#f.#g.#h.#i.#j.#k.#l.#m.#n.#o.#p.#q.#r.#s.#t.#u.#v.#w.#x.#y.#z.#A.#A.#B.#C.#D.#E.#E.#D.#C.#B.#D.#D.#F.#F.#F.#G.#H.#I.#J.#K.#L.#M.#N.#O.#P.#Q.#R.#S.#S.#T.#U.#V.#V.#W", +".#XQt#.#Y.#ZQtc.#0.#1Qtf.#2.#3.#4.#5Qtk.#6.#7.#8QtoQtp.#9QtrQtrQtsQtt.a..a#.aa.ab.ac.ad.ae.af.ag.ah.ai.aj.ak.al.am.an.ao.ap.aq.ar.as.at.au.av.aw.ax.ay.az.aA.aB.aC.aD.aE.aF.aG.aH.aI.aJ.aK.aL.aM.aN.aO.aP.aQ.aR.aS.aT.aU.aV.aW.aX.aY.aZ.a0.a1.a2.a3.a4.#o.#p.a5.#r.#s.#t.#u.#u.a6.a7.#x.#y.#z.a8.#D.#G.a9.#F.#F.a9.#G.#D.#E.#D.#F.#F.#F.#G.#E.#I.b..b#.ba.a5.bb.bc.bd.be.bf.bg.bh.bi.#P.bj.bk.bl", +".bmQt#.bn.boQtc.bp.bq.br.bs.bt.bu.bv.bw.bx.by.bz.bA.bB.#9.bC.bCQts.bD.bE.bF.bG.bH.bI.bJ.bK.bL.bM.bN.bO.bP.bQ.bR.bS.bT.bU.bV.bW.bX.bY.bZ.b0.b1.b2.b3.b4.b5.b6.b7.b8.b9.c..c#.ca.c#.cb.cc.cd.ce.cf.cg.ch.ci.cj.ck.cl.cm.cn.co.cp.cq.cr.cs.ct.cu.cv.cw.cx.cy.cz.cA.cB.cC.#v.cD.cE.cF.cG.cH.cI.#H.#y.cH.#I.a9.cJ.cJ.a9.#I.cH.#I.#E.#D.#F.#F.#G.#E.#I.b..cK.cL.cM.cN.cO.#U.bi.cP.cQ.cR.cS.cT.cU.cV.cW", +".cXQt#.cY.cZ.c0.c1.bq.c2.c3.c4.c5.c6.bw.c7.c8.c9.d..bB.d#.da.da.db.bD.dc.dd.de.df.dg.dh.di.dj.dk.dl.dm.dn.do.dp.dq.dr.ds.dt.du.dv.dw.dx.dy.dz.dA.dB.dC.dD.dE.dF.dG.dH.dI.dJ.dK.dL.dM.dN.dO.dP.dQ.dR.dS.dT.dU.dV.dW.dX.dY.dZ.d0.d1.d2.d3.d4.d5.d6.d7.d8.d9.e..e#.ea.eb.#l.ec.ec.ed.ed.ee.ef.ef.eg.eh.#I.#E.#D.#D.#E.#I.eh.eh.#I.#D.a9.#F.#D.#E.#I.b#.cK.cK.ei.ej.ek.el.bd.em.en.#S.eo.#P.ep.eq.er", +".esQt#.et.eu.c0.ev.ew.ex.c9.ey.ez.eA.eB.eB.eC.eD.eE.eF.d#.eG.eG.db.eH.eI.eJ.eK.ab.eL.eM.eN.eO.eP.eQ.eR.eS.eT.eU.eV.eW.eX.eY.eZ.e0.e1.e2.e3.e4.e5.e6.e7.e8.e9.f..f#.fa.dB.fb.fc.fd.fe.ff.fg.fh.fi.fj.fk.fl.fm.fn.fo.fp.fq.fr.fs.ft.fu.fv.fw.fx.cO.fy.fz.fA.fB.fC.fD.fE.eb.fF.fF.fF.fG.fG.fH.fH.fH.#I.#I.eh.fI.fI.eh.#I.#I.fI.eh.#I.#D.a9.#D.#D.#H.fJ.fK.fL.#M.cN.fM.fN.fO.fP.fQ.fR.fS.fT.#V.fU.bk", +".fVQt#.fW.fX.c0.fY.fZ.f0.f1.f2.f3.f4.eB.f5.f6.f7.f8.eF.f9.g..g..g#.eH.ga.gb.gc.gd.ge.gf.gg.gh.gi.gj.gk.gl.gm.gn.go.gp.gq.gr.gs.gt.gu.gv.gw.gx.gy.gz.gA.gB.gC.gD.gE.gF.gG.gH.gI.gJ.gK.gL.gM.gN.gO.gP.gQ.gR.gS.gT.gU.gV.gW.gX.gY.gZ.g0.g1.g2.g3.b#.g4.g5.g6.g7.g8.g9.h..h#.ha.ha.ha.ha.ha.ha.ha.ha.#I.eh.hb.hc.hc.hb.eh.#I.hb.fI.#I.#D.#G.#D.#D.#E.fJ.#K.hd.#M.he.fM.hf.hg.hh.hi.hj.hk.hl.hm.hn.fU", +".hoQt#.hp.hq.hr.hs.ht.hu.hv.hw.hx.hy.eB.hz.hA.hB.hC.hD.f9.g..hE.g#.hF.hGQtvQtw.hH.ge.eM.hI.hJ.hK.hL.gk.dn.hM.hN.hO.hP.eX.hQ.hR.hS.hT.hU.hV.hW.hX.hY.hZ.h0.h1.h2.h3.h4.h5.h6.h7.h8.h9.i..i#.ia.ib.ic.id.ie.if.ig.ih.ii.ij.ik.il.im.in.io.ip.iq.ir.is.g4.it.iu.iv.iw.ix.fE.iy.iy.iz.iz.iz.iz.iz.iz.hb.hb.hb.hb.hb.hb.hb.hb.hc.hb.cH.#E.#D.#D.#D.#E.b#.#K.ba.cM.iA.iB.iC.iD.iE.iF.iG.iH.iI.iJ.#V.iK", +".iLQt#.iM.iN.hr.iO.ht.hu.iP.hv.iQ.iR.eB.iS.iT.iU.hC.hD.f9.hE.hE.iV.iW.hG.iX.iY.iZ.eL.i0.gg.i1.i2.i3.i4.i5.i6.eU.i7.i8.i9.j..j#.ja.jb.jc.jd.je.jf.jg.jh.ji.jj.jk.jl.jm.jn.jo.jp.jq.jr.js.jt.ju.jv.jw.jx.jy.jz.jA.jB.jC.jD.jE.jF.jG.jH.jIQt1.jJ.jK.jL.jM.jN.jO.jP.iw.jQ.jR.jS.jS.jS.jS.jT.jU.iz.iz.jV.hc.hb.eh.eh.hb.hc.jV.hc.hb.eh.#E.#D.#D.#D.#D.jW.fK.jX.jY.jZ.iB.iC.j0.j1.j2.j3.iI.bc.j4.fU.bl", +".j5.j6.j7.j8.j6.j9.k..k#.ka.kb.kc.kd.ke.kf.kg.kh.ki.kj.kk.kl.km.kl.kn.ko.kp.kq.kr.ks.kt.ku.kv.kw.kx.ky.kz.kA.kB.kB.kC.kD.kE.kF.kG.kH.kI.kJ.kK.kL.kM.kN.kO.kP.kQ.kR.kS.kT.kU.kV.kW.kX.kY.kZ.k0.k1.k2.k3.k4.k5.k6.k7.k8.k9.l..l#.la.lb.lc.ld.le.lf.lg.lh.li.lj.lk.ll.lm.ln.lo.lo.lo.lo.lo.lo.lo.lo.lp.lq.lq.lr.lr.ls.ls.lt.lu.lv.lw.lx.ly.lu.lz.lA.lp.lt.lB.lt.ls.lq.ls.lt.hc.lC.hb.hb.lD.lE.lF.lG", +".lH.lI.lJ.lK.lL.lM.lN.lO.lP.lQ.lR.lS.lT.lU.lV.lW.lX.lY.lZ.l0.l1.l2.l3.l4.l5.l6.l7.l8.l9.m..m#.ma.kx.mb.mc.md.me.mf.mg.mh.mi.mj.mk.ml.mm.mn.mo.mp.mq.mr.ms.mt.mu.mv.mw.mx.my.mz.mA.mB.mC.mD.mE.mF.mG.mH.mI.mJ.mK.mL.mM.mN.mO.mP.mQ.mR.mS.mT.mU.mV.mW.mX.mY.mZ.m0.m1.m2.m3.m4.m4.m4.m4.m4.m4.m4.m4.lp.lq.lq.lr.lr.ls.ls.lt.m5.lu.lv.lv.m6.m5.m7.m8.lq.ls.ls.m5.m6.m9.m9.lq.hc.lC.hb.hb.hc.lE.n..lG", +".n#.na.nb.nc.nd.ne.nf.ng.nh.ni.nj.nk.nl.nm.nn.no.np.nq.nr.ns.nt.nu.nv.nw.nx.ny.nz.nA.nB.m..nC.nD.nE.nF.nG.nH.nI.nJ.no.nK.nL.nM.nN.nO.nP.l#.nQ.nR.nS.nT.nU.nV.nW.nX.nY.nZ.n0.n1.n2.n3.n4.n5.n6.n7.n8.n9.o..o#.oa.ob.oc.od.oe.of.og.l#.l#.oh.ji.oi.oj.ok.ol.om.on.oo.op.oq.fH.fH.fH.fH.fH.fH.fH.fH.lp.lq.lq.lr.lr.ls.ls.lt.lz.or.m5.os.m5.lz.lA.ot.lt.ou.lr.lr.lr.lr.lp.ls.hc.lC.lC.lC.ov.lF.lG.ow", +".ox.oy.oz.oA.oB.oC.oD.oE.oF.oG.oH.oI.oJ.oK.oL.oM.oN.oO.oP.oQ.oR.oS.oT.oU.oV.oW.e3.oX.oY.oZ.o0.o1.o2.o3.o4.o5.o6.o7.o8.o9.p..p#.pa.pb.pc.pd.pe.pf.pg.ph.pi.pj.pk.nX.pl.pm.pn.po.pp.pq.pr.ps.pt.pu.pv.pw.px.py.pz.pA.pB.pB.pC.pD.pE.pF.pG.pH.pI.mo.mT.pJ.pK.pL.pM.pN.aU.pO.ef.ef.ef.ef.ef.ef.ef.ef.lp.lq.lq.lr.lr.ls.ls.lt.m7.m7.pP.pP.pP.m7.lA.lA.pQ.lt.lr.lr.lB.lB.lt.ls.hc.hc.lD.hc.lE.n..ow.pR", +".pS.pT.pU.pV.pW.pX.pY.pZ.p0.p1.p2.p3.p4.p5.p6.p7.p8.p9.q..q#.oM.q..qa.qb.qc.qd.qe.qf.qg.qh.qi.qj.qk.ql.qm.qn.qo.qp.qq.qr.qs.qt.qu.qv.qw.qx.qy.qz.qA.qB.qC.qD.qE.qF.qG.qH.qI.po.qJ.qK.pv.qL.qM.qN.qO.qP.qQ.qR.qS.qT.qU.qV.qW.qX.qY.qZ.q0.q1.q2.q3.q4.q5.q6.q7.q8.q9.r..r#.ef.ef.ef.ef.ef.ef.ef.ef.lp.lq.lq.lr.lr.ls.ls.lt.m7.m7.m7.lA.m7.m7.pP.lz.lt.ls.lq.lq.lr.lr.ls.m9.hc.hc.hc.ov.ra.lG.pR.rb", +".rc.rd.re.rf.rg.rh.ri.rj.rk.rl.rm.rn.ro.rp.rq.rr.rs.rs.rt.ru.rv.rw.rx.ry.rz.rA.rB.rC.rD.rE.rF.rG.rH.rI.rJ.rK.rL.rM.rN.rO.rP.rQ.rR.rS.rT.rU.rV.rW.rX.rY.rZ.r0.r1.r2.r3.r4.r5.r6.r7.r8.r9.q0.s..s#.sa.sb.sc.sd.se.sf.sg.sh.si.sj.sk.sl.sm.sn.so.gP.sp.l#.sq.sr.ss.st.su.sv.fH.fH.fH.fH.fH.fH.fH.fH.lp.lq.lq.lr.lr.ls.ls.lt.lz.pP.m7.lA.m7.lz.os.lu.sw.lt.lr.lr.ou.ou.lr.ls.hc.hc.hc.lE.lF.ow.rb.sx", +".sy.sz.sA.sB.sC.sD.sE.sF.sG.sH.sI.sJ.sK.sL.sM.sN.sO.sP.sQ.sR.sS.sT.sU.sV.sW.sX.sY.sZ.s0.s1.s2.s3.s4.s5.s6.s7.s8.s9.t..t#.ta.tb.tc.td.te.tf.tg.th.ti.tj.tk.tl.tm.tn.to.tp.tq.tr.ts.tt.tu.tv.tw.tx.ty.tz.tA.tB.tC.tD.tE.tF.tG.tH.tI.tJ.tK.tL.tM.tN.tO.tP.tQ.tR.tS.tT.tU.tV.m4.m4.m4.m4.m4.m4.m4.m4.lp.lq.lq.lr.lr.ls.ls.lt.os.or.pP.m7.lz.os.lv.lx.lB.pQ.pQ.tW.sw.lt.lt.lB.hc.hc.ov.lE.lG.tX.tY.tZ", +".t0.t1.t2.t2.t3.t4.t5.t6.t7.t8.t9.u..u#.ua.ub.uc.ud.ue.uf.ug.uh.ui.uj.uk.ul.um.un.uo.up.uq.ur.us.ut.uu.uv.uw.s8.ux.uy.t#.uz.uA.uB.uC.uD.uE.uF.uG.uH.uI.uJ.uK.uL.uM.uN.uO.uP.uQ.uR.uS.uT.uU.uV.uW.uX.uY.uZ.u0.u1.u2.u3.u4.u5.u6.u7.u8.u9.v..v#.va.vb.vc.vd.ve.vf.vg.vh.vi.lo.lo.lo.lo.lo.lo.lo.lo.lp.lq.lq.lr.lr.ls.ls.lt.lu.os.lz.lz.or.lu.lx.vj.ls.lt.lB.lt.ls.lq.ls.lt.hc.hc.ov.vk.lG.pR.sx.tZ", +".vl.vm.vn.vo.vp.vq.vr.vs.vt.vu.vv.vw.vx.vy.vz.vA.vB.vC.vD.vE.vF.vG.vH.vI.vJ.vK.vL.vM.vN.vO.vP.vQ.vR.vS.vT.vU.vV.vW.vX.vY.vZ.v0.v1.v2.v3.v4.v5.v6.v7.v8.v9.w..w#.wa.wb.wc.wd.we.wf.wg.wh.wi.wj.wk.wl.wm.wn.wo.wp.wq.wr.ws.wt.wu.wv.ww.wx.wy.wz.wA.wB.wC.wD.wE.wF.wG.wH.wI.wJ.cx.wK.wL.wM.wN.wO.wP.wQ.wR.iz.wS.wR.wT.wS.wU.wV.wV.wV.wV.wW.wW.wW.wW.wX.wY.wZ.w0.wZ.w0.w1.w2.w3.w4.w5.w6.w4.w7.w8.w9", +".x..x#.xa.xb.xc.xd.xe.xf.xg.xh.xi.xj.xk.xl.xm.xn.xo.xp.xq.xr.xs.xt.xu.xv.xw.xx.xy.xz.xA.xB.xC.xD.xE.xF.xG.xH.xI.xJ.xK.xL.xM.xN.xO.xP.xQ.xR.xS.xT.xU.xV.xW.xX.xY.xZ.x0.x1.x2.x3.x4.x5.x6.x7.x8.x9.y..y#.ya.yb.yc.yd.ye.x3.yf.yg.yh.yi.yj.yk.yl.ym.yn.yo.yp.yq.yr.ys.yt.yu.yv.yw.yx.yy.yz.yA.yB.yC.wQ.wS.iz.yD.yE.wR.wS.jS.ha.ha.wW.fF.yF.yG.yG.yH.yI.yJ.yK.yK.yL.yL.yM.yN.w4.w4.w4.w3.yO.yP.w8.yQ", +".yR.yS.yT.yU.yV.yW.yX.yY.yZ.y0.y1.y2.y3.y4.y5.y6.y7.y8.y9.z..z#.za.zb.zc.zd.ze.zf.zg.zh.zi.zj.zk.zl.zm.zn.zo.zp.zq.zr.zs.zt.zu.zv.zw.zx.zv.zy.zz.zA.zB.zC.zD.zE.zF.zG.zH.zI.zJ.zK.zL.zM.zN.zO.zP.zQ.zR.zS.zT.zU.zV.zW.zX.zY.zZ.z0.z1.z2.z3.z4.z5.z6.z7.z8.z9.A..A#.Aa.Ab.Ac.Ad.Ae.Af.Ag.Ah.Ai.iB.wQ.wR.wS.wS.wR.wR.wS.iz.ha.wV.fF.yG.Aj.Ak.Al.Am.w0.yL.w2.w2.yK.yK.An.Ao.w5.w4.yO.Ap.Aq.w8.w8.w8", +".Ar.As.At.Au.Av.Aw.Ax.Ay.Az.AA.AB.AC.AD.AE.AF.AG.AH.AI.AJ.AK.AL.AM.AN.AO.AP.AQ.AR.AS.AT.AU.AV.AW.AX.AY.AZ.A0.A1.A2.A3.A4.A5.A6.A7.A8.A9.B..B#.Ba.Bb.Bc.Bd.Be.Bf.Bg.Bh.Bi.Bj.Bk.Bl.qE.Bm.Bn.Bo.Bp.Bq.Br.Bs.Bt.Bu.Bv.Bw.Bx.By.Bz.BA.BB.BC.BD.BE.BF.BG.BH.BI.BJ.BK.BL.BM.BN.BO.BP.BQ.ou.BR.BS.BT.BU.BV.BW.BW.BW.BW.BW.wQ.wQ.Aj.BX.BX.Ak.Al.Am.BY.BZ.wZ.w1.yK.yK.w1.w1.yK.An.w6.w3.Ap.B0.w9.B1.B0.w8", +".B2.B3.B4.B5.B6.B3.B7.B8.B9.C..C#.Ca.Cb.Cc.Cd.Ce.Cf.Cg.Ch.Ci.Cj.Ck.Cl.Cm.Cn.Co.Cp.Cq.Cr.Cs.Ct.Cu.Cv.Cw.Cx.Cy.Cz.CA.CB.CC.CD.CE.CE.CF.CG.CH.CI.CE.CJ.CK.CL.CM.CN.CO.CP.CQ.CR.CS.CT.CU.CV.CW.CX.CY.CZ.C0.C1.C2.C3.C4.C5.C6.C7.C8.C9.D..D#.Da.Db.Dc.Dd.De.Df.Dg.Dh.Di.Dj.Dk.Dl.Dm.Dn.jU.Do.Dp.Dq.Dr.Ds.Dt.w2.Ds.yL.Du.Du.Dv.Dt.Dw.Dx.Dv.BZ.BZ.BY.Am.w1.yK.yM.w2.yL.yL.Dy.Dz.w4.yO.Aq.w9.DA.DA.w9.yQ", +".DB.DC.DD.DE.DF.DG.DH.DI.DJ.DK.DL.DM.DN.DO.DP.DQ.DR.DS.DT.DU.DV.DW.DX.DY.DZ.D0.D1.D2.D3.D4.D5.D6.D7.D8.D9.E..E#.Ea.Eb.Ec.Ed.Ee.Ef.Eg.Eh.Ei.Ej.Ek.El.Em.En.Eo.Ep.Eq.Er.Es.Et.Eu.Ev.Ew.Ex.Ey.Ez.EA.EB.EC.ED.EE.EF.EG.EH.EI.EJ.EK.EL.EM.EN.EO.EP.EQ.ER.ES.ET.EU.EV.EW.EX.EY.EZ.E0.E1.E2.E3.E4.E5.yL.E6.E7.E7.E7.E8.E9.E9.E6.F..F..F#.Fa.Dt.Dx.BZ.BZ.Fb.Fc.Fd.Fe.yN.Dz.Ao.Ff.w7.yP.w8.B1.DA.Fg.Fg.Fg", +"Qt..Fh.Fi.Fj.Fk.Fl.Fm.Fn.Fo.Fp.Fq.Fr.Fs.Ft.Fu.Fv.Fw.Fx.Fy.Fz.FA.FB.FC.FD.FE.FF.FG.FH.FI.FJ.FK.FL.FM.FN.FO.FP.FQ.FR.FS.FT.FU.FV.FW.FX.FY.FU.FZ.F0.F1.F2.F3.F4.F5.F6.F7.F8.F9.G..G#.Ga.Gb.Gc.Gd.Ge.Gf.Gg.Gh.Gi.Gj.Gk.Gl.Gm.Gn.Go.Gp.Gq.Gr.Gs.Gq.Gt.Gu.Gv.Gw.Gx.Gy.Gz.GA.GB.GC.GD.GE.GF.GG.GH.GI.Fb.E6.GJ.GK.GJ.E6.E9.E6.E7.GL.F..F..GM.F#.Fa.GN.GN.Fc.Fd.GO.Fd.Ao.Fb.Fc.Fd.w8.w8.w8.B0.w9.Fg.GP.GQ", +".GR.GS.GT.GU.GV.GW.GX.GY.GZ.G0.G1.G2.G3.G4.G5.G6.G7.G8.G9.H..H#.Ha.Hb.Hc.Hd.He.Hf.Hg.Hh.Hi.Hj.Hk.Hl.Hm.Hn.Ho.Hp.Hq.Hr.Hs.FU.Ht.Hu.Hv.Hw.Hx.Hy.Hz.HA.HB.HC.HD.HE.HF.HG.HH.HI.HJ.HK.HL.HM.HN.HO.HP.HQ.HR.HS.HT.HU.HV.HW.HX.HY.HZ.F0.H0.H1.H2.H3.H4.H5.F0.H6.H7.H8.H9.I..I#.Ia.Ib.Ic.Id.Ie.If.GI.yK.E6.E7.GK.E7.E9.Dt.E8.E7.F..F..F..F..F..F..F..F..Dz.Ao.Fe.Ao.An.w2.An.yN.w9.yQ.w8.w8.yQ.Fg.GQ.Ig", +".Ih.Ii.bw.Ij.Ik.Il.Ih.Im.In.Io.Ip.Iq.Ir.Is.It.Iu.Iv.Iw.Ix.Iy.Iz.IA.IB.IC.ID.IE.IF.IG.zo.IH.II.IJ.IK.IL.Hn.IM.IN.IO.IP.IQ.IR.IS.IT.IU.IV.IW.IX.IY.IZ.I0.I1.I2.I3.I4.I5.HF.I6.I7.I8.I8.I9.J..J#.I7.Ja.Ja.Jb.Gb.Jc.Jd.Je.Je.Jf.Jg.Jh.Ji.Jj.Jk.Jl.Jm.Jn.Jo.Jp.Jq.Jr.Js.Jt.Ju.Jv.Jw.Jx.Jy.Jz.JA.JB.JC.JD.JD.JD.JD.JE.JF.JF.JF.JG.JH.JH.JH.JI.JJ.JJ.JJ.JK.JK.JL.JL.JM.JN.JO.JP.JQ.JR.JS.JS.JT.JU.JU.JV", +".Io.JW.bw.JX.Ik.Il.Io.Im.JY.JZ.J0.J1.J2.J3.f7.J4.J5.J6.Ix.J7.J8Qte.J9.K..K#.Ka.Kb.Kc.Kd.Ke.Kf.Kg.Kh.Ki.yn.Kj.Kk.IO.Kl.Km.Kn.Ko.Kp.Kq.Kr.Ks.Kt.Ku.Kv.Kw.Kx.Jk.Ky.Kz.Kw.KA.KB.KC.KD.KB.KE.KF.KG.KB.KH.KH.KI.KJ.KK.ES.Gl.ER.KL.KM.KN.KO.KP.KQ.KR.KS.KT.KU.KV.KW.F5.KX.KY.KZ.K0.K1.K2.K3.K4.K5.K6.K7.K8.K8.K9.L..JD.JD.L#.JE.La.Lb.Lb.Lb.JG.JH.JH.JH.JO.JN.JN.JN.JN.JN.JN.JM.JU.JR.JS.Lc.JT.JU.JU.JV", +".Io.Ld.Le.Lf.Lg.Lh.Li.Lj.Lk.Ll.J0.iP.Lm.Ln.Lo.Lp.Lq.Lr.Ls.Lt.Lu.Lv.Lw.Lx.Ly.Lz.LA.LB.LC.LD.LE.LF.LG.LH.LI.LJ.Jq.IO.I0.Km.LK.LL.LM.LN.LO.LP.F0.F0.LQ.LR.LS.LQ.LT.LQ.LU.LV.LW.LX.LY.LZ.LY.LY.L0.L1.L2.L3.L4.L5.L6.L7.L8.L8.L9.FT.M..M#.Ma.I9.Mb.Mc.Md.Me.Mf.Mg.KD.Jk.Mh.Mi.Mj.Mk.Ml.Mm.Mn.Mo.Mp.eq.Mq.Mq.Mq.Mr.K8.K8.K8.K8.Ms.Ms.Mt.Mt.Mu.Mu.La.La.Mv.Mw.Mw.Mx.JP.JN.JN.My.Mz.JT.MA.MA.JT.Mz.JU.Mz", +".MB.MC.MD.ME.lX.MF.MG.MH.MI.Ll.Ip.MJ.MK.ML.MM.MN.MO.Lr.Ix.J7.J8Qte.Lw.MP.MQ.MR.MS.MT.MU.MV.MW.MX.MY.MZ.M0.M1.M2.IO.I2.Km.H4.M3.M4.M5.M6.M6.M7.M8.M9.N..N#.Na.Nb.Nc.Nb.Nb.Nd.Ne.Nf.tj.Ng.Nh.Nf.Nf.Ni.Ni.Nj.Nk.Nl.Nm.Nm.Nn.No.Ni.Np.Nq.Nr.Ns.Nt.Nu.Jf.Gq.Nv.Nw.Nx.Ny.Nz.NA.NB.NC.ND.NE.NF.NG.NH.NI.NJ.NK.NK.NL.NL.NM.NM.Mq.NN.NN.NO.NO.NP.NP.Ms.Ms.NQ.NQ.NR.Mv.Mw.Mw.NS.NS.JU.JV.JT.JR.NT.Mz.NU.NV", +".MG.NW.NX.NY.NZ.N0.N1.N2.N3.N4.N5.N6.N7.N8.N9.J4.O..O#.Oa.Ob.Oc.bq.J9.K..Od.Oe.Of.Og.Oh.Oi.Oj.Ok.Ol.MZ.Om.On.Oo.Op.H9.L0.Oq.Or.zv.Os.Ot.Ou.Ov.Ow.Ox.Oy.Oz.OA.OB.OC.OD.OE.OF.OG.OH.OI.OJ.OK.OL.OM.ON.OO.OP.OQ.OR.OS.OT.OU.OV.OW.OX.OY.OZ.O0.O1.O2.O3.O4.O5.O6.O7.O8.O9.P..P#.Pa.Pb.Pc.Pd.Pe.Pf.Pg.Ph.Ph.Ph.Ph.Pi.Pj.Pj.Pj.Pk.Pk.Pl.Pl.Pl.NN.NN.NO.NQ.NQ.NQ.Pm.Pn.Pn.Po.Po.NV.NU.NU.JU.JU.NU.Pp.Pq", +".Pr.NW.Ps.Pt.Pu.Pv.Pw.Px.Py.Pz.PA.PB.PC.PD.PEQth.PF.O#.PG.PH.PI.PJ.J9.PK.PL.PM.PN.PO.oS.PP.PQ.PR.PS.PT.LI.PU.Kr.PV.H9.PW.PX.PY.PZ.Na.P0.P1.P2.P3.P4.P5.P6.P7.P8.P9.Q..Q#.Qa.Qb.Qc.Qd.Qe.Qf.Qg.Qh.Qi.Qj.Qk.Ql.Qm.Qn.Qo.Qp.Qq.Qr.Qs.Qt.Qu.Qv.Qw.Ox.N#.Qx.Qy.Qz.QA.QB.QC.QD.QE.QF.QG.QH.QI.QJ.QK.QL.QM.QM.QN.QO.QO.QO.QP.QP.QQ.QQ.QQ.Pk.Pl.Pl.Pl.NN.Pm.QR.Pn.QS.QT.QU.QV.QW.QX.QX.QX.Pp.QY.Pp.QZ.Q0", +".Q1.Q2.Q3.Q4.Q5.Pv.Q6.Q7.Q8.Q9.R..MJ.R#.Ra.Rb.#3.Rc.Rd.Oa.Ob.rB.bq.Re.MP.Rf.Rg.Rh.Ri.Rj.Rk.Rl.Rm.Rn.Ro.Rp.Rq.Rr.PV.I2.PW.Rs.Rt.Ru.Rv.Rw.Rx.Ry.Rz.RA.RB.RC.RD.RE.RF.RG.RH.RI.RJ.RK.RJ.RL.RM.RN.RO.RP.RQ.RR.RS.RT.RU.RV.RW.RX.RY.RZ.R0.R1.R2.R3.R4.R5.R6.M6.R7.R8.R9.S..S#.Sa.Sb.Sc.Sd.Se.Sf.Sg.Sh.Si.Si.Sj.Sk.QM.QM.QM.Sl.QQ.QQ.QP.Pk.Pl.Pl.Pl.NN.Sm.QU.QU.QV.QV.QV.QV.QV.QZ.Sn.Q0.So.Pq.QZ.Sp.Sq", +".Sr.Ss.Q3.Q4.St.Su.Sv.Sw.Sx.Io.Sy.Sz.SA.c5.SB.SC.SDQto.SE.SF.SG.bq.SH.SI.f3.SJ.SK.SL.SM.SN.SO.SP.SQ.SR.SS.ST.SU.SV.I2.PW.SW.SX.SY.SZ.S0.S1.S2.S3.S4.S5.S6.S7.S8.S9.T..T#.Ta.Tb.Tc.Td.Te.Tf.Tg.Th.Ti.Tj.Tk.Tl.Tm.Tn.To.Tp.Tq.Tr.Ts.Tt.Tu.Tv.Tw.Tx.Ty.Tz.TA.TB.TC.TD.TE.TF.TG.TH.TI.TJ.TK.TL.TM.TN.TO.TP.Si.Si.Si.Sk.Sk.QM.QQ.QQ.Pk.Pk.Pl.Pl.NN.NN.TQ.TQ.TR.TS.QV.QV.Sm.QT.Sn.TT.TU.TT.So.TV.TW.TX", +".TY.TZ.T0.T1.T2.T3.T4.T5.T6.T7.T8.T9.U..U#.Ua.Ub.Uc.nH.Ud.Ue.Uf.Ug.Uh.Ui.Uj.Uk.Ul.Um.Un.Uo.Up.Uq.Ur.Us.Ut.Uu.Uv.Uw.Ux.Uy.Uz.UA.UB.UC.UD.UE.UF.UG.UH.UI.UJ.UK.UL.UM.UN.UO.UP.UQ.UR.US.UT.UU.UV.UP.UW.UX.UY.UZ.U0.U1.U2.U3.U4.U5.U6.U7.U8.U9.V..V#.Va.Vb.Vc.Vd.Ve.Vf.Vg.Vh.Vi.Vj.Vk.Vl.Vm.Vn.Vo.Vp.Vq.Vr.Vs.Vr.Vq.Vq.Vt.Vs.Vu.Vv.Vw.Vv.Vu.Vx.Vu.Vv.Vy.Vz.VA.VB.VC.VD.VE.VF.VG.VH.VH.VH.VI.VJ.VK.VL", +".VM.VN.VO.VP.VQ.VR.VS.VT.VU.VV.VW.VX.VY.VZ.V0.V1.V2.V3.V4.dy.V5.V6.V7.V8.V9.GU.W..W#.Wa.Wb.Wc.Wd.We.Wf.Wg.Wh.Wi.Wj.Wk.Wl.Wm.Wn.Wo.Wp.Wq.Wr.Ws.Wt.Wu.Wv.Ww.Wx.Wy.Wz.WA.WB.WC.WD.WE.WF.WG.WH.WI.WJ.WK.WL.WM.WN.WO.WP.WQ.WR.WS.WT.WU.WV.WW.WX.WY.WZ.W0.W1.W2.W3.W4.Nb.W5.Jj.W6.W7.W8.W9.X..X#.Xa.Xb.Xc.Vt.Xd.Vr.Vq.Xe.Vq.Vr.Xf.Vt.Xg.Xg.Vt.Vt.Xg.Xh.Xi.Xi.Xi.Xj.Vy.Vy.Vy.Vy.VI.VI.Xk.Xk.Xl.Xl.Xl.Xl", +".Xm.Xn.Xo.Xp.Xq.Xr.Xs.Xt.Xu.Xv.Xw.Xx.nh.bw.Xy.Xz.XA.XB.XC.XD.XE.XF.XG.XH.MB.Ld.XI.XJ.XK.XL.XM.XN.XO.XP.Ov.XQ.XR.XS.XT.XU.XV.XW.XX.XY.XZ.X0.X1.X2.X3.X4.X5.X5.X6.X7.X8.X9.Y..Y#.Ya.Yb.Ya.Yc.Yd.Y..Ye.Yf.Yg.Yh.Yi.Yj.Yk.Yl.Ym.Yn.Yo.Yp.Yq.Yr.Ys.Yt.Yu.Yv.Yw.Yx.Yy.Yz.YA.YB.YC.YD.YE.YF.YG.YH.YI.YJ.Xe.Vq.Vr.Vq.Xe.YK.Xe.Vt.YL.YM.YN.Vx.YN.Vx.Xg.Xh.YO.YP.YP.YQ.YQ.YR.YR.YS.Xk.VJ.YT.YU.YU.YT.VJ.TO", +".YV.YW.YX.YY.YZ.Y0.Y1.Y2.Y3.Y4.Y5.Y6.Y7.Y8.Y9.Z..Z#.Za.Zb.Zc.Zd.Ze.Zf.Zg.hr.Zh.Zi.Zj.Zk.Zl.Zm.Zn.Zo.Zp.Zq.Zr.Wj.Zs.Zt.Zu.Zv.Zw.Zx.Zy.Zz.ZA.ZB.ZC.ZD.ZE.ZF.ZG.ZH.ZI.ZF.ZJ.ZK.ZL.ZM.ZN.ZO.ZO.ZP.ZQ.ZR.ZS.ZT.ZU.ZV.ZW.ZX.ZY.ZZ.Z0.Z1.Z1.Z2.Z3.Z4.Z5.Z6.Z7.Z8.Z9.0..0#.0a.0b.0c.0d.0e.0f.0g.0h.0i.0j.0k.Xe.Vq.Xe.0k.0l.0k.0m.0n.YL.YK.YK.YM.YM.Xf.0o.0p.0p.0p.0p.0q.0q.0q.0q.Xl.0r.0s.YU.0t.YU.0s.YT", +".0u.0v.0w.0x.0y.0z.0A.0B.0C.0D.0E.0F.0G.0H.0I.0J.0K.0L.oW.0M.0N.0O.0P.0Q.ev.0R.0S.0T.0U.0V.0W.0X.0Y.0Z.00.01.02.03.04.05.06.07.08.09.1..1#.1a.1b.1c.1d.1e.1f.1f.1g.1h.1i.1j.1k.1l.1m.1n.1o.1p.1q.1r.1s.1t.1u.1v.1w.1x.1y.1z.1A.1B.1C.1D.1E.1F.1G.1H.1I.1J.1K.1L.1M.1N.1O.1P.1Q.1R.1S.1T.1U.1V.1W.1X.0k.YK.0k.1X.1Y.1X.0k.0n.YL.1Z.YL.0n.10.0n.0l.11.12.13.14.0q.15.YS.YS.0s.YT.YT.0s.0t.16.17.18", +".19.2..2#.2a.2b.2c.2d.2e.2f.2g.2h.2i.2j.2k.2l.2m.2n.2o.2p.2q.2r.2s.2t.2u.2v.2w.2x.2y.2z.2A.2B.2C.2D.2E.2F.2G.2H.2I.2J.2K.2L.2M.2N.2O.2P.2Q.2R.2S.2T.2U.2V.2W.2X.2Y.2Z.20.21.22.23.24.25.26.27.28.U0.29.3..3#.3a.3b.3c.3d.3e.3f.3g.3h.3i.3j.3k.3l.3m.3n.3o.3p.3q.3r.3s.3t.3u.3v.3w.3x.3y.3z.3A.3B.10.1X.0l.1X.3C.3D.3C.1X.3E.0n.3F.3G.3H.3I.3J.3E.3K.3L.11.13.3M.0p.YS.YR.3N.0t.YU.YU.3N.17.3O.3P", +".3Q.3R.3S.3T.3U.3V.3W.3X.i2.3Y.3Z.30.31.32.33.34.35.36.37.38.39.4..4#.4a.4b.4c.4d.4e.4f.4g.4h.4i.4j.4k.FO.4l.A9.4m.4n.4o.4p.4q.4r.4s.4t.4u.4v.4w.4x.4y.4z.4A.4B.4z.4C.4D.4E.4F.4G.4H.4I.4J.4K.4L.4M.4N.4O.4P.4Q.4R.4S.4T.4U.4V.4W.4X.4Y.4Z.40.41.42.43.44.45.46.47.48.49.5..5#.5a.5b.5c.YM.5d.5e.5f.3C.1X.3C.5f.5f.10.3F.5g.3I.5h.3H.5i.5j.3J.3E.3K.3K.3L.11.11.5k.13.13.16.16.16.5l.17.18.5m.3O", +".5n.5o.5p.5q.5r.5s.5t.5u.5v.5w.5x.5y.5z.5A.5B.5C.5D.5E.5F.5G.5H.5I.5J.5K.5L.5MQtI.5N.5O.5P.5Q.5R.5S.5T.5U.5V.5W.5X.5Y.5Z.50.51.52.53.54.55.56.57.58.59.6..6..6..6#.6a.6b.6c.6d.6e.6f.6g.6h.6i.6j.6k.6l.6m.6n.6o.6p.6q.6r.6s.6t.6u.6v.6w.6x.6y.6z.6A.6B.6C.6D.6E.6F.6G.6H.6I.6J.6K.6L.6M.6N.6O.6P.6Q.10.3C.3C.5f.6Q.5f.3C.6R.6S.5j.5i.5j.5j.5h.0n.3L.3L.3K.3K.3K.6T.6U.6U.16.17.18.3O.3O.18.17.6V", +".6W.6X.6Y.6Z.60.61.62.63.64.65.66.67.68.69.7..7#.7a.7b.7c.7d.7e.7f.7g.7h.7i.7j.7k.zr.7l.7m.7n.7o.7p.7q.7r.7s.7t.7u.7v.7w.7x.7y.7z.7A.7B.7C.7D.7E.7F.7G.7H.7I.7J.7K.7L.7M.7N.7O.7P.7Q.7R.7S.7T.7U.7V.7W.7X.7Y.7Z.70.71.72.73.74.75.76.77.78.79.8..8#.8a.8b.8c.8d.8e.8f.8g.8h.8i.8j.8k.8l.8m.8n.8o.8p.8p.8p.8p.8p.8p.8p.8p.8q.8q.8r.8s.8s.8r.8q.8t.8u.8u.8u.8v.8w.8x.8y.8y.8z.8A.8B.8C.8D.8E.8F.8G", +".8H.8I.8J.8K.8L.8M.8N.8O.8P.8Q.8R.8S.8T.8U.8V.8W.8X.8Y.T8.8Z.80.81.82.83.84.85.86.87.88.89.9..9#.9a.9b.9c.9d.9e.9f.9g.9h.9i.9j.9k.9l.9m.9n.9o.9p.9q.9r.9s.9t.9u.9v.9w.9x.9y.9z.9A.9B.9C.9D.9E.9F.9G.9H.9I.9J.9K.9L.9M.9N.9O.9P.9Q.9R.9S.9T.9U.9V.9W.9X.9Y.9Z.90.91.92.93.94.95.96.97.98.99#..#.#.8t.8t.8t.8t.8t.8t.8t.8t#.a.8r#.b#.c#.c.8r.8q#.d.8x#.e#.e.8w.8w.8w.8w.8w.8E#.f#.f#.e.8B.8A.8z#.g", +"#.h#.i#.j#.k#.l#.m#.n#.o#.p#.q#.r#.s#.t#.u#.v#.w.SK.7b#.x#.y#.z#.A.82#.B#.C#.D.86#.E#.F.89#.G#.H#.I#.J#.K#.L#.M#.N#.O#.P#.Q#.R#.S#.T.7r#.U#.V#.W#.X#.Y#.Z#.0#.1#.2#.3#.4#.5#.6#.7#.8#.9##.#####a##b##c##d##e##f##g##h##i##j##k##l##m##n##o##p##q##r##s##t##u##v##w##x##y##z##A##B##C##D##E##F##G.8r.8r.8r.8r.8r.8r.8r.8r#.c#.c##H##H##H#.c.8r.8q##I##J##J.8y.8y.8w.8w.8v.8D#.e.8C.8B.8z#.g##K##K", +"##L##M##N##O##P##Q##R##S##T##U##V##W##X##Y##Z##0##1##2.NW##3##4##5##6##7##8##9#a.#a##aa#ab#ac#ad#ae#af#ag#ah#ai#aj#ak#al#am#an#ao#ap#aq#ar#as#at#au#av#aw#ax#ay#az#aA#aB#aC#aD#aE#aF#aG#aH#aI#aJ#aK#aL#aM#aN#aO#aP#aQ#aR#aS#aT#aU#aV#aW#aX#aY#aZ#a0#a1#a2#a3#a4#a5#a6#a7#a8#a9#b.#b##ba#bb#bc#bd##H##H##H##H##H##H##H##H#be#bf#bf#bg#bf##H#.c.8r#bh#bh#bi##J##J#bj.8y.8y#bk#bk#bl##K#.g.8z.8A.8B", +"#bm#bn#bo#bp#bq#br#bs#bt#bu#bv#bw#bx#by#bz#bA#bB.Ud#bC#bD#bE#bF#bG#bH#bI#bJ#bK#bL#bM#bN#bO#bP#bQ#bR#bS#bT#bU#bV#bW#bX#bY#bZ#b0#b1#b2#b3#b4#b5#b6#b7#b8#b9#c.#c##ca#cb#cc#cd#ce#cf#cg#ch#ci#cj#ck#cl#cm#cn#co#cp#cq#cr#cs#ct#cu#cv#cw#cx#cy#cz#cA#cB#cC#cD#cE#cF#cG#cH#cI#cJ#cK#cL#cM#cN#cO#cP#cQ#cR#cR#cR#cR#cR#cR#cR#cR#cR#cS#cT#cT#cR#bf##H#.c##J##I#bh#bh#bh#cU#cV#cV#cW#cW#cX#bk#cY##K##I#.g", +"#cZ#c0#c1.dg#c2#c3#c4#c5#c6#c7#c8#c9#d.#d##da#db.7a#dc#dd#de#df#dg#dh#di#dj#dk#ab#dl#dm#a.#dn#.H#do#dp#dq#dr#ds#dt#du#dv#dw#dx#dy#dz#dA#dB#dC#dD#dE#dF#dG#dH#dI#dJ#dK#dL#dM#dN#dO#dP#dQ#dR#dS#dT#dU#dV#dW#dX#dY#dZ#d0#d1#d2#d3#d4#d5#d6#d7#d8#d9#e.#e##ea#eb#ec#ed#ee#ef#eg#eh#ei#ej#ek#el#em#en#cT#cT#cT#cT#cT#cT#cT#cT#eo#eo#ep#eo#eq#cR#bf##H#bh#bh#bh#cV#cX#er#er#es#bl#cY#bk#cX#cW#et#eu#ev", +"#ew#ex#ey#ez#eA#eB#eC#eD#eE#eF#eG#eH#eI#eJ#eK#eL#eM#eN.oG#eO#eP#eQ#eR#eS#eT#eU.yY#eV#eW#eX#eY#eZ#e0#e1#e2#e3#e4#e5#e6#e7#aY#e8#e9#f.#f##fa#fb#fc#fd#fe#ff#fg#fh#fi#fj#fk#fl#fm#fn#fo#fp#fq#fr.To#fs#ft#fu#fv#fw#fx#fy#fz#fA#fB#fC#fD#fE#fF#fG#fH#fI#fJ#fK#fL#fM#fN#fO#fP#fQ#fR#fS#fT#fU#fV#fW#fX#eo#eo#eo#eo#eo#eo#eo#eo#fY#fZ#f0#fY#eo#cT#bg#be#er#er#er#er#er#er#er#er#f1#f1#cW#f2#eu#ev#f3#f3", +"#f4#f5#f6#f7#f8#f9#g.#g##ga#gb#gc#gd#ge#gf.gv#gg#gh.c4#gi#gj#gk#gl#gm#gn#go#gp#gq#gr#dl#ab#gs#gt#gu#gv#gw#gx#gy#gz#gA#gB#gC#gD#gE#gF#gG#gH#gI#gJ#gK#gL#gM#gN#gO#gP#gQ#gR#gS#gT#gU#gV#gW#gX#gY#gZ#g0#g1#g2#g3#g4#g5#g6#g7#g8#g9#h.#h##ha#hb#hc#hd#he#hf#hg#hh#hi#hj#hk#hl#hm#hn#ho#hp#hq#hr#hs#ht#eo#eo#eo#eo#eo#eo#eo#eo#hu#hu#hu#hu#fY#cT#cR#bf#hv#hw#hw#es#er#er#cV#cV#hx#hx#f3#ev#eu#f2#cW#cX", +"#hy#hz#hA#hB#hC#hD#hE#hF#hG#hH#hI#hJ#hK#hL.XE.DF.p1#hM#hN#hO#hP#hQ#hR.uj#hS#hT#hU#hV#hW#hX#hY#hZ#h0#h1#h2#h3#h4#h5#h6#h7#h8#h9#i.#i##ia#ib#ic#id#ie#if#ig#ih#ii#ij#ik#il#im#in#io#ip#iq#ir#is#it#iu#iv#iw#ix#iy#iz#iA#iB#iC#iD#iE#iF#iG#iH#iI#iJ#iK#iL#iM#iN#iO#iP#iQ#iR#iS#iT#iU#iV#iW#iX#iY#iZ#i0#i1#i2#i2#i3#i0#i3#i2#i3#i2#i3#i4#i4#i2#i5#i2#i6#i7#i8#i9#j.#j##ja#jb#jc#jd#je#je#jf#es#jg#jg", +"#hy#hz#hA#jh#ji#jj#jk#jl#jm#jn#jo#jp#jq#jr#js.V9#jt#ju.ux#jv#hP#hQ#hR.uj#jw.IF#jx#jy#jz#jA#jB#jC#jD#jE#jF#jG#jH#jI#jJ#jK#jL#jM#jN#jO#jP#jQ#jR#jS#jT#jU#jV#jW#jX#jY#jZ#j0#j1#j2#j3#j4#j5#j6#j7#j8#j9#k.#k##ka#kb#kc#kd#iA#ke#g5#kf#kg#kh#ki#kj#kk#kl#km#kn#ko#kp#kq#kr#ks#kt#ku#kv#kw#kx#ky#kz#kA#i0#i3#i2#i2#kB#i0#i3#i2#i4#i3#i2#i0#i3#kC#i2#i3#i6#kD#i8#i9#kE#j##kF#ja#je#je#je#jf#jg#jg#jg#jg", +"#kG.MG#kH#kI#kJ#hG#kK#kL#kM#kN#kO#kP#kQ#kR#kS#kT#kU#kV#kW#kX#hP#kY#hR.uj#kZ#k0.Lk#k1#k2#k3#k4#k5#k6#k7#k8#k9#l.#l##la#lb#lc#ld#le#lf#lg#lh#li#lj#lk#ll#lm#ln#j2#lo#jZ#lp#lq#lr#ls#lt#lu#lv#lw#lx#ly#lz#lA#lB#lC#lD#lE#lF#lG#lH#lI#lJ#lK#lL#lM#lN#lO#lP#lQ#lR#lS#lT#lU#lV#lW#lX#lY#lZ#l0#l1#l2#l3#i0#i3#i2#i3#i0#i0#l4#i2#l5#i0#i2#i3#i2#i5#i1#i4#i6#kD#i7#i8#kE#kE#j.#j##jf#jf#jg#jg#jg#l6#l7#l7", +"#l8.MG#l9#m.#m##ma#mb#mc#hy#kN#md#me#mf#m.#mg#mh#mi#mj#mk#ml#hP#mm#mn.uj#mo#mp#mq#mr#ms#mt#mu#mv#mw#mx#my#mz#mA#mB#mC#mD#mE#mF#mG#mH#mI#mJ#mK#mL#mM#mN#mO#mP#mQ#mR#mS#mT#mU#mV#mW#mX#mY#mZ#m0#m1#m2#m3#m4#m5#m6#m7#m8#m9#n.#n##na#nb#nc#nd#jV#ne#nf#ng#nh#ni#nj#nk#nl#nm#nn#no#np#nq#nr#ns#nt#nu#i4#i0#i3#i3#i0#i4#i0#i3#l5#i0#i3#i0#i0#i2#i3#i4#i6#i6#i7#i8#i8#i9#kE#kE#jg#jg#l6#l7#l7#l7#nv#nv", +"#nw#nx#ny#hL#nz#nA#nB#nC#nD#nE.Uj#nF#jh#nF#nG#nH#nI#nJ#nK#nL#hP#nM#nN.uj#nO#nP#nQ#nR#nS#nT#nU#nV#nW#nX#nY#nZ#n0#n1#n2#n3#n4#n5#n6#n7#n8#n9#o.#o##oa#ob#oc#od#oe#of#og#oh#oi#oj#ok#ol#om#on#oo#op#oq#or#os#ot#ou#ov#ow#ox#oy#oz#oA#oB#oC#oD#oE#oF#oG#oH#oI#oJ#oK#oL#oM#oN#oO#oP#oQ#oR#oS#oT#oU#oV#i4#i0#i3#i0#i4#i4#i4#kB#i4#i0#i0#l5#l5#i3#oW#i0#i6#i6#i6#kD#i7#i7#i8#i8#l7#l7#l7#nv#iY#iY#iY#oX", +"#oY#jo#ny#oZ#o0#o1#o2#o3#o4#nE#jo#nF#o5#jp#o6#o7#o8.G0#o9#p.#hP#p##nN.uj#pa#pb#pc#pd#pe#pf#pg#ph#pi#pj#pk#pl#pm#pn#po#pp#pq#pr#ps#pt#pu#pv#pw#px#py#pz#pA#pB#pC#pD#pE#pF#pG#pH#pI#pJ#pK#pL.WN#pM#pN#pO#pP#pQ#pR#pS#pT#pU#pV#pW#pX#pY#pZ#p0#p1#p2#p3#p4#p5#p6#p7#p8#p9#q.#q##qa#qb#qc#qd#qe#qf#qg#l5#i4#i0#i0#i4#l5#i4#i0#i4#i0#i4#qh#qh#i0#i3#kB#i6#i6#i6#i6#i6#i6#i6#i6#iY#iY#iY#iY#qi#qj#qj#qj", +"#qk#jo#ny#ql#qm#qn#qo#qp#qq#hH#kO#kI#kQ#hJ#qr#qs#qt#qu#qv#qw#hP#qx#qy#qz#qA#qB#qC#qD.5J#qE#qF#qG#qH#qI#qJ#qK#qL#qM#qN#qO#qP#qQ#qR#qS#qT#qU#qV#qW#qX#qY#qZ#q0#q1#q2#q3#q4#q5#q6#q7#q8#q9#r.#r##ra#rb#rc#rd#re#rf#rg#rh#ri#rj#rk#rl#rm#rn#ro#rp#rq#rr#rs#rt#ru#rv#rw#rx#ry#rz#rA#rB#rC#rD#rE#rF#rG#l5#i4#i0#i4#l5#l5#i4#i0#rH#i4#i4#qh#l5#i0#i0#i4#i6#i6#rI#rI#rI#rJ#rK#rK#oX#oX#qj#qj#qj#rL#rM#rM", +"#qk#jo#ny#ql#rN#rO#rP#rQ#rR#jn#hI.lQ#rS#kI.MS#rT#rU#rV#qv#rW#hP#rX#rY#qz#rZ#r0#r1#r2#r3#r4#r5#r6#pi#r7#r8#r9#s.#s##sa#sb#sc#sd#se#sf#sg#sh#si#sj#sk#sl#sm#sn#so#sp#sq#sr#ss#st#su#sv#sw#sx#sy#sz#sA#sB#sC#sD#sE#sF#sG#sH#sI#sJ#sK#sL#sM#sN#sO#sP#sQ#sR#sS#sT#sU#sV#sW#sX#sY#sZ#s0#s1#s2#s3#s4#s5#l5#i4#i0#i4#l5#rH#l5#i4#s6#l5#i0#i4#i4#i0#i4#qh#i6#i6#rI#rJ#rK#rK#rK#rK#qj#qj#qj#qj#rL#rM#rM#rM", +"#s7#s8#s9#t.#t##ta#tb#tc#td.qa#te#tf#tg#th#ti#tj#tk#tl#tm#tn#to#tp#tq#tr#ts#tt#tu#tv#tw#tx#ty#tz#tA#tB#tC#tD#tE#tF#tG#tH#tI#tJ#tK#tL#tM#tN#tO#tP#tQ#tR#tS#tT#tU#tV#tW#tX#tY#tZ#t0#t1#t2#t3#t4#t5#t6#t7#t8#t9#u.#u##ua#ub#uc#ud#ue#uf#ug#uh#ui#uj#uk#ul#um#tN#un#uo#up#uq#ur#us#ut#uu#uv#uw#ux#uy#uz#uA#uB#uC#uz#uD#oU#uC#uB#uA#uz#uE#nr#nr#nr#uE#uF#uG#uH#uI#uJ#uK#uL#uH#uM#uN#uO#uP#uQ#uR#uS#uT", +"#uU.8W#uV#uW#uX#uY#uZ.HF#u0#u1#u2#u3#u4#u5#u6#u7#u8#u9#v.#v##va#vb#vc#vd#ve#vf#vg#vh#vi#vj#vk#vl#vm#vn#vo#vp#vq#vr#vs#vt#vu#vv#vw#vx#vy#vz#vA#vB#vC#vD#vE#vF#vG#vH#vI#vJ#vK#vL#vM#vN#vO#vP#vQ#vR#vS#vT#vU#vV#vW#vX#vY#vZ#v0#v1#v2#v3#v4#uh#v5#v6#v7#v8#v9#w.#w##wa#wb#wc#wd#we#wf#wg#wh#wi#wj#wk#uz#uA#uC#uA#uz#wl#oU#uC#uC#oU#wl#nr#wm#wm#wm#nr#uF#uG#uH#uI#uJ#uK#uL#uH#uM#uM#uN#uP#wn#wo#uR#uS", +"#wp#wq#wr#ws#wt#wu#wv#ww#wx#wy#wz#wA#wB#wC#wD#wE#wF#wG#wH#wI#wJ#wK#wL#wM#wN#wO#wP#wQ#wR#wS#wT#wU#wV#wW#wX#wY#wZ#w0#w1#w2#w3#w4#w5#w6#w7#w8#w9#x.#x##xa#xb#xc#xd#xe#xf#xg#xh#xi#xj#xk#xl#xm#xn#xo#xp#xq#xr#xs#xt#xu#xv#xw#xx#xy#xz#xA#xB#xC#v5#xD#xE#xF#xG#xH#xI#xJ#xK#xL#xM#xN#xO#xP#xQ#xR#xS#xT#wl#oU#uC#uA#uz#wl#oU#uC#oU#wl#uE#wm#xU#xU#wm#nr#uF#uG#uH#uI#uJ#uK#uL#uH#xV#xW#uM#uO#uP#uQ#wo#uR", +"#xX#xY#xZ#x0#x1#x2#x3#x4#x5#x6.zo#x7#x8.Zd#x9#y.#y##ya#yb#yc#yd.zs#ye#yf#yg#yh#yi#yj#yk#yl#ym#yn#yo#yp#yq#tD#yr#ys#yt#yu#yv#yw#yx#yy#yz#yA#yB#yC#yD#yE#yF#yG#yH#yI#yJ#yK#yL#yM#yN#yO#xl#yP#yQ#yR#yS#yT#yU#yV#yW#yX#yY#yZ#y0#y1#y2#y3#y4#y5#y6#y7#y8#y9#z.#z##za#zb#zc#zd#ze#zf#uE#zg#oU#zh#zh#zi#wl#oU#uA#uA#wl#zj#uz#uA#uD#zj#nr#xU#zk#xU#wm#nr#uF#uG#uH#uI#uJ#uK#uL#uH#zl#zl#xV#uM#uO#uP#wn#uQ", +"#zm#zn#zo#zp#zq#zr#zs#zt#zu#zv#zw#zx#zy#zz#zA#zB#zC#zD#zE#zF#zG#zH#zI#zJ#zK#zL#zM#zN#zO#zP#zQ#zR#zS#zT#zU#zV#zW#zX#zY#zZ#z0#z1#z2#z3#z4#z5#z6#z7#z8#xa#z9#A.#A##Aa#yJ#Ab#Ac#Ad#Ae#Af#Ag#Ah#Ai#Aj#Ak#Al#Am#An#Ao#Ap#Aq#Ar#As#At#Au#Av#Aw#fy#Ax#xD#Ay#Az#AA#AB#AC#AD#AE#AF#AG#AH#AI#AJ#AK#AL#AM#AN#zj#uz#uA#oU#wl#zj#wl#uA#wl#zj#nr#xU#xU#wm#nr#uE#uF#uG#uH#uI#uJ#uK#uL#uH#AO#AP#zl#xV#uM#uO#uP#uP", +"#AQ#AR#AS#AT#AU#AV#AW.7n#AX#AY#AZ#A0#A1#A2#u0#A3#A4#A5#A6#zq#A7#A8#A9#B.#B##Ba#Bb#Bc#Bd#Be#Bf#Bg#Bh#zT#Bi#Bj#Bk#Bl#Bm#Bn#Bo#Bp#Bq#Br#Bs#Bt#Bu#Bv#Bw#Bx#By#Bz#BA#BB#BC#BD#BE#BF#BG#BH#BI#BJ#BK#BL#BM#BN#BO#BP#BQ#BR#BS#BT#BU#BV#BW#BX#BY#BZ#B0#B1#B2#B3#B4#B5#B6#B7#B8#B9#C.#C##Ca#Cb#Cc#Cd#Ce#Cf#zj#uz#oU#oU#zj#uE#wl#oU#uz#wl#uE#nr#nr#uE#wl#uz#uF#uG#uH#uI#uJ#uK#uL#uH#Cg#Cg#AO#zl#xV#uM#uN#uO", +"#Ch#Ci#Cj#Ck#Cl#Cm#Cn#Co#Cp#Cq#Cr#Cs#Ct#Cu#Cv#Cw#Cx#Cy#Cz#CA#CB#CC#CD#CE#CF#CG#CH#CI#CJ#CK#CL#CM#CN#CO#CP#CQ#CR#CS#CT#CU#CV#CW#CX#CY#CZ#C0#C1#C2#Bx#C3#C4#C5#C6#C7#C8#C9#D.#D##Da#Db#Dc#Dd#De#Df#Dg#Dh#Di#Dj#Dk#Dl#Dm#Dn#Do#Dp#Dq#Dr#Ds#Dt#Du#Dv#Dw#Dx#Dy#Dz#DA#DB#DC#DD#DE#DF#DG#DH#DI#DJ#DK#DL#uE#wl#oU#uz#zj#uE#wl#oU#oU#uz#wl#zj#zj#wl#oU#uA#uF#uG#uH#uI#uJ#uK#uL#uH#DM#DN#Cg#AP#zl#xW#uM#uN", +"#DO#DP#DQ#DR#DS#DT#DU#DV#DW#DX#DY#DZ#D0#D1#D2#D3#D4#D5#D6#D7#D8#D9#E.#E##Ea#Eb#Ec#Ed#Ee#Ef#Eg#Eh#wV#Ei#Ej#Ek#El#Em#En#Eo#Ep#Eq#Er#Es#Et#Eu#Ev#Ew#Ex#Ey#Ez#EA#EB#C7#EC#ED#EE#EF#EG#EH#EI#EJ#EK#EL#EM#EN#EO#EP#EQ#ER#ES#ET#EU#EV#EW#EX#EY#EZ#E0#E1#E2#E3#E4#E5#E6#E7#E8#E9#F.#F##kD#Fa#Fb#Fc#Fd#Fe#uE#wl#oU#uz#zj#uE#zj#uz#uA#oU#uz#wl#uD#oU#uC#uB#uF#uG#uH#uI#uJ#uK#uL#uH#DM#DM#Cg#AO#zl#xV#uM#uM", +"#Ff#Fg#Fh#Fi#Fj#Fk.9.#Fl#Fm#Fn#Fo#Fp#Fq#Fr#Fs#Ft#Fu#Fv#Fw#Fx#Fy#Fz#FA#FB#FC#FD#FE#FF#FG#FH#FI#FJ#FK#FL#FM#FN#FO#FP#FQ#FR#FS#FT#FU#FV#FW#FX#FY#FZ#F0#F1#F2#F3#F4#F5#F6#F7#F8#F9#G.#G##Ga#Gb#Gc#Gd#Ge#Gf#Gg#Gh#Gi#Gj#Gk#Gl#Gm#Gn#Go#Gp#Gq#Gr#Gs#Gt#Gu#Gv#Gw#Gx#Gy#Gz#GA#GB#GC#qj#uQ#GD#GE#uv#GF#GG#GH#GH#GH#wh#wh#qe#qe#qe#GI#GJ#GK#GL#GL#GK#GM#GN#GO#GO#GP#GQ#GR#GS#GT#GU#GV#GW#GX#GX#GY#Cc#GP#Fb", +"#GZ#G0#G1#G2#G3#G4#G5#G6#G7#hY#G8#G9#H.#H##Ha#Hb.2r#Hc#Hd#He#Hf#Hg#Hh#Hi#Hj#Hk#ty#Hl#Hm#Hn#Ho#Hp#Hq#Hr#Hs#Ht#Hu#Hv#Hw#Hx#Hy#Hz#HA#HB#HC#HD#HE#HF#HG#HH#HI#HJ#HK#HL#HM#HN#HO#HP#HQ#HR#HS#HT#HU#HV#HW#HX#HY#HZ#H0#H1#H2#H3#H4#H5#H6#H7#H8#H9#I.#I##Ia#Ib#Ic#Id#Ie#If#Ig#Ih#Ii#rM#uP#Ij#Ik#uv#Il#Im#In#GH#GH#GH#wh#qe#qe#qe#In#GH#qe#GL#Io#Io#GL#qe#Ip#GO#Iq#AK#GR#GS#AI#GU#Ca#Cc#DI#DI#GX#uM#Ip#Ir", +"#Is#It#Iu#Iv#Iw#Ix#Iy#Iz#IA#IB#IC#ID#IE#IF#IG#IH#II#IJ#IK#IL#IM#IN#IO#IP#IQ#IR#IS#IT#IU#IV#IW#IX#IY#IZ#I0#I1#I2#I3#I4#I5#I6#I7#I8#I9#J.#J##Ja#Jb#Jc#Jd#Je#Jf#Jg#Jh#Ji#Jj#Jk#Jl#Jm#Jn#Jo#Jp#Jq#Jr#Js#Jt#Ju#Jv#Jw#Jx#Jy#Jz#JA#JB#JC#JD#JE#JF#JG#JH#JI#JJ#JK#JL#JM#JN#JO#JP#JQ#JR#Ip#Cf#JS#wh#JT#JU#GJ#GJ#In#GH#GH#GH#wh#wh#GL#qe#wh#GL#GN#GN#GL#In#JV#GO#Iq#AK#GR#GS#GS#GT#GY#GX#Ip#Ip#DI#DI#Fb#JW", +"#JX#JY#JZ#J0#J1#J1#J2#J3#J4#J5#J6#J7#J8#J9#K.#K##Ka#rY#Kb#Kc#Kd#Ke#Kf#Kg#Kh#Ki#Kj#Kk#Kl#Km#Kn#Ko#Kp#Kq#Kr#Ks#Kt#Ku#Kv#Kw#Kx#Ky#Kz#KA#KB#KC#KD#KE#KF#KG#KH#KI#KJ#KK#KL#KM#KN#KO#KP#KQ#KR#KS#KT#KU#KV#KW#KX#KY#KZ#K0#K1#K2#K3#K4#K5#K6#K7#K8#K9#L.#L##La#Lb#Lc#Ld#Le#Lf#Lg#Lh#Lh#Li#Lj#Lk#uC#Ll#Ll#GJ#GJ#GJ#GJ#In#GH#GH#GH#qe#qe#qe#GL#ut#ut#GL#qe#Lm#JV#GO#AK#GR#nt#GS#AI#Ca#Cc#DI#DI#GX#GX#Ip#Ir", +"#Ln#Lo#Lp#Lq#Lr##S#Ls#Lt#Lu#Lv#J6#Fp#Lw#Lx#Ly#Lz#LA#LB#LC#LD#LE#LF#LG#LH#LI#LJ#LK#LL#LM#LN#LO#LP#LQ#LR#LS#LT#LU#LV#LW#LX#LY#LZ#L0#L1#L2#L3#L4#L5#L6#L7#L8#L9#M.#M##Ma#Mb#Mc#Md#Me#Mf#Mg#Mh#Mi#Mj#Mk#Ml#Mm#Mn#Mo#Mp#Mq#Mr#Ms#Mt#Mu#Mv#Mw#Mx#My#Mz#MA#MB#MC#MD#ME#MF#MG#MH#MI#MJ#MK#ML#MM#MN#MO#MP#MQ#MQ#MR#GJ#GJ#GJ#In#In#qf#GJ#GK#GM#GL#GL#GM#Io#Lm#Lm#GO#GP#GQ#GR#GS#GS#MS#Ca#GY#Cc#GY#GY#GX#DI", +"#MT#MU#MV#MW#MX#MY#.G#MZ#M0#M1#IC#ID#M2#M3#rO#M4#M5#M6#M7#M8#M9#N.#N##Na#Nb#Nc#Nd#Ne#Nf#Ng#Nh#Ni#Nj#Nk#Nl#Nm#Nn#No#Np#Nq#Nr#Ns#Nt#Nu#Nv#Nw#Nx#Ny#Nz#NA#NB#NC#ND#NE#NF#NG#NH#NI#NJ#NK#NL#NM#NN#NO#NP#NQ#NR#NS#NT#NU#NV#NW#NX#NY#NZ#N0#N1#N2#N3#N4#N5#N6#N7#N8#N9#O.#O##Oa#Ob#Oc#Od#Oe#Of#Og#Oh#Oi#qf#MQ#MQ#MQ#MR#GJ#GJ#GJ#Oj#MQ#qe#GK#qe#wh#GL#ut#Ok#Lm#Ip#GO#AK#GR#nt#GS#Ol#GT#GW#GY#Ca#Ca#zl#GP", +"#Om#On#Oo#Op#Oq#Or.I.#Os#Ot#Ou#Ov#Ow.0H#Ox#Oy#Oz#OA#OB#OC#OD#OE#OF#OG#OH#OI#OJ#OK#OL#OM#ON#OO#OP#OQ#OR#OS#OT#OU#OV#OW#OX#OY#OZ#O0#O1#O2#O3#O4#O5#O6#O7#O8#O9#P.#P##Pa#Pb#Pc#Pd#Pe#Pf#Pg#Ph#Pi#Pj#Pk#Pl#Pm#Pn#Po#Pp#Pq#Pr#Ps#Pt#Pu#Pv#Pw#Px#Py#Pz#PA#PB#PC#PD#PE#PF#PG#PH#PI#PJ#PK#PL#PM#PN#PO#PP#PQ#qf#qf#MQ#MQ#MR#MR#MR#GI#PQ#GJ#GH#GH#GH#GH#GH#PR#Lm#JV#GO#AK#GR#GR#GW#MS#Ca#GY#GY#GY#GY#GX#DI", +"#A5#PS#PT#PU#PV#PW#PX#PY#PZ#P0#Ov#ID#J8#P1#P2#Hb#P3#P4#P5#P6#P7#P8#P9#Q.#Q##Qa#Qb#Qc#Qd#Qe#Qf#Qg#Qh#Qi#Qj#Qk#Ql#Qm#Qn#Qo#Qp#Qq#Qr#Qs#Qt#Qu#Qv#Qw#Qx#Qy#Qz#QA#QB#QC#QD#QE#QF#QG#QH#oE#QI#QJ#QK#QL#Pg#QM#QN#QO#QP#QQ#QR#QS#QT#QU#QV#QW#QX#QY#QZ#Q0#Q1#Q2#Q3#Q4#Q5#Q6#Q7#Q8#Q9#R.#R##Ra#Rb#Rc#Rd#Re#PQ#PQ#qf#qf#MQ#MQ#MQ#MR#MQ#qf#PQ#MR#GH#GH#MQ#GI#PR#Lm#Lm#GO#AK#Cc#GR#nt#Ca#GY#GX#GX#Cc#GX#DI#Fb", +"#Rf#Rg#Rh#Ri#Rj#Rk#Rl#Rm#Rn#Ro#Rp#Rq#Rr#Rs#Rt#Ru#Rv#Rw#Rx#Ry#Rz#RA#RB#RC#RD#RE#RF#RG#RH#RI#RJ#RK#RL#RM#RN#RO#RP#RQ#RR#RS#RT#RU#RV#RW#RX#RY#RZ#R0#R1#R2#R3#R4#R5#R6#R7#R8#R9#S.#S##Sa#Sb#Sc#Sd#Se#Sf#Sg#nj#Sh#Si#Sj#Sk#Sl#Sm#Sn#So#Sp#Sq#Sr#Ss#St#Su#Sv#Sw#Sx#Sy#Sz#SA#SB#SC#SD#SE#SF#SG#SH#SI#SJ#SK#SK#SK#SK#SK#SK#SK#SK#In#GJ#qf#PQ#PQ#MQ#GJ#GH#SL#MN#JW#Lm#GO#GQ#GR#GS#GP#GP#GP#GP#GP#GP#GP#GP", +"#Rf#Rg#Rh#Ri#Rj#SM#Rl#SN#SO#SP#SQ#SR#SS#ST#SU#SV#SW#SX#SY#SZ#S0#S1#S2#S3#S4#S5#S6#S7#S8#S9#T.#T##Ta#Tb#Tc#Td#Te#Tf#Tg#Th#Ti#Tj#Tk#Tl#Tm#Tn#To#Tp#Tq#Tr#Ts#Tt#Tu#Tv#Tw#Tx#Ty#Tz#TA#TB#TC#TD#TE#TF#TG#TH#TI#TJ#TK#TL#TM#TN#TO#TP#TQ#TR#TS#TT#TU#TV#TW#TX#TY#TZ#T0#T1#T2#T3#T4#Il#T5#T6#Oc#T7#T8#T9#s4#s4#s4#s4#s4#s4#s4#s4#MQ#MQ#qf#qf#qf#MQ#MR#GJ#SL#MN#U.#Lm#GO#AK#GR#nt#DI#DI#DI#DI#DI#DI#DI#DI", +"#Rf#Rg#Rh#U##Ua#Ub#Rl#SN#Uc#Ud#Ue#pd#Uf#Ug#Uh#Ui#Uj#Uk#Ul#Um#Un#Uo#Up#Uq#Ur#Us#Ut#Uu#Uv#Uw#Ux#Uy#Uz#UA#UB#UC#UD#UE#UF#UG#UH#UI#UJ#UK#UL#UM#UN#UO#UP.21#UQ#UR#US#UT#UU#UV#UW#UX#UY#UZ#U0#U1#U2#U3#U4#U5#U6#U7#U8#U9#V.#V##Va#Vb#Vc#Vd#Ve#Vf#Vg#Vh#Vi#Vj#Vk#Vl#Vm#Vn#Vo#Vp#Vq#Vr#MJ#Vs#Vt#Vu#Vv#Vw#Vx#Vx#Vx#Vx#Vx#Vx#Vx#Vx#Vy#PQ#qf#MQ#MR#MQ#MQ#MQ#SL#Vz#U.#PR#JV#Iq#AK#Cc#DI#DI#DI#DI#DI#DI#DI#DI", +"#VA#VB#VC#VD#VE#Ub#Rl#VF#VG#VH#VI#VJ#VK#VL#VM#VN#VO#VP#VQ#VR#VS#VT#VU#VV#VW#VX#VY#VZ#V0#V1#V2#V3#V4#V5#V6#V7#V8#V9#W.#W##Wa#Wb#Wc#Wd#We#Wf#Wg#Wh#Wi#Wj#Wk#Wl#Wm#Wn#Wo#Wp#Wq#Wr#Ws#Wt#Wu#Wv#Ww#Wx#Wy#Wz#aB#WA#WB#WC#WD#WE#WF#WG#WH#WI#WJ#WK#WL.N#.BK#WM#WN#WO#WP#WQ#WR#WS#WT#WU#WV#WW#WX#WY#WZ#W0#W1#W1#W1#W1#W1#W1#W1#W1#W2#W3#PQ#MR#GJ#MR#qf#PQ#SL#W4#MN#W5#Lm#GO#Iq#AK#Fb#Fb#Fb#Fb#Fb#Fb#Fb#Fb", +"#VA#VB#VC#W6#W7#Ub#W8#VF#W9#X.#X##Xa#Xb#Xc#Xd#Xe#Xf#Xg#Xh#Xi#Xj#Xk#Xl#Xm#Xn#Xo#Xp#Xq#Xr#Xs#Xt#Xu#Xv#Xw#Xx#Xy#Xz#XA#XB#XC#XD#XE#XF#XG#XH#XI#XJ#XK#XL#XM#XN#XO#XP#XQ#XR#XS#XT#XU#XV#XW#XX#XY#XZ#X0#X1#X2#X3#X4#X5#X6#X7#X8#X9#Y.#Y##Ya#Yb#Yc#Yd#Ye#Yf#Yg#Yh#Yi#Yj#Yk#Yl#Ym#Yn#Yo#Yp#Yq#Yr#Ys#Yt#Yu#W1#W1#W1#W1#W1#W1#W1#W1#Yv#W2#GI#MQ#MR#MQ#qf#PQ#SL#SL#MN#U.#W5#Lm#JV#GO#Fb#Fb#Fb#Fb#Fb#Fb#Fb#Fb", +"#Yw.lN#Yx#Yy#Yz#Ub#YA#YB#YC#YD#YE#YF#YG#YH#YI#YJ#YK#YL#YM#YN#YO#YP#S2#YQ#YR#YS#YT#YU#YV#YW#YX#YY#YZ#Y0#Y1#Y2#Y3#Y4#Y5#Y6#Y7#Y8#Y9#Z..Om.FT#Z##Za#Zb#Zc#Zd#Ze#Zf#Zg#Zh#Zi#Zj#Zk#Zl#Zm#Zn#Zo#Zp#Zq#Zr#Zs#Zt#Zu#Zv#Zw#Zx#Zy#Zz#ZA#K8#ZB#ZC#ZD#ZE#ZF#ZG#ZH#ZI#ZJ#ZK#ZL#ZM#ZN#ZO#ZP#ZQ#ZR#ZS#ZT#ZU#T4#ZV#ZV#ZV#ZV#ZV#ZV#ZV#ZV#Yv#ZW#Oj#GI#PQ#qf#qf#qf#SL#SL#W4#MN#JW#W5#PR#Lm#Ir#Ir#Ir#Ir#Ir#Ir#Ir#Ir", +"#Yw#ZX#ZY#ZZ#Z0#Ub#Z1#YB#Z2#Z3#Z4#Z5#Z6#Z7#Z8#Z9#0.#0##0a#0b#0c#0d#0e#0f#0g#0h#0i#0j#0k#0l#0m#0n#0o#0p#0q#0r#0s#0t#0u#0v#0w#0x#0y#0z#0A#0B#0C#0D#0E#0F#0G#0H#0I#0J#0K#0L#0M#0N#0O#0P#0Q#0R#0S#0T#0U#0V#0W#0X#0Y#0Z#00#01#02#03#04#05.Ym#06#07.L8#08#09.XR#1..HC#1##1a#1b#1c#1d#1e#1f#1g#Yq#1h#1i#1j#1j#1j#1j#1j#1j#1j#1j#ZW#ZW#ZW#W2#W3#PQ#MQ#GJ#SL#SL#W4#MN#MN#JW#W5#W5#Ir#Ir#Ir#Ir#Ir#Ir#Ir#Ir", +"#pc#ZX#ZY#1k.5J#1l#Z1#YB#1m#1n#1o#1p#1q#1r#1s#1t#1u#1v#1w#1x#1y#1z#1A#1B#1C#1D#1E#1F#1G#1H#1I#1J#1K#1L#1M#1N#1O#1P#1Q#1R#1S#1T#1U#1V#hY#1W#1X#1Y#1Z#10#11#12#13#14#15#16#17#18#19#2.#2##2a#2b#2c#2d#2e#2f#2g#2h#2i#2j#2k#2l#2m#2n#2o#2p#2q#2r#2s##9#2t#2u#2v#2w#2x#2y#2z#2A#2B#2C#2D#2E#2F#2G#2H#1j#1j#1j#1j#1j#1j#1j#1j#W2#ZW#Yv#Yv#W2#Vy#MQ#In#SL#SL#SL#Vz#MN#MN#U.#JW#JW#JW#JW#JW#JW#JW#JW#JW", +"#2I#2J#2K#2L#2M#2N#2O.Jf#2P#2Q#2R#2S#2T#2U#2V#2W#2X#2Y#2Z#20#21#22#23#24#25#26#27#28#YM#29#3.#3##3a#3b#3c#3d#3e#3f#3g#3h#3i#Rm#3j#3k.B.#3l#3m#3n#3o#3p#3q#3r#3s#3t#3u#3v#3w#3x#3y#3z#3A#3B#3C#3D#XY#3E#3F#3G#3H#3I#3J#3K#3L#3M#3N#3O#3P#3Q#3R#ve#3S#3S#3S#3S#3S#3S#3S#3S#3T#3U#3V#3W#3X#3Y#3Z#30#31#32#33#34#35#36#37#38#39#4.#4##4a#4b#4c#4d#4e#4f#1j#4g#4h#2F#PN#4i#4i#4j#4k#4l#4m#4n#4o#4p#4q", +"#4r#4s#4t#4u#4v#4w#4x#4y#4z#4A#4B#4C#4D#4E#4F#4G#4H#4I#4J#4K#4L#4M#4N#4O#4P#4Q#4R#4S#4T#4U#4V.6w#4W#4X#4Y#4Z#40#41.Hs#42#43.I4#44#45.B.#46#47#48#49#5.#5##5a#5b#5c#5d#5e#5f#5g#5h#5i#5j#5k#5l#5m#5n#5o#5p#5q#5r#5s#5t#5u#5v#5w#5x#5y#5z#5A#5B#5C#3S#3S#3S#3S#3S#3S#3S#3S#5D#5E#5F#5G#5H#5I#5J#5K#5L#5M#5N#5O#5P#5Q#5R#5S#5T#5U#5V#5W#5X#5Y#5Z#50#s4#W1#Yq#39#51#52#Rc#53#54#55#56#Lk#57#58#59#6.", +"#6##6a#6b#6c#6d#6e#6f#6g#6h#6i#6j#6k#6l#6m#6n#6o#6p#6q#6r#6s#6t#6u#6v#6w#6x#6y#6z#6A#6B#6C#6D#6E#6F.7s.Np#41#6G#6H.F9.HK.O9.Mb#6I#6J#6K#6L#6M#6N#6O#6P#6Q#6R#6S#6T#6U#6V#6W#6X#6Y#6Z#60#61#62#63#64#65#66#67#68#69#7.#7##7a#7b#7c#7d#7e#7f#7g#7h#3S#3S#3S#3S#3S#3S#3S#3S#7i#7j#7k#7l#Rr#7m#7n#7o#7p#7q#7r#7s#7t#7u#7v#7w#5P#7x#7y#7z#7A#7B#7C#7D#7E#W1#7F#7G#7H#7I#53#7J#7K#54#In#Lk#57#7L#7M#7N", +"#7O.yR#7P#7Q#7R#7S#7T#7U#7V#7W#7X#7Y#7Z#70#71#72#73#74#75#76#77#78#79#8.#8##8a#8b#8c#8d#8e#8f#8g.N.#8h#8i#8j#8k#8l#8m#8n#8o#8p#8q#8r#8s#8t#8u#8v#8w#8x#8y#8z#8A#8B#8C#8D#8E#8F#8G#8H#8I#8J#8K#8L#8M#8N#8O#8P#8Q#8R#8S#8T#8U#8V#8W#8X#8Y#8Z#80#81#3S#3S#3S#3S#3S#3S#3S#3S#82#83#84#85#86#87#88#89#9.#9##9a#9b#9c#9d#9e#9f#9g#9h#9i#9j#9k#9l#MK#9m#s4#1j#7F#9n#9n#52#53#7J#9o#9p#9q#4m#9r#7L#9s#7N", +"#9t#9u#9v#9w#9x.LF#9y#9z#9A#9B#9C#9D#9E#9F#9G#9H#9I#9J#9K#9L#9M#9N#9O#9P#9Q#9R#9S#9T#9U.Np.Jg.Hv#9V#9W#9X#9Y#9Z#90#91#92#93#94#95#96.Ep#97#98#99a..a.#a.aa.ba.ca.da.ea.fa.ga.ha.ia.ja.ka.la.ma.na.oa.pa.qa.ra.sa.ta.ua.va.wa.xa.ya.za.Aa.Ba.Ca.Da.Ea.Ea.Ea.Ea.Ea.Ea.Ea.Ea.Fa.Ga.Ha.Ia.Ja.Ka.La.Ma.Na.Oa.Pa.Qa.Ra.Sa.Ta.Ua.Va.Wa.Xa.Ya.Za.0#ZWa.1a.2a.3#Yqa.4#2F#Oga.5#53a.6a.7#wh#AN#9r#MPa.8a.9", +"a#.a##a#aa#ba#ca#da#ea#fa#ga#ha#ia#ja#ka#la#ma#na#oa#pa#qa#ra#sa#ta#ua#va#wa#xa#ya#za#Aa#Ba#Ca#Da#Ea#E#YAa#Fa#Ga#H.SV.SUa#Ia#Ja#Ka#La#Ma#Na#Oa#Pa#Qa#Ra#Sa#Ta#Ua#Va#Wa#Xa#Ya#Za#0a#1a#2a#3a#4a#5a#6a#7a#8a#9aa.aa#aaaaabaacaadaaeaafaagaahaaiaaja.Ea.Ea.Ea.Ea.Ea.Ea.Ea.EaakaalaamaanaaoaapaaqaaraasaataauaavaawaaxaayaazaaAaaBaaCaaDaaEaaFaaGaaHaaI#7F#Yq#4haaJaaKa.5aaL#55aaMaaN#AN#9r#MPa.8#9s", +"aaOaaP#zwaaQaaRaaSaaTaaUaaVaaWaaXaaYaaZaa0aa1aa2aa3aa4aa5aa6.Npaa7aa8aa9ab.ab#.S.abaabbabcabdabeabfabgabhabiabjabk.FVablablabmabnaboabpabqabrabsabtabuabvabwabxabyabzabAabBabCabDabE#WBabFabGabHabIabJabKabLabMabNabOabPabQabRabSabTabUabVabWabXa.Ea.Ea.Ea.Ea.Ea.Ea.Ea.EabYabZab0ab1.Gh.XQ.0Xab2#VEab3ab4ab5ab6ab7ab8ab9ac.ac#acaacbaccacdaceacfa.2a.2#7F#39#Yo#Ogacg#7J#WX#55#wh#AN#9r#GDachaci", +"acjackaclacmacnacoacpacqacracsactacuacvacwacxacyacz.O3acAacBacCacDacEacEacF.KW.KSacGacH.I2acIacJacKacLacMacNacOacPacPacQ.JnacRacSacTacUacVacWacXacYacZac0ac1ac2ac3ac4ac5ac6ac7ac8ac9ad.ad#adaadb#G#adcaddadeadfadgadhadiadjadkadladmadnadoadpadqa.Ea.Ea.Ea.Ea.Ea.Ea.Ea.EadradsadtaduadvadwadxadyadzadA#RjadBadCadDadEadFadGadHadI.ViadJadKadLadM#ZVa.3adN#7G#9nadOadP#woadQadR#In#AN#IjadSadTadU", +"adVadWadXadYadZad0ad1ad2ad3ad4ad5ad6ad7ad8ad9ae.ae#ae#aeaaeb.HM.HMaecaecaedaedaedaedaedaedaedaedaeeaeeaeeaeeaeeaeeaeeaeeaefaegaehaeiaefaefaejaekaelaemaenaeoaepaeqaeraesaetaeuaevaewaexaeyaezaeAaeBaeCaeDaeEaeFaeGaeHaeIaeJaeKaeLaeMaeNaeOaecaea#3Sa.EaePaeQaeQaePa.E#3SaeRaeSaeT.0AaeUaeVaeWaeW.LE.LE.LE.LE.LE.LE.LE.LEaeXaeYaeZae0ae1ae2ae3ae4ae5ae6ae7ae8ae9af.#wlaf##Ll#MNafaafbafcafdafeaff", +"afgafhafiafj.Hoafkaflafmafnafoafpafqafrafs.KIaft.Nwafuafuae#aeaaebaeb.HMaedaedaedaedaedaedaedaed.O8.O8.O8.O8.O8.O8.O8.O8afvafvafwafxafyafzafAafBafCafDafEafFafGafHafIafJafKafLafMafNafOafPafQafRafSafTafUafVafWafXafYafZaf0af1af2af3af4af5af6af6#3S#3Sa.EaeQaeQa.E#3S#3Saf7af8af9ag.ag#agaagbagc.LE.LE.LE.LE.LE.LE.LE.LEagdageagfaggaghagiagjagkaglagmagnagoagp#2Bagq#nsagragsafaafbafcagtaguagv", +"agwagxagyagzagAagBagCagDagEagFagGagHagIagJagKagLagMagMagNagN.Nwafuae#ae#agOagOagOagOagOagOagOagO#44#44#44#44#44#44#44#44afzaegagPafxaefagPagQagRagSagTagUagVagWagXagYagZag0ag1ag2ag3ag4ag5ag6ag7ag8ag9ah.ah#ahaahbahcahd.moaheahfaeJahg.H2ahhaf6ahi#3Sahja.Ea.Eahj#3SahiahkaeTaf9.EWahlahmagbahn.pS.pS.pS.pS.pS.pS.pS.pSahoahpahqahrahsahtahuahvahwahxahyahzahA#7rahBahCahD#58#MHahEahFagtahGahH", +"ahIahJahKahLahMahNahOahPahQahRacAahS.JfahT.F7.Mfaf5af5ahUagMagNagN.Nw.NwagOagOagOagOagOagOagOagO.H9.H9.H9.H9.H9.H9.H9.H9agPafx#3jafzaefahVahWahXahY.ptahZ.mEah0ah1ah2ah3ah4ah5ah6ah7ah8ah9ah8ai.ai#aiaaibaicaidaieaif.nRaigaihaiiaijaikaaiail.HMaimahi#3S#3S#3S#3Sahiaimainainaioaipag#ahmaiqairaisaisaisaisaisaisaisaisaitaiuaivaiwaixaiyaizaiAaiBaiCaiDaiEaiFaiGaiHaiIaiJaiK#LgaiLaiMaiN#uFaiO", +".CtaiPaiQaiRaiSaiTaiUaiVaiWaiXaiY.xN.xNaiZai0ai1af5af5ahUagMagNagN.Nw.Nwai2ai2ai2ai2ai2ai2ai2ai2.H9.H9.H9.H9.H9.H9.H9.H9ai3afzai4ai5ai6ai5afxai7ai8ai9aj.aj#ajaajbajcajdajeajfajgajhajiajjajkajlajmajnajoajpajqajrajs.q0ajt.l#ahfajuajvaecaf6aeaabhabhahiajwajwahiabhabhahkaio.DhajxahmajyajyajzaisaisaisaisaisaisaisaisajAajAajBajCajDajEajFajGajHajIajJajKajLajMajNajOajPajQajRaiLajSajTajUajV", +"ajWajXajYajZaj0.F9aj1aj2aj3aj4.Jgaj5.HVaj6aj7aj8agMagMagNagN.Nwafuae#ae#ai2ai2ai2ai2ai2ai2ai2ai2#44#44#44#44#44#44#44#44aeiaj9aegagPaj9ai6aehak.ak#akaakbakcagUakdakeakfakgakhakiakjakkaklakmaknakoakpakqakraksaktaku.ibakvaigaiiakwakxajvahUafu#8oakyabhahiahiabhaky#8oainakzakAakBaeUajyakCakDaisaisaisaisaisaisaisaisakEakFakGakHakIakJakKakLakMakNakOakPakQakRakSakTakUakVakWakXakY#iYakZak0", +"ak1ak2.IMak3ak4ak5ak6ak7ak8ak9al.al#.HNalaalbabf.Nwafuafuae#aeaaebaeb.HMalcalcalcalcalcalcalcalc.O8.O8.O8.O8.O8.O8.O8.O8aefafzaldaleaefai4afwaj9alfalgalhalialjalkallalmalnaloalpalqalralsaltalualvalwalxalyalzalAalBaku.moaihalCagNalDafualEalFalG#8oabhabhabhabh#8oalGalHalIakAajxalJahmalKakCalLalLalLalLalLalLalLalLalMalNalOalPagcalQalRalS.CYalTalUa#E.DjalValWalXalYalZakWal0al1al2al3al4", +"al5al6al7al8al9am.am#amaambamcamdameamfamgamh.Nzae#ae#aeaaeb.HM.HMaecaecalcalcalcalcalcalcalcalcaeeaeeaeeaeeaeeaeeaeeaeeamiaegafwai4aldaegafzamjamkamlammamnamoampamqakuamramsamtamuamvamwamxamyamzamAamB.l#.mSamCamDamE.jiamFamGamHamIamJamKamLalGamM#8oabhabh#8oamMalGamNamOalIamPamQamRajyamSamTamTamTamTamTamTamTamTamUamVamWamXamYamZam0am1am2am3am4am5am6am7am8am9an.an#anaal0anbal2ancand", +"aneanfang.Nhanh.R7anianjaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.KC.KC.O8.O8anlanmannannanoanoanoanoanoanoanoano#44#44anpanp.Mg.Mganqanq.rcanransantanuanv.jianuanwanxanyanzanAanBanCanDanEanFanGanH.mo.ji.l#anIanJanKanLanLanManNanOanPanQanRanR.HGanSanTanUanUanVanWanXanY.KfanZan0an1#YCan2an3an4an5#YCan6an7an8an8an9ao.ao#aoaaobaobaocaodaoeaofaoc.Klaogaohaoiaojaokaolaomaonaoo#ML", +"aopaoqaor.Neaosaotaouaovaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.KC.KC.KC.O8anlanmanmannanoanoanoanoanoanoanoano#44#44anpanp.Mg.Mgaowanqaoxaoy.rgaozaoA.l#.AF.qNaoBaoCaoDaoEaoFaoGaoHaoIaoJanF.JbaoKaoLaoM.l#aoNaoOaoPaoQaoRanOaoSaoTaoUaoVaoWaoXaoYaoZao0anZanZao1anYanYanYanYanYanYanYan4an3an9ao2an4an4ao3an9an7an3an8ao.ao#aobao4#SUaocaodaoeaofaf7.Klaogao5ao6ao7ao8ao9#9qap.ap#apa", +"apbapcapdape.Ncapfapgaphaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankapiapi.KC.O8.O8anlanmanmapjapjapjapjapjapjapjapj#44#44apkanp.S..Mgaplanqapmapnapoaoz.l#appaiganuapqaprapsaptapuahNam4apvapwapxapyapzapAapB.jiapCapDapEapFapGanNanLapGaoUaoWaoWaoXaoYapHao0anZapIanWanWanWanWanWanWanWanW#YCan6apJan6#YC#YCan5apJapJan4an3an9apKao4apLapMapNapOapPaof#VF.KlaogapQapRapSapTapU#GHapV#rHapW", +"apXapYapZap0ap1ap2ap3ap4aeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankankankapi.KC.O8.O8anlanlapjapjapjapjapjapjapjapj#6I#44#44anpanp.Mg.Mg.Mgap5ap6ap7.moanuanvap8ap9aq.aq#aqaaqbaqcaqdaqeaqfaqgaqhaqiaqjaqkaqlaqmaqnaqoapFaqpaqqapGaqraqsaqpaqtaquaqvaqwaqxaqy.Kf.KfaqzaqAaqBaqCanWanY.KfaqDaqEaqF#YC#YCaqEaqEaqE#YCaqGapJan7an9aoa#SUapMaqHapNapOaqIao5aqJaqKaogapQaqLaqMaqNaqOaqPaqQaqR#Ol", +"aqSaqTaqUaqV.uIaqW.FTaqXaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankaqYankankapi.KC.O8.O8.O8#6I#6I#6I#6I#6I#6I#6I#6IaqZaq0#44apkanp.S..Mg.Mgaq1aq2aq3.rfaq4.moaq5.BJaq6aftaq7aq8aq9ar.ar#araarbarcardakxarearfargarhariarjarkanPapGaoRanParlaquaquaqvarmarnaro.KfarparqaqzarrarsanWanY.KfapIan7an4an4an4an4an7an4an4aqGapJan7an9aoa#SUapMaqHartaruaqIarvaqJ.I2#VFapQarwarxaryarz#uwarAarBarC", +"arDarEarFarGarHarIarJanhaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankankaqYaqYankankapi.KC.KC.O8arfarfarfarfarfarfarfarfaqZaqZ#44#44anpanp.Mg.Mg.HKarKarLarM.l#arNarOarP.LOarQapiarRanlarS.LN.LOarTarUarVagNaecarWarXarYapFanOarZanOanMar0ar1anNar2ar3ar4ar5ar6ar7anYanYarsarsar8ar9anVanWanXanYan3an4an6an2an7an3an4apJapJan4an3an9apKao4apLapMaqJaruas.as#asa.I2#YAasbascasdaseasfasgashasiasj", +"askaslasmasnasoaspasqarJaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.LL.LLaqYankankapi.KC.KC.KP.KP.KP.KP.KP.KP.KP.KPaqZaqZaq0#44apkanp.S..Mgasrassastasuasva.C.HL.HLaswasxasyaszasAaswaswasBaqhasCasDaf5asEasFasGasHaqq.F6asIarjaqsarjaoQanPar3ar3ar4asJasKar7anY.HEarsar8ar9anVanWanYao1.Kfan7an6#YCan6an4ao3an4an5an7an3an8ao.ao#aobao4#SU.KlasLaohas##SN.I2#YAasMasNasOasPasQasRasSaiNasT", +"asUasVasWasXasYasZas0.Hsaeaaeaaeaaeaaeaaeaaeaaeaankankankankankankankank.LL.LLaqYaqYankapi.KC.KC.KP.KP.KP.KP.KP.KP.KP.KPaqZaqZaqZ#44apkanpanp.S.as1adpaoxas2a.Cap5arPadpas3as4as5as6as7abjas6as8aqhas9at.at#aaiataatbatcanManPanMaqratdateanOanLatfar3ar4asJasKatganY.HEaqzatharsatianYanZan1atjapKan9an3an8ao#aoaao.an3an8an8an9ao.ao#aoaaobaob.KlasLatkas##SNatl#YAasMatmatnatoatpatq#Ceatrats", +"attatuatvatwatxaty.Hsatzapjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiankankapi.KC.O8anlanmanmarfarfarfarfarfarfarfarfanpanpanpanpanpanpanpanpatAatBatCatDatDarn.McatEatFatGatHatIatJ.OqatKatLatMatNatOatPatQahWahVatRatSa.DatTaj8atUatVatWatSatfatXatYatZat0at1anWanVatianXanYatiarsat2ar9anYat3at4an0at5at6at7at3at8at7at7at7an0at6at6at5at5at9au.au##9Y.XR.S..TEaqY#YAauaaubaucaudaueaufaug", +"auhauiaujaukaulaum.Hsatzapjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapi.KC.KC.KC.KC.O8.O8.O8.O8arfarfarfarfarfarfarfarfanpanpanpanpanpanpanpanpatEacHarn#ZHaunauoaupauoauqatI.HYaurausautauuauvauwauxauyauzatMauAatQaldaj8atTauBa.DauCatVauDauCauEaoWaoXauF.HGauGauHat5arp.KfanYar9aqBarrar9anYarpapIat6auIauIan0at4auJarpat4apIan0at5auIaapaapau.au.auKauLauM.S..TEaqYauN#wLauOauPauQauRauSauT", +"auUauVauWauXauYauZau0au1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapi.O8.O8.O8.KC.KCapiankank#6I#6I#6I#6I#6I#6I#6I#6IanpanpanpanpanpanpanpanpatAau2arnatBauoau3au4aunau5au6au7au8au9av.aig.ji.mo.moav#avaavbavcaj9ai4avdalbalbaveavfauDauDatWanQ.H8avgavh#ZHaviavjau#anUanZanYar9aqBarsanW.Kfat4at7auIaapaapauIat7at4at3arpat4an0auIaapau.avkau#auK#9YauL.I0avlanpanpavmavnavoavpavqavravsavt", +"avuavvavwavxavy.Utavzau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapianl.O8.O8.KCankankaqYaqYapjapjapjapjapjapjapjapjanpanpanpanpanpanpanpanpatE.HGavAavhavBaoYatCavC#6JavDavEavFavGavHavI.l#avJavJavKavLavMai5afzavcavNavNavda.DatSatUatVatVaqtavOaoX.HGavPavQatjatjanZ.KfanWar8ar8anWanZatjapIan0avR.3Wau.aapat5at7at4apIat7at6avRavSau.avT#9YauLauLavBavB.I0avlaegavUavVavWavXavYavZav0av1", +"av2avvav3avxav4.Ow.5Vau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapi.KCapiapiankankankaqYaqYanoanoanoanoanoanoanoanoanpanpanpanpanpanpanpanpau2av5av6av7av8avAav8atCau6av9aw.aw#.l#aig.jiaw#.l#awaawbawcawdafwaweaehalbauBa.DawfatSatUatVawgaquaquaqvarmarnawh.KfarpanYanWar9ar9anYanUawiawjat4an0avRau.au.avSauIat6at6at6at5auIavRaapavSavS.XRauM.I0avBavBawkawlawlawmawnawoawpawqawraws#Sz", +"awtawuav3awvaww#41avzau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiawxawx.LLaqYankapi.KC.KCanoanoanoanoanoanoanoanoanpanpanpanpanpanpanpanpatAatBatBapHawyawyar5apHawzawAawBaigawCawD.l#.moawEav#awFawGavcaweawHawIaj8atSatUatUauCauCauDawJaquaquaqvar5asKatganXanWar9arsar8anXanUawKauKawLarpat7auIavSau.aapauIat6auIauIavRaapaapaapavSavS.S..S.avl.I0awkawMawNa#EawOawPawQawRawSawTawUawV", +"awtawWawXawYawZ.Owavzau1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiaw0aw1.LNawxank.O8anmannamgamgamgamgamgamgamgamganpanpanpanpanpanpanpanpacHarnar5av6atBatBatEaw2aw3au8aw4aw5aig.l#aw6aw7aoOaw8aw9ax.afwax#ak.auAatUawgaxaawgatWauCauDawJasGaxbatYatZat0axcanWanWarsarsatiaqDawiauKauKavj.HEat4at6aapavSaapauIat6at5auIauIaapavSau.avkavk.TE.TEanpavlawlawNa#Eaxdaxearkaxfaxgaxhaxiaxj#4i", +"awtaxkaxlaxmaxnaxoau0au1apjapjapjapjapjapjapjapjapiapiapiapiapiapiapiapiarSaxpaw1awxankanlarR.KF.TE.TE.TE.TE.TE.TE.TE.TEanpanpanpanpanpanpanpanp#ZHaxqarnaxraxsacHaxtaxuaxvaxw.l#.moaw4aig.l#.l#axxaxyaxzaxAaxBaxCafzaxDawJaxEaxFaxEatVauCauDawJaxGaxGaxHaxIaxJaxKar9atiaqCar9anXan1avjawLavjatjat8at3an0auIaapavRat5an0at7axLat5aapau.avkaxdaxMaqYaqYanpaegawla#EaxdaxNaxO#2uaxPaxQaxRaceaxSaxT", +"axUaxVaxWaxXaxYaxZas9ax0apiapiapiapiapiapiapiapi.KO.KOax1.F0ax2ax3a#La#L.Gbax4ax4ax5.KHax6.Gb.Oqaj6ax7#2uadpax8ax9ay.#8q.XR.XR.I0avlavl.HH.HH.HHaroay#ayaaybaycar7aydayeayfaygayhaigaigayiawDayjaykaylaymaynayoaypavPayqayraysaytayuatWayvaszaywayxayyayzayAayBayCayDayEayFayGahqayHayIayJayKayLayMayNat1ayOayPayQ#2xayRaySayTayUayVayWayXayYayZay0ahiay1aqKasa#VFaogay2ay3ay4ay5ay6ay7ay8ay9az.", +"az#azaazbazcazdaze.Maaeiapiapiapiapiapiapiapiapi.IJ.KOacTazfazgazgazhacTaziazjax5azk.KHazl.Gbaziazm.F0abbaznazoax7azpazq.XR.XR.I0avlavl.H9.HH.HHay#aqyazrazsaztazuazvazwazx.l#avGawCaw6aigazyazzazAazBazCazDazEazFazGazHazIazJazKazLatXazMayvazNazOazPar1azQawgazRaxJazS.CsazTazUazVazWazXazYazZaz0akMaz1az2az3az4alOaz5#wMaz6.HRaz7az8az9.QCaA.#3S#3SaA#.I2#Rm#YA#Z1awLaAaaAbaAcay6aAdaAeaAfaAg", +"aAhaAiaAjaAkaAl.QxaAmafxapiapiapiapiapiapiapiapiaAnaAoaApaAqaAraAsaAtaAuaAvaAwaAxaAyaAzaAA.F9aABareahUaACalEailalEaADafu.XR.XR.I0.I0avlavlavl.H9aAEayaaAFaAGauGaAHaAIaAJaAKaALaAMaig.qNaANaAOaAPaAQaARaASanYaATaAUaAVaAWaAXaAYaAZaytatVamhaA0aA1aqxazSaA2aA2aA3ab2aA4aA5aA6aA7aA8aA9aB.aB#aBaaBbaBcaBdaBeaBfaBgaBhaBiaBjaBkaBlaBkaBmaBn.XR.KkaBoaq0aBpaBqaA#aeR#SN#YA#Z1.MiaBra.HaBsaBtaBuaBvaBw", +"aBxaByaBzaBAaBBaiUaBCaj9apiapiapiapiapiapiapiapiaBD#2OaBEaBFaBGaBHaBIaBJaBKaBL#viaBMaBNaBOaBPaBQaBRaBSaBTaBUaBV.H0aBWaAC.XR.XR.I0.I0.I0.I0avlavlaAGaroawlaBXao0aroaBYaBZaB0aB1aB2aB3aB4aB5aB6aB7aB8aB9aC.aC#aCaaCbaCcaCdaCeaCfaAYaAZaCgatUavfaChaCiaCjaCkaClaCmaA4aCnaCoaCpaCqaCraCsaCtaCuaCvaCwaCxaCyaCzaCAaCBaCCabpaCDaCEaCFaCGaCHaCIaCJaCKaCLa.Eahj.Mb.EVaCMaCN#W8#W8aCOaCPaCQaCRaCSaCTaCUaCV", +"aCWaCXaCYaCZaC0aC1.F9ai4apiapiapiapiapiapiapiapiaC2aC3aC4aC5aC6aBHaC7aC8aC9aD.aD#aDaaDbaDcaDdaDeaDfaDgaDhaDiaDjaDk.HOaDl.I0.I0auM.XR.XR.XR.XR.XRaDmaAEaDmaBXaxcaqyaDnaDoaDpaDqaDraDsaDtaDuaDvaDwaDxaDyaeUaDzaDAaDBaDCaDDaDEaDFaDGanXaDHazKaCgauDaDI.HQayWaDJaDKaDLaDMaDNaDOaDPaDQaDRaDSaDTaDUaDVaDWaDXaDYaDZaD0aD1.EN.RvaD2#6GaD3aD4aD5aD6.I3axqaq0aBp.MbaA#aeR#SN#YA#Z1aD7aeTasLaD8aD9aE.aE#aEa", +"aEbaEcazbaEdaBBaxZaEeawIapiapiapiapiapiapiapiapiacoaEfaEgaEhaEiaEjaEkaElaEmaEnaEoaEpaEqaEraEsaEtaEuaEvaEwaExaEyaEzaEAarU.I0.I0auM.XR.XR.IP.IPaEBar7ay#aqyaECat1aEDar7aEEaEFaEGaEHaEIaEJaEKaELaEMaENamRaEOaEPaEQaERaESaETamVaEUaEVaEWaEXaAZazKaEYaEZaE0aE1aE2aE3aE4aE5aE6aE7aE8aE9aF.aF#aFaaFbaFcaFdaFeaFfaFgaFh.UyaFiaFjaFk#6F#8gaFlaFm#94aFnaFo#3SaBqaFp.I2#Rm#YAawLawLaFqaf8aFraFsaFtaFuaFvaFw", +"aFxaAiaFyaFzaFAaxZaEeauMapiapiapiapiapiapiapiapiadxaFBaFCaFDaFEaFFaFGaFHaFIaFJaFKaFLaFMaFNaFOaFPaFQaFRaFSaFTaFRaFUaFVaFWavl.I0auM.XR.IPaEBaEBaEBaqyanSauGanSaroayaaFXanSaFYaFZaF0aF1aF2aF3aF4aF5aF6aF7#YCaF8aF9aG.aG#aGaaGbaGcaGdaGeaGfaAYaAZaGgaGhaGiaGjaGkaGlaGmaGnaGoaGpaGqaGraGsaGtaGuaGvaGwaGxaGyaGzaGAaGBaGCaGDaGEaGFaGGaGHaGIaGJ.4iaGKaGLahiay1aGM.KlaqJaogawjawjaGNaGOatgaGPaGQaGRaGSaGT", +"aGUaxVaFyaGVaGWaC1aGXagPapiapiapiapiapiapiapiapiaGYaGZaG0aG1aG2aG3aG4aG5aG6aG7aG8aG9aH.aH#aHaaHbaHcaHdaHeaHfaHgaHhaFUaHiavlavlauM.XR.IPaEBaHjaHjay#anSawlaHkaqyaECayaaztaHlaHmaHnaHoaHpaHqaHraHsan7aHtaHuaHvaHwaHxaHyaHzaHAaHBaGcaGeaGfaCfaAZ.GAaHCaHDaHEaHFaHGaHHaHIaHJaHKaHLaHMaHNaHOaHPaHQaHRaHSaHTaHUaHVaHWaHXaHYaHZaH0aH1aH2aH3aH4aH5aH6aH7abhabhaH8apNaocaH9aruaruaGNaI.aI#aIaaIbaIcaIdaIe", +"aIfaIgaIhaIi.L8aIj.NyaIkai2agOagOaedaedabnabnabnaIlaImaInaIoaIpaIqaIraIsaItaIuaIvaIwaIxaIyaIzaIAaIBaHgaHfaICaIDaIEaIFaIG#44#44apkanpanpanp.S..S.acHauoatB#ZHatDavA.HG.HGaIHaIIaIJaIKaILaIMaINaIOaIPaIQaIRaISaITar5aCgaIUaIVaIWarmaIXaIYaIZaI0aI1aI2aI3aI4aI5aI6aI7aI8aI9aJ.aJ#aJaaJbaJcaJdaJeaJfaJgaJhaJiaJjaJkaJlaJmaJnaJoaJpaJqaJraJsaJtaJuaJvaJwatEaJxaviaruaBraJyaJzaJAaJBar5aap#RnaJCaJDaJE", +"aJFaJGaJHaJIaJJaIjaJKaJLai2ai2agOaJMaedaedabnabnaJNaJO.HZ.LLaJPanlaJQaJRaJSaJTaJUaJVaJWaJXaJYaJZaJ0aJ1aJ2aFUaJ3aJ4aJ5aJ6#44#44#44apkanpanpanp.S.acHauoatBatB#ZH#ZHatDavAaJ7aJ8aJ9aK.aK#aKaaKbaKcaKdaKeaKfaJwatDacGaKgaKhaKiaKjaqw.7oaKkaKlaKmaKnaKoaKpaKqaKraKsaKtaKuaKvaKwaKxaKyaKzaKAaKBaKCaKDaKEaKFaKGaKHaKIaKJaKKaKLaKMaKNaKOaKPaKQ.N#aKR.GAaJwatEaJxaviaruaBraJyaJzaKS.Nz.HGat7#M4aKTaKUaKV", +"aKWaKXaKYaKZ.L7.GpaK0.IUai2ai2ai2agOaedaedaedabn.F7apiaJNaJNanlaK1aK2arRaK3aK3aK4aK5aK6aKRaK7aK8aK9aqha#daL.aL#aLaaLbaLc#44#44#44#44anpanpanpanpacHacHauoauoauoauoauoauoaLdaLeaLfaLgaLhaLiaLjaLkaLlaLmatBaocaLnaLoaLpaLqaLraLsaLtaLuaLvaLwaLxaLyaLzaLAaLBaLCaLDaLEaLFaLGaLHaLIaLJaLKaLLaLMaLNaLOaLPaLQaLRaLSaLTaLUaLVaLWaLXaLYaLZaL0aL1aL2aL3.KtaJwatEaJxaviaruaBraJyaJzaL4aGOatEaEWaL5aL6aL7aJE", +"aL8aKXaL9.Nf.L6agJaD6aM.alcai2ai2ai2agOaedaedaedaJN.GwaK1aK1aK1apiaK1aznaM#aM#.HKaMaaMb.Ja.BJaMcarcaMdaMdaBCaqiaMdaGX.HOaq0#44#44#44apkanpanpanpacHacHacHacHaunavBavBavBaphaMeaMfaMgaMhaFWaMiaMeaMjaI.aMkaMlaMmaLoaMnaMoaMpaMqahWaMraMsaMtaMuaMvaMwaMxaMyaMzaMAaMBaMCaMDaMEaMFaMGaMHaMIaMJaMKaMLaMMaMNaMOaMPaMQaMRaMSaMTaMUaMVaMWaMXaMYaMZ.Jk.KRaJwatEaJxaviaruaBraJyaJzaL4.NzauoanVaM0aM1aM2aM3", +"aM4aM5aM6.P0aM7.GraD6aM.alcalc.Gvai2ai2agOaJMaedaJOazmaJO.GwaaiaaiaK2aM8.I7.I7.I7.I7.I7.I7.I7.I7.OqaM9aN..HM.HMaN#aNaaNbaqZaq0aq0#44#44#44apkanpacHacHacHaunavBavBavBavBaf5aNcaNdaNeaNfakx.HUaNgaNhaNiaNjaNjaMmaJBacIaNkaNlaNmatDaNnaNoaNpaNqaNraNsaNtaNuaNvaNwaNxaNyaNzaNAaNBaNCaNDaNEaNFaNGaNH.ihaNIaNJaNKaNLaNMaNNaNOaNPaNQaNRaNS.L8aAnaNTaNUaJwatEaJxaviaruaBraJyaJzaFqaJBatEaNVaL5ascaNWaNX", +"aNYaNZaN0.P0.O3aN1#93aN2aN3aN3alc.Gvai2ai2agOagOaK1aK2aK1.H1aN4.LO.HZazmanNanNanNanNanNanNaN5aN5amIaN6aikaADazpawmailaecaqZaqZaqZaq0#44#44#44apkaunaunaun.IPacHacHacHacHano.TEaN7ano.KPaN8aN9aO.aO#aNiaNjaOaacGapHar5aObaOcaOd.O9.LFaOeaOfaOgaOhaOiaOjaOk.SYaOlaOmaOnaOoaOpaOqaOraEDaHkaOsaynaOtaOuaOvaOwaOxaOyaOzaOAaOBaOCaODaOE.FT.NuaaiaOFaOGaJwatEaJxaviaruaBraJyaJzaOHaOIavAaOJaOKaOLaOMaON", +"aOOaOPaOQaORaOS.QAaOTaIkaN3aN3aN3alcai2ai2ai2agOaN4aM8.GwaJN.GwaK1aK2.Gwanpanpamg#44#6IaqZaqZ.H7azn#94#2uaOU.Nwaaiax8alEaqZaqZaqZaq0#44#44#44#44avBavBaunacHauoatBatBatEaOVaOWaOXaJKaOVaOYaOZ.Kx.NzaLnaO0aO1aO2aO3aO4aO5aO6aO7aO8azMaO9aP.aP#aPaaPbaPcaPdaPeaPfaPgaPhaPiaPjaPkaPlaPmaPnaPoaPpaPqaPraPsaPtaPuaPv.H8aPwaPxaPyaPzaPAaPBaPC.LJajwaPDaJwatEaJxaviaruaBraJyaJzaPEaI.aPFau.aPGaPHaPIaPJ", +"aPKaPLaPM.ZqaPNaPO.Kp.RraN3aN3aN3alcalcai2ai2ai2aJOaM8aM8aPPaJNarR.LNaPQanpanpanpanp.S..S.aimaimaPRano.EUasFaPSai0aPTabbaqZaqZaqZaqZaq0#44#44#44avBavBaunacHatB#ZHatDavAaPUaPVaPWaPXaPYaPZ.KAaP0aGgaP1aO1aP2aP3aP4aP5aP6aP7aP8aP9aQ.aQ#aQaaQbaQcaQdaEYaQeaQfaKSaQgaQhaQiaQjaFXaQkaQlaQmaQnay#aOsaQoaQpaQqaQrayWaQsazGaQtaQuaQvaQwaQxaQyaQzaQAaQBaJwatEaJxaviaruaBraJyaJzaQCaQDaPFavkaQEaOLaQFaQG"}; diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 7b0429301..04df023cb 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -20,11 +20,17 @@ def test_sanity() -> None: assert_image_similar(im.convert("RGB"), hopper(), 23) -def test_read_bpp2() -> None: +def test_bpp2() -> None: with Image.open("Tests/images/hopper_bpp2.xpm") as im: assert_image_similar(im.convert("RGB"), hopper(), 11) +def test_rgb() -> None: + with Image.open("Tests/images/hopper_rgb.xpm") as im: + assert im.mode == "RGB" + assert_image_similar(im, hopper(), 16) + + def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bfa462c04..51d829abe 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1650,7 +1650,8 @@ handler. :: XPM ^^^ -Pillow reads X pixmap files (mode ``P``) with 256 colors or less. +Pillow reads X pixmap files as P mode images if there are 256 colors or less, and as +RGB images otherwise. .. _xpm-opening: diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index fcee142e3..d80ca7ce4 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -56,10 +56,6 @@ class XpmImageFile(ImageFile.ImageFile): palette_length = int(m.group(3)) bpp = int(m.group(4)) - if palette_length > 256: - msg = "cannot read this XPM file" - raise ValueError(msg) - # # load palette description @@ -93,15 +89,16 @@ class XpmImageFile(ImageFile.ImageFile): msg = "cannot read this XPM file" raise ValueError(msg) - self._mode = "P" - self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) + args: tuple[int, dict[bytes, bytes] | tuple[bytes, ...]] + if palette_length > 256: + self._mode = "RGB" + args = (bpp, palette) + else: + self._mode = "P" + self.palette = ImagePalette.raw("RGB", b"".join(palette.values())) + args = (bpp, tuple(palette.keys())) - palette_keys = tuple(palette.keys()) - self.tile = [ - ImageFile._Tile( - "xpm", (0, 0) + self.size, self.fp.tell(), (bpp, palette_keys) - ) - ] + self.tile = [ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), args)] def load_read(self, read_bytes: int) -> bytes: # @@ -119,17 +116,27 @@ class XpmDecoder(ImageFile.PyDecoder): def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: assert self.fd is not None - self.fd.readline() # Read '/* pixels */' data = bytearray() - bpp, palette_keys = self.args + bpp, palette = self.args dest_length = self.state.xsize * self.state.ysize + if self.mode == "RGB": + dest_length *= 3 + pixel_header = False while len(data) < dest_length: - s = self.fd.readline().rstrip()[1:] - s = s[: -1 if s.endswith(b'"') else -2] + s = self.fd.readline() + if s.rstrip() == b"/* pixels */" and not pixel_header: + pixel_header = True + continue + if not s: + break + s = b'"'.join(s.split(b'"')[1:-1]) for i in range(0, len(s), bpp): key = s[i : i + bpp] - data += o8(palette_keys.index(key)) + if self.mode == "RGB": + data += palette[key] + else: + data += o8(palette.index(key)) self.set_as_raw(bytes(data)) return -1, 0 From 6512a8e371476b91bc5adfbe076ace2c0f076da2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Apr 2025 23:41:45 +1000 Subject: [PATCH 449/628] Test not enough image data --- Tests/test_file_xpm.py | 10 ++++++++++ src/PIL/XpmImagePlugin.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 04df023cb..96365d7f4 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL import Image, XpmImagePlugin @@ -31,6 +33,14 @@ def test_rgb() -> None: assert_image_similar(im, hopper(), 16) +def test_not_enough_image_data() -> None: + with open(TEST_FILE, "rb") as fp: + data = fp.read().split(b"/* pixels */")[0] + with Image.open(BytesIO(data)) as im: + with pytest.raises(ValueError, match="not enough image data"): + im.load() + + def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index d80ca7ce4..ff216a6c1 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -125,11 +125,11 @@ class XpmDecoder(ImageFile.PyDecoder): pixel_header = False while len(data) < dest_length: s = self.fd.readline() + if not s: + break if s.rstrip() == b"/* pixels */" and not pixel_header: pixel_header = True continue - if not s: - break s = b'"'.join(s.split(b'"')[1:-1]) for i in range(0, len(s), bpp): key = s[i : i + bpp] From 34efaaddf306f3a435f490ca6082fc80bc7cad37 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 18:57:31 +1000 Subject: [PATCH 450/628] Improved type hints --- src/PIL/XpmImagePlugin.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index ff216a6c1..3be240fbc 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -37,17 +37,18 @@ class XpmImageFile(ImageFile.ImageFile): format_description = "X11 Pixel Map" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) # skip forward to next string while True: - s = self.fp.readline() - if not s: + line = self.fp.readline() + if not line: msg = "broken XPM file" raise SyntaxError(msg) - m = xpm_head.match(s) + m = xpm_head.match(line) if m: break @@ -62,10 +63,10 @@ class XpmImageFile(ImageFile.ImageFile): palette = {} for _ in range(palette_length): - s = self.fp.readline().rstrip() + line = self.fp.readline().rstrip() - c = s[1 : bpp + 1] - s = s[bpp + 1 : -2].split() + c = line[1 : bpp + 1] + s = line[bpp + 1 : -2].split() for i in range(0, len(s), 2): if s[i] == b"c": @@ -74,9 +75,11 @@ class XpmImageFile(ImageFile.ImageFile): if rgb == b"None": self.info["transparency"] = c elif rgb.startswith(b"#"): - rgb = int(rgb[1:], 16) + rgb_int = int(rgb[1:], 16) palette[c] = ( - o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) + o8((rgb_int >> 16) & 255) + + o8((rgb_int >> 8) & 255) + + o8(rgb_int & 255) ) else: # unknown colour @@ -106,6 +109,7 @@ class XpmImageFile(ImageFile.ImageFile): xsize, ysize = self.size + assert self.fp is not None s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] return b"".join(s) @@ -124,15 +128,15 @@ class XpmDecoder(ImageFile.PyDecoder): dest_length *= 3 pixel_header = False while len(data) < dest_length: - s = self.fd.readline() - if not s: + line = self.fd.readline() + if not line: break - if s.rstrip() == b"/* pixels */" and not pixel_header: + if line.rstrip() == b"/* pixels */" and not pixel_header: pixel_header = True continue - s = b'"'.join(s.split(b'"')[1:-1]) - for i in range(0, len(s), bpp): - key = s[i : i + bpp] + line = b'"'.join(line.split(b'"')[1:-1]) + for i in range(0, len(line), bpp): + key = line[i : i + bpp] if self.mode == "RGB": data += palette[key] else: From af52060e973063b259708a8e91bfbbf13376c247 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 20:45:53 +1000 Subject: [PATCH 451/628] Mention that tobytes() with the raw encoder uses Pack.c --- src/PIL/Image.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 88ea6f3b5..b419405fb 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -767,18 +767,20 @@ class Image: .. warning:: - This method returns the raw image data from the internal - storage. For compressed image data (e.g. PNG, JPEG) use - :meth:`~.save`, with a BytesIO parameter for in-memory - data. + This method returns raw image data derived from Pillow's internal + storage. For compressed image data (e.g. PNG, JPEG) use + :meth:`~.save`, with a BytesIO parameter for in-memory data. - :param encoder_name: What encoder to use. The default is to - use the standard "raw" encoder. + :param encoder_name: What encoder to use. - A list of C encoders can be seen under - codecs section of the function array in - :file:`_imaging.c`. Python encoders are - registered within the relevant plugins. + The default is to use the standard "raw" encoder. + To see how this packs pixel data into the returned + bytes, see :file:`libImaging/Pack.c`. + + A list of C encoders can be seen under codecs + section of the function array in + :file:`_imaging.c`. Python encoders are registered + within the relevant plugins. :param args: Extra arguments to the encoder. :returns: A :py:class:`bytes` object. """ From dce96089614a2bfac112b5baa5f19d87874f526b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 21:59:04 +1000 Subject: [PATCH 452/628] Test unknown colour and missing colour key --- Tests/test_file_xpm.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 96365d7f4..de2d9bb79 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -33,12 +33,23 @@ def test_rgb() -> None: assert_image_similar(im, hopper(), 16) +def test_cannot_read() -> None: + with open(TEST_FILE, "rb") as fp: + data = fp.read().split(b"#")[0] + with pytest.raises(ValueError, match="cannot read this XPM file"): + with Image.open(BytesIO(data)) as im: + pass + with pytest.raises(ValueError, match="cannot read this XPM file"): + with Image.open(BytesIO(data+b"invalid")) as im: + pass + + def test_not_enough_image_data() -> None: with open(TEST_FILE, "rb") as fp: data = fp.read().split(b"/* pixels */")[0] - with Image.open(BytesIO(data)) as im: - with pytest.raises(ValueError, match="not enough image data"): - im.load() + with Image.open(BytesIO(data)) as im: + with pytest.raises(ValueError, match="not enough image data"): + im.load() def test_invalid_file() -> None: From b2945ec2aaf20d4698479c843f0d26ffcaf6222b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Apr 2025 22:07:55 +1000 Subject: [PATCH 453/628] Test truncated header --- Tests/test_file_xpm.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index de2d9bb79..86d86602f 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -33,14 +33,22 @@ def test_rgb() -> None: assert_image_similar(im, hopper(), 16) -def test_cannot_read() -> None: +def test_truncated_header() -> None: + data = b"/* XPM */" + with pytest.raises(SyntaxError, match="broken XPM file"): + with XpmImagePlugin.XpmImageFile(BytesIO(data)): + pass + + +def test_cannot_read_color() -> None: with open(TEST_FILE, "rb") as fp: data = fp.read().split(b"#")[0] with pytest.raises(ValueError, match="cannot read this XPM file"): - with Image.open(BytesIO(data)) as im: + with Image.open(BytesIO(data)): pass + with pytest.raises(ValueError, match="cannot read this XPM file"): - with Image.open(BytesIO(data+b"invalid")) as im: + with Image.open(BytesIO(data + b"invalid")): pass From 04909483a70e29cd265446199a8f22c1acdc6db7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 12 Apr 2025 17:29:06 +1000 Subject: [PATCH 454/628] Remove GPL v2 license (#8884) --- wheels/dependency_licenses/FREETYPE2.txt | 345 ----------------------- 1 file changed, 345 deletions(-) diff --git a/wheels/dependency_licenses/FREETYPE2.txt b/wheels/dependency_licenses/FREETYPE2.txt index 93efc6126..8f2345992 100644 --- a/wheels/dependency_licenses/FREETYPE2.txt +++ b/wheels/dependency_licenses/FREETYPE2.txt @@ -211,351 +211,6 @@ Legal Terms --- end of FTL.TXT --- --------------------------------------------------------------------------- - - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. - --------------------------------------------------------------------------- - The following license details are part of `src/bdf/README`: ``` From 07d78002488063f29cd24e49dd38b5fc2f319989 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 19:08:45 +1000 Subject: [PATCH 455/628] Removed release notes update --- docs/releasenotes/11.2.0.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index ed41c2116..de3db3c84 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -106,6 +106,5 @@ Pillow images can also be converted to Arrow objects:: Reading and writing AVIF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Pillow can now read and write AVIF images. However, due to concern over size, this -functionality is not included in our prebuilt wheels. You will need to build Pillow -from source with libavif 1.0.0 or later. +Pillow can now read and write AVIF images. If you are building Pillow from source, this +will require libavif 1.0.0 or later. From 8dafc38371b99616d3fdcc926f4f5a1f7762ae3f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 19:24:35 +1000 Subject: [PATCH 456/628] Added 11.2.1 release notes --- docs/releasenotes/11.2.0.rst | 6 ++++++ docs/releasenotes/11.2.1.rst | 11 +++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 18 insertions(+) create mode 100644 docs/releasenotes/11.2.1.rst diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index de3db3c84..3a7d618e4 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -1,6 +1,12 @@ 11.2.0 ------ +.. warning:: + + The release of Pillow 11.2.0 was halted prematurely, due to concern over the size + of Pillow wheels containing libavif. Instead, Pillow 11.2.1 has been released, + without libavif included in the wheels. + Security ======== diff --git a/docs/releasenotes/11.2.1.rst b/docs/releasenotes/11.2.1.rst new file mode 100644 index 000000000..7d9a40382 --- /dev/null +++ b/docs/releasenotes/11.2.1.rst @@ -0,0 +1,11 @@ +11.2.1 +------ + +Reading and writing AVIF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The release of Pillow 11.2.0 was halted prematurely, due to concern over the size of +Pillow wheels containing libavif. + +Instead, Pillow 11.2.1's wheels do not contain libavif. If you wish to read and write +AVIF images, you will need to build Pillow from source with libavif 1.0.0 or later. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index be9f623d0..0d159fc51 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.1 11.2.0 11.1.0 11.0.0 From 7a0092f2072a5deca433e2a6b752eced49e4ee16 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 12 Apr 2025 18:56:38 +0300 Subject: [PATCH 457/628] Remove incomplete 11.2.0 release, bill as 11.2.1 instead --- docs/deprecations.rst | 2 +- docs/handbook/image-file-formats.rst | 2 +- docs/reference/ImageDraw.rst | 8 +- docs/reference/ImageGrab.rst | 2 +- docs/releasenotes/11.2.0.rst | 116 -------------------------- docs/releasenotes/11.2.1.rst | 117 +++++++++++++++++++++++++-- docs/releasenotes/index.rst | 1 - src/PIL/Image.py | 2 +- 8 files changed, 120 insertions(+), 130 deletions(-) delete mode 100644 docs/releasenotes/11.2.0.rst diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 634cee689..7f8e76bcc 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -186,7 +186,7 @@ ExifTags.IFD.Makernote Image.Image.get_child_images() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. deprecated:: 11.2.0 +.. deprecated:: 11.2.1 ``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 diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bfa462c04..49431b3d0 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -170,7 +170,7 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode. in ``P`` mode. -.. versionadded:: 11.2.0 +.. versionadded:: 11.2.1 DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved:: im.save(out, pixel_format="DXT1") diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index b2f1bdc93..bd6f6b048 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -391,7 +391,7 @@ Methods the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - .. versionadded:: 11.2.0 ``"justify"`` + .. versionadded:: 11.2.1 ``"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. @@ -462,7 +462,7 @@ Methods the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - .. versionadded:: 11.2.0 ``"justify"`` + .. versionadded:: 11.2.1 ``"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. @@ -609,7 +609,7 @@ Methods the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - .. versionadded:: 11.2.0 ``"justify"`` + .. versionadded:: 11.2.1 ``"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. @@ -663,7 +663,7 @@ Methods the relative alignment of lines. Use the ``anchor`` parameter to specify the alignment to ``xy``. - .. versionadded:: 11.2.0 ``"justify"`` + .. versionadded:: 11.2.1 ``"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/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index d59ed0bd6..1e827a676 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -44,7 +44,7 @@ or the clipboard to a PIL image memory. :param window: HWND, to capture a single window. Windows only. - .. versionadded:: 11.2.0 + .. versionadded:: 11.2.1 :return: An image .. py:function:: grabclipboard() diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst deleted file mode 100644 index 3a7d618e4..000000000 --- a/docs/releasenotes/11.2.0.rst +++ /dev/null @@ -1,116 +0,0 @@ -11.2.0 ------- - -.. warning:: - - The release of Pillow 11.2.0 was halted prematurely, due to concern over the size - of Pillow wheels containing libavif. Instead, Pillow 11.2.1 has been released, - without libavif included in the wheels. - -Security -======== - -Undefined shift when loading compressed DDS images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When loading some compressed DDS formats, an integer was bitshifted by 24 places to -generate the 32 bits of the lookup table. This was undefined behaviour, and has been -present since Pillow 3.4.0. - -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 -=========== - -``append_images`` no longer requires ``save_all`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Previously, ``save_all`` was required to in order to use ``append_images``. Now, -``save_all`` will default to ``True`` if ``append_images`` is not empty and the format -supports saving multiple frames:: - - im.save("out.gif", append_images=ims) - -API Additions -============= - -``"justify"`` multiline text alignment -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -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)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") - draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") - -Specify window in ImageGrab on Windows -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the -HWND:: - - from PIL import ImageGrab - ImageGrab.grab(window=hwnd) - -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 - -Saving compressed DDS images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3, -DXT5, BC2, BC3 and BC5 are supported:: - - im.save("out.dds", pixel_format="DXT1") - -Other Changes -============= - -Arrow support -^^^^^^^^^^^^^ - -`Arrow `__ is an in-memory data exchange format that is the -spiritual successor to the NumPy array interface. It provides for zero-copy access to -columnar data, which in our case is ``Image`` data. - -To create an image with zero-copy shared memory from an object exporting the -arrow_c_array interface protocol:: - - from PIL import Image - import pyarrow as pa - arr = pa.array([0]*(5*5*4), type=pa.uint8()) - im = Image.fromarrow(arr, 'RGBA', (5, 5)) - -Pillow images can also be converted to Arrow objects:: - - from PIL import Image - import pyarrow as pa - im = Image.open('hopper.jpg') - arr = pa.array(im) - -Reading and writing AVIF images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pillow can now read and write AVIF images. If you are building Pillow from source, this -will require libavif 1.0.0 or later. diff --git a/docs/releasenotes/11.2.1.rst b/docs/releasenotes/11.2.1.rst index 7d9a40382..5c6d40d9d 100644 --- a/docs/releasenotes/11.2.1.rst +++ b/docs/releasenotes/11.2.1.rst @@ -1,11 +1,118 @@ 11.2.1 ------ +.. warning:: + + The release of Pillow *11.2.0* was halted prematurely, due to hitting PyPI's + project size limit and concern over the size of Pillow wheels containing libavif. + The PyPI limit has now been increased and Pillow *11.2.1* has been released + instead, without libavif included in the wheels. + To avoid confusion, the incomplete 11.2.0 release has been removed from PyPI. + +Security +======== + +Undefined shift when loading compressed DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When loading some compressed DDS formats, an integer was bitshifted by 24 places to +generate the 32 bits of the lookup table. This was undefined behaviour, and has been +present since Pillow 3.4.0. + +Deprecations +============ + +Image.Image.get_child_images() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.2.1 + +``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 +=========== + +``append_images`` no longer requires ``save_all`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, ``save_all`` was required to in order to use ``append_images``. Now, +``save_all`` will default to ``True`` if ``append_images`` is not empty and the format +supports saving multiple frames:: + + im.save("out.gif", append_images=ims) + +API Additions +============= + +``"justify"`` multiline text alignment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") + draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") + +Specify window in ImageGrab on Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the +HWND:: + + from PIL import ImageGrab + ImageGrab.grab(window=hwnd) + +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 + +Saving compressed DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3, +DXT5, BC2, BC3 and BC5 are supported:: + + im.save("out.dds", pixel_format="DXT1") + +Other Changes +============= + +Arrow support +^^^^^^^^^^^^^ + +`Arrow `__ is an in-memory data exchange format that is the +spiritual successor to the NumPy array interface. It provides for zero-copy access to +columnar data, which in our case is ``Image`` data. + +To create an image with zero-copy shared memory from an object exporting the +arrow_c_array interface protocol:: + + from PIL import Image + import pyarrow as pa + arr = pa.array([0]*(5*5*4), type=pa.uint8()) + im = Image.fromarrow(arr, 'RGBA', (5, 5)) + +Pillow images can also be converted to Arrow objects:: + + from PIL import Image + import pyarrow as pa + im = Image.open('hopper.jpg') + arr = pa.array(im) + Reading and writing AVIF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The release of Pillow 11.2.0 was halted prematurely, due to concern over the size of -Pillow wheels containing libavif. - -Instead, Pillow 11.2.1's wheels do not contain libavif. If you wish to read and write -AVIF images, you will need to build Pillow from source with libavif 1.0.0 or later. +Pillow can now read and write AVIF images when built from source with libavif 1.0.0 +or later. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 0d159fc51..a116ef056 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -15,7 +15,6 @@ expected to be backported to earlier versions. :maxdepth: 2 11.2.1 - 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 88ea6f3b5..ded40bc5d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3362,7 +3362,7 @@ def fromarrow( See: :ref:`arrow-support` for more detailed information - .. versionadded:: 11.2.0 + .. versionadded:: 11.2.1 """ if not hasattr(obj, "__arrow_c_array__"): From 339bc5db93bd95decf65a59fab273f300db6594d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 12 Apr 2025 19:55:46 +0300 Subject: [PATCH 458/628] 11.2.1 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 e93c7887b..9380e9927 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.2.0.dev0" +__version__ = "11.2.1" From f9083264ff561389ee5931598df9bffe269db504 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 12 Apr 2025 20:56:35 +0300 Subject: [PATCH 459/628] 11.3.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 9380e9927..ac678c7d2 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.2.1" +__version__ = "11.3.0.dev0" From 529402143826732973463104000fd91d0a271103 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Apr 2025 12:09:46 +1100 Subject: [PATCH 460/628] Removed Fedora 40 --- .github/workflows/test-docker.yml | 1 - docs/installation/platform-support.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 25aef55fb..9e42ed884 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,7 +47,6 @@ jobs: centos-stream-10-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-40-amd64, fedora-41-amd64, gentoo, ubuntu-22.04-jammy-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 9eafad3c4..36b9a7bdd 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,8 +31,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 40 | 3.12 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 41 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | From c6434dbbbc667d42ad236aa2eeb407a44bbd2174 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 13 Apr 2025 23:00:06 +1000 Subject: [PATCH 461/628] Set color table fourth channel to zero for 1 and L mode when saving --- src/PIL/BmpImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 43131cfe2..54fc69ab4 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -445,9 +445,9 @@ def _save( image = stride * im.size[1] if im.mode == "1": - palette = b"".join(o8(i) * 4 for i in (0, 255)) + palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255)) elif im.mode == "L": - palette = b"".join(o8(i) * 4 for i in range(256)) + palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256)) elif im.mode == "P": palette = im.im.getpalette("RGB", "BGRX") colors = len(palette) // 4 From 4716bb78189108ceffcc80db226fe099cca20448 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 13 Apr 2025 23:59:05 +1000 Subject: [PATCH 462/628] Update macOS tested Pillow versions (#8890) --- 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 36b9a7bdd..ca810dc2a 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -71,7 +71,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.1.0 |arm | +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.2.1 |arm | | +----------------------------+------------------+ | | | 3.8 | 10.4.0 | | +----------------------------------+----------------------------+------------------+--------------+ From 8b1777b9997a48cd59a3bddd888bebddd8adc6de Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Mon, 14 Apr 2025 14:51:01 -0400 Subject: [PATCH 463/628] Move XV Thumbnails to read only section --- docs/handbook/image-file-formats.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 49431b3d0..46fe8b630 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1664,6 +1664,11 @@ The :py:meth:`~PIL.Image.open` method sets the following Transparency color index. This key is omitted if the image is not transparent. +XV Thumbnails +^^^^^^^^^^^^^ + +Pillow can read XV thumbnail files. + Write-only formats ------------------ @@ -1769,11 +1774,6 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 5.3.0 -XV Thumbnails -^^^^^^^^^^^^^ - -Pillow can read XV thumbnail files. - Identify-only formats --------------------- From 507fefbce4c3b8c46c6c21483d488884f1b5a8e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 15 Apr 2025 21:02:35 +1000 Subject: [PATCH 464/628] Python 3.13 is tested on Arch (#8894) --- 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 ca810dc2a..d5634b384 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -23,7 +23,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2023 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Arch | 3.12 | x86-64 | +| Arch | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ From 6ea7dc8eeafaf7528d2817bdd443de367eca2940 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:06:52 +1000 Subject: [PATCH 465/628] Add Fedora 42 (#8899) --- .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 9e42ed884..0b90732eb 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -48,6 +48,7 @@ jobs: debian-12-bookworm-x86, debian-12-bookworm-amd64, fedora-41-amd64, + fedora-42-amd64, gentoo, ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index d5634b384..d751620fd 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -33,6 +33,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 41 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 42 | 3.13 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 13 Ventura | 3.9 | x86-64 | From f630ec097b4d5d2a132d4ade5e5b903452bf27d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 16 Apr 2025 21:05:08 +1000 Subject: [PATCH 466/628] Build Windows arm64 wheels on arm64 runner (#8898) --- .github/workflows/wheels.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 40d3dc7e8..33e1976f0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -121,14 +121,17 @@ jobs: windows: if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' name: Windows ${{ matrix.cibw_arch }} - runs-on: windows-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - cibw_arch: x86 + os: windows-latest - cibw_arch: AMD64 + os: windows-latest - cibw_arch: ARM64 + os: windows-11-arm steps: - uses: actions/checkout@v4 with: From 3d4119521c853e1014012f73fdf9b2a8dc137722 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 17 Apr 2025 01:49:57 +1000 Subject: [PATCH 467/628] Close file pointer earlier (#8895) --- Tests/test_file_bmp.py | 12 ++++++------ Tests/test_file_jpeg2k.py | 4 ++-- Tests/test_file_libtiff.py | 12 ++++++------ Tests/test_file_libtiff_small.py | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 757650711..746b2e180 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -190,9 +190,9 @@ def test_rle8() -> None: # Signal end of bitmap before the image is finished with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp: data = fp.read(1063) + b"\x01" - with Image.open(io.BytesIO(data)) as im: - with pytest.raises(ValueError): - im.load() + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() def test_rle4() -> None: @@ -214,9 +214,9 @@ def test_rle4() -> None: def test_rle8_eof(file_name: str, length: int) -> None: with open(file_name, "rb") as fp: data = fp.read(length) - with Image.open(io.BytesIO(data)) as im: - with pytest.raises(ValueError): - im.load() + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() def test_offset() -> None: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 4095bfaf2..a5365a90d 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -457,8 +457,8 @@ def test_comment() -> None: # Test an image that is truncated partway through a codestream with open("Tests/images/comment.jp2", "rb") as fp: b = BytesIO(fp.read(130)) - with Image.open(b) as im: - pass + with Image.open(b) as im: + pass def test_save_comment(card: ImageFile.ImageFile) -> None: diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9916215fb..1ec39eba5 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -81,7 +81,7 @@ class TestFileLibTiff(LibTiffTestCase): s = io.BytesIO() with open(test_file, "rb") as f: s.write(f.read()) - s.seek(0) + s.seek(0) with Image.open(s) as im: assert im.size == (500, 500) self._assert_noerr(tmp_path, im) @@ -1050,12 +1050,12 @@ class TestFileLibTiff(LibTiffTestCase): with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp: data = fp.read() - # Set EXIF Orientation to 2 - data = data[:102] + b"\x02" + data[103:] + # Set EXIF Orientation to 2 + data = data[:102] + b"\x02" + data[103:] - with Image.open(io.BytesIO(data)) as im: - im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + with Image.open(io.BytesIO(data)) as im: + im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") def test_open_missing_samplesperpixel(self) -> None: with Image.open( diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 617e1e89c..65ba80c20 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -32,7 +32,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): s = BytesIO() with open(test_file, "rb") as f: s.write(f.read()) - s.seek(0) + s.seek(0) with Image.open(s) as im: assert im.size == (128, 128) self._assert_noerr(tmp_path, im) From ccc4668d4ed27754a09f819462732dc42c584fc0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 08:04:34 +1000 Subject: [PATCH 468/628] Updated harfbuzz to 11.1.0 --- .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 0df22c4cb..d53cf059b 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=11.0.1 +HARFBUZZ_VERSION=11.1.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3e75c1411..17fc37572 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": "11.0.1", + "HARFBUZZ": "11.1.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", "LIBAVIF": "1.2.1", From cccc07269ae60226f0bb6475970d6fab255538b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 19:23:24 +1000 Subject: [PATCH 469/628] Do not justify a single word --- .../multiline_text_justify_single_word.png | Bin 0 -> 2436 bytes Tests/test_imagefont.py | 12 ++++++ src/PIL/ImageDraw.py | 36 +++++++++--------- 3 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 Tests/images/multiline_text_justify_single_word.png diff --git a/Tests/images/multiline_text_justify_single_word.png b/Tests/images/multiline_text_justify_single_word.png new file mode 100644 index 0000000000000000000000000000000000000000..e124e91f5e34d64b002b06f5c66fc72ddb80a31c GIT binary patch literal 2436 zcmY*beLRzEA0J0yB(}_ud7mN7_>h-jBJ%#S6r0e=A!LqpWpoYZ?RWh!4&n+g#Fg+L<5j4t5r#GU+=GKvJekb!n|JNhWUPt9 zc^|#kHd}rgFLU3^f{E8JRg_;!n-Rw$$N!R6_4e#U2Z5kCJ=I%2#>%{yn3%dzS#+Wu zis~bqn=&_Q5o#_&bJF&+0JS885_d(Tw!_WYY<6l#W@_pK4rhf=fsPINA(y8FMck}S zEgcDgW>S}9iBZ(Fz{iY5M4o*(m+Psr&3F;BPU)jU z;U5t#$D5vHtlf=C;8YAqOpJ~7g)J>DWHOmTq0~}-I6$6o4n^Gxta3SBg(~aU(b3uY zJ25soS`USKo~mC^P|#8WL^uFb>PV@ptDBjbvGmt;v+rDfCl+%v){>kRCteYk@$X zJP80N#BWA{m6X_PUO7ikj!VkjnfPPJDkFNu1L9(dQ~Lbn%a>_8PmZt7`up!$9V+xN zu;$$5LW|?R>+XNupL?Mj_8NJIt0^9L2dZ4}3qO^VsO7r7|NPn6s*cT8BgasEVq;?1 zXWrVmRBbOy0y~IVpGDS5wB(}2f>}ismC<-xv@-Y7Mi@cgj=lSnqLNZ#LP7_k)42m{C8iFG~*YPyTz|nw=f0 z3#`)7v9r1P%+#|RV_pCbMMZ%mlGRn7K7?YcB^|Eyoeh41n4}Ivb#<#~v<98q<>i_h z8gaJ}QBhG>+;R1<73E}e-4s+*=A}~qz(B#XS-KLU=nWuqEvW76^Qaex86y`CGzxrj;ip{bdf)!8A#n%l+2s!(YD zEkIO31LWL`7#o`oqNN=VlX>*eOrBth?s7@XR3H$<#l|ungGm+a_4V;nknK7Z zZ=(>vO5)-qBO@b&gF)xduXJD9up>=0={VTiYrx@vIyieZXVp%qVd-=_-b~oj(=$Ab z!D2T7YI}DT7Wy7LRu+7bf71FYF9r>75QiHWpF4?-9YCb8cvDwb zZb!%Z#FGpg8=J<)M!=#)?ZQQp(}d2`7a9G%=OeW|Fm*A zItNp70_v6IWN#dXs;dh{BGK4bLK9-Yib-joC9ypRA4w#(6O&J$Hod4B@jX_yr7v=s zl)(N`aMPYN5l!$m8qTX!ABWHHptd09x@P{H5+5Ibbf>A)16(gG>ZLWq8|rIocXdS; z_kSc>YGeeXQ!M}4$@o)lhpLQ>%*&TAftoQoI;x_gl4M6fmf}>;%?|2IqZ{$}s?B-W zp8huv6W_4*>bl^=!VHe)x($ww&P-3AYGN>%%%!DNts7+CrWrVr_gX!xkllq?(S~+y@-R3j!WyANv;eA<2h9C z0FTED3kzHB#Jn?nydSHOpPvudqN#w-KhQ21zApiC2&{KyWyOq|keT_owl;Pm3xpF8 zoqt)9JMz5TCQkJWTG{Y-e`ZJ{TKUb$NRwOE=#y+{i`}<9TmDbST5fJu=5S>GwU_y> z{HBbjPAM|5S(_|_lH)|;RSjcsxIasdjWoo%TL)g5hoCeviZ9 zXjq+SWwWjCL7$HO_%327OP&>cSXGXFoERKzZEI@;D^v?abtZ#mt}AY(=bvyjed-+E zyrHKvyHYCqXQ^0h{`&Rn3(i?*$Rl$myJ#j{Nm5dh@>QNO5(%MrKq%9H|6N{E!uIK| zZzi<;My1AV)&Ldi None: + im = Image.new("RGB", (185, 65)) + draw = ImageDraw.Draw(im) + draw.multiline_text( + (0, 0), "hey you\nyou are awesome\nthis", font=font, align="justify" + ) + + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_single_word.png") + + def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e6c7b0298..e865f4516 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -772,24 +772,26 @@ class ImageDraw: 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)) + if len(words) > 1: + 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) + top += line_spacing + continue + parts.append(((left, top), line)) top += line_spacing return font, anchor, parts From b955cee725da2613b34145eea56227c57ec414d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 19:36:52 +1000 Subject: [PATCH 470/628] Do not justify last line --- .../images/multiline_text_justify_last_line.png | Bin 0 -> 3581 bytes .../multiline_text_justify_single_word.png | Bin 2436 -> 0 bytes Tests/test_imagefont.py | 11 +++++++---- src/PIL/ImageDraw.py | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 Tests/images/multiline_text_justify_last_line.png delete mode 100644 Tests/images/multiline_text_justify_single_word.png diff --git a/Tests/images/multiline_text_justify_last_line.png b/Tests/images/multiline_text_justify_last_line.png new file mode 100644 index 0000000000000000000000000000000000000000..bcc1afd72969c4a476cabc5a074154994a222a94 GIT binary patch literal 3581 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 literal 0 HcmV?d00001 diff --git a/Tests/images/multiline_text_justify_single_word.png b/Tests/images/multiline_text_justify_single_word.png deleted file mode 100644 index e124e91f5e34d64b002b06f5c66fc72ddb80a31c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2436 zcmY*beLRzEA0J0yB(}_ud7mN7_>h-jBJ%#S6r0e=A!LqpWpoYZ?RWh!4&n+g#Fg+L<5j4t5r#GU+=GKvJekb!n|JNhWUPt9 zc^|#kHd}rgFLU3^f{E8JRg_;!n-Rw$$N!R6_4e#U2Z5kCJ=I%2#>%{yn3%dzS#+Wu zis~bqn=&_Q5o#_&bJF&+0JS885_d(Tw!_WYY<6l#W@_pK4rhf=fsPINA(y8FMck}S zEgcDgW>S}9iBZ(Fz{iY5M4o*(m+Psr&3F;BPU)jU z;U5t#$D5vHtlf=C;8YAqOpJ~7g)J>DWHOmTq0~}-I6$6o4n^Gxta3SBg(~aU(b3uY zJ25soS`USKo~mC^P|#8WL^uFb>PV@ptDBjbvGmt;v+rDfCl+%v){>kRCteYk@$X zJP80N#BWA{m6X_PUO7ikj!VkjnfPPJDkFNu1L9(dQ~Lbn%a>_8PmZt7`up!$9V+xN zu;$$5LW|?R>+XNupL?Mj_8NJIt0^9L2dZ4}3qO^VsO7r7|NPn6s*cT8BgasEVq;?1 zXWrVmRBbOy0y~IVpGDS5wB(}2f>}ismC<-xv@-Y7Mi@cgj=lSnqLNZ#LP7_k)42m{C8iFG~*YPyTz|nw=f0 z3#`)7v9r1P%+#|RV_pCbMMZ%mlGRn7K7?YcB^|Eyoeh41n4}Ivb#<#~v<98q<>i_h z8gaJ}QBhG>+;R1<73E}e-4s+*=A}~qz(B#XS-KLU=nWuqEvW76^Qaex86y`CGzxrj;ip{bdf)!8A#n%l+2s!(YD zEkIO31LWL`7#o`oqNN=VlX>*eOrBth?s7@XR3H$<#l|ungGm+a_4V;nknK7Z zZ=(>vO5)-qBO@b&gF)xduXJD9up>=0={VTiYrx@vIyieZXVp%qVd-=_-b~oj(=$Ab z!D2T7YI}DT7Wy7LRu+7bf71FYF9r>75QiHWpF4?-9YCb8cvDwb zZb!%Z#FGpg8=J<)M!=#)?ZQQp(}d2`7a9G%=OeW|Fm*A zItNp70_v6IWN#dXs;dh{BGK4bLK9-Yib-joC9ypRA4w#(6O&J$Hod4B@jX_yr7v=s zl)(N`aMPYN5l!$m8qTX!ABWHHptd09x@P{H5+5Ibbf>A)16(gG>ZLWq8|rIocXdS; z_kSc>YGeeXQ!M}4$@o)lhpLQ>%*&TAftoQoI;x_gl4M6fmf}>;%?|2IqZ{$}s?B-W zp8huv6W_4*>bl^=!VHe)x($ww&P-3AYGN>%%%!DNts7+CrWrVr_gX!xkllq?(S~+y@-R3j!WyANv;eA<2h9C z0FTED3kzHB#Jn?nydSHOpPvudqN#w-KhQ21zApiC2&{KyWyOq|keT_owl;Pm3xpF8 zoqt)9JMz5TCQkJWTG{Y-e`ZJ{TKUb$NRwOE=#y+{i`}<9TmDbST5fJu=5S>GwU_y> z{HBbjPAM|5S(_|_lH)|;RSjcsxIasdjWoo%TL)g5hoCeviZ9 zXjq+SWwWjCL7$HO_%327OP&>cSXGXFoERKzZEI@;D^v?abtZ#mt}AY(=bvyjed-+E zyrHKvyHYCqXQ^0h{`&Rn3(i?*$Rl$myJ#j{Nm5dh@>QNO5(%MrKq%9H|6N{E!uIK| zZzi<;My1AV)&Ldi None: - im = Image.new("RGB", (185, 65)) + im = Image.new("RGB", (280, 60)) draw = ImageDraw.Draw(im) draw.multiline_text( - (0, 0), "hey you\nyou are awesome\nthis", font=font, align="justify" + (0, 0), + "hey you you are awesome\nthis\nlooks awkward", + font=font, + align="justify", ) - assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_single_word.png") + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_last_line.png") def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e865f4516..47ae575c9 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -770,7 +770,7 @@ class ImageDraw: msg = 'align must be "left", "center", "right" or "justify"' raise ValueError(msg) - if align == "justify" and width_difference != 0: + if align == "justify" and width_difference != 0 and idx != len(lines) - 1: words = line.split(" " if isinstance(text, str) else b" ") if len(words) > 1: word_widths = [ From bc05a88ce664dff5eee1bb024e4922d86ad86f96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Apr 2025 20:56:02 +1000 Subject: [PATCH 471/628] Anchor left when justifying words --- .../images/multiline_text_justify_anchor.png | Bin 0 -> 11282 bytes .../multiline_text_justify_last_line.png | Bin 3581 -> 0 bytes Tests/test_imagefont.py | 20 +++++----- src/PIL/ImageDraw.py | 37 ++++++++++-------- 4 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 Tests/images/multiline_text_justify_anchor.png delete mode 100644 Tests/images/multiline_text_justify_last_line.png diff --git a/Tests/images/multiline_text_justify_anchor.png b/Tests/images/multiline_text_justify_anchor.png new file mode 100644 index 0000000000000000000000000000000000000000..6d3fb421d1832e82dcfba255891240cf2aa5b92d GIT binary patch literal 11282 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 ^Ow{8V4_rXU6&3`dV6!`l3PEJl{WwF}! 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 HzUTh}m8~Dn literal 0 HcmV?d00001 diff --git a/Tests/images/multiline_text_justify_last_line.png b/Tests/images/multiline_text_justify_last_line.png deleted file mode 100644 index bcc1afd72969c4a476cabc5a074154994a222a94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3581 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 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f99275925..fd622c945 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -267,19 +267,21 @@ def test_render_multiline_text_align( assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_render_multiline_text_align_justify_last_line( +def test_render_multiline_text_justify_anchor( font: ImageFont.FreeTypeFont, ) -> None: - im = Image.new("RGB", (280, 60)) + im = Image.new("RGB", (280, 240)) draw = ImageDraw.Draw(im) - draw.multiline_text( - (0, 0), - "hey you you are awesome\nthis\nlooks awkward", - font=font, - align="justify", - ) + for xy, anchor in (((0, 0), "la"), ((140, 80), "ma"), ((280, 160), "ra")): + draw.multiline_text( + xy, + "hey you you are awesome\nthis looks awkward\nthis\nlooks awkward", + font=font, + anchor=anchor, + align="justify", + ) - assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_last_line.png") + assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_anchor.png") def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 47ae575c9..d35cda602 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -702,8 +702,7 @@ class ImageDraw: font_size: float | None, ) -> tuple[ ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, - str, - list[tuple[tuple[float, float], AnyStr]], + list[tuple[tuple[float, float], str, AnyStr]], ]: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" @@ -753,13 +752,7 @@ class ImageDraw: 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 + # align by align parameter if align in ("left", "justify"): pass elif align == "center": @@ -773,6 +766,12 @@ class ImageDraw: if align == "justify" and width_difference != 0 and idx != len(lines) - 1: words = line.split(" " if isinstance(text, str) else b" ") if len(words) > 1: + # align left by anchor + if anchor[0] == "m": + left -= max_width / 2.0 + elif anchor[0] == "r": + left -= max_width + word_widths = [ self.textlength( word, @@ -784,17 +783,23 @@ class ImageDraw: ) for word in words ] + word_anchor = "l" + anchor[1] width_difference = max_width - sum(word_widths) for i, word in enumerate(words): - parts.append(((left, top), word)) + parts.append(((left, top), word_anchor, word)) left += word_widths[i] + width_difference / (len(words) - 1) top += line_spacing continue - parts.append(((left, top), line)) + # align left by anchor + if anchor[0] == "m": + left -= width_difference / 2.0 + elif anchor[0] == "r": + left -= width_difference + parts.append(((left, top), anchor, line)) top += line_spacing - return font, anchor, parts + return font, parts def multiline_text( self, @@ -819,7 +824,7 @@ class ImageDraw: *, font_size: float | None = None, ) -> None: - font, anchor, lines = self._prepare_multiline_text( + font, lines = self._prepare_multiline_text( xy, text, font, @@ -834,7 +839,7 @@ class ImageDraw: font_size, ) - for xy, line in lines: + for xy, anchor, line in lines: self.text( xy, line, @@ -949,7 +954,7 @@ class ImageDraw: *, font_size: float | None = None, ) -> tuple[float, float, float, float]: - font, anchor, lines = self._prepare_multiline_text( + font, lines = self._prepare_multiline_text( xy, text, font, @@ -966,7 +971,7 @@ class ImageDraw: bbox: tuple[float, float, float, float] | None = None - for xy, line in lines: + for xy, anchor, line in lines: bbox_line = self.textbbox( xy, line, From 3d77723a0c9d245f61e124c58a11e3a1779b3c0d Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2025 21:42:42 +0100 Subject: [PATCH 472/628] Added arrow support for a flat array of 4*uint8 for image32 modes --- Tests/test_pyarrow.py | 66 +++++++++++++++++++++++++++++++++++++--- src/libImaging/Storage.c | 14 +++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index ece9f8f26..e7f2bc5f9 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -18,18 +18,25 @@ TEST_IMAGE_SIZE = (10, 10) def _test_img_equals_pyarray( - img: Image.Image, arr: Any, mask: list[int] | None + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 ) -> None: - assert img.height * img.width == len(arr) + assert img.height * img.width * elts_per_pixel == len(arr) px = img.load() assert px is not None + if elts_per_pixel > 1 and mask is None: + # have to do element wise comparison when we're comparing + # flattened r,g,b,a to a pixel. + mask = list(range(elts_per_pixel)) for x in range(0, img.size[0], int(img.size[0] / 10)): for y in range(0, img.size[1], int(img.size[1] / 10)): if mask: + pixel = px[x, y] + assert isinstance(pixel, tuple) for ix, elt in enumerate(mask): - pixel = px[x, y] - assert isinstance(pixel, tuple) - assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + if elts_per_pixel == 1: + assert pixel[ix] == arr[y * img.width + x].as_py()[elt] + else: + assert pixel[ix] == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() else: assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) @@ -110,3 +117,52 @@ def test_lifetime2() -> None: px = img2.load() assert px # make mypy happy assert isinstance(px[0, 0], int) + + +UINT_ARR = ( + fl_uint8_4_type, + [1,2,3,4], + 1 +) +UINT = ( + pyarrow.uint8(), + 3, + 4 +) + + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("L", (pyarrow.uint8(), 3, 1), None), + ("I", (pyarrow.int32(), 1<<24, 1), None), + ("F", (pyarrow.float32(), 3.14159, 1), None), + ("LA", UINT_ARR, [0, 3]), + ("LA", UINT, [0, 3]), + ("RGB", UINT_ARR, [0, 1, 2]), + ("RGBA", UINT_ARR, None), + ("RGBA", UINT_ARR, None), + ("CMYK", UINT_ARR, None), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), + ("RGB", UINT, [0, 1, 2]), + ("RGBA", UINT, None), + ("RGBA", UINT, None), + ("CMYK", UINT, None), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), + ), +) +def test_fromarray(mode: str, + data_tp: tuple, + mask:list[int] | None) -> None: + (dtype, + elt, + elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = pyarrow.array([elt]*(ct_pixels*elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 4fa4ecd1c..7f8d9c4a0 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -723,6 +723,8 @@ ImagingNewArrow( int64_t pixels = (int64_t)xsize * (int64_t)ysize; // fmt:off // don't reformat this + // stored as a single array, one element per pixel, either single band + // or multiband, where each pixel is an I32. if (((strcmp(schema->format, "I") == 0 // int32 && im->pixelsize == 4 // 4xchar* storage && im->bands >= 2) // INT32 into any INT32 Storage mode @@ -735,6 +737,7 @@ ImagingNewArrow( return im; } } + // Stored as [[r,g,b,a],....] if (strcmp(schema->format, "+w:4") == 0 // 4 up array && im->pixelsize == 4 // storage as 32 bpc && schema->n_children > 0 // make sure schema is well formed. @@ -750,6 +753,17 @@ ImagingNewArrow( return im; } } + // Stored as [r,g,b,a,r,g,b,a....] + if (strcmp(schema->format, "C") == 0 // uint8 + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children == 0 // make sure schema is well formed. + && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && 4* pixels == external_array->length) { // expected length + // single flat array, interleaved storage. + if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { + return im; + } + } // fmt: on ImagingDelete(im); return NULL; From c729d4e2085b96662b89ad09f99327f4516ce4ed Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2025 22:16:27 +0100 Subject: [PATCH 473/628] Test uint32 array creation -> image32 images --- Tests/test_pyarrow.py | 61 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index e7f2bc5f9..92bc4c807 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -9,6 +9,7 @@ from PIL import Image from .helper import ( assert_deep_equal, assert_image_equal, + is_big_endian, hopper, ) @@ -41,6 +42,34 @@ def _test_img_equals_pyarray( assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) +def _test_img_equals_int32_pyarray( + img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1 +) -> None: + assert img.height * img.width * elts_per_pixel == len(arr) + px = img.load() + assert px is not None + if mask is None: + # have to do element wise comparison when we're comparing + # flattened rgba in an uint32 to a pixel. + mask = list(range(elts_per_pixel)) + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + pixel = px[x, y] + assert isinstance(pixel, tuple) + arr_pixel_int = arr[y * img.width + x].as_py() + arr_pixel_tuple = ( + arr_pixel_int % 256, + (arr_pixel_int // 256) % 256, + (arr_pixel_int // 256**2) % 256, + (arr_pixel_int // 256**3), + ) + if is_big_endian(): + arr_pixel_tuple = arr_pixel_tuple[::-1] + + for ix, elt in enumerate(mask): + assert pixel[ix] == arr_pixel_tuple[elt] + + # really hard to get a non-nullable list type fl_uint8_4_type = pyarrow.field( "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) @@ -129,7 +158,11 @@ UINT = ( 3, 4 ) - +INT32 = ( + pyarrow.uint32(), + 0xabcdef45, + 1 +) @pytest.mark.parametrize( @@ -166,3 +199,29 @@ def test_fromarray(mode: str, img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) + + +@pytest.mark.parametrize( + "mode, data_tp, mask", + ( + ("LA", INT32, [0, 3]), + ("RGB", INT32, [0, 1, 2]), + ("RGBA", INT32, None), + ("RGBA", INT32, None), + ("CMYK", INT32, None), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), + ), +) +def test_from_int32array(mode: str, + data_tp: tuple, + mask:list[int] | None) -> None: + (dtype, + elt, + elts_per_pixel) = data_tp + + ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] + arr = pyarrow.array([elt]*(ct_pixels*elts_per_pixel), type=dtype) + img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) + + _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) From ac500460dfc6ddaa7c0660de3f0233d05e207852 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2025 22:22:31 +0100 Subject: [PATCH 474/628] lint --- Tests/test_pyarrow.py | 44 ++++++++++++++++------------------------ src/libImaging/Storage.c | 10 ++++----- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 92bc4c807..bcdd7ddc9 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -9,8 +9,8 @@ from PIL import Image from .helper import ( assert_deep_equal, assert_image_equal, - is_big_endian, hopper, + is_big_endian, ) pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") @@ -37,7 +37,10 @@ def _test_img_equals_pyarray( if elts_per_pixel == 1: assert pixel[ix] == arr[y * img.width + x].as_py()[elt] else: - assert pixel[ix] == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + assert ( + pixel[ix] + == arr[(y * img.width + x) * elts_per_pixel + elt].as_py() + ) else: assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) @@ -169,33 +172,27 @@ INT32 = ( "mode, data_tp, mask", ( ("L", (pyarrow.uint8(), 3, 1), None), - ("I", (pyarrow.int32(), 1<<24, 1), None), + ("I", (pyarrow.int32(), 1 << 24, 1), None), ("F", (pyarrow.float32(), 3.14159, 1), None), ("LA", UINT_ARR, [0, 3]), ("LA", UINT, [0, 3]), ("RGB", UINT_ARR, [0, 1, 2]), ("RGBA", UINT_ARR, None), - ("RGBA", UINT_ARR, None), ("CMYK", UINT_ARR, None), - ("YCbCr", UINT_ARR, [0, 1, 2]), - ("HSV", UINT_ARR, [0, 1, 2]), + ("YCbCr", UINT_ARR, [0, 1, 2]), + ("HSV", UINT_ARR, [0, 1, 2]), ("RGB", UINT, [0, 1, 2]), ("RGBA", UINT, None), - ("RGBA", UINT, None), ("CMYK", UINT, None), - ("YCbCr", UINT, [0, 1, 2]), - ("HSV", UINT, [0, 1, 2]), + ("YCbCr", UINT, [0, 1, 2]), + ("HSV", UINT, [0, 1, 2]), ), ) -def test_fromarray(mode: str, - data_tp: tuple, - mask:list[int] | None) -> None: - (dtype, - elt, - elts_per_pixel) = data_tp +def test_fromarray(mode: str, data_tp: tuple, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] - arr = pyarrow.array([elt]*(ct_pixels*elts_per_pixel), type=dtype) + arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) _test_img_equals_pyarray(img, arr, mask, elts_per_pixel) @@ -207,21 +204,16 @@ def test_fromarray(mode: str, ("LA", INT32, [0, 3]), ("RGB", INT32, [0, 1, 2]), ("RGBA", INT32, None), - ("RGBA", INT32, None), ("CMYK", INT32, None), - ("YCbCr", INT32, [0, 1, 2]), - ("HSV", INT32, [0, 1, 2]), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), ), ) -def test_from_int32array(mode: str, - data_tp: tuple, - mask:list[int] | None) -> None: - (dtype, - elt, - elts_per_pixel) = data_tp +def test_from_int32array(mode: str, data_tp: tuple, mask: list[int] | None) -> None: + (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] - arr = pyarrow.array([elt]*(ct_pixels*elts_per_pixel), type=dtype) + arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype) img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE) _test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7f8d9c4a0..2c57165c1 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -754,11 +754,11 @@ ImagingNewArrow( } } // Stored as [r,g,b,a,r,g,b,a....] - if (strcmp(schema->format, "C") == 0 // uint8 - && im->pixelsize == 4 // storage as 32 bpc - && schema->n_children == 0 // make sure schema is well formed. - && strcmp(im->arrow_band_format, "C") == 0 // Expected Format - && 4* pixels == external_array->length) { // expected length + if (strcmp(schema->format, "C") == 0 // uint8 + && im->pixelsize == 4 // storage as 32 bpc + && schema->n_children == 0 // make sure schema is well formed. + && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && 4 * pixels == external_array->length) { // expected length // single flat array, interleaved storage. if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { return im; From 00ae9dda35e512e3bdfa69daff1625fd006cff21 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Apr 2025 18:49:11 +1000 Subject: [PATCH 475/628] Changed harfbuzz buildtype to minsize --- .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 d53cf059b..a4592871f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -92,7 +92,7 @@ function build_harfbuzz { local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) (cd $out_dir/build \ && meson install) touch harfbuzz-stamp From cf48bbf0c48f871ecd62fb473611a7e2552580d9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Apr 2025 20:26:03 +1000 Subject: [PATCH 476/628] Removed indentation from list --- docs/handbook/concepts.rst | 44 +++++++++++++++--------------- docs/reference/block_allocator.rst | 18 ++++++------ 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 7da1078c1..fe874a740 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -30,35 +30,35 @@ image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a r INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release supports the following standard modes: - * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) - * ``L`` (8-bit pixels, grayscale) - * ``P`` (8-bit pixels, mapped to any other mode using a color palette) - * ``RGB`` (3x8-bit pixels, true color) - * ``RGBA`` (4x8-bit pixels, true color with transparency mask) - * ``CMYK`` (4x8-bit pixels, color separation) - * ``YCbCr`` (3x8-bit pixels, color video format) +* ``1`` (1-bit pixels, black and white, stored with one pixel per byte) +* ``L`` (8-bit pixels, grayscale) +* ``P`` (8-bit pixels, mapped to any other mode using a color palette) +* ``RGB`` (3x8-bit pixels, true color) +* ``RGBA`` (4x8-bit pixels, true color with transparency mask) +* ``CMYK`` (4x8-bit pixels, color separation) +* ``YCbCr`` (3x8-bit pixels, color video format) - * Note that this refers to the JPEG, and not the ITU-R BT.2020, standard + * Note that this refers to the JPEG, and not the ITU-R BT.2020, standard - * ``LAB`` (3x8-bit pixels, the L*a*b color space) - * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) +* ``LAB`` (3x8-bit pixels, the L*a*b color space) +* ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) - * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees + * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees - * ``I`` (32-bit signed integer pixels) - * ``F`` (32-bit floating point pixels) +* ``I`` (32-bit signed integer pixels) +* ``F`` (32-bit floating point pixels) Pillow also provides limited support for a few additional modes, including: - * ``LA`` (L with alpha) - * ``PA`` (P with alpha) - * ``RGBX`` (true color with padding) - * ``RGBa`` (true color with premultiplied alpha) - * ``La`` (L with premultiplied alpha) - * ``I;16`` (16-bit unsigned integer pixels) - * ``I;16L`` (16-bit little endian unsigned integer pixels) - * ``I;16B`` (16-bit big endian unsigned integer pixels) - * ``I;16N`` (16-bit native endian unsigned integer pixels) +* ``LA`` (L with alpha) +* ``PA`` (P with alpha) +* ``RGBX`` (true color with padding) +* ``RGBa`` (true color with premultiplied alpha) +* ``La`` (L with premultiplied alpha) +* ``I;16`` (16-bit unsigned integer pixels) +* ``I;16L`` (16-bit little endian unsigned integer pixels) +* ``I;16B`` (16-bit big endian unsigned integer pixels) +* ``I;16N`` (16-bit native endian unsigned integer pixels) Premultiplied alpha is where the values for each other channel have been multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index f4d27e24e..c6be5b7e6 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -37,14 +37,14 @@ fresh allocation. This caching of free blocks is currently disabled by default, but can be enabled and tweaked using three environment variables: - * ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory - allocations. Valid values are powers of 2 between 1 and - 128, inclusive. Defaults to 1. +* ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory + allocations. Valid values are powers of 2 between 1 and + 128, inclusive. Defaults to 1. - * ``PILLOW_BLOCK_SIZE``, in bytes, K, or M. Specifies the maximum - block size for ``ImagingAllocateArray``. Valid values are - integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M. +* ``PILLOW_BLOCK_SIZE``, in bytes, K, or M. Specifies the maximum + block size for ``ImagingAllocateArray``. Valid values are + integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M. - * ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to - retain to fill future memory requests. Any freed blocks over this - threshold will be returned to the OS immediately. Defaults to 0. +* ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to + retain to fill future memory requests. Any freed blocks over this + threshold will be returned to the OS immediately. Defaults to 0. From 03e7871afdb8f558cddc10cc9013e1db143298d9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 20 Apr 2025 00:18:01 +0300 Subject: [PATCH 477/628] Add `make [-C docs] htmllive` to rebuild and reload HTML files (#8913) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Makefile | 5 +++++ docs/Guardfile | 10 ---------- docs/Makefile | 9 +++++---- pyproject.toml | 1 + 4 files changed, 11 insertions(+), 14 deletions(-) delete mode 100755 docs/Guardfile diff --git a/Makefile b/Makefile index 53164b08a..5a8152454 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,10 @@ doc html: htmlview: $(MAKE) -C docs htmlview +.PHONY: htmllive +htmllive: + $(MAKE) -C docs htmllive + .PHONY: doccheck doccheck: $(MAKE) doc @@ -43,6 +47,7 @@ help: @echo " docserve run an HTTP server on the docs directory" @echo " html make HTML docs" @echo " htmlview open the index page built by the html target in your browser" + @echo " htmllive rebuild and reload HTML files in your browser" @echo " install make and install" @echo " install-coverage make and install with C coverage" @echo " lint run the lint checks" diff --git a/docs/Guardfile b/docs/Guardfile deleted file mode 100755 index 16a891a73..000000000 --- a/docs/Guardfile +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -from livereload.compiler import shell -from livereload.task import Task - -Task.add("*.rst", shell("make html")) -Task.add("*/*.rst", shell("make html")) -Task.add("Makefile", shell("make html")) -Task.add("conf.py", shell("make html")) diff --git a/docs/Makefile b/docs/Makefile index e90af0519..4412fc806 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -20,8 +20,8 @@ help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " htmlview to open the index page built by the html target in your browser" + @echo " htmllive to rebuild and reload HTML files in your browser" @echo " serve to start a local server for viewing docs" - @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @@ -201,9 +201,10 @@ doctest: htmlview: html $(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" -.PHONY: livehtml -livehtml: html - livereload $(BUILDDIR)/html -p 33233 +.PHONY: htmllive +htmllive: SPHINXBUILD = $(PYTHON) -m sphinx_autobuild +htmllive: SPHINXOPTS = --open-browser --delay 0 +htmllive: html .PHONY: serve serve: diff --git a/pyproject.toml b/pyproject.toml index e8e76796a..a3ff9723b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ optional-dependencies.docs = [ "furo", "olefile", "sphinx>=8.2", + "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph", From 4402797b35137666413efcc04fb91064e846cd90 Mon Sep 17 00:00:00 2001 From: Adian Kozlica <105174725+AdianKozlica@users.noreply.github.com> Date: Mon, 21 Apr 2025 04:36:40 +0200 Subject: [PATCH 478/628] Add support for Grim in Wayland sessions ImageGrab (#8912) Co-authored-by: Andrew Murray --- Tests/test_imagegrab.py | 1 + docs/reference/ImageGrab.rst | 6 +++--- src/PIL/ImageGrab.py | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 5f51171f1..01fa090dc 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -43,6 +43,7 @@ class TestImageGrab: if ( sys.platform not in ("win32", "darwin") and not shutil.which("gnome-screenshot") + and not shutil.which("grim") and not shutil.which("spectacle") ): with pytest.raises(OSError) as e: diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 1e827a676..0fd8f68df 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -16,9 +16,9 @@ or the clipboard to a PIL image memory. the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return - a snapshot of the screen, ``gnome-screenshot`` or ``spectacle`` will be used as a - fallback if they are installed. To disable this behaviour, pass ``xdisplay=""`` - instead. + a snapshot of the screen, ``gnome-screenshot``, ``grim`` or ``spectacle`` will be + used as a fallback if they are installed. To disable this behaviour, pass + ``xdisplay=""`` instead. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 4da14f8e4..c29350b7a 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -89,6 +89,8 @@ def grab( if display_name is None and sys.platform not in ("darwin", "win32"): if shutil.which("gnome-screenshot"): args = ["gnome-screenshot", "-f"] + elif shutil.which("grim"): + args = ["grim"] elif shutil.which("spectacle"): args = ["spectacle", "-n", "-b", "-f", "-o"] else: From 8fe7a7aaf89a5f312fe60382c1c5196876f53452 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Apr 2025 17:32:47 +1000 Subject: [PATCH 479/628] Update redirected URL --- docs/releasenotes/10.1.0.rst | 2 +- src/PIL/ImageFont.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst index fd556bdf1..e4efb069e 100644 --- a/docs/releasenotes/10.1.0.rst +++ b/docs/releasenotes/10.1.0.rst @@ -71,7 +71,7 @@ size and font_size arguments when using default font Pillow has had a "better than nothing" default font, which can only be drawn at one font size. Now, if FreeType support is available, a version of -`Aileron Regular `_ is loaded, which can be +`Aileron Regular `_ is loaded, which can be drawn at chosen font sizes. The following ``size`` and ``font_size`` arguments can now be used to specify a diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ebe510ba9..329c463ff 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -1093,7 +1093,7 @@ w7IkEbzhVQAAAABJRU5ErkJggg== def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """If FreeType support is available, load a version of Aileron Regular, - https://dotcolon.net/font/aileron, with a more limited character set. + https://dotcolon.net/fonts/aileron, with a more limited character set. Otherwise, load a "better than nothing" font. From d03ce3d235c9a80fb5a336634115a331749af9f8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 21 Apr 2025 11:22:03 +0300 Subject: [PATCH 480/628] Docs: remove unused Makefile targets (#8917) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/Makefile | 135 -------------------------------------------------- docs/conf.py | 91 ---------------------------------- docs/make.bat | 123 --------------------------------------------- 3 files changed, 349 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 4412fc806..1e6c06ede 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -24,22 +24,7 @@ help: @echo " serve to start a local server for viewing docs" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" .PHONY: clean clean: @@ -69,119 +54,6 @@ singlehtml: @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." -.PHONY: pickle -pickle: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PillowPILfork.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" - -.PHONY: devhelp -devhelp: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/PillowPILfork" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork" - @echo "# devhelp" - -.PHONY: epub -epub: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: latex -latex: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - .PHONY: linkcheck linkcheck: $(MAKE) install-sphinx @@ -190,13 +62,6 @@ linkcheck: @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." -.PHONY: doctest -doctest: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - .PHONY: htmlview htmlview: html $(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" diff --git a/docs/conf.py b/docs/conf.py index bfbcf9151..040301433 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -229,97 +229,6 @@ html_js_files = [ # implements a search results scorer. If empty, the default will be used. # html_search_scorer = 'scorer.js' -# Output file base name for HTML help builder. -htmlhelp_basename = "PillowPILForkdoc" - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements: dict[str, str] = { - # The paper size ('letterpaper' or 'a4paper'). - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # 'preamble': '', - # Latex figure (float) alignment - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ( - master_doc, - "PillowPILFork.tex", - "Pillow (PIL Fork) Documentation", - "Jeffrey A. Clark", - "manual", - ) -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, "pillowpilfork", "Pillow (PIL Fork) Documentation", [author], 1) -] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "PillowPILFork", - "Pillow (PIL Fork) Documentation", - author, - "PillowPILFork", - "Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors.", - "Miscellaneous", - ) -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False - linkcheck_allowed_redirects = { r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*", diff --git a/docs/make.bat b/docs/make.bat index 0ed5ee1a5..4126f786b 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -22,20 +22,7 @@ if "%1" == "help" ( echo. htmlview to open the index page built by the html target in your browser echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled goto end ) @@ -80,107 +67,6 @@ if "%1" == "singlehtml" ( goto end ) -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PillowPILfork.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PillowPILfork.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 @@ -190,13 +76,4 @@ or in %BUILDDIR%/linkcheck/output.txt. goto end ) -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - :end From 348589a367bba81bd9e2d1f4b6280ada91caae2e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 21 Apr 2025 12:03:31 +0300 Subject: [PATCH 481/628] Docs: use sentence case for headers (#8914) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/README.rst | 2 +- docs/PIL.rst | 32 +++---- docs/deprecations.rst | 2 +- docs/handbook/concepts.rst | 2 +- docs/handbook/image-file-formats.rst | 4 +- docs/handbook/overview.rst | 6 +- docs/handbook/tutorial.rst | 4 +- .../writing-your-own-image-plugin.rst | 6 +- docs/installation.rst | 10 +- docs/installation/basic-installation.rst | 2 +- docs/installation/building-from-source.rst | 8 +- docs/installation/platform-support.rst | 6 +- docs/installation/python-support.rst | 2 +- docs/reference/ExifTags.rst | 2 +- docs/reference/Image.rst | 6 +- docs/reference/ImageChops.rst | 2 +- docs/reference/ImageCms.rst | 2 +- docs/reference/ImageColor.rst | 4 +- docs/reference/ImageDraw.rst | 8 +- docs/reference/ImageEnhance.rst | 2 +- docs/reference/ImageFile.rst | 2 +- docs/reference/ImageFilter.rst | 2 +- docs/reference/ImageFont.rst | 2 +- docs/reference/ImageGrab.rst | 2 +- docs/reference/ImageMath.rst | 10 +- docs/reference/ImageMorph.rst | 2 +- docs/reference/ImageOps.rst | 2 +- docs/reference/ImagePalette.rst | 2 +- docs/reference/ImagePath.rst | 2 +- docs/reference/ImageQt.rst | 2 +- docs/reference/ImageSequence.rst | 2 +- docs/reference/ImageShow.rst | 4 +- docs/reference/ImageStat.rst | 2 +- docs/reference/ImageTk.rst | 2 +- docs/reference/ImageTransform.rst | 2 +- docs/reference/ImageWin.rst | 2 +- docs/reference/JpegPresets.rst | 2 +- docs/reference/PSDraw.rst | 2 +- docs/reference/PixelAccess.rst | 4 +- docs/reference/TiffTags.rst | 2 +- docs/reference/arrow_support.rst | 10 +- docs/reference/block_allocator.rst | 8 +- docs/reference/c_extension_debugging.rst | 8 +- docs/reference/features.rst | 2 +- docs/reference/internal_design.rst | 2 +- docs/reference/internal_modules.rst | 16 ++-- docs/reference/limits.rst | 6 +- docs/reference/open_files.rst | 6 +- docs/reference/plugins.rst | 92 +++++++++---------- docs/releasenotes/10.0.0.rst | 8 +- docs/releasenotes/10.0.1.rst | 2 +- docs/releasenotes/10.1.0.rst | 6 +- docs/releasenotes/10.2.0.rst | 6 +- docs/releasenotes/10.3.0.rst | 6 +- docs/releasenotes/10.4.0.rst | 4 +- docs/releasenotes/11.0.0.rst | 12 +-- docs/releasenotes/11.1.0.rst | 6 +- docs/releasenotes/11.2.1.rst | 6 +- docs/releasenotes/2.7.0.rst | 6 +- docs/releasenotes/3.0.0.rst | 12 +-- docs/releasenotes/3.1.0.rst | 4 +- docs/releasenotes/3.2.0.rst | 4 +- docs/releasenotes/3.3.0.rst | 4 +- docs/releasenotes/3.3.2.rst | 4 +- docs/releasenotes/3.4.0.rst | 8 +- docs/releasenotes/4.0.0.rst | 2 +- docs/releasenotes/4.1.0.rst | 10 +- docs/releasenotes/4.1.1.rst | 2 +- docs/releasenotes/4.2.0.rst | 12 +-- docs/releasenotes/4.2.1.rst | 2 +- docs/releasenotes/4.3.0.rst | 26 +++--- docs/releasenotes/5.0.0.rst | 22 ++--- docs/releasenotes/5.1.0.rst | 10 +- docs/releasenotes/5.2.0.rst | 6 +- docs/releasenotes/5.3.0.rst | 6 +- docs/releasenotes/5.4.0.rst | 4 +- docs/releasenotes/6.0.0.rst | 8 +- docs/releasenotes/6.1.0.rst | 4 +- docs/releasenotes/6.2.0.rst | 6 +- docs/releasenotes/6.2.1.rst | 4 +- docs/releasenotes/7.0.0.rst | 6 +- docs/releasenotes/7.1.0.rst | 6 +- docs/releasenotes/7.2.0.rst | 2 +- docs/releasenotes/8.0.0.rst | 8 +- docs/releasenotes/8.1.0.rst | 6 +- docs/releasenotes/8.1.1.rst | 2 +- docs/releasenotes/8.2.0.rst | 6 +- docs/releasenotes/8.3.0.rst | 6 +- docs/releasenotes/8.3.2.rst | 2 +- docs/releasenotes/8.4.0.rst | 4 +- docs/releasenotes/9.0.0.rst | 8 +- docs/releasenotes/9.0.1.rst | 2 +- docs/releasenotes/9.1.0.rst | 6 +- docs/releasenotes/9.2.0.rst | 4 +- docs/releasenotes/9.3.0.rst | 4 +- docs/releasenotes/9.4.0.rst | 4 +- docs/releasenotes/9.5.0.rst | 4 +- docs/releasenotes/index.rst | 2 +- docs/releasenotes/template.rst | 8 +- 99 files changed, 313 insertions(+), 313 deletions(-) diff --git a/Tests/README.rst b/Tests/README.rst index 2d014e5a4..a955ec4fa 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -1,4 +1,4 @@ -Pillow Tests +Pillow tests ============ Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. diff --git a/docs/PIL.rst b/docs/PIL.rst index bdbf1373d..5225e9644 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -1,10 +1,10 @@ -PIL Package (autodoc of remaining modules) +PIL package (autodoc of remaining modules) ========================================== Reference for modules whose documentation has not yet been ported or written can be found here. -:mod:`PIL` Module +:mod:`PIL` module ----------------- .. py:module:: PIL @@ -12,7 +12,7 @@ can be found here. .. autoexception:: UnidentifiedImageError :show-inheritance: -:mod:`~PIL.BdfFontFile` Module +:mod:`~PIL.BdfFontFile` module ------------------------------ .. automodule:: PIL.BdfFontFile @@ -20,7 +20,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ContainerIO` Module +:mod:`~PIL.ContainerIO` module ------------------------------ .. automodule:: PIL.ContainerIO @@ -28,7 +28,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.FontFile` Module +:mod:`~PIL.FontFile` module --------------------------- .. automodule:: PIL.FontFile @@ -36,7 +36,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.GdImageFile` Module +:mod:`~PIL.GdImageFile` module ------------------------------ .. automodule:: PIL.GdImageFile @@ -44,7 +44,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.GimpGradientFile` Module +:mod:`~PIL.GimpGradientFile` module ----------------------------------- .. automodule:: PIL.GimpGradientFile @@ -52,7 +52,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.GimpPaletteFile` Module +:mod:`~PIL.GimpPaletteFile` module ---------------------------------- .. automodule:: PIL.GimpPaletteFile @@ -60,7 +60,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageDraw2` Module +:mod:`~PIL.ImageDraw2` module ----------------------------- .. automodule:: PIL.ImageDraw2 @@ -69,7 +69,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageMode` Module +:mod:`~PIL.ImageMode` module ---------------------------- .. automodule:: PIL.ImageMode @@ -77,7 +77,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.PaletteFile` Module +:mod:`~PIL.PaletteFile` module ------------------------------ .. automodule:: PIL.PaletteFile @@ -85,7 +85,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.PcfFontFile` Module +:mod:`~PIL.PcfFontFile` module ------------------------------ .. automodule:: PIL.PcfFontFile @@ -93,7 +93,7 @@ can be found here. :undoc-members: :show-inheritance: -:class:`.PngImagePlugin.iTXt` Class +:class:`.PngImagePlugin.iTXt` class ----------------------------------- .. autoclass:: PIL.PngImagePlugin.iTXt @@ -107,7 +107,7 @@ can be found here. :param lang: language code :param tkey: UTF-8 version of the key name -:class:`.PngImagePlugin.PngInfo` Class +:class:`.PngImagePlugin.PngInfo` class -------------------------------------- .. autoclass:: PIL.PngImagePlugin.PngInfo @@ -116,7 +116,7 @@ can be found here. :show-inheritance: -:mod:`~PIL.TarIO` Module +:mod:`~PIL.TarIO` module ------------------------ .. automodule:: PIL.TarIO @@ -124,7 +124,7 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.WalImageFile` Module +:mod:`~PIL.WalImageFile` module ------------------------------- .. automodule:: PIL.WalImageFile diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 7f8e76bcc..0490ba439 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -155,7 +155,7 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). -Specific WebP Feature Checks +Specific WebP feature checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index fe874a740..c9d3f5e91 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -84,7 +84,7 @@ pixels. .. _coordinate-system: -Coordinate System +Coordinate system ----------------- The Python Imaging Library uses a Cartesian pixel coordinate system, with (0,0) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 46fe8b630..5ca549c37 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1222,7 +1222,7 @@ numbers are returned as a tuple of ``(numerator, denominator)``. .. deprecated:: 3.0.0 -Reading Multi-frame TIFF Images +Reading multi-frame TIFF images ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and @@ -1664,7 +1664,7 @@ The :py:meth:`~PIL.Image.open` method sets the following Transparency color index. This key is omitted if the image is not transparent. -XV Thumbnails +XV thumbnails ^^^^^^^^^^^^^ Pillow can read XV thumbnail files. diff --git a/docs/handbook/overview.rst b/docs/handbook/overview.rst index 17964d1c5..ab22b9807 100644 --- a/docs/handbook/overview.rst +++ b/docs/handbook/overview.rst @@ -13,7 +13,7 @@ processing tool. Let’s look at a few possible uses of this library. -Image Archives +Image archives -------------- The Python Imaging Library is ideal for image archival and batch processing @@ -24,7 +24,7 @@ The current version identifies and reads a large number of formats. Write support is intentionally restricted to the most commonly used interchange and presentation formats. -Image Display +Image display ------------- The current release includes Tk :py:class:`~PIL.ImageTk.PhotoImage` and @@ -36,7 +36,7 @@ support. For debugging, there’s also a :py:meth:`~PIL.Image.Image.show` method which saves an image to disk, and calls an external display utility. -Image Processing +Image processing ---------------- The library contains basic image processing functionality, including point operations, filtering with a set of built-in convolution kernels, and colour space conversions. diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index f1a2849b8..28c0abe44 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -122,7 +122,7 @@ This means that opening an image file is a fast operation, which is independent of the file size and compression type. Here’s a simple script to quickly identify a set of image files: -Identify Image Files +Identify image files ^^^^^^^^^^^^^^^^^^^^ :: @@ -399,7 +399,7 @@ Applying filters .. image:: enhanced_hopper.webp :align: center -Point Operations +Point operations ^^^^^^^^^^^^^^^^ The :py:meth:`~PIL.Image.Image.point` method can be used to translate the pixel diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 9e7d14c57..21a9124d7 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -1,6 +1,6 @@ .. _image-plugins: -Writing Your Own Image Plugin +Writing your own image plugin ============================= Pillow uses a plugin model which allows you to add your own @@ -329,7 +329,7 @@ The fields are used as follows: .. _file-codecs: -Writing Your Own File Codec in C +Writing your own file codec in C ================================ There are 3 stages in a file codec's lifetime: @@ -414,7 +414,7 @@ memory and release any resources from external libraries. .. _file-codecs-py: -Writing Your Own File Codec in Python +Writing your own file codec in Python ===================================== Python file decoders and encoders should derive from diff --git a/docs/installation.rst b/docs/installation.rst index b4bf2fa00..03f18c195 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -3,27 +3,27 @@ Installation ============ -Basic Installation +Basic installation ------------------ .. Note:: This section has moved to :ref:`basic-installation`. Please update references accordingly. -Python Support +Python support -------------- .. Note:: This section has moved to :ref:`python-support`. Please update references accordingly. -Platform Support +Platform support ---------------- .. Note:: This section has moved to :ref:`platform-support`. Please update references accordingly. -Building From Source +Building from source -------------------- .. Note:: This section has moved to :ref:`building-from-source`. Please update references accordingly. -Old Versions +Old versions ------------ .. Note:: This section has moved to :ref:`old-versions`. Please update references accordingly. diff --git a/docs/installation/basic-installation.rst b/docs/installation/basic-installation.rst index 989f72ddd..f66ee8707 100644 --- a/docs/installation/basic-installation.rst +++ b/docs/installation/basic-installation.rst @@ -8,7 +8,7 @@ .. _basic-installation: -Basic Installation +Basic installation ================== .. note:: diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 9ba389b66..c72568b20 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -8,12 +8,12 @@ .. _building-from-source: -Building From Source +Building from source ==================== .. _external-libraries: -External Libraries +External libraries ------------------ .. note:: @@ -271,7 +271,7 @@ After navigating to the Pillow directory, run:: .. _compressed archive from PyPI: https://pypi.org/project/pillow/#files -Build Options +Build options ^^^^^^^^^^^^^ * Config setting: ``-C parallel=n``. Can also be given @@ -319,7 +319,7 @@ Sample usage:: .. _old-versions: -Old Versions +Old versions ============ You can download old distributions from the `release history at PyPI diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index d751620fd..93486d034 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -1,6 +1,6 @@ .. _platform-support: -Platform Support +Platform support ================ Current platform support for Pillow. Binary distributions are @@ -9,7 +9,7 @@ should compile and run everywhere platform support is listed. In general, we aim to support all current versions of Linux, macOS, and Windows. -Continuous Integration Targets +Continuous integration targets ------------------------------ These platforms are built and tested for every change. @@ -59,7 +59,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ -Other Platforms +Other platforms --------------- These platforms have been reported to work at the versions mentioned. diff --git a/docs/installation/python-support.rst b/docs/installation/python-support.rst index dd5765b6b..7daee8afc 100644 --- a/docs/installation/python-support.rst +++ b/docs/installation/python-support.rst @@ -1,6 +1,6 @@ .. _python-support: -Python Support +Python support ============== Pillow supports these Python versions. diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 06965ead3..e6bcd9d59 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ExifTags .. py:currentmodule:: PIL.ExifTags -:py:mod:`~PIL.ExifTags` Module +:py:mod:`~PIL.ExifTags` module ============================== The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index a3ba8cfd8..e68722900 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.Image .. py:currentmodule:: PIL.Image -:py:mod:`~PIL.Image` Module +:py:mod:`~PIL.Image` module =========================== The :py:mod:`~PIL.Image` module provides a class with the same name which is @@ -113,7 +113,7 @@ Registering plugins .. autofunction:: register_decoder .. autofunction:: register_encoder -The Image Class +The Image class --------------- .. autoclass:: PIL.Image.Image @@ -261,7 +261,7 @@ method. :: .. automethod:: PIL.Image.Image.load .. automethod:: PIL.Image.Image.close -Image Attributes +Image attributes ---------------- Instances of the :py:class:`Image` class have the following attributes: diff --git a/docs/reference/ImageChops.rst b/docs/reference/ImageChops.rst index 9519361a7..505181db6 100644 --- a/docs/reference/ImageChops.rst +++ b/docs/reference/ImageChops.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageChops .. py:currentmodule:: PIL.ImageChops -:py:mod:`~PIL.ImageChops` ("Channel Operations") Module +:py:mod:`~PIL.ImageChops` ("channel operations") module ======================================================= The :py:mod:`~PIL.ImageChops` module contains a number of arithmetical image diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 4a1f5a3ee..238390e75 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageCms .. py:currentmodule:: PIL.ImageCms -:py:mod:`~PIL.ImageCms` Module +:py:mod:`~PIL.ImageCms` module ============================== The :py:mod:`~PIL.ImageCms` module provides color profile management diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index 31faeac78..68e228dba 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageColor .. py:currentmodule:: PIL.ImageColor -:py:mod:`~PIL.ImageColor` Module +:py:mod:`~PIL.ImageColor` module ================================ The :py:mod:`~PIL.ImageColor` module contains color tables and converters from @@ -11,7 +11,7 @@ others. .. _color-names: -Color Names +Color names ----------- The ImageColor module supports the following string formats: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index bd6f6b048..6e73233a1 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageDraw .. py:currentmodule:: PIL.ImageDraw -:py:mod:`~PIL.ImageDraw` Module +:py:mod:`~PIL.ImageDraw` module =============================== The :py:mod:`~PIL.ImageDraw` module provides simple 2D graphics for @@ -54,7 +54,7 @@ later, you can also use RGB 3-tuples or color names (see below). The drawing layer will automatically assign color indexes, as long as you don’t draw with more than 256 colors. -Color Names +Color names ^^^^^^^^^^^ See :ref:`color-names` for the color names supported by Pillow. @@ -75,7 +75,7 @@ To load a OpenType/TrueType font, use the truetype function in the :py:mod:`~PIL.ImageFont` module. Note that this function depends on third-party libraries, and may not available in all PIL builds. -Example: Draw Partial Opacity Text +Example: Draw partial opacity text ---------------------------------- :: @@ -102,7 +102,7 @@ Example: Draw Partial Opacity Text out.show() -Example: Draw Multiline Text +Example: Draw multiline text ---------------------------- :: diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 529acad4a..334d1d4b2 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageEnhance .. py:currentmodule:: PIL.ImageEnhance -:py:mod:`~PIL.ImageEnhance` Module +:py:mod:`~PIL.ImageEnhance` module ================================== The :py:mod:`~PIL.ImageEnhance` module contains a number of classes that can be used diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 64abd71d1..043559352 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageFile .. py:currentmodule:: PIL.ImageFile -:py:mod:`~PIL.ImageFile` Module +:py:mod:`~PIL.ImageFile` module =============================== The :py:mod:`~PIL.ImageFile` module provides support functions for the image open diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index 5f2b6af7c..1c201cacc 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageFilter .. py:currentmodule:: PIL.ImageFilter -:py:mod:`~PIL.ImageFilter` Module +:py:mod:`~PIL.ImageFilter` module ================================= The :py:mod:`~PIL.ImageFilter` module contains definitions for a pre-defined set of diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index d9d9cac6e..8b2f92323 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageFont .. py:currentmodule:: PIL.ImageFont -:py:mod:`~PIL.ImageFont` Module +:py:mod:`~PIL.ImageFont` module =============================== The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instances of diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 0fd8f68df..f6a2ec5bc 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageGrab .. py:currentmodule:: PIL.ImageGrab -:py:mod:`~PIL.ImageGrab` Module +:py:mod:`~PIL.ImageGrab` module =============================== The :py:mod:`~PIL.ImageGrab` module can be used to copy the contents of the screen diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index f4e1081e6..0ee49b150 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageMath .. py:currentmodule:: PIL.ImageMath -:py:mod:`~PIL.ImageMath` Module +:py:mod:`~PIL.ImageMath` module =============================== The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that @@ -86,7 +86,7 @@ Expression syntax It is not recommended to process expressions without considering this. :py:meth:`lambda_eval` is a more secure alternative. -Standard Operators +Standard operators ^^^^^^^^^^^^^^^^^^ You can use standard arithmetical operators for addition (+), subtraction (-), @@ -102,7 +102,7 @@ an 8-bit image, the result will be a 32-bit floating point image. You can force conversion using the ``convert()``, ``float()``, and ``int()`` functions described below. -Bitwise Operators +Bitwise operators ^^^^^^^^^^^^^^^^^ The module also provides operations that operate on individual bits. This @@ -116,7 +116,7 @@ mask off unwanted bits. Bitwise operators don’t work on floating point images. -Logical Operators +Logical operators ^^^^^^^^^^^^^^^^^ Logical operators like ``and``, ``or``, and ``not`` work @@ -128,7 +128,7 @@ treated as true. Note that ``and`` and ``or`` return the last evaluated operand, while not always returns a boolean value. -Built-in Functions +Built-in functions ^^^^^^^^^^^^^^^^^^ These functions are applied to each individual pixel. diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst index d4522a06a..30b89a54d 100644 --- a/docs/reference/ImageMorph.rst +++ b/docs/reference/ImageMorph.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageMorph .. py:currentmodule:: PIL.ImageMorph -:py:mod:`~PIL.ImageMorph` Module +:py:mod:`~PIL.ImageMorph` module ================================ The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images. diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index fcaa3c8f6..1ecff09f0 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageOps .. py:currentmodule:: PIL.ImageOps -:py:mod:`~PIL.ImageOps` Module +:py:mod:`~PIL.ImageOps` module ============================== The :py:mod:`~PIL.ImageOps` module contains a number of ‘ready-made’ image diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst index 72ccfac7d..42ce5cb13 100644 --- a/docs/reference/ImagePalette.rst +++ b/docs/reference/ImagePalette.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImagePalette .. py:currentmodule:: PIL.ImagePalette -:py:mod:`~PIL.ImagePalette` Module +:py:mod:`~PIL.ImagePalette` module ================================== The :py:mod:`~PIL.ImagePalette` module contains a class of the same name to diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 23544b613..5f5606349 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImagePath .. py:currentmodule:: PIL.ImagePath -:py:mod:`~PIL.ImagePath` Module +:py:mod:`~PIL.ImagePath` module =============================== The :py:mod:`~PIL.ImagePath` module is used to store and manipulate 2-dimensional diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 7e67a44d3..88d7b8a20 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageQt .. py:currentmodule:: PIL.ImageQt -:py:mod:`~PIL.ImageQt` Module +:py:mod:`~PIL.ImageQt` module ============================= The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6 diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index a27b2fb4e..0d6f394dd 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageSequence .. py:currentmodule:: PIL.ImageSequence -:py:mod:`~PIL.ImageSequence` Module +:py:mod:`~PIL.ImageSequence` module =================================== The :py:mod:`~PIL.ImageSequence` module contains a wrapper class that lets you diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index 5cedede69..12c8741ce 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -1,10 +1,10 @@ .. py:module:: PIL.ImageShow .. py:currentmodule:: PIL.ImageShow -:py:mod:`~PIL.ImageShow` Module +:py:mod:`~PIL.ImageShow` module =============================== -The :py:mod:`~PIL.ImageShow` Module is used to display images. +The :py:mod:`~PIL.ImageShow` module is used to display images. All default viewers convert the image to be shown to PNG format. .. autofunction:: PIL.ImageShow.show diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index f69466382..ede119920 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageStat .. py:currentmodule:: PIL.ImageStat -:py:mod:`~PIL.ImageStat` Module +:py:mod:`~PIL.ImageStat` module =============================== The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or diff --git a/docs/reference/ImageTk.rst b/docs/reference/ImageTk.rst index 134ef5651..3ab72b83d 100644 --- a/docs/reference/ImageTk.rst +++ b/docs/reference/ImageTk.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageTk .. py:currentmodule:: PIL.ImageTk -:py:mod:`~PIL.ImageTk` Module +:py:mod:`~PIL.ImageTk` module ============================= The :py:mod:`~PIL.ImageTk` module contains support to create and modify Tkinter diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst index 5b0a5ce49..530279934 100644 --- a/docs/reference/ImageTransform.rst +++ b/docs/reference/ImageTransform.rst @@ -2,7 +2,7 @@ .. py:module:: PIL.ImageTransform .. py:currentmodule:: PIL.ImageTransform -:py:mod:`~PIL.ImageTransform` Module +:py:mod:`~PIL.ImageTransform` module ==================================== The :py:mod:`~PIL.ImageTransform` module contains implementations of diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 4151be4a7..c0b9bd2ba 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.ImageWin .. py:currentmodule:: PIL.ImageWin -:py:mod:`~PIL.ImageWin` Module (Windows-only) +:py:mod:`~PIL.ImageWin` module (Windows-only) ============================================= The :py:mod:`~PIL.ImageWin` module contains support to create and display images on diff --git a/docs/reference/JpegPresets.rst b/docs/reference/JpegPresets.rst index aafae44cf..b0a3ba8b5 100644 --- a/docs/reference/JpegPresets.rst +++ b/docs/reference/JpegPresets.rst @@ -1,6 +1,6 @@ .. py:currentmodule:: PIL.JpegPresets -:py:mod:`~PIL.JpegPresets` Module +:py:mod:`~PIL.JpegPresets` module ================================= .. automodule:: PIL.JpegPresets diff --git a/docs/reference/PSDraw.rst b/docs/reference/PSDraw.rst index 3e8512e7a..9eed775fc 100644 --- a/docs/reference/PSDraw.rst +++ b/docs/reference/PSDraw.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.PSDraw .. py:currentmodule:: PIL.PSDraw -:py:mod:`~PIL.PSDraw` Module +:py:mod:`~PIL.PSDraw` module ============================ The :py:mod:`~PIL.PSDraw` module provides simple print support for PostScript diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 1ac3d034b..9d7cf83b6 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -1,6 +1,6 @@ .. _PixelAccess: -:py:class:`PixelAccess` Class +:py:class:`PixelAccess` class ============================= The PixelAccess class provides read and write access to @@ -40,7 +40,7 @@ Access using negative indexes is also possible. :: -:py:class:`PixelAccess` Class +:py:class:`PixelAccess` class ----------------------------- .. class:: PixelAccess diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst index 7cb7d16ae..d75a48478 100644 --- a/docs/reference/TiffTags.rst +++ b/docs/reference/TiffTags.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.TiffTags .. py:currentmodule:: PIL.TiffTags -:py:mod:`~PIL.TiffTags` Module +:py:mod:`~PIL.TiffTags` module ============================== The :py:mod:`~PIL.TiffTags` module exposes many of the standard TIFF diff --git a/docs/reference/arrow_support.rst b/docs/reference/arrow_support.rst index 4a5c45e86..063046d8c 100644 --- a/docs/reference/arrow_support.rst +++ b/docs/reference/arrow_support.rst @@ -1,7 +1,7 @@ .. _arrow-support: ============= -Arrow Support +Arrow support ============= `Arrow `__ @@ -18,7 +18,7 @@ with any Arrow provider or consumer in the Python ecosystem. full-copy memory cost to reading an Arrow image. -Data Formats +Data formats ============ Pillow currently supports exporting Arrow images in all modes @@ -43,7 +43,7 @@ interpreted using the mode-specific interpretation of the bytes. The image mode must match the Arrow band format when reading single channel images. -Memory Allocator +Memory allocator ================ Pillow's default memory allocator, the :ref:`block_allocator`, @@ -59,7 +59,7 @@ To enable the single block allocator:: Note that this is a global setting, not a per-image setting. -Unsupported Features +Unsupported features ==================== * Table/dataframe protocol. We support a single array. @@ -71,7 +71,7 @@ Unsupported Features parameter. * Array metadata. -Internal Details +Internal details ================ Python Arrow C interface: diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst index c6be5b7e6..5ad9d9fd1 100644 --- a/docs/reference/block_allocator.rst +++ b/docs/reference/block_allocator.rst @@ -1,10 +1,10 @@ .. _block_allocator: -Block Allocator +Block allocator =============== -Previous Design +Previous design --------------- Historically there have been two image allocators in Pillow: @@ -16,7 +16,7 @@ large images and makes one allocation for each scan line of size between one allocation and potentially thousands of small allocations, leading to unpredictable performance penalties around the transition. -New Design +New design ---------- ``ImagingAllocateArray`` now allocates space for images as a chain of @@ -28,7 +28,7 @@ line. This is now the default for all internal allocations. specifically requesting a single segment of memory for sharing with other code. -Memory Pools +Memory pools ------------ There is now a memory pool to contain a supply of recently freed diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 5e8586905..12dca6cf2 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -1,5 +1,5 @@ -C Extension debugging on Linux, with gbd/valgrind. -================================================== +C extension debugging on Linux, with GBD/Valgrind +================================================= Install the tools ----------------- @@ -17,7 +17,7 @@ Then ``sudo apt-get install libtiff5-dbgsym`` - There's a bug with the ``python3-dbg`` package for at least Python 3.8 on Ubuntu 20.04, and you need to add a new link or two to make it autoload when - running python: + running Python: :: @@ -49,7 +49,7 @@ Then ``sudo apt-get install libtiff5-dbgsym`` source ~/vpy38-dbg/bin/activate cd ~/Pillow && make install -Test Case +Test case --------- Take your test image, and make a really simple harness. diff --git a/docs/reference/features.rst b/docs/reference/features.rst index c5d89b838..381d7830a 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -1,7 +1,7 @@ .. py:module:: PIL.features .. py:currentmodule:: PIL.features -:py:mod:`~PIL.features` Module +:py:mod:`~PIL.features` module ============================== The :py:mod:`PIL.features` module can be used to detect which Pillow features are available on your system. diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 041177953..6bba673b9 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -1,4 +1,4 @@ -Internal Reference +Internal reference ================== .. toctree:: diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 93fd82cf9..19f78864d 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -1,7 +1,7 @@ -Internal Modules +Internal modules ================ -:mod:`~PIL._binary` Module +:mod:`~PIL._binary` module -------------------------- .. automodule:: PIL._binary @@ -9,7 +9,7 @@ Internal Modules :undoc-members: :show-inheritance: -:mod:`~PIL._deprecate` Module +:mod:`~PIL._deprecate` module ----------------------------- .. automodule:: PIL._deprecate @@ -17,7 +17,7 @@ Internal Modules :undoc-members: :show-inheritance: -:mod:`~PIL._tkinter_finder` Module +:mod:`~PIL._tkinter_finder` module ---------------------------------- .. automodule:: PIL._tkinter_finder @@ -25,7 +25,7 @@ Internal Modules :undoc-members: :show-inheritance: -:mod:`~PIL._typing` Module +:mod:`~PIL._typing` module -------------------------- .. module:: PIL._typing @@ -58,7 +58,7 @@ on some Python versions. See :py:obj:`typing.TypeGuard`. -:mod:`~PIL._util` Module +:mod:`~PIL._util` module ------------------------ .. automodule:: PIL._util @@ -66,7 +66,7 @@ on some Python versions. :undoc-members: :show-inheritance: -:mod:`~PIL._version` Module +:mod:`~PIL._version` module --------------------------- .. module:: PIL._version @@ -78,7 +78,7 @@ on some Python versions. This is the master version number for Pillow, all other uses reference this module. -:mod:`PIL.Image.core` Module +:mod:`PIL.Image.core` module ---------------------------- .. module:: PIL._imaging diff --git a/docs/reference/limits.rst b/docs/reference/limits.rst index a71b514b5..d2f8f7d1f 100644 --- a/docs/reference/limits.rst +++ b/docs/reference/limits.rst @@ -4,7 +4,7 @@ Limits This page is documentation to the various fundamental size limits in the Pillow implementation. -Internal Limits +Internal limits =============== * Image sizes cannot be negative. These are checked both in @@ -25,10 +25,10 @@ Internal Limits is smaller than 2GB, as calculated by ``y*stride`` (so 2Gpx for 'L' images, and .5Gpx for 'RGB' -Format Size Limits +Format size limits ================== * ICO: Max size is 256x256 -* Webp: 16383x16383 (underlying library size limit: +* WebP: 16383x16383 (underlying library size limit: https://developers.google.com/speed/webp/docs/api) diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 730c8da5b..0d43cbc73 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -1,6 +1,6 @@ .. _file-handling: -File Handling in Pillow +File handling in Pillow ======================= When opening a file as an image, Pillow requires a filename, ``os.PathLike`` @@ -36,7 +36,7 @@ have multiple frames. Pillow cannot in general close and reopen a file, so any access to that file needs to be prior to the close. -Image Lifecycle +Image lifecycle --------------- * ``Image.open()`` Filenames and ``Path`` objects are opened as a file. @@ -97,7 +97,7 @@ Complications im6.load() # FAILS, closed file -Proposed File Handling +Proposed file handling ---------------------- * ``Image.Image.load()`` should close the image file, unless there are diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index c789f5757..243d4f353 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -1,7 +1,7 @@ Plugin reference ================ -:mod:`~PIL.AvifImagePlugin` Module +:mod:`~PIL.AvifImagePlugin` module ---------------------------------- .. automodule:: PIL.AvifImagePlugin @@ -9,7 +9,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.BmpImagePlugin` Module +:mod:`~PIL.BmpImagePlugin` module --------------------------------- .. automodule:: PIL.BmpImagePlugin @@ -17,7 +17,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.BufrStubImagePlugin` Module +:mod:`~PIL.BufrStubImagePlugin` module -------------------------------------- .. automodule:: PIL.BufrStubImagePlugin @@ -25,7 +25,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.CurImagePlugin` Module +:mod:`~PIL.CurImagePlugin` module --------------------------------- .. automodule:: PIL.CurImagePlugin @@ -33,7 +33,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.DcxImagePlugin` Module +:mod:`~PIL.DcxImagePlugin` module --------------------------------- .. automodule:: PIL.DcxImagePlugin @@ -41,7 +41,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.DdsImagePlugin` Module +:mod:`~PIL.DdsImagePlugin` module --------------------------------- .. automodule:: PIL.DdsImagePlugin @@ -49,7 +49,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.EpsImagePlugin` Module +:mod:`~PIL.EpsImagePlugin` module --------------------------------- .. automodule:: PIL.EpsImagePlugin @@ -57,15 +57,15 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.FitsImagePlugin` Module --------------------------------------- +:mod:`~PIL.FitsImagePlugin` module +---------------------------------- .. automodule:: PIL.FitsImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`~PIL.FliImagePlugin` Module +:mod:`~PIL.FliImagePlugin` module --------------------------------- .. automodule:: PIL.FliImagePlugin @@ -73,7 +73,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.FpxImagePlugin` Module +:mod:`~PIL.FpxImagePlugin` module --------------------------------- .. automodule:: PIL.FpxImagePlugin @@ -81,7 +81,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.GbrImagePlugin` Module +:mod:`~PIL.GbrImagePlugin` module --------------------------------- .. automodule:: PIL.GbrImagePlugin @@ -89,7 +89,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.GifImagePlugin` Module +:mod:`~PIL.GifImagePlugin` module --------------------------------- .. automodule:: PIL.GifImagePlugin @@ -97,7 +97,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.GribStubImagePlugin` Module +:mod:`~PIL.GribStubImagePlugin` module -------------------------------------- .. automodule:: PIL.GribStubImagePlugin @@ -105,7 +105,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.Hdf5StubImagePlugin` Module +:mod:`~PIL.Hdf5StubImagePlugin` module -------------------------------------- .. automodule:: PIL.Hdf5StubImagePlugin @@ -113,7 +113,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.IcnsImagePlugin` Module +:mod:`~PIL.IcnsImagePlugin` module ---------------------------------- .. automodule:: PIL.IcnsImagePlugin @@ -121,7 +121,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.IcoImagePlugin` Module +:mod:`~PIL.IcoImagePlugin` module --------------------------------- .. automodule:: PIL.IcoImagePlugin @@ -129,7 +129,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.ImImagePlugin` Module +:mod:`~PIL.ImImagePlugin` module -------------------------------- .. automodule:: PIL.ImImagePlugin @@ -137,7 +137,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.ImtImagePlugin` Module +:mod:`~PIL.ImtImagePlugin` module --------------------------------- .. automodule:: PIL.ImtImagePlugin @@ -145,7 +145,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.IptcImagePlugin` Module +:mod:`~PIL.IptcImagePlugin` module ---------------------------------- .. automodule:: PIL.IptcImagePlugin @@ -153,7 +153,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.JpegImagePlugin` Module +:mod:`~PIL.JpegImagePlugin` module ---------------------------------- .. automodule:: PIL.JpegImagePlugin @@ -161,7 +161,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.Jpeg2KImagePlugin` Module +:mod:`~PIL.Jpeg2KImagePlugin` module ------------------------------------ .. automodule:: PIL.Jpeg2KImagePlugin @@ -169,7 +169,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.McIdasImagePlugin` Module +:mod:`~PIL.McIdasImagePlugin` module ------------------------------------ .. automodule:: PIL.McIdasImagePlugin @@ -177,7 +177,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MicImagePlugin` Module +:mod:`~PIL.MicImagePlugin` module --------------------------------- .. automodule:: PIL.MicImagePlugin @@ -185,7 +185,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MpegImagePlugin` Module +:mod:`~PIL.MpegImagePlugin` module ---------------------------------- .. automodule:: PIL.MpegImagePlugin @@ -193,15 +193,15 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.MpoImagePlugin` Module ----------------------------------- +:mod:`~PIL.MpoImagePlugin` module +--------------------------------- .. automodule:: PIL.MpoImagePlugin :members: :undoc-members: :show-inheritance: -:mod:`~PIL.MspImagePlugin` Module +:mod:`~PIL.MspImagePlugin` module --------------------------------- .. automodule:: PIL.MspImagePlugin @@ -209,7 +209,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PalmImagePlugin` Module +:mod:`~PIL.PalmImagePlugin` module ---------------------------------- .. automodule:: PIL.PalmImagePlugin @@ -217,7 +217,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PcdImagePlugin` Module +:mod:`~PIL.PcdImagePlugin` module --------------------------------- .. automodule:: PIL.PcdImagePlugin @@ -225,7 +225,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PcxImagePlugin` Module +:mod:`~PIL.PcxImagePlugin` module --------------------------------- .. automodule:: PIL.PcxImagePlugin @@ -233,7 +233,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PdfImagePlugin` Module +:mod:`~PIL.PdfImagePlugin` module --------------------------------- .. automodule:: PIL.PdfImagePlugin @@ -241,7 +241,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PixarImagePlugin` Module +:mod:`~PIL.PixarImagePlugin` module ----------------------------------- .. automodule:: PIL.PixarImagePlugin @@ -249,7 +249,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PngImagePlugin` Module +:mod:`~PIL.PngImagePlugin` module --------------------------------- .. automodule:: PIL.PngImagePlugin @@ -260,7 +260,7 @@ Plugin reference :member-order: groupwise -:mod:`~PIL.PpmImagePlugin` Module +:mod:`~PIL.PpmImagePlugin` module --------------------------------- .. automodule:: PIL.PpmImagePlugin @@ -268,7 +268,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.PsdImagePlugin` Module +:mod:`~PIL.PsdImagePlugin` module --------------------------------- .. automodule:: PIL.PsdImagePlugin @@ -276,7 +276,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SgiImagePlugin` Module +:mod:`~PIL.SgiImagePlugin` module --------------------------------- .. automodule:: PIL.SgiImagePlugin @@ -284,7 +284,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SpiderImagePlugin` Module +:mod:`~PIL.SpiderImagePlugin` module ------------------------------------ .. automodule:: PIL.SpiderImagePlugin @@ -292,7 +292,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.SunImagePlugin` Module +:mod:`~PIL.SunImagePlugin` module --------------------------------- .. automodule:: PIL.SunImagePlugin @@ -300,7 +300,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.TgaImagePlugin` Module +:mod:`~PIL.TgaImagePlugin` module --------------------------------- .. automodule:: PIL.TgaImagePlugin @@ -308,7 +308,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.TiffImagePlugin` Module +:mod:`~PIL.TiffImagePlugin` module ---------------------------------- .. automodule:: PIL.TiffImagePlugin @@ -316,7 +316,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.WebPImagePlugin` Module +:mod:`~PIL.WebPImagePlugin` module ---------------------------------- .. automodule:: PIL.WebPImagePlugin @@ -324,7 +324,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.WmfImagePlugin` Module +:mod:`~PIL.WmfImagePlugin` module --------------------------------- .. automodule:: PIL.WmfImagePlugin @@ -332,7 +332,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XVThumbImagePlugin` Module +:mod:`~PIL.XVThumbImagePlugin` module ------------------------------------- .. automodule:: PIL.XVThumbImagePlugin @@ -340,7 +340,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XbmImagePlugin` Module +:mod:`~PIL.XbmImagePlugin` module --------------------------------- .. automodule:: PIL.XbmImagePlugin @@ -348,7 +348,7 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.XpmImagePlugin` Module +:mod:`~PIL.XpmImagePlugin` module --------------------------------- .. automodule:: PIL.XpmImagePlugin diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 2ea973c5c..3e2aa84b1 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -28,7 +28,7 @@ This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. -Backwards Incompatible Changes +Backwards incompatible changes ============================== Categories @@ -164,7 +164,7 @@ Since Pillow's C API is now faster than PyAccess on PyPy, ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is similarly deprecated. -API Changes +API changes =========== Added line width parameter to ImageDraw regular_polygon @@ -173,7 +173,7 @@ Added line width parameter to ImageDraw regular_polygon An optional line ``width`` parameter has been added to ``ImageDraw.Draw.regular_polygon``. -API Additions +API additions ============= Added ``alpha_only`` argument to ``getbbox()`` @@ -184,7 +184,7 @@ Added ``alpha_only`` argument to ``getbbox()`` and the image has an alpha channel, trim transparent pixels. Otherwise, trim pixels when all channels are zero. -Other Changes +Other changes ============= 32-bit wheels diff --git a/docs/releasenotes/10.0.1.rst b/docs/releasenotes/10.0.1.rst index 02189d514..aa17a62e0 100644 --- a/docs/releasenotes/10.0.1.rst +++ b/docs/releasenotes/10.0.1.rst @@ -11,7 +11,7 @@ This release provides an updated install script and updated wheels to include libwebp 1.3.2, preventing a potential heap buffer overflow in WebP. -Other Changes +Other changes ============= Updated tests to pass with latest zlib version diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst index fd556bdf1..649c2bdf9 100644 --- a/docs/releasenotes/10.1.0.rst +++ b/docs/releasenotes/10.1.0.rst @@ -1,7 +1,7 @@ 10.1.0 ------ -API Changes +API changes =========== Setting image mode @@ -35,7 +35,7 @@ to be specified, rather than a single number for both dimensions. :: ImageFilter.BoxBlur((2, 5)) ImageFilter.GaussianBlur((2, 5)) -API Additions +API additions ============= EpsImagePlugin.gs_binary @@ -84,7 +84,7 @@ font size for this new builtin font:: draw.multiline_text((0, 0), "test", font_size=24) draw.multiline_textbbox((0, 0), "test", font_size=24) -Other Changes +Other changes ============= Python 3.12 diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 1c6b78b08..337748785 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -53,7 +53,7 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant for internal use, so there is no replacement. They can each be replaced by a single line of code using builtin functions in Python. -API Changes +API changes =========== Zero or negative font size error @@ -63,7 +63,7 @@ When creating a :py:class:`~PIL.ImageFont.FreeTypeFont` instance, either directl through :py:func:`~PIL.ImageFont.truetype`, if the font size is zero or less, a :py:exc:`ValueError` will now be raised. -API Additions +API additions ============= Added DdsImagePlugin enums @@ -95,7 +95,7 @@ JPEG tables-only streamtype When saving JPEG files, ``streamtype`` can now be set to 1, for tables-only. This will output only the quantization and Huffman tables for the image. -Other Changes +Other changes ============= Added DDS BC4U and DX10 BC1 and BC4 reading diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 2f0437d94..6c7d8ea0a 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -65,7 +65,7 @@ ImageMath.eval() :py:meth:`~PIL.ImageMath.unsafe_eval` instead. See earlier security notes for more information. -API Changes +API changes =========== Added alpha_quality argument when saving WebP images @@ -87,7 +87,7 @@ Negative P1-P3 PPM value error If a P1-P3 PPM image contains a negative value, a :py:exc:`ValueError` will now be raised. -API Additions +API additions ============= Added PerspectiveTransform @@ -97,7 +97,7 @@ Added PerspectiveTransform that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding subclass of :py:class:`~PIL.ImageTransform.Transform`. -Other Changes +Other changes ============= Portable FloatMap (PFM) images diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst index 8d3706be6..84a6091c9 100644 --- a/docs/releasenotes/10.4.0.rst +++ b/docs/releasenotes/10.4.0.rst @@ -41,7 +41,7 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. -API Additions +API additions ============= ImageDraw.circle @@ -51,7 +51,7 @@ Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functiona :py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it takes a center point and radius. -Other Changes +Other changes ============= Python 3.13 beta diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index c3f18140f..020fbf7df 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -1,7 +1,7 @@ 11.0.0 ------ -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.8 @@ -103,7 +103,7 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). -Specific WebP Feature Checks +Specific WebP feature checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 @@ -113,7 +113,7 @@ Specific WebP Feature Checks ``True`` if the WebP module is installed, until they are removed in Pillow 12.0.0 (2025-10-15). -API Changes +API changes =========== Default resampling filter for I;16* image modes @@ -122,7 +122,7 @@ Default resampling filter for I;16* image modes The default resampling filter for I;16, I;16L, I;16B and I;16N has been changed from ``Image.NEAREST`` to ``Image.BICUBIC``, to match the majority of modes. -API Additions +API additions ============= Writing XMP bytes to JPEG and MPO @@ -138,7 +138,7 @@ either JPEG or MPO images:: im.info["xmp"] = b"test" im.save("out.jpg") -Other Changes +Other changes ============= Python 3.13 @@ -154,7 +154,7 @@ Support has also been added for the experimental free-threaded mode of :pep:`703 Python 3.13 only supports macOS versions 10.13 and later. -C-level Flags +C-level flags ^^^^^^^^^^^^^ Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index 0d56cb420..4888ddf56 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -10,7 +10,7 @@ ExifTags.IFD.Makernote ``ExifTags.IFD.Makernote`` has been deprecated. Instead, use ``ExifTags.IFD.MakerNote``. -API Changes +API changes =========== Writing XMP bytes to JPEG and MPO @@ -34,7 +34,7 @@ be used:: second_im.encoderinfo = {"xmp": b"test"} im.save("out.mpo", save_all=True, append_images=[second_im]) -API Additions +API additions ============= Check for zlib-ng @@ -54,7 +54,7 @@ TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument:: im.save("out.tiff", big_tiff=True) -Other Changes +Other changes ============= Reading JPEG 2000 comments diff --git a/docs/releasenotes/11.2.1.rst b/docs/releasenotes/11.2.1.rst index 5c6d40d9d..f55b0d7d7 100644 --- a/docs/releasenotes/11.2.1.rst +++ b/docs/releasenotes/11.2.1.rst @@ -32,7 +32,7 @@ Image.Image.get_child_images() 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 +API changes =========== ``append_images`` no longer requires ``save_all`` @@ -44,7 +44,7 @@ supports saving multiple frames:: im.save("out.gif", append_images=ims) -API Additions +API additions ============= ``"justify"`` multiline text alignment @@ -86,7 +86,7 @@ DXT5, BC2, BC3 and BC5 are supported:: im.save("out.dds", pixel_format="DXT1") -Other Changes +Other changes ============= Arrow support diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index e9b0995bb..a1ddd1178 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -1,13 +1,13 @@ 2.7.0 ----- -Sane Plugin +Sane plugin ^^^^^^^^^^^ The Sane plugin has now been split into its own repo: https://github.com/python-pillow/Sane . -Png text chunk size limits +PNG text chunk size limits ^^^^^^^^^^^^^^^^^^^^^^^^^^ To prevent potential denial of service attacks using compressed text @@ -155,7 +155,7 @@ so the quality was worse compared to other Gaussian blur software. The new implementation does not have this drawback. -TIFF Parameter Changes +TIFF parameter changes ^^^^^^^^^^^^^^^^^^^^^^ Several kwarg parameters for saving TIFF images were previously diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst index 8bc477f70..dcd8031f5 100644 --- a/docs/releasenotes/3.0.0.rst +++ b/docs/releasenotes/3.0.0.rst @@ -1,7 +1,7 @@ 3.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Several methods that have been marked as deprecated for many releases @@ -18,10 +18,10 @@ have been removed in this release: * ``ImageWin.fromstring()`` * ``ImageWin.tostring()`` -Other Changes +Other changes ============= -Saving Multipage Images +Saving multipage images ^^^^^^^^^^^^^^^^^^^^^^^ There is now support for saving multipage images in the ``GIF`` and @@ -30,10 +30,10 @@ as a keyword argument to the save:: im.save('test.pdf', save_all=True) -Tiff ImageFileDirectory Rewrite +TIFF ImageFileDirectory rewrite ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The Tiff ImageFileDirectory metadata code has been rewritten. Where +The TIFF ImageFileDirectory metadata code has been rewritten. Where previously it returned a somewhat arbitrary set of values and tuples, it now returns bare values where appropriate and tuples when the metadata item is a sequence or collection. @@ -41,7 +41,7 @@ metadata item is a sequence or collection. The original metadata is still available in the TiffImage.tags, the new values are available in the TiffImage.tags_v2 member. The old structures will be deprecated at some point in the future. When -saving Tiff metadata, new code should use the +saving TIFF metadata, new code should use the TiffImagePlugin.ImageFileDirectory_v2 class. LibJpeg and Zlib are required by default diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst index 951819f19..90f77ff61 100644 --- a/docs/releasenotes/3.1.0.rst +++ b/docs/releasenotes/3.1.0.rst @@ -22,7 +22,7 @@ not the absolute height of each line. There is also now a default spacing of 4px between lines. -Exif, Jpeg and Tiff Metadata +EXIF, JPEG and TIFF metadata ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There were major changes in the TIFF ImageFileDirectory support in @@ -63,7 +63,7 @@ single item tuples have been unwrapped and return a bare element. The format returned by Pillow 3.0 has been abandoned. A more fully featured interface for EXIF is anticipated in a future release. -Out of Spec Metadata +Out of spec metadata ++++++++++++++++++++ In Pillow 3.0 and 3.1, images that contain metadata that is internally diff --git a/docs/releasenotes/3.2.0.rst b/docs/releasenotes/3.2.0.rst index 3ed8fae57..20d7d073e 100644 --- a/docs/releasenotes/3.2.0.rst +++ b/docs/releasenotes/3.2.0.rst @@ -1,7 +1,7 @@ 3.2.0 ----- -New DDS and FTEX Image Plugins +New DDS and FTEX image plugins ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``DdsImagePlugin`` reading DXT1 and DXT5 encoded ``.dds`` images was @@ -18,7 +18,7 @@ Updates to the GbrImagePlugin The ``GbrImagePlugin`` (GIMP brush format) has been updated to fix support for version 1 files and add support for version 2 files. -Passthrough Parameters for ImageDraw.text +Passthrough parameters for ImageDraw.text ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``ImageDraw.multiline_text`` and ``ImageDraw.multiline_size`` take extra diff --git a/docs/releasenotes/3.3.0.rst b/docs/releasenotes/3.3.0.rst index cd6f7e2f9..9447245c4 100644 --- a/docs/releasenotes/3.3.0.rst +++ b/docs/releasenotes/3.3.0.rst @@ -11,7 +11,7 @@ libimagequant. We cannot distribute binaries due to licensing differences. -New Setup.py options +New setup.py options ^^^^^^^^^^^^^^^^^^^^ There are two new options to control the ``build_ext`` task in ``setup.py``: @@ -43,7 +43,7 @@ This greatly improves both quality and performance in this case. Also, the bug with wrong image size calculation when rotating by 90 degrees was fixed. -Image Metadata +Image metadata ^^^^^^^^^^^^^^ The return type for binary data in version 2 Exif and Tiff metadata diff --git a/docs/releasenotes/3.3.2.rst b/docs/releasenotes/3.3.2.rst index 73156a65d..60ffbdcba 100644 --- a/docs/releasenotes/3.3.2.rst +++ b/docs/releasenotes/3.3.2.rst @@ -4,7 +4,7 @@ Security ======== -Integer overflow in Map.c +Integer overflow in map.c ^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow prior to 3.3.2 may experience integer overflow errors in map.c @@ -27,7 +27,7 @@ memory without duplicating the image first. This issue was found by Cris Neckar at Divergent Security. -Sign Extension in Storage.c +Sign extension in Storage.c ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow prior to 3.3.2 and PIL 1.1.7 (at least) do not check for diff --git a/docs/releasenotes/3.4.0.rst b/docs/releasenotes/3.4.0.rst index 8a5a7efe3..01ec77a58 100644 --- a/docs/releasenotes/3.4.0.rst +++ b/docs/releasenotes/3.4.0.rst @@ -1,7 +1,7 @@ 3.4.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Image.core.open_ppm removed @@ -14,7 +14,7 @@ been removed. If you were using this function, please use Deprecations ============ -Deprecation Warning when Saving JPEGs +Deprecation warning when saving JPEGs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0 @@ -22,7 +22,7 @@ silently drops the alpha channel. With this release Pillow will now issue a :py:exc:`DeprecationWarning` when attempting to save a ``RGBA`` mode image as a JPEG. This will become an error in Pillow 4.2. -API Additions +API additions ============= New resizing filters @@ -37,7 +37,7 @@ two times shorter window than ``BILINEAR``. It can be used for image reduction providing the image downscaling quality comparable to ``BICUBIC``. Both new filters don't show good quality for the image upscaling. -New DDS Decoders +New DDS decoders ^^^^^^^^^^^^^^^^ Pillow can now decode DXT3 images, as well as the previously supported diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst index 625f237e8..dd97463f6 100644 --- a/docs/releasenotes/4.0.0.rst +++ b/docs/releasenotes/4.0.0.rst @@ -1,7 +1,7 @@ 4.0.0 ----- -Python 2.6 and 3.2 Dropped +Python 2.6 and 3.2 dropped ^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow 4.0 no longer supports Python 2.6 and 3.2. We will not be diff --git a/docs/releasenotes/4.1.0.rst b/docs/releasenotes/4.1.0.rst index 80ad9b9fb..1f809ad18 100644 --- a/docs/releasenotes/4.1.0.rst +++ b/docs/releasenotes/4.1.0.rst @@ -15,10 +15,10 @@ Several deprecated items have been removed. ``PIL.ImageDraw.ImageDraw.setfont`` have been removed. -Other Changes +Other changes ============= -Closing Files When Opening Images +Closing files when opening images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The file handling when opening images has been overhauled. Previously, @@ -41,7 +41,7 @@ is specified: the underlying file until we are done with the image. The mapping will be closed in the ``close`` or ``__del__`` method. -Changes to GIF Handling When Saving +Changes to GIF handling when saving ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :py:class:`PIL.GifImagePlugin` code has been refactored to fix the flow when @@ -57,14 +57,14 @@ saving images. There are two external changes that arise from this: This refactor fixed some bugs with palette handling when saving multiple frame GIFs. -New Method: Image.remap_palette +New method: Image.remap_palette ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The method :py:meth:`PIL.Image.Image.remap_palette()` has been added. This method was hoisted from the GifImagePlugin code used to optimize the palette. -Added Decoder Registry and Support for Python Based Decoders +Added decoder registry and support for Python-based decoders ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There is now a decoder registry similar to the image plugin diff --git a/docs/releasenotes/4.1.1.rst b/docs/releasenotes/4.1.1.rst index 8c8055bfa..1cbd3853b 100644 --- a/docs/releasenotes/4.1.1.rst +++ b/docs/releasenotes/4.1.1.rst @@ -1,7 +1,7 @@ 4.1.1 ----- -Fix Regression with reading DPI from EXIF data +Fix regression with reading DPI from EXIF data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Some JPEG images don't contain DPI information in the image metadata, diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst index bc2a45f02..0ea3de399 100644 --- a/docs/releasenotes/4.2.0.rst +++ b/docs/releasenotes/4.2.0.rst @@ -1,7 +1,7 @@ 4.2.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Several deprecated items have been removed @@ -17,17 +17,17 @@ Several deprecated items have been removed was shown. From Pillow 4.2.0, the deprecation warning is removed and an :py:exc:`IOError` is raised. -Removed Core Image Function +Removed core Image function ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The unused function ``Image.core.new_array`` was removed. This is an internal function that should not have been used by user code, but it was accessible from the python layer. -Other Changes +Other changes ============= -Added Complex Text Rendering +Added complex text rendering ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow now supports complex text rendering for scripts requiring glyph @@ -36,7 +36,7 @@ dependencies: harfbuzz, fribidi, and raqm. See the :doc:`install documentation <../installation>` for further details. This feature is tested and works on Unix and Mac, but has not yet been built on Windows platforms. -New Optional Parameters +New optional parameters ^^^^^^^^^^^^^^^^^^^^^^^ * :py:meth:`PIL.ImageDraw.floodfill` has a new optional parameter: @@ -47,7 +47,7 @@ New Optional Parameters optional parameter for specifying additional images to create multipage outputs. -New DecompressionBomb Warning +New DecompressionBomb warning ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :py:meth:`PIL.Image.Image.crop` now may raise a DecompressionBomb diff --git a/docs/releasenotes/4.2.1.rst b/docs/releasenotes/4.2.1.rst index 2061f6467..617d51e52 100644 --- a/docs/releasenotes/4.2.1.rst +++ b/docs/releasenotes/4.2.1.rst @@ -3,7 +3,7 @@ There are no functional changes in this release. -Fixed Windows PyPy Build +Fixed Windows PyPy build ^^^^^^^^^^^^^^^^^^^^^^^^ A change in the 4.2.0 cycle broke the Windows PyPy build. This has diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst index ea81fc45e..87a57799f 100644 --- a/docs/releasenotes/4.3.0.rst +++ b/docs/releasenotes/4.3.0.rst @@ -1,7 +1,7 @@ 4.3.0 ----- -API Changes +API changes =========== Deprecations @@ -12,7 +12,7 @@ Several undocumented functions in ImageOps have been deprecated: ``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. These functions will be removed in a future release. -TIFF Metadata Changes +TIFF metadata changes ^^^^^^^^^^^^^^^^^^^^^ * TIFF tags with unknown type/quantity now default to being bare @@ -27,7 +27,7 @@ TIFF Metadata Changes items, as there can be multiple items, one for UTF-8, and one for UTF-16. -Core Image API Changes +Core Image API changes ^^^^^^^^^^^^^^^^^^^^^^ These are internal functions that should not have been used by user @@ -44,10 +44,10 @@ The ``PIL.Image.core.getcount`` methods have been removed, use ``PIL.Image.core.get_stats()['new_count']`` property instead. -API Additions +API additions ============= -Get One Channel From Image +Get one channel from image ^^^^^^^^^^^^^^^^^^^^^^^^^^ A new method :py:meth:`PIL.Image.Image.getchannel` has been added to @@ -56,14 +56,14 @@ return a single channel by index or name. For example, ``getchannel`` should work up to 6 times faster than ``image.split()[0]`` in previous Pillow versions. -Box Blur +Box blur ^^^^^^^^ A new filter, :py:class:`PIL.ImageFilter.BoxBlur`, has been added. This is a filter with similar results to a Gaussian blur, but is much faster. -Partial Resampling +Partial resampling ^^^^^^^^^^^^^^^^^^ Added new argument ``box`` for :py:meth:`PIL.Image.Image.resize`. This @@ -71,14 +71,14 @@ argument defines a source rectangle from within the source image to be resized. This is very similar to the ``image.crop(box).resize(size)`` sequence except that ``box`` can be specified with subpixel accuracy. -New Transpose Operation +New transpose operation ^^^^^^^^^^^^^^^^^^^^^^^ The ``Image.TRANSVERSE`` operation has been added to :py:meth:`PIL.Image.Image.transpose`. This is equivalent to a transpose operation about the opposite diagonal. -Multiband Filters +Multiband filters ^^^^^^^^^^^^^^^^^ There is a new :py:class:`PIL.ImageFilter.MultibandFilter` base class @@ -87,10 +87,10 @@ operation. The original :py:class:`PIL.ImageFilter.Filter` class remains for image filters that can process only single band images, or require splitting of channels prior to filtering. -Other Changes +Other changes ============= -Loading 16-bit TIFF Images +Loading 16-bit TIFF images ^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow now can read 16-bit multichannel TIFF files including files @@ -101,7 +101,7 @@ Pillow now can read 16-bit signed integer single channel TIFF files. The image data is promoted to 32-bit for storage and processing. -SGI Images +SGI images ^^^^^^^^^^ Pillow can now read and write uncompressed 16-bit multichannel SGI @@ -129,7 +129,7 @@ This release contains several performance improvements: falling back to an allocation for each scan line for images larger than the block size. -CMYK Conversion +CMYK conversion ^^^^^^^^^^^^^^^ The basic CMYK->RGB conversion has been tweaked to match the formula diff --git a/docs/releasenotes/5.0.0.rst b/docs/releasenotes/5.0.0.rst index be00a45cd..2b93e0322 100644 --- a/docs/releasenotes/5.0.0.rst +++ b/docs/releasenotes/5.0.0.rst @@ -1,10 +1,10 @@ 5.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== -Python 3.3 Dropped +Python 3.3 dropped ^^^^^^^^^^^^^^^^^^ Python 3.3 is EOL and no longer supported due to moving testing from nose, @@ -12,7 +12,7 @@ which is deprecated, to pytest, which doesn't support Python 3.3. We will not be creating binaries, testing, or retaining compatibility with this version. The final version of Pillow for Python 3.3 is 4.3.0. -Decompression Bombs now raise Exceptions +Decompression bombs now raise exceptions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow has previously emitted warnings for images that are @@ -31,7 +31,7 @@ separate package, pillow-scripts, living at https://github.com/python-pillow/pillow-scripts. -API Changes +API changes =========== OleFileIO.py @@ -54,7 +54,7 @@ Several image plugins supported a named ``check`` parameter on their nominally private ``_save`` method to preflight if the image could be saved in that format. That parameter has been removed. -API Additions +API additions ============= Image.transform @@ -65,16 +65,16 @@ A new named parameter, ``fillcolor``, has been added to the area outside the transformed area in the output image. This parameter takes the same color specifications as used in ``Image.new``. -GIF Disposal +GIF disposal ^^^^^^^^^^^^ Multiframe GIF images now take an optional disposal parameter to specify the disposal option for changed pixels. -Other Changes +Other changes ============= -Compressed TIFF Images +Compressed TIFF images ^^^^^^^^^^^^^^^^^^^^^^ Previously, there were some compression modes (JPEG, Packbits, and @@ -82,7 +82,7 @@ LZW) that were supported with Pillow's internal TIFF decoder. All compressed TIFFs are now read using the ``libtiff`` decoder, as it implements the compression schemes more correctly. -Libraqm is now Dynamically Linked +Libraqm is now dynamically linked ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The libraqm dependency for complex text scripts is now linked @@ -90,14 +90,14 @@ dynamically at runtime rather than at packaging time. This allows us to release binaries with support for libraqm if it is installed on the user's machine. -Source Layout Changes +Source layout changes ^^^^^^^^^^^^^^^^^^^^^ The Pillow source is now stored within the ``src`` directory of the distribution. This prevents accidental imports of the PIL directory when running Python from the project directory. -Setup.py Changes +Setup.py changes ^^^^^^^^^^^^^^^^ Multiarch support on Linux should be more robust, especially on Debian diff --git a/docs/releasenotes/5.1.0.rst b/docs/releasenotes/5.1.0.rst index 4e3d10ac5..4b80e8521 100644 --- a/docs/releasenotes/5.1.0.rst +++ b/docs/releasenotes/5.1.0.rst @@ -1,7 +1,7 @@ 5.1.0 ----- -API Changes +API changes =========== Optional channels for TIFF files @@ -12,22 +12,22 @@ and ``CMYK`` with up to 6 8-bit channels, discarding any extra channels if the content is tagged as UNSPECIFIED. Pillow still does not store more than 4 8-bit channels of image data. -API Additions +API additions ============= -Append to PDF Files +Append to PDF files ^^^^^^^^^^^^^^^^^^^ Images can now be appended to PDF files in place by passing in ``append=True`` when saving the image. -New BLP File Format +New BLP file format ^^^^^^^^^^^^^^^^^^^ Pillow now supports reading the BLP "Blizzard Mipmap" file format used for tiles in Blizzard's engine. -Other Changes +Other changes ============= WebP memory leak diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst index d9b8f0fb7..d18337820 100644 --- a/docs/releasenotes/5.2.0.rst +++ b/docs/releasenotes/5.2.0.rst @@ -1,7 +1,7 @@ 5.2.0 ----- -API Changes +API changes =========== Deprecations @@ -17,7 +17,7 @@ Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that. Use ``PIL.__version__`` instead. -API Additions +API additions ============= 3D color lookup tables @@ -75,7 +75,7 @@ TGA file format Pillow can now read and write LA data (in addition to L, P, RGB and RGBA), and write RLE data (in addition to uncompressed). -Other Changes +Other changes ============= Support added for Python 3.7 diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst index 8f276da24..6adce95b2 100644 --- a/docs/releasenotes/5.3.0.rst +++ b/docs/releasenotes/5.3.0.rst @@ -1,7 +1,7 @@ 5.3.0 ----- -API Changes +API changes =========== Image size @@ -20,7 +20,7 @@ The exceptions to this are: as direct image size setting was previously necessary to work around an issue with tile extents. -API Additions +API additions ============= Added line width parameter to rectangle and ellipse-based shapes @@ -59,7 +59,7 @@ and size, new method ``ImageOps.pad`` pads images to fill a requested aspect ratio and size, filling new space with a provided ``color`` and positioning the image within the new area through a ``centering`` argument. -Other Changes +Other changes ============= Added support for reading tiled TIFF images through LibTIFF. Compressed TIFF diff --git a/docs/releasenotes/5.4.0.rst b/docs/releasenotes/5.4.0.rst index 6d7277c70..13b540d60 100644 --- a/docs/releasenotes/5.4.0.rst +++ b/docs/releasenotes/5.4.0.rst @@ -1,7 +1,7 @@ 5.4.0 ----- -API Changes +API changes =========== APNG extension to PNG plugin @@ -55,7 +55,7 @@ TIFF images can now be saved with custom integer, float and string TIFF tags:: print(im2.tag_v2[37002]) # "custom tag value" print(im2.tag_v2[37004]) # b"custom tag value" -Other Changes +Other changes ============= ImageOps.fit diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst index 5e69f0b6b..b788b2eeb 100644 --- a/docs/releasenotes/6.0.0.rst +++ b/docs/releasenotes/6.0.0.rst @@ -1,7 +1,7 @@ 6.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.4 dropped @@ -32,7 +32,7 @@ Removed deprecated VERSION ``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__`` instead. -API Changes +API changes =========== Deprecations @@ -137,7 +137,7 @@ loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anym The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and "image/x-icon" respectively. -API Additions +API additions ============= DIB file format @@ -186,7 +186,7 @@ EXIF data can now be read from and saved to PNG images. However, unlike other im formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info` until :py:meth:`~PIL.Image.Image.load` has been called. -Other Changes +Other changes ============= Reading new DDS image format diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index ce3edc5fa..761f435f3 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -23,7 +23,7 @@ Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") -API Additions +API additions ============= Image.entropy @@ -61,7 +61,7 @@ file. ``ImageFont.FreeTypeFont`` has four new methods, instead. An :py:exc:`IOError` will be raised if the font is not a variation font. FreeType 2.9.1 or greater is required. -Other Changes +Other changes ============= ImageTk.getimage diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index b851c56fc..b37cd7160 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -29,7 +29,7 @@ perform operations on it. The CVE is regarding DOS problems, such as consuming large amounts of memory, or taking a large amount of time to process an image. -API Changes +API changes =========== Image.getexif @@ -48,7 +48,7 @@ There has been a longstanding warning that the defaults of ``Image.frombuffer`` may change in the future for the "raw" decoder. The change will now take place in Pillow 7.0. -API Additions +API additions ============= Text stroking @@ -93,7 +93,7 @@ ImageGrab on multi-monitor Windows An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, all monitors will be included in the created image. -Other Changes +Other changes ============= Removed bdist_wininst .exe installers diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst index 372298fbc..0ede05917 100644 --- a/docs/releasenotes/6.2.1.rst +++ b/docs/releasenotes/6.2.1.rst @@ -1,7 +1,7 @@ 6.2.1 ----- -API Changes +API changes =========== Deprecations @@ -15,7 +15,7 @@ Python 2.7 reaches end-of-life on 2020-01-01. Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python 2.7, making Pillow 6.2.x the last release series to support Python 2. -Other Changes +Other changes ============= Support added for Python 3.8 diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index ed6026593..9504c974a 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -1,7 +1,7 @@ 7.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 2.7 @@ -78,7 +78,7 @@ bounds of resulting image. This may be useful in a subsequent .. _chain methods: https://en.wikipedia.org/wiki/Method_chaining -API Additions +API additions ============= Custom unidentified image error @@ -124,7 +124,7 @@ now also be loaded at another resolution:: with Image.open("drawing.wmf") as im: im.load(dpi=144) -Other Changes +Other changes ============= Image.__del__ diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0dd8669a5..c2aeb0f74 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -35,7 +35,7 @@ out-of-bounds reads via a crafted JP2 file. In ``libImaging/SgiRleDecode.c`` in Pillow through 7.0.0, a number of out-of-bounds reads exist in the parsing of SGI image files, a different issue than :cve:`2020-5311`. -API Changes +API changes =========== Allow saving of zero quality JPEG images @@ -50,7 +50,7 @@ been resolved. :: im = Image.open("hopper.jpg") im.save("out.jpg", quality=0) -API Additions +API additions ============= New channel operations @@ -101,7 +101,7 @@ Passing a different value on Windows or macOS will force taking a snapshot using the selected X server; pass an empty string to use the default X server. XCB support is not included in pre-compiled wheels for Windows and macOS. -Other Changes +Other changes ============= If present, only use alpha channel for bounding box diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst index 91e54da19..12bafa8ce 100644 --- a/docs/releasenotes/7.2.0.rst +++ b/docs/releasenotes/7.2.0.rst @@ -1,7 +1,7 @@ 7.2.0 ----- -API Changes +API changes =========== Replaced TiffImagePlugin DEBUG with logging diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 1fc245c9a..d0dde756f 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -1,7 +1,7 @@ 8.0.0 ----- -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.5 @@ -44,7 +44,7 @@ Removed Use instead ``product_model`` Unicode :py:attr:`~.CmsProfile.model` ======================== =================================================== -API Changes +API changes =========== ImageDraw.text: stroke_width @@ -67,7 +67,7 @@ Add MIME type to PsdImagePlugin "image/vnd.adobe.photoshop" is now registered as the :py:class:`.PsdImagePlugin.PsdImageFile` MIME type. -API Additions +API additions ============= Image.open: add formats parameter @@ -135,7 +135,7 @@ and :py:meth:`.FreeTypeFont.getbbox` return the bounding box of rendered text. These functions accept an ``anchor`` parameter, see :ref:`text-anchors` for details. -Other Changes +Other changes ============= Improved ellipse-drawing algorithm diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst index 5c3993318..06e6d9974 100644 --- a/docs/releasenotes/8.1.0.rst +++ b/docs/releasenotes/8.1.0.rst @@ -26,7 +26,7 @@ leading to an out-of-bounds write in ``TiffDecode.c``. This potentially affects versions from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through `Tidelift`_. -:cve:`2020-35655`: SGI Decode buffer overrun +:cve:`2020-35655`: SGI decode buffer overrun ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly @@ -64,7 +64,7 @@ Makefile The ``install-venv`` target has been deprecated. -API Additions +API additions ============= Append images to ICO @@ -77,7 +77,7 @@ With this release, a list of images can be provided to the ``append_images`` par when saving, to replace the scaled down versions. This is the same functionality that already exists for the ICNS format. -Other Changes +Other changes ============= Makefile diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst index 690421c2a..b8ad5a898 100644 --- a/docs/releasenotes/8.1.1.rst +++ b/docs/releasenotes/8.1.1.rst @@ -32,7 +32,7 @@ DOS attack. There is an out-of-bounds read in ``SgiRleDecode.c`` since Pillow 4.3.0. -Other Changes +Other changes ============= A crash with the feature flags for libimagequant, libjpeg-turbo, WebP and XCB on diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 50fe9aa19..a59560695 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -74,7 +74,7 @@ Tk/Tcl 8.4 Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), when Tk/Tcl 8.5 will be the minimum supported. -API Changes +API changes =========== Image.alpha_composite: dest @@ -107,7 +107,7 @@ removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``, ``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()`` can be used. -API Additions +API additions ============= getxmp() for JPEG images @@ -177,7 +177,7 @@ be specified through a keyword argument:: im.save("out.tif", icc_profile=...) -Other Changes +Other changes ============= GIF writer uses LZW encoding diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index 4ef914f64..c46240854 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -33,7 +33,7 @@ dictionary. The ``convert_dict_qtables`` method no longer performs any operations on the data given to it, has been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). -API Changes +API changes =========== Changed WebP default "method" value when saving @@ -73,7 +73,7 @@ through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GP EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and ``im.getexif().get_ifd(0x8769)`` respectively. -API Additions +API additions ============= ImageOps.contain @@ -100,7 +100,7 @@ format, through the new ``bitmap_format`` argument:: im.save("out.ico", bitmap_format="bmp") -Other Changes +Other changes ============= Added DDS BC5 reading and uncompressed saving diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst index 34ba703f7..e26a6ceda 100644 --- a/docs/releasenotes/8.3.2.rst +++ b/docs/releasenotes/8.3.2.rst @@ -20,7 +20,7 @@ bytes off the end of the allocated buffer from the heap. Present since Pillow 7. This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs. -Other Changes +Other changes ============= Python 3.10 wheels diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index bdc8e8020..3bdf77d56 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -13,7 +13,7 @@ Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular length default, and the size parameter could be used to override that. Pillow 8.3.0 removed the default required length, also removing the need for the size parameter. -API Additions +API additions ============= Added "transparency" argument for loading EPS images @@ -33,7 +33,7 @@ Added WalImageFile class :py:class:`PIL.Image.Image` instance. It now returns a dedicated :py:class:`PIL.WalImageFile.WalImageFile` class. -Other Changes +Other changes ============= Speed improvement when rotating square images diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index fee66b6d0..660e5514c 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -59,7 +59,7 @@ initializing ``ImagePath.Path``. .. _OSS-Fuzz: https://github.com/google/oss-fuzz -Backwards Incompatible Changes +Backwards incompatible changes ============================== Python 3.6 @@ -102,7 +102,7 @@ ImageFile.raise_ioerror has been removed. Use ``ImageFile.raise_oserror`` instead. -API Changes +API changes =========== Added line width parameter to ImageDraw polygon @@ -111,7 +111,7 @@ Added line width parameter to ImageDraw polygon An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``. -API Additions +API additions ============= ImageShow.XDGViewer @@ -132,7 +132,7 @@ Support has been added for the "title" argument in argument will also now be supported, e.g. ``im.show(title="My Image")`` and ``ImageShow.show(im, title="My Image")``. -Other Changes +Other changes ============= Convert subsequent GIF frames to RGB or RGBA diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst index f65e3bcc2..5326afe78 100644 --- a/docs/releasenotes/9.0.1.rst +++ b/docs/releasenotes/9.0.1.rst @@ -21,7 +21,7 @@ While Pillow 9.0 restricted top-level builtins available to :py:meth:`!PIL.ImageMath.eval`, it did not prevent builtins available to lambda expressions. These are now also restricted. -Other Changes +Other changes ============= Pillow 9.0 added support for ``xdg-open`` as an image viewer, but there have been diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 5b83d1e9c..72749ce8c 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -94,7 +94,7 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. -API Changes +API changes =========== Raise an error when performing a negative crop @@ -137,7 +137,7 @@ On macOS, the last argument may need to be wrapped in quotes, e.g. Therefore ``requirements.txt`` has been removed along with the ``make install-req`` command for installing its contents. -API Additions +API additions ============= Added get_photoshop_blocks() to parse Photoshop TIFF tag @@ -193,7 +193,7 @@ palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY -Other Changes +Other changes ============= musllinux wheels diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6e0647343..a3c9800b6 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -126,7 +126,7 @@ Use instead:: draw = ImageDraw.Draw(im) draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm") -API Additions +API additions ============= Image.apply_transparency @@ -137,7 +137,7 @@ with "transparency" in ``im.info``, and apply the transparency to the palette in The image's palette mode will become "RGBA", and "transparency" will be removed from ``im.info``. -Other Changes +Other changes ============= Using gnome-screenshot on Linux diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index e5987ce08..bb1e731fd 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -28,7 +28,7 @@ This was introduced in Pillow 9.2.0, found with `OSS-Fuzz`_ and fixed by limitin ``SAMPLESPERPIXEL`` to the number of planes that we can decode. -API Additions +API additions ============= Allow default ImageDraw font to be set @@ -65,7 +65,7 @@ The data from :py:data:`~PIL.ExifTags.TAGS` and :py:data:`~PIL.ExifTags.GPS`. -Other Changes +Other changes ============= Python 3.11 wheels diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 37f26a22c..3b202157d 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -20,7 +20,7 @@ Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a crash. An error is now raised instead. This has been present since Pillow 8.0.0. -API Additions +API additions ============= Added start position for getmask and getmask2 @@ -88,7 +88,7 @@ When saving a JPEG image, a comment can now be written from im.save(out, comment="Test comment") -Other Changes +Other changes ============= Added support for DDS L and LA images diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 501479bb6..6bf2079c8 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -37,7 +37,7 @@ be removed in Pillow 11 (2024-10-15). This class was only made as a helper to be used internally, so there is no replacement. If you need this functionality though, it is a very short class that can easily be recreated in your own code. -API Additions +API additions ============= QOI file format @@ -71,7 +71,7 @@ If OpenJPEG 2.4.0 or later is available and the ``plt`` keyword argument is present and true when saving JPEG2000 images, tell the encoder to generate PLT markers. -Other Changes +Other changes ============= Added support for saving PDFs in RGBA mode diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index a116ef056..5d7b21d59 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -1,4 +1,4 @@ -Release Notes +Release notes ============= Pillow is released quarterly on January 2nd, April 1st, July 1st and October 15th. diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index cfc7221a3..a453d2a43 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -14,7 +14,7 @@ TODO TODO -Backwards Incompatible Changes +Backwards incompatible changes ============================== TODO @@ -28,7 +28,7 @@ TODO TODO -API Changes +API changes =========== TODO @@ -36,7 +36,7 @@ TODO TODO -API Additions +API additions ============= TODO @@ -44,7 +44,7 @@ TODO TODO -Other Changes +Other changes ============= TODO From 58e48745cc7b6c6f7dd26a50fe68d1a82ea51562 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:14:08 +1000 Subject: [PATCH 482/628] Add list of third-party plugins (#8910) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/handbook/appendices.rst | 1 + docs/handbook/third-party-plugins.rst | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 docs/handbook/third-party-plugins.rst diff --git a/docs/handbook/appendices.rst b/docs/handbook/appendices.rst index 347a8848b..c20d8bc8b 100644 --- a/docs/handbook/appendices.rst +++ b/docs/handbook/appendices.rst @@ -8,4 +8,5 @@ Appendices image-file-formats text-anchors + third-party-plugins writing-your-own-image-plugin diff --git a/docs/handbook/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst new file mode 100644 index 000000000..a189a5773 --- /dev/null +++ b/docs/handbook/third-party-plugins.rst @@ -0,0 +1,18 @@ +Third-party plugins +=================== + +Pillow uses a plugin model which allows users to add their own +decoders and encoders to the library, without any changes to the library +itself. + +Here is a list of PyPI projects that offer additional plugins: + +* :pypi:`DjvuRleImagePlugin`: Plugin for the DjVu RLE image format as defined in the DjVuLibre docs. +* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library. +* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL. +* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images. +* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implemetation. Python bindings implemented using pybind11. +* :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings. +* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format. +* :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text. +* :pypi:`raw-pillow-opener`: Simple camera raw opener, based on the rawpy library. From 6bf791a3e7b2490bcb34ae9eb44419ee65c3caee Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Apr 2025 10:27:49 +0100 Subject: [PATCH 483/628] Use a named tuple for the packed parameters --- Tests/test_pyarrow.py | 56 ++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index bcdd7ddc9..822cd18ac 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any # undone +from typing import Any, NamedTuple import pytest @@ -151,29 +151,37 @@ def test_lifetime2() -> None: assert isinstance(px[0, 0], int) -UINT_ARR = ( - fl_uint8_4_type, - [1,2,3,4], - 1 +class DataShape(NamedTuple): + dtype: Any + elt: Any + elts_per_pixel: int + + +UINT_ARR = DataShape( + dtype=fl_uint8_4_type, + elt=[1, 2, 3, 4], # array of 4 uint 8 per pixel + elts_per_pixel=1, # only one array per pixel ) -UINT = ( - pyarrow.uint8(), - 3, - 4 + +UINT = DataShape( + dtype=pyarrow.uint8(), + elt=3, # one uint8, + elts_per_pixel=4, # but repeated 4x per pixel ) -INT32 = ( - pyarrow.uint32(), - 0xabcdef45, - 1 + +UINT32 = DataShape( + dtype=pyarrow.uint32(), + elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000 + elts_per_pixel=1, # one per pixel ) @pytest.mark.parametrize( "mode, data_tp, mask", ( - ("L", (pyarrow.uint8(), 3, 1), None), - ("I", (pyarrow.int32(), 1 << 24, 1), None), - ("F", (pyarrow.float32(), 3.14159, 1), None), + ("L", DataShape(pyarrow.uint8(), 3, 1), None), + ("I", DataShape(pyarrow.int32(), 1 << 24, 1), None), + ("F", DataShape(pyarrow.float32(), 3.14159, 1), None), ("LA", UINT_ARR, [0, 3]), ("LA", UINT, [0, 3]), ("RGB", UINT_ARR, [0, 1, 2]), @@ -188,7 +196,7 @@ INT32 = ( ("HSV", UINT, [0, 1, 2]), ), ) -def test_fromarray(mode: str, data_tp: tuple, mask: list[int] | None) -> None: +def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] @@ -201,15 +209,15 @@ def test_fromarray(mode: str, data_tp: tuple, mask: list[int] | None) -> None: @pytest.mark.parametrize( "mode, data_tp, mask", ( - ("LA", INT32, [0, 3]), - ("RGB", INT32, [0, 1, 2]), - ("RGBA", INT32, None), - ("CMYK", INT32, None), - ("YCbCr", INT32, [0, 1, 2]), - ("HSV", INT32, [0, 1, 2]), + ("LA", UINT32, [0, 3]), + ("RGB", UINT32, [0, 1, 2]), + ("RGBA", UINT32, None), + ("CMYK", UINT32, None), + ("YCbCr", UINT32, [0, 1, 2]), + ("HSV", UINT32, [0, 1, 2]), ), ) -def test_from_int32array(mode: str, data_tp: tuple, mask: list[int] | None) -> None: +def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: (dtype, elt, elts_per_pixel) = data_tp ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1] From ce204f47f45f2ecdc831faac9c1b7ad8192a9fc7 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Apr 2025 10:37:32 +0100 Subject: [PATCH 484/628] lint --- src/libImaging/Storage.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 2c57165c1..1a9171a0c 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -737,7 +737,7 @@ ImagingNewArrow( return im; } } - // Stored as [[r,g,b,a],....] + // Stored as [[r,g,b,a],...] if (strcmp(schema->format, "+w:4") == 0 // 4 up array && im->pixelsize == 4 // storage as 32 bpc && schema->n_children > 0 // make sure schema is well formed. @@ -753,7 +753,7 @@ ImagingNewArrow( return im; } } - // Stored as [r,g,b,a,r,g,b,a....] + // Stored as [r,g,b,a,r,g,b,a,...] if (strcmp(schema->format, "C") == 0 // uint8 && im->pixelsize == 4 // storage as 32 bpc && schema->n_children == 0 // make sure schema is well formed. From bc4b664b7094a311eb516e7eab1b88acf7496b67 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Apr 2025 10:46:45 +0100 Subject: [PATCH 485/628] Add integer range tests --- Tests/test_pyarrow.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 822cd18ac..6eedcafe7 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -153,7 +153,10 @@ def test_lifetime2() -> None: class DataShape(NamedTuple): dtype: Any - elt: Any + elt: Any # Strictly speaking, this should be a pixel or pixel component, + # so list[uint8][4], float, int, uint32, uint8, etc. + # But more correctly, it should be exactly the dtype from the + # line above. elts_per_pixel: int @@ -175,6 +178,12 @@ UINT32 = DataShape( elts_per_pixel=1, # one per pixel ) +INT32 = DataShape( + dtype=pyarrow.uint32(), + elt=0x12CDEF45, # one packed int + elts_per_pixel=1, # one per pixel +) + @pytest.mark.parametrize( "mode, data_tp, mask", @@ -215,6 +224,12 @@ def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> Non ("CMYK", UINT32, None), ("YCbCr", UINT32, [0, 1, 2]), ("HSV", UINT32, [0, 1, 2]), + ("LA", INT32, [0, 3]), + ("RGB", INT32, [0, 1, 2]), + ("RGBA", INT32, None), + ("CMYK", INT32, None), + ("YCbCr", INT32, [0, 1, 2]), + ("HSV", INT32, [0, 1, 2]), ), ) def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None: From 45e24e429f7d443000a6955d228fa00055104414 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 21 Apr 2025 10:54:00 +0100 Subject: [PATCH 486/628] Rearrance so black doesn't screw up the formatting --- Tests/test_pyarrow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 6eedcafe7..e7fce1e33 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -153,10 +153,10 @@ def test_lifetime2() -> None: class DataShape(NamedTuple): dtype: Any - elt: Any # Strictly speaking, this should be a pixel or pixel component, - # so list[uint8][4], float, int, uint32, uint8, etc. - # But more correctly, it should be exactly the dtype from the - # line above. + # Strictly speaking, elt should be a pixel or pixel component, so + # list[uint8][4], float, int, uint32, uint8, etc. But more + # correctly, it should be exactly the dtype from the line above. + elt: Any elts_per_pixel: int From 7a48a9fae083697bb30a6bec42c0c73399f3979a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Apr 2025 20:34:53 +1000 Subject: [PATCH 487/628] Do not load image more than once --- src/PIL/IptcImagePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 60ab7c83f..4336b8154 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -179,6 +179,7 @@ class IptcImageFile(ImageFile.ImageFile): with Image.open(o) as _im: _im.load() self.im = _im.im + self.tile = [] return None From 1e365d8c7282f77f5959ba9b772c5fd1b9aa4315 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Apr 2025 21:10:54 +1000 Subject: [PATCH 488/628] Return PixelAccess on first load --- Tests/test_file_ico.py | 1 + Tests/test_file_iptc.py | 3 +++ src/PIL/IcoImagePlugin.py | 2 +- src/PIL/IptcImagePlugin.py | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 5d2ace35e..0eef7c07a 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -99,6 +99,7 @@ def test_getpixel(tmp_path: Path) -> None: reloaded.load() reloaded.size = (32, 32) + assert reloaded.load() is not None assert reloaded.getpixel((0, 0)) == (18, 20, 62) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index c6c0c1aab..424820ce4 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -23,6 +23,9 @@ def test_open() -> None: assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] assert_image_equal(im, expected) + with Image.open(f) as im: + assert im.load() is not None + def test_getiptcinfo_jpg_none() -> None: # Arrange diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 55c57f203..bd35ac890 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -362,7 +362,7 @@ class IcoImageFile(ImageFile.ImageFile): self.info["sizes"] = set(sizes) self.size = im.size - return None + return Image.Image.load(self) def load_seek(self, pos: int) -> None: # Flag the ImageFile.Parser so that it diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 4336b8154..637f67810 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -180,7 +180,7 @@ class IptcImageFile(ImageFile.ImageFile): _im.load() self.im = _im.im self.tile = [] - return None + return Image.Image.load(self) Image.register_open(IptcImageFile.format, IptcImageFile) From 225182414c3c5cccc2fa42b3dd47147ef06790f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Apr 2025 17:14:13 +1000 Subject: [PATCH 489/628] libavif below 1.0 is not supported --- src/PIL/AvifImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py index b2c5ab15d..366e0c864 100644 --- a/src/PIL/AvifImagePlugin.py +++ b/src/PIL/AvifImagePlugin.py @@ -16,7 +16,6 @@ except ImportError: # Decoder options as module globals, until there is a way to pass parameters # to Image.open (see https://github.com/python-pillow/Pillow/issues/569) DECODE_CODEC_CHOICE = "auto" -# Decoding is only affected by this for libavif **0.8.4** or greater. DEFAULT_MAX_THREADS = 0 From 4c2227758ec7bda10213220dc022e0e105533391 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 07:09:53 -0400 Subject: [PATCH 490/628] Add template for quarterly release issue --- .github/ISSUE_TEMPLATE/RELEASE.md | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/RELEASE.md diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md new file mode 100644 index 000000000..db4c94a09 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -0,0 +1,45 @@ +--- +name: Release +about: Schedule a release +--- + +## Main Release + +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. + * [ ] Add release notes for 11.2.1 https://github.com/python-pillow/Pillow/pull/8885 +* [ ] 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` +* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. +* [ ] Create branch and tag for release e.g.: + ```bash + git branch [[MAJOR.MINOR.PATCH]] + git tag [[MAJOR.MINOR.PATCH]] + git push --tags + ``` +* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag. +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases). +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: + ```bash + git push --all + ``` + +## Publicize Release + +* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 + +## Documentation + +* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes + +## Docker Images + +* [ ] Update Pillow in the Docker Images repository + ```bash + git clone https://github.com/python-pillow/docker-images + cd docker-images + ./update-pillow-tag.sh [[release tag]] + ``` From da9d5522f7c7cd96d99a25a580ac6488f63bedaf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:35:08 +1000 Subject: [PATCH 491/628] Update dependency cibuildwheel to v2.23.3 (#8931) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index db5f89c9a..0e314b8bf 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.23.2 +cibuildwheel==2.23.3 From 8ab3bc469ec4ebcc7e5bc5a848fd59bd4a9a8221 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 08:21:48 -0400 Subject: [PATCH 492/628] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index db4c94a09..c5bae8b2a 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.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. - * [ ] Add release notes for 11.2.1 https://github.com/python-pillow/Pillow/pull/8885 + * [ ] Add release notes e.g. https://github.com/python-pillow/Pillow/pull/8885 * [ ] 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` From 0205fb4fa2e9905dcec12715d60b5f4a75e30266 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 08:21:57 -0400 Subject: [PATCH 493/628] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index c5bae8b2a..72499e01d 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -16,8 +16,8 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: ```bash - git branch [[MAJOR.MINOR.PATCH]] - git tag [[MAJOR.MINOR.PATCH]] + git branch [[MAJOR.MINOR]].0 + git tag [[MAJOR.MINOR]].0 git push --tags ``` * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag. From 6f672191ad83b522c47e1facdeb12889f91a5824 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 27 Apr 2025 22:30:35 +1000 Subject: [PATCH 494/628] Branch uses .x --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index 72499e01d..5dd070886 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -16,7 +16,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: ```bash - git branch [[MAJOR.MINOR]].0 + git branch [[MAJOR.MINOR]].x git tag [[MAJOR.MINOR]].0 git push --tags ``` From 6881863eab8106a6a0e3cb788f11c7d0d71cf124 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:37:42 -0400 Subject: [PATCH 495/628] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index 5dd070886..97701f30e 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -1,6 +1,7 @@ --- -name: Release -about: Schedule a release +name: "Maintainers only: Release" +about: For maintainers to schedule a quarterly release +labels: Release --- ## Main Release From fcaffa22293c41851e9e28b3229b981398deacda Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:37:50 -0400 Subject: [PATCH 496/628] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index 97701f30e..d7b246378 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -4,7 +4,7 @@ about: For maintainers to schedule a quarterly release labels: Release --- -## Main Release +## Main release Released quarterly on January 2nd, April 1st, July 1st and October 15th. From 1eba198b62d1444e72f598db805121022a19ab04 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:37:56 -0400 Subject: [PATCH 497/628] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index d7b246378..16bf30fdc 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -28,7 +28,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. git push --all ``` -## Publicize Release +## Publicize release * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 From e6ff42303b54d16e213b2152984a39f07742fcaf Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:38:02 -0400 Subject: [PATCH 498/628] Update .github/ISSUE_TEMPLATE/RELEASE.md Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/RELEASE.md b/.github/ISSUE_TEMPLATE/RELEASE.md index 16bf30fdc..20d1ac79b 100644 --- a/.github/ISSUE_TEMPLATE/RELEASE.md +++ b/.github/ISSUE_TEMPLATE/RELEASE.md @@ -36,7 +36,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes -## Docker Images +## Docker images * [ ] Update Pillow in the Docker Images repository ```bash From e140027262b95d26080b5d58e095d2e294609e69 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Sun, 27 Apr 2025 15:45:40 -0400 Subject: [PATCH 499/628] Move checklist to issue template --- RELEASING.md | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 932beb2c2..b626d7d23 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,27 +7,8 @@ information about how the version numbers line up with releases. 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) 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` -* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. -* [ ] Create branch and tag for release e.g.: - ```bash - git branch 5.2.x - git tag 5.2.0 - git push --tags - ``` -* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) - has passed, including the "Upload release to PyPI" job. This will have been triggered - by the new tag. -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases). -* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), - increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: - ```bash - git push --all - ``` +* [ ] Create a new issue and select the "Release" template. + ## Point Release Released as needed for security, installation or critical bug fixes. From f1d5cdaa07fbf4377d3cf4c68377e64998619b60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Apr 2025 06:17:47 +1000 Subject: [PATCH 500/628] Use sentence case --- README.md | 4 ++-- RELEASING.md | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1cae558ad..365d356a0 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ This library provides extensive file format support, an efficient internal repre The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. -## More Information +## More information - [Documentation](https://pillow.readthedocs.io/) - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) @@ -107,6 +107,6 @@ The core image library is designed for fast access to data stored in a few basic - [Changelog](https://github.com/python-pillow/Pillow/releases) - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) -## Report a Vulnerability +## Report a vulnerability To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). diff --git a/RELEASING.md b/RELEASING.md index b626d7d23..c160e96f5 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,15 +1,15 @@ -# Release Checklist +# Release checklist See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for information about how the version numbers line up with releases. -## Main Release +## Main release Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Create a new issue and select the "Release" template. -## Point Release +## Point release Released as needed for security, installation or critical bug fixes. @@ -39,7 +39,7 @@ Released as needed for security, installation or critical bug fixes. git push ``` -## Embargoed Release +## Embargoed release Released as needed privately to individual vendors for critical security-related bug fixes. @@ -63,7 +63,7 @@ Released as needed privately to individual vendors for critical security-related git push origin 2.5.x ``` -## Publicize Release +## Publicize release * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 @@ -71,7 +71,7 @@ Released as needed privately to individual vendors for critical security-related * [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes -## Docker Images +## Docker images * [ ] Update Pillow in the Docker Images repository ```bash From dbe538a1307dc14c3ecb685819f28f22c02060ab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Apr 2025 06:19:18 +1000 Subject: [PATCH 501/628] Updated template name --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index c160e96f5..3c6188c82 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,7 +7,7 @@ information about how the version numbers line up with releases. Released quarterly on January 2nd, April 1st, July 1st and October 15th. -* [ ] Create a new issue and select the "Release" template. +* [ ] Create a new issue and select the "Maintainers only: Release" template. ## Point release From 47bebfc801c3905979715d0d22a9560c395581e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Apr 2025 14:57:10 +1000 Subject: [PATCH 502/628] Allow loading state from Pillow < 11.2.1 --- Tests/test_pickle.py | 10 ++++++++++ src/PIL/ImageFile.py | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 1c48cb743..54cef00ad 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -162,3 +162,13 @@ def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Assert helper_assert_pickled_font_images(font, unpickled_font) + + +def test_load_earlier_data() -> None: + im = pickle.loads( + b"\x80\x04\x95@\x00\x00\x00\x00\x00\x00\x00\x8c\x12PIL.PngImagePlugin" + b"\x94\x8c\x0cPngImageFile\x94\x93\x94)\x81\x94]\x94(}\x94\x8c\x01L\x94K\x01" + b"K\x01\x86\x94NC\x01\x00\x94eb." + ) + assert im.mode == "L" + assert im.size == (1, 1) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index bcb7d462e..bf556a2c6 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -257,7 +257,8 @@ class ImageFile(Image.Image): def __setstate__(self, state: list[Any]) -> None: self.tile = [] - self.filename = state[5] + if len(state) > 5: + self.filename = state[5] super().__setstate__(state) def verify(self) -> None: From 07df26aa5d2cf0937737c787bc88ff05454e53d2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:37:45 +0300 Subject: [PATCH 503/628] Refactor docs `Makefile` (#8933) Co-authored-by: Andrew Murray --- docs/Makefile | 42 ++++++++++++++++++------------------------ docs/make.bat | 2 -- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 1e6c06ede..8c1019294 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,17 +3,23 @@ # You can set these variables from the command line. PYTHON = python3 -SPHINXOPTS = SPHINXBUILD = $(PYTHON) -m sphinx.cmd.build -PAPER = +SPHINXOPTS = --fail-on-warning BUILDDIR = _build +BUILDER = html +JOBS = auto +PAPER = # Internal variables. PAPEROPT_a4 = --define latex_paper_size=a4 PAPEROPT_letter = --define latex_paper_size=letter -ALLSPHINXOPTS = --doctree-dir $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +ALLSPHINXOPTS = --builder $(BUILDER) \ + --doctree-dir $(BUILDDIR)/doctrees \ + --jobs $(JOBS) \ + $(PAPEROPT_$(PAPER)) \ + $(SPHINXOPTS) \ + . $(BUILDDIR)/$(BUILDER) .PHONY: help help: @@ -36,31 +42,19 @@ install-sphinx: .PHONY: html html: $(MAKE) install-sphinx - $(SPHINXBUILD) --builder html --fail-on-warning --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + $(SPHINXBUILD) $(ALLSPHINXOPTS) .PHONY: dirhtml -dirhtml: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." +dirhtml: BUILDER = dirhtml +dirhtml: html .PHONY: singlehtml -singlehtml: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." +singlehtml: BUILDER = singlehtml +singlehtml: html .PHONY: linkcheck -linkcheck: - $(MAKE) install-sphinx - $(SPHINXBUILD) --builder linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." +linkcheck: BUILDER = linkcheck +linkcheck: html .PHONY: htmlview htmlview: html diff --git a/docs/make.bat b/docs/make.bat index 4126f786b..9d15537fb 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -7,10 +7,8 @@ if "%SPHINXBUILD%" == "" ( ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help From 2245fd09de9c358bce2cb7f6f49b3f9710229489 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 30 Apr 2025 07:54:07 +1000 Subject: [PATCH 504/628] Updated Ghostscript to 10.5.1 --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bf8ec2f2c..abbfd95c8 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -98,8 +98,8 @@ jobs: choco install nasm --no-progress echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.5.0 --no-progress - echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.5.1 --no-progress + echo "C:\Program Files\gs\gs10.05.1\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From 0e292a80c8bed0f98cd56141431632a9433c5274 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 May 2025 00:52:35 +1000 Subject: [PATCH 505/628] Restore original encoderinfo after saving --- Tests/test_file_mpo.py | 3 +++ src/PIL/Image.py | 8 +++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 73838ef44..716422537 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -315,6 +315,9 @@ def test_save_xmp() -> None: im2.encoderinfo = {"xmp": b"Second frame"} im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) + # Test that encoderinfo is unchanged + assert im2.encoderinfo == {"xmp": b"Second frame"} + assert im_reloaded.info["xmp"] == b"First frame" im_reloaded.seek(1) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ded40bc5d..02627c37a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2551,7 +2551,8 @@ class Image: self.load() save_all = params.pop("save_all", None) - self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} + encoderinfo = getattr(self, "encoderinfo", {}) + self.encoderinfo = {**encoderinfo, **params} self.encoderconfig: tuple[Any, ...] = () if format.upper() not in SAVE: @@ -2589,10 +2590,7 @@ class Image: pass raise finally: - try: - del self.encoderinfo - except AttributeError: - pass + self.encoderinfo = encoderinfo if open_fp: fp.close() From 4d56b90f38eda564ce8913bdc9b5222c3407652f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 May 2025 07:12:20 +1000 Subject: [PATCH 506/628] Updated docstring --- src/PIL/DdsImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 26307817c..f9ade18f9 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -1,5 +1,5 @@ """ -A Pillow loader for .dds files (S3TC-compressed aka DXTC) +A Pillow plugin for .dds files (S3TC-compressed aka DXTC) Jerome Leclanche Documentation: From d02f7868732cdacc227dd794f6ff84bd6f1858c9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 19:16:40 +1000 Subject: [PATCH 507/628] [pre-commit.ci] pre-commit autoupdate (#8944) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/zizmor.yml | 7 +++++++ .pre-commit-config.yaml | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .github/zizmor.yml diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000..5bdc48c30 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,7 @@ +# Configuration for the zizmor static analysis tool, run via pre-commit in CI +# https://woodruffw.github.io/zizmor/configuration/ +rules: + unpinned-uses: + config: + policies: + "*": ref-pin diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 140ce33be..e15e6f639 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.11.4 + rev: v0.11.8 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: v20.1.0 + rev: v20.1.3 hooks: - id: clang-format types: [c] @@ -51,14 +51,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.32.1 + rev: 0.33.0 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.5.2 + rev: v1.6.0 hooks: - id: zizmor From c7193f74fc5ce1a0fe1742a0845165024be45ef5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 May 2025 20:10:34 +1000 Subject: [PATCH 508/628] Updated error message --- Tests/test_image_resample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index ce6209c0d..73b25ed51 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -462,7 +462,7 @@ class TestCoreResampleBox: im.resize((32, 32), resample, (20, 20, 20, 100)) im.resize((32, 32), resample, (20, 20, 100, 20)) - with pytest.raises(TypeError, match="must be sequence of length 4"): + with pytest.raises(TypeError, match="must be (sequence|tuple) of length 4"): im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type] with pytest.raises(ValueError, match="can't be negative"): From 71a916ad53502ed8cb8ea71c40081c169c3eae0f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 May 2025 22:10:22 +1000 Subject: [PATCH 509/628] Do not install PyQt6 on Python 3.14 --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bf8ec2f2c..12f06ee03 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -84,7 +84,7 @@ jobs: python3 -m pip install --upgrade pip - name: Install CPython dependencies - if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" + if: "!contains(matrix.python-version, 'pypy') && !contains(matrix.python-version, '3.14') && matrix.architecture != 'x86'" run: | python3 -m pip install PyQt6 From 215069af5ddec6f4d3b92b8bc7554a10e2efb669 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 May 2025 22:13:13 +1000 Subject: [PATCH 510/628] Added support for Python 3.14 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5ecd6b816..5d41e27d9 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ WEBP_ROOT = None ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 14): +if sys.platform == "win32" and sys.version_info >= (3, 15): import atexit atexit.register( From 78887f6114e68d4208a6d5e8f3d5134a6da6831a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 May 2025 23:52:18 +1000 Subject: [PATCH 511/628] Corrected comment --- Tests/test_pyarrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index e7fce1e33..c5872231b 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -162,7 +162,7 @@ class DataShape(NamedTuple): UINT_ARR = DataShape( dtype=fl_uint8_4_type, - elt=[1, 2, 3, 4], # array of 4 uint 8 per pixel + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel elts_per_pixel=1, # only one array per pixel ) From 74ab5ac4cda564714545eee52ab789d4bddf1516 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Sun, 11 May 2025 23:46:21 +0200 Subject: [PATCH 512/628] Fix memory leak in arrow export using array structure --- src/libImaging/Arrow.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 33ff2ce77..36f4554fc 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -127,9 +127,7 @@ static void release_const_array(struct ArrowArray *array) { Imaging im = (Imaging)array->private_data; - if (array->n_children == 0) { - ImagingDelete(im); - } + ImagingDelete(im); // Free the buffers and the buffers array if (array->buffers) { From c64a7b50983d64b15bc8551315e28f4ac0cd8e84 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 May 2025 07:41:00 +1000 Subject: [PATCH 513/628] Updated harfbuzz to 11.2.1 --- .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 a4592871f..a6b52064c 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=11.1.0 +HARFBUZZ_VERSION=11.2.1 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 17fc37572..fbe479291 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": "11.1.0", + "HARFBUZZ": "11.2.1", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", "LIBAVIF": "1.2.1", From 4984c45da2f6b854cb49dc81fc56372f335d43a0 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:27:38 +0200 Subject: [PATCH 514/628] valgrind memory leak check --- Makefile | 6 ++++++ Tests/oss-fuzz/python.supp | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Makefile b/Makefile index 53164b08a..fd124d124 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,12 @@ valgrind: --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output +.PHONY: valgrind-leak +valgrind-leak: + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ + .PHONY: readme readme: python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 36385d672..1ea2a8eb5 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -14,3 +14,23 @@ fun:_TIFFReadEncodedTileAndAllocBuffer ... } + +{ + + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:_PyMem_RawMalloc + fun:PyObject_Malloc + ... +} + +{ + + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:_PyMem_RawRealloc + fun:PyMem_Realloc + ... +} From fdfba982c8d514240435f3ecef540939fb97f120 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:28:09 +0200 Subject: [PATCH 515/628] fix memory leak in arrow schema --- src/libImaging/Arrow.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 36f4554fc..7d3306123 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -37,6 +37,10 @@ ReleaseExportedSchema(struct ArrowSchema *array) { child->release = NULL; } // UNDONE -- should I be releasing the children? + free(array->children[i]); + } + if (array->children) { + free(array->children); } // Release dictionary @@ -117,6 +121,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); if (retval != 0) { free(schema->children[0]); + free(schema->children); schema->release(schema); return retval; } From 84b88a9fbc9c4cfd2bfb7d7a8d18225ee43efedb Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:58:12 +0200 Subject: [PATCH 516/628] Suppress all python level leaks for now --- Tests/oss-fuzz/python.supp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 1ea2a8eb5..4803497ad 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -18,7 +18,7 @@ { Memcheck:Leak - match-leak-kinds: possible + match-leak-kinds: all fun:malloc fun:_PyMem_RawMalloc fun:PyObject_Malloc @@ -28,7 +28,7 @@ { Memcheck:Leak - match-leak-kinds: possible + match-leak-kinds: all fun:malloc fun:_PyMem_RawRealloc fun:PyMem_Realloc From eaab43540344e26889116262651001f4e42b1630 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:58:37 +0200 Subject: [PATCH 517/628] Fix leak in webp_encode * Free the output buffer on webp encode error --- src/_webp.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 3aa4c408b..a7809c40e 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -641,6 +641,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ImagingSectionLeave(&cookie); WebPPictureFree(&pic); + + output = writer.mem; + ret_size = writer.size; + if (!ok) { int error_code = (&pic)->error_code; char message[50] = ""; @@ -652,10 +656,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); + free(output); return NULL; } - output = writer.mem; - ret_size = writer.size; { /* I want to truncate the *_size items that get passed into WebP From a9bcd7db884d89bbfe1966c0611f7f3dda1f8f08 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 19:50:55 +0200 Subject: [PATCH 518/628] Fix leak of destination image in ImagingUnsharpMask when an error occurs --- src/_imaging.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_imaging.c b/src/_imaging.c index 72f122143..79e0a2b23 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2226,6 +2226,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) { } if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { + ImagingDelete(imOut); return NULL; } From e2e40c54568236d2504921eb0b335cdab734a7d5 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 22:33:27 +0200 Subject: [PATCH 519/628] Fix memory leak in TiffEncode * If setimage errors out, the tiff client state was not freed. --- src/encode.c | 2 ++ src/libImaging/TiffDecode.c | 42 ++++++++++++++++++------------------- src/libImaging/TiffDecode.h | 2 ++ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/encode.c b/src/encode.c index 7c365a74f..e56494036 100644 --- a/src/encode.c +++ b/src/encode.c @@ -703,6 +703,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { return NULL; } + encoder->cleanup = ImagingLibTiffEncodeCleanup; + num_core_tags = sizeof(core_tags) / sizeof(int); for (pos = 0; pos < tags_size; pos++) { item = PyList_GetItemRef(tags, pos); diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 9a2db95b4..173eca160 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -929,6 +929,27 @@ ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) { return status; } +int +ImagingLibTiffEncodeCleanup(ImagingCodecState state) { + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + TIFF *tiff = clientstate->tiff; + + if (!tiff) { + return 0; + } + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup + if (clientstate->fp) { + // Python will manage the closing of the file rather than libtiff + // So only call TIFFCleanup + TIFFCleanup(tiff); + } else { + // When tif_closeproc refers to our custom _tiffCloseProc though, + // that is fine, as it does not close the file + TIFFClose(tiff); + } + return 0; +} + int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) { /* One shot encoder. Encode everything to the tiff in the clientstate. @@ -1010,16 +1031,6 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup - if (clientstate->fp) { - // Python will manage the closing of the file rather than libtiff - // So only call TIFFCleanup - TIFFCleanup(tiff); - } else { - // When tif_closeproc refers to our custom _tiffCloseProc though, - // that is fine, as it does not close the file - TIFFClose(tiff); - } if (!clientstate->fp) { free(clientstate->data); } @@ -1036,22 +1047,11 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Error flushing the tiff")); // likely reason is memory. state->errcode = IMAGING_CODEC_MEMORY; - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } if (!clientstate->fp) { free(clientstate->data); } return -1; } - TRACE(("Closing \n")); - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } // reset the clientstate metadata to use it to read out the buffer. clientstate->loc = 0; clientstate->size = clientstate->eof; // redundant? diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 22361210d..77808b543 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -40,6 +40,8 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int +ImagingLibTiffEncodeCleanup(ImagingCodecState state); +extern int ImagingLibTiffMergeFieldInfo( ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length ); From f792e0b1ef4f25e0df33e8e971056142f9f5248d Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 22:48:36 +0200 Subject: [PATCH 520/628] Fix memory leak * Return after setting the error for advanced features without libraqm. Not returning here leads to an alloc that's never freed. --- src/_imagingft.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index 62dab73e5..ca8e556f0 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -425,6 +425,7 @@ text_layout_fallback( "setting text direction, language or font features is not supported " "without libraqm" ); + return 0; } if (PyUnicode_Check(string)) { From 789631c60c3760beeb623cd1728a737502fd9ca3 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 23:31:09 +0200 Subject: [PATCH 521/628] Fix memory leak when JpegEncode returns an error. --- src/libImaging/JpegEncode.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 3c11eac22..79a38e12f 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -131,6 +131,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { break; default: state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } @@ -161,6 +162,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Would subsample the green and blue channels, which doesn't make sense */ state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } jpeg_set_colorspace(&context->cinfo, JCS_RGB); From 7aa6a61d430c585cd10c91c5a73ce13f9f851b9e Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 23:50:52 +0200 Subject: [PATCH 522/628] Wrap Makefile --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fd124d124..15f03ba45 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,8 @@ valgrind: .PHONY: valgrind-leak valgrind-leak: - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ + --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ From efa2288643a3d2840b573d14b1aec41f6fd2b80c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 May 2025 08:38:33 +1000 Subject: [PATCH 523/628] Updated libavif to 1.3.0 --- Tests/test_file_avif.py | 2 +- depends/install_libavif.sh | 2 +- winbuild/build_prepare.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index bd87947c0..b2e586637 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -233,7 +233,7 @@ class TestFileAvif: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) - assert difference <= 3 + assert difference <= 6 def test_save_single_frame(self, tmp_path: Path) -> None: temp_file = tmp_path / "temp.avif" diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index fc10d3e54..26af8a36c 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eo pipefail -version=1.2.1 +version=1.3.0 ./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 17fc37572..9fee5bd90 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ V = { "HARFBUZZ": "11.1.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", - "LIBAVIF": "1.2.1", + "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", "LIBPNG": "1.6.47", "LIBWEBP": "1.5.0", @@ -399,7 +399,6 @@ DEPS: dict[str, dict[str, Any]] = { "-DAVIF_CODEC_DAV1D=LOCAL", "-DAVIF_CODEC_RAV1E=LOCAL", "-DAVIF_CODEC_SVT=LOCAL", - "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", ), cmd_xcopy("include", "{inc_dir}"), ], From fb126af7a6a12e0870e56187257f75f35fe8558b Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 21:10:48 +0200 Subject: [PATCH 524/628] Adding pytest-valgrind install --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 15f03ba45..bdddecda5 100644 --- a/Makefile +++ b/Makefile @@ -101,6 +101,7 @@ valgrind: .PHONY: valgrind-leak valgrind-leak: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ From d5449d576013566100d8a0d41bbc1a756df86da5 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 21:11:31 +0200 Subject: [PATCH 525/628] Guess so. --- src/libImaging/Arrow.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 7d3306123..0b8c89a07 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -36,7 +36,6 @@ ReleaseExportedSchema(struct ArrowSchema *array) { child->release(child); child->release = NULL; } - // UNDONE -- should I be releasing the children? free(array->children[i]); } if (array->children) { From 218f055865a4f0abd05ac221c48cf86127907ca9 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 21:59:02 +0200 Subject: [PATCH 526/628] Add github workflow/test-script --- .github/workflows/test-valgrind-memory.yml | 60 ++++++++++++++++++++++ depends/docker-test-valgrind-memory.sh | 11 ++++ 2 files changed, 71 insertions(+) create mode 100644 .github/workflows/test-valgrind-memory.yml create mode 100644 depends/docker-test-valgrind-memory.sh diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml new file mode 100644 index 000000000..e6a5f6e77 --- /dev/null +++ b/.github/workflows/test-valgrind-memory.yml @@ -0,0 +1,60 @@ +name: Test Valgrind Memory Leaks + +# like the Docker tests, but running valgrind only on *.c/*.h changes. + +# this is very expensive. Only run on the pull request. +on: + # push: + # branches: + # - "**" + # paths: + # - ".github/workflows/test-valgrind.yml" + # - "**.c" + # - "**.h" + pull_request: + paths: + - ".github/workflows/test-valgrind.yml" + - "**.c" + - "**.h" + - "depends/docker-test-valgrind-memory.sh" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + docker: [ + ubuntu-22.04-jammy-amd64-valgrind, + ] + dockerTag: [main] + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Docker pull + run: | + docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Build and Run Valgrind + run: | + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE + docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh + sudo chown -R runner $GITHUB_WORKSPACE diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh new file mode 100644 index 000000000..4fd6652d8 --- /dev/null +++ b/depends/docker-test-valgrind-memory.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +## Run this as the test script in the docker valgrind image. +## Note -- can be included directly into the docker image, +## but requires the currnet python.supp. + +source /vpy3/bin/activate +cd /Pillow +make clean +make install +make valgrind-memory From a6b8b3af7709081d8c53818e68b8bc15e9a48f34 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 22:04:14 +0200 Subject: [PATCH 527/628] executable --- depends/docker-test-valgrind-memory.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 depends/docker-test-valgrind-memory.sh diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh old mode 100644 new mode 100755 From 2d506f6f5a478b4a798b3ce71f31b5e5f6f6b60f Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 22:06:35 +0200 Subject: [PATCH 528/628] correct target --- depends/docker-test-valgrind-memory.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index 4fd6652d8..29fc6f230 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -8,4 +8,4 @@ source /vpy3/bin/activate cd /Pillow make clean make install -make valgrind-memory +make valgrind-leak From f1957b49b2d01a9d063aed4000f985b220e30fa0 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 16 May 2025 12:08:45 +0200 Subject: [PATCH 529/628] Xfail timouts in Valgrind tests * ensure that the env variable is set in the makefile --- Makefile | 4 ++-- Tests/test_file_jpeg.py | 5 +++++ Tests/test_imagefontpil.py | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bdddecda5..82c2c085f 100644 --- a/Makefile +++ b/Makefile @@ -95,14 +95,14 @@ test: .PHONY: valgrind valgrind: python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output .PHONY: valgrind-leak valgrind-leak: python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ + PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 79f0ec1a8..fb9f26fc7 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1034,6 +1034,11 @@ class TestFileJpeg: im.save(f, xmp=b"1" * 65505) @pytest.mark.timeout(timeout=1) + @pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower", + raises=TimeoutError + ) 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 diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 695aecbde..adce4a75c 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -2,6 +2,7 @@ from __future__ import annotations import struct from io import BytesIO +import os import pytest @@ -73,6 +74,11 @@ def test_decompression_bomb() -> None: @pytest.mark.timeout(4) +@pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower", + raises=TimeoutError +) def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 From ff50e30d3e9f1425ca6af95ac044d365c63719d1 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 16 May 2025 12:47:22 +0200 Subject: [PATCH 530/628] Fix memory leak in text_layout_raqm on 0 length string --- src/_imagingft.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index ca8e556f0..0d70544a5 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -275,6 +275,7 @@ text_layout_raqm( if (!text || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ + PyMem_Free(text); goto failed; } set_text = raqm_set_text(rq, text, size); From 20b49a332bd0f0f39660fbb3587cfc4b6d539f0c Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Sat, 17 May 2025 10:45:43 +0200 Subject: [PATCH 531/628] Remove timeout as the specific reason, pytest-timeout doesn't raise a timeout error. --- Tests/test_file_jpeg.py | 3 +-- Tests/test_imagefontpil.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fb9f26fc7..d923020c8 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1036,8 +1036,7 @@ class TestFileJpeg: @pytest.mark.timeout(timeout=1) @pytest.mark.xfail( "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower", - raises=TimeoutError + reason="Valgrind is slower" ) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index adce4a75c..bd9bafb55 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -76,8 +76,7 @@ def test_decompression_bomb() -> None: @pytest.mark.timeout(4) @pytest.mark.xfail( "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower", - raises=TimeoutError + reason="Valgrind is slower" ) def test_oom() -> None: glyph = struct.pack( From c35082b619899d5351ba249e8ea23a4412d0c728 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 08:47:59 +0000 Subject: [PATCH 532/628] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 3 +-- Tests/test_imagefontpil.py | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index d923020c8..7c33c7517 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1035,8 +1035,7 @@ class TestFileJpeg: @pytest.mark.timeout(timeout=1) @pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index bd9bafb55..e5b770745 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,8 +1,8 @@ from __future__ import annotations +import os import struct from io import BytesIO -import os import pytest @@ -74,10 +74,7 @@ def test_decompression_bomb() -> None: @pytest.mark.timeout(4) -@pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" -) +@pytest.mark.xfail("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 From 45d1c4162b9666281857f3b5d38fdefdbf9b1979 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 May 2025 15:55:43 +1000 Subject: [PATCH 533/628] Do not build against libavif < 1 --- setup.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 5d41e27d9..ab36c6b17 100644 --- a/setup.py +++ b/setup.py @@ -224,13 +224,14 @@ def _add_directory( path.insert(where, subdir) -def _find_include_file(self: pil_build_ext, include: str) -> int: +def _find_include_file(self: pil_build_ext, include: str) -> str | None: for directory in self.compiler.include_dirs: _dbg("Checking for include file %s in %s", (include, directory)) - if os.path.isfile(os.path.join(directory, include)): + path = os.path.join(directory, include) + if os.path.isfile(path): _dbg("Found %s", include) - return 1 - return 0 + return path + return None def _find_library_file(self: pil_build_ext, library: str) -> str | None: @@ -852,9 +853,13 @@ class pil_build_ext(build_ext): if feature.want("avif"): _dbg("Looking for avif") - if _find_include_file(self, "avif/avif.h"): - if _find_library_file(self, "avif"): - feature.set("avif", "avif") + if avif_h := _find_include_file(self, "avif/avif.h"): + with open(avif_h, "rb") as fp: + major_version = int( + fp.read().split(b"#define AVIF_VERSION_MAJOR ")[1].split()[0] + ) + if major_version >= 1 and _find_library_file(self, "avif"): + feature.set("avif", "avif") for f in feature: if not feature.get(f) and feature.require(f): From 7824d2f8c61648c6ea0185b50e55df1a71213168 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 May 2025 08:48:38 +1000 Subject: [PATCH 534/628] Update rust when building libavif --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9fee5bd90..6cdcf6f0d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -389,6 +389,7 @@ DEPS: dict[str, dict[str, Any]] = { "filename": f"libavif-{V['LIBAVIF']}.zip", "license": "LICENSE", "build": [ + "rustup update", f"{sys.executable} -m pip install meson", *cmds_cmake( "avif_static", From 2603a249df9223133b74c671acfcdc6a51567843 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 23 May 2025 10:57:03 +0100 Subject: [PATCH 535/628] Update depends/docker-test-valgrind-memory.sh Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/docker-test-valgrind-memory.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index 29fc6f230..5f7805421 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -2,7 +2,7 @@ ## Run this as the test script in the docker valgrind image. ## Note -- can be included directly into the docker image, -## but requires the currnet python.supp. +## but requires the current python.supp. source /vpy3/bin/activate cd /Pillow From 9526d949b07bbddfc7e515810dc23738b778bee4 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 23 May 2025 10:58:28 +0100 Subject: [PATCH 536/628] Update Tests/test_pyarrow.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_pyarrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index e7fce1e33..c5872231b 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -162,7 +162,7 @@ class DataShape(NamedTuple): UINT_ARR = DataShape( dtype=fl_uint8_4_type, - elt=[1, 2, 3, 4], # array of 4 uint 8 per pixel + elt=[1, 2, 3, 4], # array of 4 uint8 per pixel elts_per_pixel=1, # only one array per pixel ) From 6807bd3d70cc5873b3cad29d598e08a34fdc1fa0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 May 2025 00:03:08 +1000 Subject: [PATCH 537/628] Added type hints --- .ci/requirements-mypy.txt | 1 + Tests/test_pyarrow.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 2e3610478..86ac2e0b2 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -4,6 +4,7 @@ IceSpringPySideStubs-PySide6 ipython numpy packaging +pyarrow-stubs pytest sphinx types-atheris diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index c5872231b..2029f96f5 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -13,7 +13,11 @@ from .helper import ( is_big_endian, ) -pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") +TYPE_CHECKING = False +if TYPE_CHECKING: + import pyarrow +else: + pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") TEST_IMAGE_SIZE = (10, 10) @@ -94,14 +98,14 @@ fl_uint8_4_type = pyarrow.field( ("HSV", fl_uint8_4_type, [0, 1, 2]), ), ) -def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None: +def test_to_array(mode: str, dtype: pyarrow.DataType, mask: list[int] | None) -> None: img = hopper(mode) # Resize to non-square img = img.crop((3, 0, 124, 127)) assert img.size == (121, 127) - arr = pyarrow.array(img) + arr = pyarrow.array(img) # type: ignore[call-overload] _test_img_equals_pyarray(img, arr, mask) assert arr.type == dtype @@ -118,8 +122,8 @@ def test_lifetime() -> None: img = hopper("L") - arr_1 = pyarrow.array(img) - arr_2 = pyarrow.array(img) + arr_1 = pyarrow.array(img) # type: ignore[call-overload] + arr_2 = pyarrow.array(img) # type: ignore[call-overload] del img @@ -136,8 +140,8 @@ def test_lifetime2() -> None: img = hopper("L") - arr_1 = pyarrow.array(img) - arr_2 = pyarrow.array(img) + arr_1 = pyarrow.array(img) # type: ignore[call-overload] + arr_2 = pyarrow.array(img) # type: ignore[call-overload] assert arr_1.sum().as_py() > 0 del arr_1 @@ -152,7 +156,7 @@ def test_lifetime2() -> None: class DataShape(NamedTuple): - dtype: Any + dtype: pyarrow.DataType # Strictly speaking, elt should be a pixel or pixel component, so # list[uint8][4], float, int, uint32, uint8, etc. But more # correctly, it should be exactly the dtype from the line above. From 60a1a20536fe18cfe936e90140ed56c3eb31bd81 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 23 May 2025 15:32:46 +0200 Subject: [PATCH 538/628] add timeouts to two more tests --- Tests/test_file_tiff.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 502d9df9a..050bfe578 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -990,6 +990,10 @@ class TestFileTiff: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") + @pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower" + ) def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) @@ -1002,6 +1006,10 @@ class TestFileTiff: ], ) @pytest.mark.timeout(2) + @pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower" + ) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): From c63db77db3850c51df38af8f4a96f5c13f286b42 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 13:37:02 +0000 Subject: [PATCH 539/628] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_tiff.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 050bfe578..b6985b83b 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -991,8 +991,7 @@ class TestFileTiff: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") @pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: @@ -1007,8 +1006,7 @@ class TestFileTiff: ) @pytest.mark.timeout(2) @pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): From 4d0678ca33b65af2686fe93be5b77c2b28027959 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 23 May 2025 16:35:57 +0200 Subject: [PATCH 540/628] Add parallel test target, using pytest-xdist --- Makefile | 8 +++++++- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5a8152454..a56fe8fec 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,13 @@ sdist: .PHONY: test test: python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq + python3 -m pytest -qq Tests/ + +.PHONY: test-p +test-p: + python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist + python3 -m pytest -qq -n auto Tests/ + .PHONY: valgrind valgrind: diff --git a/pyproject.toml b/pyproject.toml index a3ff9723b..683ab24ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ optional-dependencies.tests = [ "pytest", "pytest-cov", "pytest-timeout", + "pytest-xdist", "trove-classifiers>=2024.10.12", ] From e018dc99faf8d7196ec2a2e7d2760cede851fbd9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 May 2025 08:51:51 +1000 Subject: [PATCH 541/628] Updated libpng to 1.6.48 --- .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 a6b52064c..1583435c1 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=11.2.1 -LIBPNG_VERSION=1.6.47 +LIBPNG_VERSION=1.6.48 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d23f0eb5b..6e176e29c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -118,7 +118,7 @@ V = { "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", - "LIBPNG": "1.6.47", + "LIBPNG": "1.6.48", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", From 4eb89f8e5bcd19ad64ed2328c9566061a7116cc2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 May 2025 20:36:19 +1000 Subject: [PATCH 542/628] Reduced number of bytes read for header --- src/PIL/WmfImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index f709d026b..d569cb4b8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -81,7 +81,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _open(self) -> None: # check placable header - s = self.fp.read(80) + s = self.fp.read(44) if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): # placeable windows metafile From 57b77bde96484d4a1d6f92adec9a2c2b86485f55 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 May 2025 11:55:18 +1000 Subject: [PATCH 543/628] Removed CMAKE_POLICY_VERSION_MINIMUM=3.5 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index d065e7ab5..acb84f046 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -66,7 +66,7 @@ if [[ $(uname) != CYGWIN* ]]; then pushd depends && ./install_raqm.sh && popd # libavif - pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd + pushd depends && ./install_libavif.sh && popd # extra test images pushd depends && ./install_extra_test_images.sh && popd From 041acf13440f9307dcc129eff21bcd065362ae91 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 25 May 2025 15:00:47 +1000 Subject: [PATCH 544/628] Clear core image if memory mapping was used for last load --- Tests/test_tiff_crashes.py | 14 ++++++++++++++ src/PIL/TiffImagePlugin.py | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 073e5415c..976f62384 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -52,3 +52,17 @@ def test_tiff_crashes(test_file: str) -> None: pytest.skip("test image not found") except OSError: pass + + +def test_tiff_mmap() -> None: + try: + with Image.open("Tests/images/crash_mmap.tif") as im: + im.seek(1) + im.load() + + im.seek(0) + im.load() + except FileNotFoundError: + if on_ci(): + raise + pytest.skip("test image not found") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e..5cbac0c26 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1217,9 +1217,10 @@ class TiffImageFile(ImageFile.ImageFile): return self._seek(frame) if self._im is not None and ( - self.im.size != self._tile_size or self.im.mode != self.mode + self.im.size != self._tile_size + or self.im.mode != self.mode + or self.readonly ): - # The core image will no longer be used self._im = None def _seek(self, frame: int) -> None: From eff667a8614ba3b567684597795924387c8cbaaa Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 23 May 2025 10:22:59 +0100 Subject: [PATCH 545/628] Mark the image read-only in the C layer if it's created from a read only buffer --- src/map.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/map.c b/src/map.c index c66702981..9a3144ab9 100644 --- a/src/map.c +++ b/src/map.c @@ -137,6 +137,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { } } + im->read_only = view.readonly; im->destroy = mapping_destroy_buffer; Py_INCREF(target); From 5a04b9581b16a7f1e1109f1e31a206a6550f314c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 28 May 2025 08:20:35 +1000 Subject: [PATCH 546/628] Run slow tests on valgrind, but without timeout (#8975) --- Tests/helper.py | 6 ++++++ Tests/test_file_eps.py | 3 ++- Tests/test_file_fli.py | 9 +++++++-- Tests/test_file_jpeg.py | 3 ++- Tests/test_file_pdf.py | 10 +++++++--- Tests/test_file_tiff.py | 5 +++-- Tests/test_image.py | 6 ++---- Tests/test_imagefontpil.py | 4 ++-- 8 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 909fff879..ec61cd263 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -161,6 +161,12 @@ def assert_tuple_approx_equal( pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) +def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator: + if "PILLOW_VALGRIND_TEST" in os.environ: + return pytest.mark.pil_noop_mark() + return pytest.mark.timeout(timeout) + + def skip_unless_feature(feature: str) -> pytest.MarkDecorator: reason = f"{feature} not available" return pytest.mark.skipif(not features.check(feature), reason=reason) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index b484a8cfa..d94de7287 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -15,6 +15,7 @@ from .helper import ( is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() @@ -398,7 +399,7 @@ def test_emptyline() -> None: assert image.format == "EPS" -@pytest.mark.timeout(timeout=5) +@timeout_unless_slower_valgrind(5) @pytest.mark.parametrize( "test_file", ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 81df1ab0b..0fadd01d0 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -7,7 +7,12 @@ import pytest from PIL import FliImagePlugin, Image, ImageFile -from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + is_pypy, + timeout_unless_slower_valgrind, +) # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. @@ -189,7 +194,7 @@ def test_seek() -> None: "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", ], ) -@pytest.mark.timeout(timeout=3) +@timeout_unless_slower_valgrind(3) def test_timeouts(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 79f0ec1a8..b9eec591d 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -32,6 +32,7 @@ from .helper import ( is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -1033,7 +1034,7 @@ class TestFileJpeg: with pytest.raises(ValueError): im.save(f, xmp=b"1" * 65505) - @pytest.mark.timeout(timeout=1) + @timeout_unless_slower_valgrind(1) 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 diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index bde1e3ab8..a2218673b 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -13,7 +13,12 @@ import pytest from PIL import Image, PdfParser, features -from .helper import hopper, mark_if_feature_version, skip_unless_feature +from .helper import ( + hopper, + mark_if_feature_version, + skip_unless_feature, + timeout_unless_slower_valgrind, +) def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: @@ -339,8 +344,7 @@ def test_pdf_append_to_bytesio() -> None: assert len(f.getvalue()) > initial_size -@pytest.mark.timeout(1) -@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") +@timeout_unless_slower_valgrind(1) @pytest.mark.parametrize("newline", (b"\r", b"\n")) def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 502d9df9a..d0d394aa9 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -26,6 +26,7 @@ from .helper import ( hopper, is_pypy, is_win32, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -988,7 +989,7 @@ class TestFileTiff: with pytest.raises(OSError): im.load() - @pytest.mark.timeout(6) + @timeout_unless_slower_valgrind(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: @@ -1001,7 +1002,7 @@ class TestFileTiff: "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", ], ) - @pytest.mark.timeout(2) + @timeout_unless_slower_valgrind(2) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): diff --git a/Tests/test_image.py b/Tests/test_image.py index 7e6118d52..14a067127 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -34,6 +34,7 @@ from .helper import ( is_win32, mark_if_feature_version, skip_unless_feature, + timeout_unless_slower_valgrind, ) ElementTree: ModuleType | None @@ -572,10 +573,7 @@ class TestImage: i = Image.new("RGB", [1, 1]) assert isinstance(i.size, tuple) - @pytest.mark.timeout(0.75) - @pytest.mark.skipif( - "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" - ) + @timeout_unless_slower_valgrind(0.75) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) def test_empty_image(self, size: tuple[int, int]) -> None: Image.new("RGB", size) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 695aecbde..3eb98d379 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -7,7 +7,7 @@ import pytest from PIL import Image, ImageDraw, ImageFont, _util, features -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal_tofile, timeout_unless_slower_valgrind fonts = [ImageFont.load_default_imagefont()] if not features.check_module("freetype2"): @@ -72,7 +72,7 @@ def test_decompression_bomb() -> None: font.getmask("A" * 1_000_000) -@pytest.mark.timeout(4) +@timeout_unless_slower_valgrind(4) def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 From 5000c83bcc1514d371be53b52b50aaf7237506d5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 May 2025 23:50:18 +1000 Subject: [PATCH 547/628] Use multi-phase initialization --- src/_avif.c | 24 ++++++++++-------------- src/_imaging.c | 25 ++++++++++--------------- src/_imagingcms.c | 26 +++++++++++--------------- src/_imagingft.c | 24 ++++++++++-------------- src/_imagingmath.c | 24 ++++++++++-------------- src/_imagingmorph.c | 19 +++++++++---------- src/_imagingtk.c | 22 ++++++++++------------ src/_webp.c | 24 ++++++++++-------------- 8 files changed, 80 insertions(+), 108 deletions(-) diff --git a/src/_avif.c b/src/_avif.c index 7e7bee703..3585297fe 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -881,26 +881,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__avif(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_avif", - .m_size = -1, .m_methods = avifMethods, + .m_slots = slots }; - m = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imaging.c b/src/_imaging.c index 72f122143..0c93a96bc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4460,27 +4460,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imaging(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imaging", - .m_size = -1, .m_methods = functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index f93c1613b..e2f29d1b7 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1463,28 +1463,24 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingcms(void) { - PyObject *m; + PyDateTime_IMPORT; static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingcms", - .m_size = -1, .m_methods = pyCMSdll_methods, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - - PyDateTime_IMPORT; - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingft.c b/src/_imagingft.c index 62dab73e5..c3e6e2f39 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1599,26 +1599,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingft(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingft", - .m_size = -1, .m_methods = _functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 4b9bf08ba..48c395900 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -302,26 +302,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingmath(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingmath", - .m_size = -1, .m_methods = _functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - - if (setup_module(m) < 0) { - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index a20888294..5995f9d42 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -246,23 +246,22 @@ static PyMethodDef functions[] = { {NULL, NULL, 0, NULL} }; +static PyModuleDef_Slot slots[] = { +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingmorph(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingmorph", .m_doc = "A module for doing image morphology", - .m_size = -1, .m_methods = functions, + .m_slots = slots }; - m = PyModule_Create(&module_def); - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_imagingtk.c b/src/_imagingtk.c index 4e06fe9b8..68d7bf4cd 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -46,24 +46,22 @@ static PyMethodDef functions[] = { {NULL, NULL} /* sentinel */ }; +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, load_tkinter_funcs}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__imagingtk(void) { static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_imagingtk", - .m_size = -1, .m_methods = functions, + .m_slots = slots }; - PyObject *m; - m = PyModule_Create(&module_def); - if (load_tkinter_funcs() != 0) { - Py_DECREF(m); - return NULL; - } -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } diff --git a/src/_webp.c b/src/_webp.c index 3aa4c408b..0dff9f6dd 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -777,26 +777,22 @@ setup_module(PyObject *m) { return 0; } +static PyModuleDef_Slot slots[] = { + {Py_mod_exec, setup_module}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + PyMODINIT_FUNC PyInit__webp(void) { - PyObject *m; - static PyModuleDef module_def = { PyModuleDef_HEAD_INIT, .m_name = "_webp", - .m_size = -1, .m_methods = webpMethods, + .m_slots = slots }; - m = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - Py_DECREF(m); - return NULL; - } - -#ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); -#endif - - return m; + return PyModuleDef_Init(&module_def); } From 2ee2a1496d9d4b2cc1f8455342d0e2f5da8f542c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 May 2025 22:06:27 +1000 Subject: [PATCH 548/628] Simplified code --- src/PIL/ImageGrab.py | 5 +---- src/PIL/JpegImagePlugin.py | 9 +++------ src/libImaging/Arrow.c | 6 +++--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index c29350b7a..d11609483 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -134,10 +134,7 @@ def grabclipboard() -> Image.Image | list[str] | None: import struct o = struct.unpack_from("I", data)[0] - if data[16] != 0: - files = data[o:].decode("utf-16le").split("\0") - else: - files = data[o:].decode("mbcs").split("\0") + files = data[o:].decode("mbcs" if data[16] == 0 else "utf-16le").split("\0") return files[: files.index("")] if isinstance(data, bytes): data = io.BytesIO(data) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 969528841..defe9f773 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -762,8 +762,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") - if xmp: + if xmp := info.get("xmp"): overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len if len(xmp) > max_data_bytes_in_marker: @@ -772,8 +771,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: size = o16(2 + overhead_len + len(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: + if icc_profile := info.get("icc_profile"): overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len markers = [] @@ -831,7 +829,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is # channels*size, this is a value that's been used in a django patch. # https://github.com/matthewwithanm/django-imagekit/issues/50 - bufsize = 0 if optimize or progressive: # CMYK can be bigger if im.mode == "CMYK": @@ -848,7 +845,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: else: # The EXIF info needs to be written as one block, + APP1, + one spare byte. # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) + bufsize = max(len(exif) + 5, len(extra) + 1) ImageFile._save( im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 33ff2ce77..3d34076dc 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -98,7 +98,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { } /* for now, single block images */ - if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + if (im->blocks_count > 1) { return IMAGING_ARROW_MEMORY_LAYOUT; } @@ -157,7 +157,7 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) { int length = im->xsize * im->ysize; /* for now, single block images */ - if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + if (im->blocks_count > 1) { return IMAGING_ARROW_MEMORY_LAYOUT; } @@ -200,7 +200,7 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { int length = im->xsize * im->ysize; /* for now, single block images */ - if (!(im->blocks_count == 0 || im->blocks_count == 1)) { + if (im->blocks_count > 1) { return IMAGING_ARROW_MEMORY_LAYOUT; } From fcac6e78966f7cac4813731dc2303f80f844b320 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 May 2025 18:27:17 +1000 Subject: [PATCH 549/628] Removed hasAlpha argument --- src/libImaging/Draw.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index d5aff8709..046293379 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -439,9 +439,7 @@ draw_horizontal_lines( * Filled polygon draw function using scan line algorithm. */ static inline int -polygon_generic( - Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha -) { +polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline) { Edge **edge_table; float *xx; int edge_count = 0; @@ -461,6 +459,7 @@ polygon_generic( return -1; } + int hasAlpha = hline == hline32rgba; for (i = 0; i < n; i++) { if (ymin > e[i].ymin) { ymin = e[i].ymin; @@ -592,17 +591,17 @@ polygon_generic( static inline int polygon8(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline8, 0); + return polygon_generic(im, n, e, ink, eofill, hline8); } static inline int polygon32(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32, 0); + return polygon_generic(im, n, e, ink, eofill, hline32); } static inline int polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32rgba, 1); + return polygon_generic(im, n, e, ink, eofill, hline32rgba); } static inline void From 62da23bf83a568079cd514cbac6a99bf37009820 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 May 2025 18:22:49 +1000 Subject: [PATCH 550/628] Removed polygon from DRAW struct --- src/libImaging/Draw.c | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 046293379..4c08e9855 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -589,21 +589,6 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h return 0; } -static inline int -polygon8(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline8); -} - -static inline int -polygon32(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32); -} - -static inline int -polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) { - return polygon_generic(im, n, e, ink, eofill, hline32rgba); -} - static inline void add_edge(Edge *e, int x0, int y0, int x1, int y1) { /* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */ @@ -640,12 +625,11 @@ typedef struct { void (*point)(Imaging im, int x, int y, int ink); void (*hline)(Imaging im, int x0, int y0, int x1, int ink); void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink); - int (*polygon)(Imaging im, int n, Edge *e, int ink, int eofill); } DRAW; -DRAW draw8 = {point8, hline8, line8, polygon8}; -DRAW draw32 = {point32, hline32, line32, polygon32}; -DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba, polygon32rgba}; +DRAW draw8 = {point8, hline8, line8}; +DRAW draw32 = {point32, hline32, line32}; +DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba}; /* -------------------------------------------------------------------- */ /* Interface */ @@ -730,7 +714,7 @@ ImagingDrawWideLine( add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); - draw->polygon(im, 4, e, ink, 0); + polygon_generic(im, 4, e, ink, 0, draw->hline); } return 0; } @@ -838,7 +822,7 @@ ImagingDrawPolygon( if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); } - draw->polygon(im, n, e, ink, 0); + polygon_generic(im, n, e, ink, 0, draw->hline); free(e); } else { @@ -1988,7 +1972,7 @@ ImagingDrawOutline( DRAWINIT(); - draw->polygon(im, outline->count, outline->edges, ink, 0); + polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline); return 0; } From 6a60b2e6dd0909f627d093cbc431891a79d2b987 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:27:11 +0100 Subject: [PATCH 551/628] Remove Tests/ path arg, this is already configured --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a56fe8fec..1f9d2ce13 100644 --- a/Makefile +++ b/Makefile @@ -95,12 +95,12 @@ sdist: .PHONY: test test: python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq Tests/ + python3 -m pytest -qq .PHONY: test-p test-p: python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist - python3 -m pytest -qq -n auto Tests/ + python3 -m pytest -qq -n auto .PHONY: valgrind From 98cf15e9e487cbc53b101498d43cc0cc141ee7e7 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:35:13 +0100 Subject: [PATCH 552/628] Update depends/docker-test-valgrind-memory.sh Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/docker-test-valgrind-memory.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index 5f7805421..f0d1d851d 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -1,7 +1,7 @@ #!/bin/bash -## Run this as the test script in the docker valgrind image. -## Note -- can be included directly into the docker image, +## Run this as the test script in the Docker valgrind image. +## Note -- can be included directly into the Docker image, ## but requires the current python.supp. source /vpy3/bin/activate From 399b6c1045ff2387e7db8206e72baec33f996030 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:40:07 +0100 Subject: [PATCH 553/628] Update Makefile Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4f63cfe02..27d70dcb7 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,7 @@ valgrind-leak: PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ - python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ + python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output .PHONY: readme readme: From 506691729a2f9d33228f8693cdbe90418e1b321a Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:40:35 +0100 Subject: [PATCH 554/628] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_pyarrow.py | 4 ++-- src/libImaging/Storage.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py index 2029f96f5..8dad94fe0 100644 --- a/Tests/test_pyarrow.py +++ b/Tests/test_pyarrow.py @@ -29,7 +29,7 @@ def _test_img_equals_pyarray( px = img.load() assert px is not None if elts_per_pixel > 1 and mask is None: - # have to do element wise comparison when we're comparing + # have to do element-wise comparison when we're comparing # flattened r,g,b,a to a pixel. mask = list(range(elts_per_pixel)) for x in range(0, img.size[0], int(img.size[0] / 10)): @@ -56,7 +56,7 @@ def _test_img_equals_int32_pyarray( px = img.load() assert px is not None if mask is None: - # have to do element wise comparison when we're comparing + # have to do element-wise comparison when we're comparing # flattened rgba in an uint32 to a pixel. mask = list(range(elts_per_pixel)) for x in range(0, img.size[0], int(img.size[0] / 10)): diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 1a9171a0c..6f0a1bfa3 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -757,7 +757,7 @@ ImagingNewArrow( if (strcmp(schema->format, "C") == 0 // uint8 && im->pixelsize == 4 // storage as 32 bpc && schema->n_children == 0 // make sure schema is well formed. - && strcmp(im->arrow_band_format, "C") == 0 // Expected Format + && strcmp(im->arrow_band_format, "C") == 0 // expected format && 4 * pixels == external_array->length) { // expected length // single flat array, interleaved storage. if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) { From 3944db288a5b54ea6171fd1334e517fbcc3c9136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=93=E9=BC=A0?= Date: Sat, 31 May 2025 09:10:45 +0800 Subject: [PATCH 555/628] Update MinGW package names (#8987) --- docs/installation/building-from-source.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index c72568b20..8988a92ce 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -194,9 +194,9 @@ Many of Pillow's features require external libraries: pacman -S \ mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools + mingw-w64-x86_64-python \ + mingw-w64-x86_64-python-pip \ + mingw-w64-x86_64-python-setuptools Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: From bc4138f1692d718ec9fe7b3b7449dc20d0e2d85e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 May 2025 11:48:49 +1000 Subject: [PATCH 556/628] ubuntu-latest now uses Ubuntu 24.04 --- docs/installation/platform-support.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 93486d034..1071380fd 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -42,11 +42,13 @@ These platforms are built and tested for every change. | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | | | PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, | -| | | ppc64le, s390x | +| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, 3.13, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.12 | arm64v8, ppc64le, | +| | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2019 | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ From 9327e425ba77523ec9d98eb9558806ecf29b9365 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 May 2025 12:02:16 +1000 Subject: [PATCH 557/628] Stop testing deprecated Windows Server 2019 --- .github/workflows/test-windows.yml | 5 ++--- docs/installation/platform-support.rst | 6 +++--- winbuild/README.md | 3 +-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bfa4c7cd3..6b76351b0 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -31,16 +31,15 @@ env: jobs: build: - runs-on: ${{ matrix.os }} + runs-on: windows-latest strategy: fail-fast: false matrix: python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] architecture: ["x64"] - os: ["windows-latest"] include: # Test the oldest Python on 32-bit - - { python-version: "3.9", architecture: "x86", os: "windows-2019" } + - { python-version: "3.9", architecture: "x86" } timeout-minutes: 45 diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 93486d034..f262d861c 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -48,9 +48,9 @@ These platforms are built and tested for every change. | Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, | | | | ppc64le, s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2019 | 3.9 | x86 | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| Windows Server 2022 | 3.9 | x86 | +| +----------------------------+---------------------+ +| | 3.10, 3.11, 3.12, 3.13, | x86-64 | | | PyPy3 | | | +----------------------------+---------------------+ | | 3.12 (MinGW) | x86-64 | diff --git a/winbuild/README.md b/winbuild/README.md index c474f12ce..0d3ec8d8a 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,8 +11,7 @@ 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 and Windows Server - 2019 with Visual Studio 2019 Enterprise (GitHub Actions). +* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). Here's an example script to build on Windows: From 892fd2c2affa4980121a059bc2b7875834571804 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 1 Jun 2025 15:41:48 +1000 Subject: [PATCH 558/628] Removed unreachable code (#8918) --- src/PIL/MpegImagePlugin.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 5aa00d05b..47ebe9d62 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -33,11 +33,7 @@ class BitStream: def peek(self, bits: int) -> int: while self.bits < bits: - c = self.next() - if c < 0: - self.bits = 0 - continue - self.bitbuffer = (self.bitbuffer << 8) + c + self.bitbuffer = (self.bitbuffer << 8) + self.next() self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 From 95603e9717c81d3492933c3a8d094bfbb7e90340 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:14:11 +1000 Subject: [PATCH 559/628] Use ImageFile.MAXBLOCK in tobytes() (#8906) --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index aaa3332ee..ed2f728aa 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -802,7 +802,9 @@ class Image: e = _getencoder(self.mode, encoder_name, encoder_args) e.setimage(self.im) - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c + from . import ImageFile + + bufsize = max(ImageFile.MAXBLOCK, self.size[0] * 4) # see RawEncode.c output = [] while True: From 070e1eba626736a5cfa4a90a8a97dfbbf6278b91 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:08:24 +1000 Subject: [PATCH 560/628] [pre-commit.ci] pre-commit autoupdate (#8993) --- .pre-commit-config.yaml | 8 ++++---- src/_imaging.c | 9 +++++---- src/display.c | 8 ++++---- src/libImaging/Fill.c | 5 +++-- src/libImaging/Filter.c | 6 ++++-- src/libImaging/Jpeg2KEncode.c | 8 ++++---- src/libImaging/Point.c | 5 +++-- src/libImaging/Resample.c | 6 ++++-- src/libImaging/Storage.c | 5 +++-- src/libImaging/TiffDecode.c | 3 ++- 10 files changed, 36 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e15e6f639..a1a054e00 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.11.8 + rev: v0.11.12 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: v20.1.3 + rev: v20.1.5 hooks: - id: clang-format types: [c] @@ -58,7 +58,7 @@ repos: - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.6.0 + rev: v1.9.0 hooks: - id: zizmor @@ -68,7 +68,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.5.1 + rev: v2.6.0 hooks: - id: pyproject-fmt diff --git a/src/_imaging.c b/src/_imaging.c index 79e0a2b23..9213ba13d 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -308,9 +308,9 @@ _new_arrow(PyObject *self, PyObject *args) { } // ImagingBorrowArrow is responsible for retaining the array_capsule - ret = - PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) - ); + ret = PyImagingNew( + ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) + ); if (!ret) { return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); } @@ -1665,7 +1665,8 @@ _putdata(ImagingObject *self, PyObject *args) { int bigendian = 0; if (image->type == IMAGING_TYPE_SPECIAL) { // I;16* - if (strcmp(image->mode, "I;16B") == 0 + if ( + strcmp(image->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(image->mode, "I;16N") == 0 #endif diff --git a/src/display.c b/src/display.c index 11742a895..3215f6691 100644 --- a/src/display.c +++ b/src/display.c @@ -327,11 +327,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { // added in Windows 10 (1607) // loaded dynamically to avoid link errors user32 = LoadLibraryA("User32.dll"); - SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext - )GetProcAddress(user32, "SetThreadDpiAwarenessContext"); + SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext) + GetProcAddress(user32, "SetThreadDpiAwarenessContext"); if (SetThreadDpiAwarenessContext_function != NULL) { - GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext - )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); + GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext) + GetProcAddress(user32, "GetWindowDpiAwarenessContext"); if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) { dpiAwareness = GetWindowDpiAwarenessContext_function(wnd); } diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 8fb481e7e..28f427370 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -118,8 +118,9 @@ ImagingFillRadialGradient(const char *mode) { for (y = 0; y < 256; y++) { for (x = 0; x < 256; x++) { - d = (int - )sqrt((double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0); + d = (int)sqrt( + (double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0 + ); if (d >= 255) { d = 255; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 7b7b2e429..c46dd3cd1 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -155,7 +155,8 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif @@ -308,7 +309,8 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 34d1a2294..61e095ad6 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -207,8 +207,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { if (params->cp_cinema == OPJ_CINEMA4K_24) { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_24_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_24_CS_LENGTH * 8)); params->POC[0].tile = 1; params->POC[0].resno0 = 0; @@ -243,8 +243,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { params->max_comp_size = COMP_24_CS_MAX_LENGTH; } else { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_48_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_48_CS_LENGTH * 8)); for (n = 0; n < params->tcp_numlayers; ++n) { rate = 0; diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 6a4060b4b..b11ea62ed 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -197,8 +197,9 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { return imOut; mode_mismatch: - return (Imaging - )ImagingError_ValueError("point operation not supported for this mode"); + return (Imaging)ImagingError_ValueError( + "point operation not supported for this mode" + ); } Imaging diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index f5e386dc2..b114e0023 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -470,7 +470,8 @@ ImagingResampleHorizontal_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif @@ -509,7 +510,8 @@ ImagingResampleVertical_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 6f0a1bfa3..11d6c06cc 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -602,8 +602,9 @@ ImagingBorrowArrow( } if (!borrowed_buffer) { - return (Imaging - )ImagingError_ValueError("Arrow Array, exactly 2 buffers required"); + return (Imaging)ImagingError_ValueError( + "Arrow Array, exactly 2 buffers required" + ); } for (y = i = 0; y < im->ysize; y++) { diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 173eca160..2e83fb847 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -557,7 +557,8 @@ _decodeStrip( (tdata_t)state->buffer, strip_size ) == -1) { - TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) + TRACE( + ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) ); state->errcode = IMAGING_CODEC_BROKEN; return -1; From fa7413904b4eee630401c31994eeb5bcda2441a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Jun 2025 14:13:22 +1000 Subject: [PATCH 561/628] Updated ruff ID --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1a054e00..1b8fa7199 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.12 hooks: - - id: ruff + - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror From eb0256acc082e362b4172f3256a551a412ef4b09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Jun 2025 22:44:26 +1000 Subject: [PATCH 562/628] Fixed test --- Tests/test_deprecate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 82ff14181..88479ff0d 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -47,7 +47,6 @@ def test_unknown_version() -> None: ], ) def test_old_version(deprecated: str, plural: bool, expected: str) -> None: - expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) From cb077a16c80e9d23bb3976182acae7fc090aa5dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jun 2025 20:07:13 +1000 Subject: [PATCH 563/628] Handle UNDEFINED XMP data --- Tests/test_file_tiff.py | 24 ++++++++++++++++++++++++ src/PIL/TiffImagePlugin.py | 5 ++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index d0d394aa9..73046eb5f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -14,6 +14,7 @@ from PIL import ( ImageFile, JpegImagePlugin, TiffImagePlugin, + TiffTags, UnidentifiedImageError, ) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -900,6 +901,29 @@ class TestFileTiff: assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_getxmp_undefined(self, tmp_path: Path) -> None: + tmpfile = tmp_path / "temp.tif" + im = Image.new("L", (1, 1)) + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd.tagtype[700] = TiffTags.UNDEFINED + with Image.open("Tests/images/lab.tif") as im_xmp: + ifd[700] = im_xmp.info["xmp"] + im.save(tmpfile, tiffinfo=ifd) + + with Image.open(tmpfile) as im_reloaded: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im_reloaded.getxmp() == {} + else: + assert "xmp" in im_reloaded.info + xmp = im_reloaded.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e..22c5208e2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1259,7 +1259,10 @@ class TiffImageFile(ImageFile.ImageFile): self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) if XMP in self.tag_v2: - self.info["xmp"] = self.tag_v2[XMP] + xmp = self.tag_v2[XMP] + if isinstance(xmp, tuple) and len(xmp) == 1: + xmp = xmp[0] + self.info["xmp"] = xmp elif "xmp" in self.info: del self.info["xmp"] self._reload_exif() From f03c23683ed83a9d8f73e73073ac28f1ab2b74ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jun 2025 20:08:58 +1000 Subject: [PATCH 564/628] Trim whitespace from end when parsing XMP data --- Tests/test_image.py | 2 +- src/PIL/Image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 14a067127..ac358f5bf 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -989,7 +989,7 @@ class TestImage: im = Image.new("RGB", (1, 1)) im.info["xmp"] = ( b'\n' - b'\n\x00\x00' + b'\n\x00\x00 ' ) if ElementTree is None: with pytest.warns( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ed2f728aa..e03e9cc8a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1511,7 +1511,7 @@ class Image: return {} if "xmp" not in self.info: return {} - root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00")) + root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 ")) return {get_name(root.tag): get_value(root)} def getexif(self) -> Exif: From 9d5ea827e4ac401b85ec0ed61b7d2e97a101b05a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Jun 2025 18:16:05 +1000 Subject: [PATCH 565/628] Call startswith once with a tuple --- setup.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index ab36c6b17..3716a7b9f 100644 --- a/setup.py +++ b/setup.py @@ -163,7 +163,7 @@ def _find_library_dirs_ldconfig() -> list[str]: args: list[str] env: dict[str, str] expr: str - if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): + if sys.platform.startswith(("linux", "gnu")): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" else: @@ -623,11 +623,7 @@ class pil_build_ext(build_ext): for extension in self.extensions: extension.extra_compile_args = ["-Wno-nullability-completeness"] - elif ( - sys.platform.startswith("linux") - or sys.platform.startswith("gnu") - or sys.platform.startswith("freebsd") - ): + elif sys.platform.startswith(("linux", "gnu", "freebsd")): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): From f3b05d6fab2a8fb0db033fc507d0d6f19ed2330e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:07:21 +1000 Subject: [PATCH 566/628] Update dependency mypy to v1.16.0 (#8991) Co-authored-by: Andrew Murray --- .ci/requirements-mypy.txt | 2 +- Tests/test_file_jpeg.py | 10 ++++++---- Tests/test_image.py | 1 + src/PIL/GifImagePlugin.py | 9 ++++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 86ac2e0b2..a9c18ae2b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.15.0 +mypy==1.16.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index b9eec591d..2827937cf 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -145,14 +145,16 @@ class TestFileJpeg: assert k > 0.9 # roundtrip, and check again im = self.roundtrip(im) - c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + cmyk = im.getpixel((0, 0)) + assert isinstance(cmyk, tuple) + c, m, y, k = (x / 255.0 for x in cmyk) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) + cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) + assert isinstance(cmyk, tuple) + k = cmyk[3] / 255.0 assert k > 0.9 def test_rgb(self) -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 14a067127..4cc841603 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -671,6 +671,7 @@ class TestImage: im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) assert im.palette is not None + assert im_remapped.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4392c4cb9..c98e02f69 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, Any, Literal, NamedTuple, Union +from typing import IO, Any, Literal, NamedTuple, Union, cast from . import ( Image, @@ -350,12 +350,15 @@ class GifImageFile(ImageFile.ImageFile): if self._frame_palette: if color * 3 + 3 > len(self._frame_palette.palette): color = 0 - return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + return cast( + tuple[int, int, int], + tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), + ) else: return (color, color, color) self.dispose = None - self.dispose_extent = frame_dispose_extent + self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent if self.dispose_extent and self.disposal_method >= 2: try: if self.disposal_method == 2: From 0d1edba311ffdc9683c6541a5133c60d425debe8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 14:16:49 +1000 Subject: [PATCH 567/628] Assert tile args is tuple --- Tests/test_file_gif.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 20d58a9dd..2712e683c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1422,7 +1422,9 @@ def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None: def test_lzw_bits() -> None: # see https://github.com/python-pillow/Pillow/issues/2811 with Image.open("Tests/images/issue_2811.gif") as im: - assert im.tile[0][3][0] == 11 # LZW bits + args = im.tile[0][3] + assert isinstance(args, tuple) + assert args[0] == 11 # LZW bits # codec error prepatch im.load() From 33460d2f82ee148eff2451cfe7e8bc4b6f33b66a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 14:22:02 +1000 Subject: [PATCH 568/628] Assert _getmp() does not return None --- Tests/test_file_mpo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 73838ef44..462c95535 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -156,6 +156,7 @@ def test_reload_exif_after_seek() -> None: def test_mp(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -165,6 +166,7 @@ def test_mp_offset() -> None: # in APP2 data, in contrast to normal 8 with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -181,6 +183,7 @@ def test_mp_no_data() -> None: def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() + assert mpinfo is not None for frame_number, mpentry in enumerate(mpinfo[0xB002]): mpattr = mpentry["Attribute"] if frame_number: From cba096b4a99f92eb865c7fb127b9783dbd38643e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jun 2025 11:13:12 +1000 Subject: [PATCH 569/628] Assert pixel data is tuple --- Tests/test_file_gif.py | 8 ++++++-- Tests/test_file_jpeg.py | 10 ++++++---- Tests/test_file_tga.py | 8 ++++++-- Tests/test_file_webp.py | 2 ++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 2712e683c..f46a28971 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -540,7 +540,9 @@ def test_dispose_background_transparency() -> None: img.seek(2) px = img.load() assert px is not None - assert px[35, 30][3] == 0 + value = px[35, 30] + assert isinstance(value, tuple) + assert value[3] == 0 @pytest.mark.parametrize( @@ -1479,7 +1481,9 @@ def test_saving_rgba(tmp_path: Path) -> None: with Image.open(out) as reloaded: reloaded_rgba = reloaded.convert("RGBA") - assert reloaded_rgba.load()[0, 0][3] == 0 + value = reloaded_rgba.load()[0, 0] + assert isinstance(value, tuple) + assert value[3] == 0 @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 2827937cf..50ee04611 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -133,15 +133,17 @@ class TestFileJpeg: f = "Tests/images/pil_sample_cmyk.jpg" with Image.open(f) as im: # the source image has red pixels in the upper left corner. - c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + cmyk = im.getpixel((0, 0)) + assert isinstance(cmyk, tuple) + c, m, y, k = (x / 255.0 for x in cmyk) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 # the opposite corner is black - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) + cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) + assert isinstance(cmyk, tuple) + k = cmyk[3] / 255.0 assert k > 0.9 # roundtrip, and check again im = self.roundtrip(im) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 8b6ed3ed2..d3cceb37f 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -220,12 +220,16 @@ def test_horizontal_orientations() -> None: with Image.open("Tests/images/rgb32rle_top_right.tga") as im: px = im.load() assert px is not None - assert px[90, 90][:3] == (0, 0, 0) + value = px[90, 90] + assert isinstance(value, tuple) + assert value[:3] == (0, 0, 0) with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: px = im.load() assert px is not None - assert px[90, 90][:3] == (0, 255, 0) + value = px[90, 90] + assert isinstance(value, tuple) + assert value[:3] == (0, 255, 0) def test_save_rle(tmp_path: Path) -> None: diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f61e2c82e..4ea7629d1 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -219,6 +219,7 @@ class TestFileWebp: # Save P mode GIF with background with Image.open("Tests/images/chi.gif") as im: original_value = im.convert("RGB").getpixel((1, 1)) + assert isinstance(original_value, tuple) # Save as WEBP im.save(out_webp, save_all=True) @@ -230,6 +231,7 @@ class TestFileWebp: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) + assert isinstance(reread_value, tuple) difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3)) assert difference < 5 From a3da70e76e14da1bc0e5b1c2331d746e4a095a6b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 14:23:58 +1000 Subject: [PATCH 570/628] Assert load() does not return None --- Tests/test_file_gif.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index f46a28971..e418af45c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1481,7 +1481,9 @@ def test_saving_rgba(tmp_path: Path) -> None: with Image.open(out) as reloaded: reloaded_rgba = reloaded.convert("RGBA") - value = reloaded_rgba.load()[0, 0] + px = reloaded_rgba.load() + assert px is not None + value = px[0, 0] assert isinstance(value, tuple) assert value[3] == 0 From 89c38258dc33fd410858c6b760d533789578e15e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Apr 2025 14:24:20 +1000 Subject: [PATCH 571/628] Assert getcolors() does not return None --- Tests/test_file_avif.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index b2e586637..6d0cc74f9 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -254,7 +254,9 @@ class TestFileAvif: assert_image(im, "RGBA", (64, 64)) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0] == (876, 0) + colors = im.getchannel("A").getcolors() + assert colors is not None + assert colors[0] == (876, 0) def test_save_transparent(self, tmp_path: Path) -> None: im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) From 04c984f2f202639479a161dc44ba378b9ebee931 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jun 2025 11:29:11 +1000 Subject: [PATCH 572/628] Removed duplicate code --- Tests/test_file_jpeg.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 50ee04611..00f2c004d 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -130,9 +130,7 @@ class TestFileJpeg: def test_cmyk(self) -> None: # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. - f = "Tests/images/pil_sample_cmyk.jpg" - with Image.open(f) as im: - # the source image has red pixels in the upper left corner. + def check(im: ImageFile.ImageFile) -> None: cmyk = im.getpixel((0, 0)) assert isinstance(cmyk, tuple) c, m, y, k = (x / 255.0 for x in cmyk) @@ -145,19 +143,13 @@ class TestFileJpeg: assert isinstance(cmyk, tuple) k = cmyk[3] / 255.0 assert k > 0.9 + + with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: + # the source image has red pixels in the upper left corner. + check(im) + # roundtrip, and check again - im = self.roundtrip(im) - cmyk = im.getpixel((0, 0)) - assert isinstance(cmyk, tuple) - c, m, y, k = (x / 255.0 for x in cmyk) - assert c == 0.0 - assert m > 0.8 - assert y > 0.8 - assert k == 0.0 - cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) - assert isinstance(cmyk, tuple) - k = cmyk[3] / 255.0 - assert k > 0.9 + check(self.roundtrip(im)) def test_rgb(self) -> None: def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]: From ef1f90fe1c92ec4d038ddf4d03638f467ba94181 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:06:08 +1000 Subject: [PATCH 573/628] Check for equality rather than inequality Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageGrab.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index d11609483..1eb450734 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -134,7 +134,10 @@ def grabclipboard() -> Image.Image | list[str] | None: import struct o = struct.unpack_from("I", data)[0] - files = data[o:].decode("mbcs" if data[16] == 0 else "utf-16le").split("\0") + if data[16] == 0: + files = data[o:].decode("mbcs").split("\0") + else: + files = data[o:].decode("utf-16le").split("\0") return files[: files.index("")] if isinstance(data, bytes): data = io.BytesIO(data) From 313969cf0bcf6b6185d486830478d2864eb56fe1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Jun 2025 12:21:49 +1000 Subject: [PATCH 574/628] Removed unnecessary seek --- src/PIL/PcxImagePlugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 299405ae0..47b6e80e2 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -66,6 +66,8 @@ class PcxImageFile(ImageFile.ImageFile): raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) + offset = self.fp.tell() + # format version = s[1] bits = s[3] @@ -102,7 +104,6 @@ class PcxImageFile(ImageFile.ImageFile): break if mode == "P": self.palette = ImagePalette.raw("RGB", s[1:]) - self.fp.seek(128) elif version == 5 and bits == 8 and planes == 3: mode = "RGB" @@ -128,9 +129,7 @@ class PcxImageFile(ImageFile.ImageFile): bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) - self.tile = [ - ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride)) - ] + self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))] # -------------------------------------------------------------------- From 7341e70f6be9c3e910c81f563bb7900167873c02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Jun 2025 12:20:52 +1000 Subject: [PATCH 575/628] Reduced number of bytes read for header --- src/PIL/PcxImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 47b6e80e2..458d586c4 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -54,7 +54,7 @@ class PcxImageFile(ImageFile.ImageFile): # header assert self.fp is not None - s = self.fp.read(128) + s = self.fp.read(68) if not _accept(s): msg = "not a PCX file" raise SyntaxError(msg) @@ -66,7 +66,7 @@ class PcxImageFile(ImageFile.ImageFile): raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) - offset = self.fp.tell() + offset = self.fp.tell() + 60 # format version = s[1] From 7b163cc35d3ef9bb0204613add92f05eda65ab63 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:46:12 +1000 Subject: [PATCH 576/628] Use mask in C when drawing wide polygon lines (#8984) --- src/PIL/ImageDraw.py | 16 +----- src/_imaging.c | 20 +++++-- src/libImaging/Draw.c | 113 ++++++++++++++++++++++++++++----------- src/libImaging/Imaging.h | 19 ++++++- 4 files changed, 116 insertions(+), 52 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e6c7b0298..98ae67539 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -365,22 +365,10 @@ class ImageDraw: # use the fill as a mask mask = Image.new("1", self.im.size) mask_ink = self._getink(1)[0] - - fill_im = mask.copy() - draw = Draw(fill_im) + draw = Draw(mask) draw.draw.draw_polygon(xy, mask_ink, 1) - ink_im = mask.copy() - draw = Draw(ink_im) - width = width * 2 - 1 - draw.draw.draw_polygon(xy, mask_ink, 0, width) - - mask.paste(ink_im, mask=fill_im) - - im = Image.new(self.mode, self.im.size) - draw = Draw(im) - draw.draw.draw_polygon(xy, ink, 0, width) - self.im.paste(im.im, (0, 0) + im.size, mask.im) + self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im) def regular_polygon( self, diff --git a/src/_imaging.c b/src/_imaging.c index 9213ba13d..2a7bc8d3f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3220,7 +3220,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { (int)p[3], &ink, width, - self->blend + self->blend, + NULL ) < 0) { free(xy); return NULL; @@ -3358,7 +3359,10 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { int ink; int fill = 0; int width = 0; - if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) { + ImagingObject *maskp = NULL; + if (!PyArg_ParseTuple( + args, "Oi|iiO!", &data, &ink, &fill, &width, &Imaging_Type, &maskp + )) { return NULL; } @@ -3388,8 +3392,16 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { free(xy); - if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < - 0) { + if (ImagingDrawPolygon( + self->image->image, + n, + ixy, + &ink, + fill, + width, + self->blend, + maskp ? maskp->image : NULL + ) < 0) { free(ixy); return NULL; } diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 4c08e9855..70f267ae4 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -63,7 +63,7 @@ typedef struct { } Edge; /* Type used in "polygon*" functions */ -typedef void (*hline_handler)(Imaging, int, int, int, int); +typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging); static inline void point8(Imaging im, int x, int y, int ink) { @@ -103,7 +103,7 @@ point32rgba(Imaging im, int x, int y, int ink) { } static inline void -hline8(Imaging im, int x0, int y0, int x1, int ink) { +hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { int pixelwidth; if (y0 >= 0 && y0 < im->ysize) { @@ -119,15 +119,30 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { } if (x0 <= x1) { pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; - memset( - im->image8[y0] + x0 * pixelwidth, (UINT8)ink, (x1 - x0 + 1) * pixelwidth - ); + if (mask == NULL) { + memset( + im->image8[y0] + x0 * pixelwidth, + (UINT8)ink, + (x1 - x0 + 1) * pixelwidth + ); + } else { + UINT8 *p = im->image8[y0]; + while (x0 <= x1) { + if (mask->image8[y0][x0]) { + p[x0 * pixelwidth] = ink; + if (pixelwidth == 2) { + p[x0 * pixelwidth + 1] = ink; + } + } + x0++; + } + } } } } static inline void -hline32(Imaging im, int x0, int y0, int x1, int ink) { +hline32(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { INT32 *p; if (y0 >= 0 && y0 < im->ysize) { @@ -143,13 +158,16 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { } p = im->image32[y0]; while (x0 <= x1) { - p[x0++] = ink; + if (mask == NULL || mask->image8[y0][x0]) { + p[x0] = ink; + } + x0++; } } } static inline void -hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { +hline32rgba(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { @@ -167,9 +185,11 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { - out[0] = BLEND(in[3], out[0], in[0], tmp); - out[1] = BLEND(in[3], out[1], in[1], tmp); - out[2] = BLEND(in[3], out[2], in[2], tmp); + if (mask == NULL || mask->image8[y0][x0]) { + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); + } x0++; out += 4; } @@ -407,7 +427,14 @@ x_cmp(const void *x0, const void *x1) { static void draw_horizontal_lines( - Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline + Imaging im, + int n, + Edge *e, + int ink, + int *x_pos, + int y, + hline_handler hline, + Imaging mask ) { int i; for (i = 0; i < n; i++) { @@ -429,7 +456,7 @@ draw_horizontal_lines( } } - (*hline)(im, xmin, e[i].ymin, xmax, ink); + (*hline)(im, xmin, e[i].ymin, xmax, ink, mask); *x_pos = xmax + 1; } } @@ -439,7 +466,9 @@ draw_horizontal_lines( * Filled polygon draw function using scan line algorithm. */ static inline int -polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline) { +polygon_generic( + Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, Imaging mask +) { Edge **edge_table; float *xx; int edge_count = 0; @@ -469,7 +498,7 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h } if (e[i].ymin == e[i].ymax) { if (hasAlpha != 1) { - (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); + (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink, mask); } continue; } @@ -557,7 +586,7 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h // Line would be before the current position continue; } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask); if (x_end < x_pos) { // Line would be before the current position continue; @@ -573,13 +602,13 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h continue; } } - (*hline)(im, x_start, ymin, x_end, ink); + (*hline)(im, x_start, ymin, x_end, ink, mask); x_pos = x_end + 1; } - draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline); + draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask); } else { for (i = 1; i < j; i += 2) { - (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); + (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink, mask); } } } @@ -623,7 +652,7 @@ add_edge(Edge *e, int x0, int y0, int x1, int y1) { typedef struct { void (*point)(Imaging im, int x, int y, int ink); - void (*hline)(Imaging im, int x0, int y0, int x1, int ink); + void (*hline)(Imaging im, int x0, int y0, int x1, int ink, Imaging mask); void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink); } DRAW; @@ -674,7 +703,15 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, in int ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink_, + int width, + int op, + Imaging mask ) { DRAW *draw; INT32 ink; @@ -714,7 +751,7 @@ ImagingDrawWideLine( add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); - polygon_generic(im, 4, e, ink, 0, draw->hline); + polygon_generic(im, 4, e, ink, 0, draw->hline, mask); } return 0; } @@ -757,7 +794,7 @@ ImagingDrawRectangle( } for (y = y0; y <= y1; y++) { - draw->hline(im, x0, y, x1, ink); + draw->hline(im, x0, y, x1, ink, NULL); } } else { @@ -766,8 +803,8 @@ ImagingDrawRectangle( width = 1; } for (i = 0; i < width; i++) { - draw->hline(im, x0, y0 + i, x1, ink); - draw->hline(im, x0, y1 - i, x1, ink); + draw->hline(im, x0, y0 + i, x1, ink, NULL); + draw->hline(im, x0, y1 - i, x1, ink, NULL); draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink); draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink); } @@ -778,7 +815,14 @@ ImagingDrawRectangle( int ImagingDrawPolygon( - Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op + Imaging im, + int count, + int *xy, + const void *ink_, + int fill, + int width, + int op, + Imaging mask ) { int i, n, x0, y0, x1, y1; DRAW *draw; @@ -822,7 +866,7 @@ ImagingDrawPolygon( if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); } - polygon_generic(im, n, e, ink, 0, draw->hline); + polygon_generic(im, n, e, ink, 0, draw->hline, mask); free(e); } else { @@ -844,11 +888,12 @@ ImagingDrawPolygon( xy[i * 2 + 3], ink_, width, - op + op, + mask ); } ImagingDrawWideLine( - im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op + im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op, mask ); } } @@ -1519,7 +1564,9 @@ ellipseNew( ellipse_init(&st, a, b, width); int32_t X0, Y, X1; while (ellipse_next(&st, &X0, &Y, &X1) != -1) { - draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + draw->hline( + im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL + ); } return 0; } @@ -1554,7 +1601,9 @@ clipEllipseNew( int32_t X0, Y, X1; int next_code; while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) { - draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); + draw->hline( + im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL + ); } clip_ellipse_free(&st); return next_code == -1 ? 0 : -1; @@ -1972,7 +2021,7 @@ ImagingDrawOutline( DRAWINIT(); - polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline); + polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline, NULL); return 0; } diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 234f9943c..39ecdbff6 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -510,7 +510,15 @@ extern int ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op); extern int ImagingDrawWideLine( - Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op + Imaging im, + int x0, + int y0, + int x1, + int y1, + const void *ink, + int width, + int op, + Imaging mask ); extern int ImagingDrawPieslice( @@ -530,7 +538,14 @@ extern int ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); extern int ImagingDrawPolygon( - Imaging im, int points, int *xy, const void *ink, int fill, int width, int op + Imaging im, + int points, + int *xy, + const void *ink, + int fill, + int width, + int op, + Imaging mask ); extern int ImagingDrawRectangle( From 6bd55684e0d172749a74dd2098ebc18ce5f34fdd Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:00:08 +1000 Subject: [PATCH 577/628] Only accept missing tkinter when building wheels on Windows (#8981) --- Tests/check_wheel.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 8ba40ba3f..9602410da 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -11,13 +11,14 @@ from .helper import is_pypy def test_wheel_modules() -> None: expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} - # tkinter is not available in cibuildwheel installed CPython on Windows - try: - import tkinter + if sys.platform == "win32": + # tkinter is not available in cibuildwheel installed CPython on Windows + try: + import tkinter - assert tkinter - except ImportError: - expected_modules.remove("tkinter") + assert tkinter + except ImportError: + expected_modules.remove("tkinter") assert set(features.get_supported_modules()) == expected_modules From e65e5bea45e92a118590c69c022d6e6741e3b101 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Jun 2025 20:30:18 +1000 Subject: [PATCH 578/628] Start decoding with a zero-initialized array of previously seen pixels --- Tests/images/op_index.qoi | Bin 0 -> 15 bytes Tests/test_file_qoi.py | 6 ++++++ src/PIL/QoiImagePlugin.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Tests/images/op_index.qoi diff --git a/Tests/images/op_index.qoi b/Tests/images/op_index.qoi new file mode 100644 index 0000000000000000000000000000000000000000..e626aafe6a433487cbacb4bdbcbbe5e66e8d07db GIT binary patch literal 15 TcmXTS&rD-rU| None: with pytest.raises(SyntaxError): QoiImagePlugin.QoiImageFile(invalid_file) + + +def test_op_index() -> None: + # QOI_OP_INDEX as the first chunk + with Image.open("Tests/images/op_index.qoi") as im: + assert im.getpixel((0, 0)) == (0, 0, 0, 0) diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index df552243e..75070abd7 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -51,7 +51,7 @@ class QoiDecoder(ImageFile.PyDecoder): assert self.fd is not None self._previously_seen_pixels = {} - self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) + self._previous_pixel = bytearray((0, 0, 0, 255)) data = bytearray() bands = Image.getmodebands(self.mode) From 646885e546ecd02a8162d91b51d32eed9da67b7a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:06:28 +1000 Subject: [PATCH 579/628] Parse XMP tag bytes without decoding to string (#8960) Co-authored-by: Andrew Murray --- Tests/test_image.py | 5 +++++ src/PIL/Image.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 4cc841603..512a52433 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -974,6 +974,11 @@ class TestImage: assert tag not in exif.get_ifd(0x8769) assert exif.get_ifd(0xA005) + def test_exif_from_xmp_bytes(self) -> None: + im = Image.new("RGB", (1, 1)) + im.info["xmp"] = b'\xff tiff:Orientation="2"' + assert im.getexif()[274] == 2 + def test_empty_xmp(self) -> None: with Image.open("Tests/images/hopper.gif") as im: if ElementTree is None: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ed2f728aa..216022565 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1542,10 +1542,11 @@ class Image: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") + pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' if not xmp_tags and (xmp_tags := self.info.get("xmp")): - xmp_tags = xmp_tags.decode("utf-8") + pattern = rb'tiff:Orientation(="|>)([0-9])' if xmp_tags: - match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) + match = re.search(pattern, xmp_tags) if match: self._exif[ExifTags.Base.Orientation] = int(match[2]) From 36cea1953231d71f1184ef1396c1f01ff11c939a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:08:29 +1000 Subject: [PATCH 580/628] Do not decode bytes in PPM error message (#8958) --- Tests/test_file_ppm.py | 7 ++++--- src/PIL/PpmImagePlugin.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 41e2b5416..c7d1f4df4 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -288,12 +288,13 @@ def test_non_integer_token(tmp_path: Path) -> None: pass -def test_header_token_too_long(tmp_path: Path) -> None: +@pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890")) +def test_header_token_too_long(tmp_path: Path, data: bytes) -> None: path = tmp_path / "temp.ppm" with open(path, "wb") as f: - f.write(b"P6\n 01234567890") + f.write(data) - with pytest.raises(ValueError, match="Token too long in file header: 01234567890"): + with pytest.raises(ValueError, match="Token too long in file header: "): with Image.open(path): pass diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 03afa2d2e..db34d107a 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -94,8 +94,8 @@ class PpmImageFile(ImageFile.ImageFile): msg = "Reached EOF while reading header" raise ValueError(msg) elif len(token) > 10: - msg = f"Token too long in file header: {token.decode()}" - raise ValueError(msg) + msg_too_long = b"Token too long in file header: %s" % token + raise ValueError(msg_too_long) return token def _open(self) -> None: From d7a45cc250f8ae35ee8095753eff0cad1c9f8216 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:57:37 +1000 Subject: [PATCH 581/628] ImageFont does not handle multiline text (#9000) --- docs/reference/ImageFont.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 8b2f92323..aac55fe6b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -18,6 +18,9 @@ OpenType fonts (as well as other font formats supported by the FreeType library). For earlier versions, TrueType support is only available as part of the imToolkit package. +When measuring text sizes, this module will not break at newline characters. For +multiline text, see the :py:mod:`~PIL.ImageDraw` module. + .. warning:: To protect against potential DOS attacks when using arbitrary strings as text input, Pillow will raise a :py:exc:`ValueError` if the number of characters From 056dc89a3c85cbd6d6c960cbfc5aaa52f996bd3d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:12:40 +1000 Subject: [PATCH 582/628] Correct drawing I;16 horizontal lines (#8985) --- Tests/images/imagedraw_rectangle_I.tiff | Bin 20122 -> 20122 bytes Tests/test_imagedraw.py | 3 ++- src/libImaging/Draw.c | 34 +++++++++++++++--------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Tests/images/imagedraw_rectangle_I.tiff b/Tests/images/imagedraw_rectangle_I.tiff index 9b9eda883a371d9cc88b4677b09d2e351c42e609..f0cb534b63e47c940ecb6c3323cb9de3dce573d4 100644 GIT binary patch literal 20122 zcmeI&F%AJy7=_V)j0hbKjY4fF8mq7hdz`h*7CbV=ls6F~a){*Rweqr`|14bRhJA-KYQ None: draw = ImageDraw.Draw(im) # Act - draw.rectangle(bbox, outline=0xFFFF) + draw.rectangle(bbox, outline=0xCDEF) # Assert + assert im.getpixel((X0, Y0)) == 0xCDEF assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 70f267ae4..27cac687e 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -104,8 +104,6 @@ point32rgba(Imaging im, int x, int y, int ink) { static inline void hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { - int pixelwidth; - if (y0 >= 0 && y0 < im->ysize) { if (x0 < 0) { x0 = 0; @@ -118,20 +116,30 @@ hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { x1 = im->xsize - 1; } if (x0 <= x1) { - pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; - if (mask == NULL) { - memset( - im->image8[y0] + x0 * pixelwidth, - (UINT8)ink, - (x1 - x0 + 1) * pixelwidth - ); + int bigendian = -1; + if (strncmp(im->mode, "I;16", 4) == 0) { + bigendian = + ( +#ifdef WORDS_BIGENDIAN + strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0 +#else + strcmp(im->mode, "I;16B") == 0 +#endif + ) + ? 1 + : 0; + } + if (mask == NULL && bigendian == -1) { + memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1)); } else { UINT8 *p = im->image8[y0]; while (x0 <= x1) { - if (mask->image8[y0][x0]) { - p[x0 * pixelwidth] = ink; - if (pixelwidth == 2) { - p[x0 * pixelwidth + 1] = ink; + if (mask == NULL || mask->image8[y0][x0]) { + if (bigendian == -1) { + p[x0] = ink; + } else { + p[x0 * 2 + (bigendian ? 1 : 0)] = ink; + p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8; } } x0++; From 3eb893f0c16c958962758c03a597454aacac8f84 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 11 Jun 2025 20:56:28 +1000 Subject: [PATCH 583/628] Updated libjpeg-turbo to 3.1.1 (#9009) --- .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 1583435c1..b46811f5a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 LIBPNG_VERSION=1.6.48 -JPEGTURBO_VERSION=3.1.0 +JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 6e176e29c..0cc383733 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,7 +114,7 @@ V = { "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "11.2.1", - "JPEGTURBO": "3.1.0", + "JPEGTURBO": "3.1.1", "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", From 8ccdc399df1254c89bdb4e8fda6d6daf98943ab6 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:19:09 +1000 Subject: [PATCH 584/628] Remove padding between interleaved PCX palette data (#9005) --- Tests/images/p_4_planes.pcx | Bin 0 -> 136 bytes Tests/test_file_pcx.py | 5 +++++ src/libImaging/PcxDecode.c | 22 ++++++++++++++++------ 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 Tests/images/p_4_planes.pcx diff --git a/Tests/images/p_4_planes.pcx b/Tests/images/p_4_planes.pcx new file mode 100644 index 0000000000000000000000000000000000000000..8c5743a98554c89fa46188308332461a4aa87f91 GIT binary patch literal 136 ocmd;LWn^T4f)s`n7?XkFKZ1#u#lpnE2!?o7;goD(XaLIr0O6ei;s5{u literal 0 HcmV?d00001 diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 5d7fd1c1b..2e999eff6 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -37,6 +37,11 @@ def test_sanity(tmp_path: Path) -> None: im.save(f) +def test_p_4_planes() -> None: + with Image.open("Tests/images/p_4_planes.pcx") as im: + assert im.getpixel((0, 0)) == 3 + + def test_bad_image_size() -> None: with open("Tests/images/pil184.pcx", "rb") as fp: data = fp.read() diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index 942c8dc22..a65952fb1 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -60,15 +60,25 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt } if (state->x >= state->bytes) { - if (state->bytes % state->xsize && state->bytes > state->xsize) { - int bands = state->bytes / state->xsize; - int stride = state->bytes / bands; + int bands; + int xsize = 0; + int stride = 0; + if (state->bits == 2 || state->bits == 4) { + xsize = (state->xsize + 7) / 8; + bands = state->bits; + stride = state->bytes / state->bits; + } else { + xsize = state->xsize; + bands = state->bytes / state->xsize; + if (bands != 0) { + stride = state->bytes / bands; + } + } + if (stride > xsize) { int i; for (i = 1; i < bands; i++) { // note -- skipping first band memmove( - &state->buffer[i * state->xsize], - &state->buffer[i * stride], - state->xsize + &state->buffer[i * xsize], &state->buffer[i * stride], xsize ); } } From b65a7acf259682c9d02d7ab6bef57bd4ca596c74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:20:34 +0000 Subject: [PATCH 585/628] Update dependency cibuildwheel to v3 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 0e314b8bf..520b6e320 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.23.3 +cibuildwheel==3.0.0 From d2295c0843e828816af892da53b03d1698a3a616 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jun 2025 18:53:35 +1000 Subject: [PATCH 586/628] Do not activate virtualenv --- .github/workflows/wheels-test.ps1 | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index a1edc14ef..256e84edf 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -9,17 +9,16 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null } $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 + & $venv\Scripts\python.exe -m pip install numpy } cd $pillow -& python -VV +& $venv\Scripts\python.exe -VV if (!$?) { exit $LASTEXITCODE } -& python selftest.py +& $venv\Scripts\python.exe selftest.py if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests\check_wheel.py +& $venv\Scripts\python.exe -m pytest -vx Tests\check_wheel.py if (!$?) { exit $LASTEXITCODE } -& python -m pytest -vx Tests +& $venv\Scripts\python.exe -m pytest -vx Tests if (!$?) { exit $LASTEXITCODE } From b9aac77003cda8c4af13fcf7f4fd712506374951 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jun 2025 22:48:27 +1000 Subject: [PATCH 587/628] Test Python 3.14t --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 006d574f3..b4b516228 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,6 +43,7 @@ jobs: python-version: [ "pypy3.11", "pypy3.10", + "3.14t", "3.14", "3.13t", "3.13", @@ -55,6 +56,7 @@ jobs: - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.10", PYTHONOPTIMIZE: 2 } # Free-threaded + - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } # M1 only available for 3.10+ - { os: "macos-13", python-version: "3.9" } From 9bffc015e6d97151cf49e71eed31deb20548652f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jun 2025 23:52:51 +1000 Subject: [PATCH 588/628] Use pypy.exe if it exists --- .github/workflows/wheels-test.ps1 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index 256e84edf..54e7fbbfc 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -9,16 +9,21 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null } $env:path += ";$pillow\winbuild\build\bin\" +if (Test-Path $venv\Scripts\pypy.exe) { + $python = "pypy.exe" +} else { + $python = "python.exe" +} & 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\*") { - & $venv\Scripts\python.exe -m pip install numpy + & $venv\Scripts\$python -m pip install numpy } cd $pillow -& $venv\Scripts\python.exe -VV +& $venv\Scripts\$python -VV if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\python.exe selftest.py +& $venv\Scripts\$python selftest.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\python.exe -m pytest -vx Tests\check_wheel.py +& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\python.exe -m pytest -vx Tests +& $venv\Scripts\$python -m pytest -vx Tests if (!$?) { exit $LASTEXITCODE } From 4a1eea84669dec76e573db2fc25e9b0ec3ea58e3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 9 Jun 2025 13:15:49 +0300 Subject: [PATCH 589/628] Add Python 3.14 beta wheels --- docs/releasenotes/11.3.0.rst | 56 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + tox.ini | 2 +- 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 docs/releasenotes/11.3.0.rst diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst new file mode 100644 index 000000000..b0595def9 --- /dev/null +++ b/docs/releasenotes/11.3.0.rst @@ -0,0 +1,56 @@ +11.3.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +TODO +^^^^ + +TODO + +Other changes +============= + +Python 3.14 beta +^^^^^^^^^^^^^^^^ + +To help other projects prepare for Python 3.14, wheels are now built for the +3.14 beta as a preview. This is not official support for Python 3.14, but rather +an opportunity for you to test how Pillow works with the beta and report any +problems. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 5d7b21d59..a85f1e075 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.3.0 11.2.1 11.1.0 11.0.0 diff --git a/tox.ini b/tox.ini index 4065245ee..967d4b537 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 313, 312, 311, 310, 39} + py{py3, 314, 313, 312, 311, 310, 39} [testenv] deps = From aca0e57126ce5938dfd3cd57eed1a668cac5abc7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 12 Jun 2025 19:22:35 +0300 Subject: [PATCH 590/628] Add 3.14 to CI targets --- docs/installation/platform-support.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 57a2298f8..a56f94316 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -40,12 +40,12 @@ These platforms are built and tested for every change. | macOS 13 Ventura | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | -| | PyPy3 | | +| | 3.14, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | +| | 3.12, 3.13, 3.14, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 | arm64v8, ppc64le, | | | | s390x | @@ -53,7 +53,7 @@ These platforms are built and tested for every change. | Windows Server 2022 | 3.9 | x86 | | +----------------------------+---------------------+ | | 3.10, 3.11, 3.12, 3.13, | x86-64 | -| | PyPy3 | | +| | 3.14, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 (MinGW) | x86-64 | | +----------------------------+---------------------+ From 3841db0252cc2dfd485eae96363dc91cffd65c0c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:08:52 +0300 Subject: [PATCH 591/628] Fix: Invalid skip selector: 'pp39-*' --- .github/workflows/wheels.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 33e1976f0..72516651f 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -110,7 +110,6 @@ jobs: 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-* MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - uses: actions/upload-artifact@v4 @@ -188,7 +187,6 @@ jobs: CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy - CIBW_SKIP: pp39-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow From a3d91cb0ce30bc5c8418956ab9dd32d1b7a13f00 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 14 Jun 2025 05:21:31 +0300 Subject: [PATCH 592/628] CI: Require Python >= 3.13.5 on Windows (#9017) --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6b76351b0..6d8acc44f 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.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] architecture: ["x64"] include: # Test the oldest Python on 32-bit From a219e96fd3e0d4be53b2dad9dfa08bed993c6f80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jun 2025 21:03:08 +1000 Subject: [PATCH 593/628] Fixed warning --- Tests/test_file_ppm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index c7d1f4df4..68f2f9468 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -294,9 +294,10 @@ def test_header_token_too_long(tmp_path: Path, data: bytes) -> None: with open(path, "wb") as f: f.write(data) - with pytest.raises(ValueError, match="Token too long in file header: "): + with pytest.raises(ValueError) as e: with Image.open(path): pass + assert "Token too long in file header: " in repr(e) def test_truncated_file(tmp_path: Path) -> None: From 4ba97d13276f7406c6355e9f81df3255ad392f92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jun 2025 19:36:31 +1000 Subject: [PATCH 594/628] Removed entries for non-existent modes --- src/PIL/TiffImagePlugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 946fbd531..4c0ed01ef 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1680,7 +1680,6 @@ SAVE_INFO = { "PA": ("PA", II, 3, 1, (8, 8), 2), "I": ("I;32S", II, 1, 2, (32,), None), "I;16": ("I;16", II, 1, 1, (16,), None), - "I;16S": ("I;16S", II, 1, 2, (16,), None), "F": ("F;32F", II, 1, 3, (32,), None), "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), @@ -1688,10 +1687,7 @@ SAVE_INFO = { "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), - "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), "I;16B": ("I;16B", MM, 1, 1, (16,), None), - "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), - "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), } From 925fe519043a5056384b2eb4caa4cab792c92780 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jun 2025 19:41:20 +1000 Subject: [PATCH 595/628] Support saving I;16L images --- Tests/test_file_tiff.py | 23 ++++------------------- src/PIL/TiffImagePlugin.py | 3 ++- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 73046eb5f..e92b97c8a 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -49,25 +49,10 @@ class TestFileTiff: assert im.size == (128, 128) assert im.format == "TIFF" - hopper("1").save(filename) - with Image.open(filename): - pass - - hopper("L").save(filename) - with Image.open(filename): - pass - - hopper("P").save(filename) - with Image.open(filename): - pass - - hopper("RGB").save(filename) - with Image.open(filename): - pass - - hopper("I").save(filename) - with Image.open(filename): - pass + for mode in ("1", "L", "P", "RGB", "I", "I;16", "I;16L"): + hopper(mode).save(filename) + with Image.open(filename): + pass @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self) -> None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 4c0ed01ef..146b01e5f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1680,6 +1680,7 @@ SAVE_INFO = { "PA": ("PA", II, 3, 1, (8, 8), 2), "I": ("I;32S", II, 1, 2, (32,), None), "I;16": ("I;16", II, 1, 1, (16,), None), + "I;16L": ("I;16L", II, 1, 1, (16,), None), "F": ("F;32F", II, 1, 3, (32,), None), "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), @@ -1963,7 +1964,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # we're storing image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. - if im.mode in ("I;16B", "I;16"): + if im.mode in ("I;16", "I;16B", "I;16L"): rawmode = "I;16N" # Pass tags as sorted list so that the tags are set in a fixed order. From 5aa09cd1078440578b6cd040f86977358c7983b9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jun 2025 14:56:18 +1000 Subject: [PATCH 596/628] Updated libpng to 1.6.49 --- .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 b46811f5a..996d32bc2 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=11.2.1 -LIBPNG_VERSION=1.6.48 +LIBPNG_VERSION=1.6.49 JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0cc383733..098716b60 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -118,7 +118,7 @@ V = { "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", - "LIBPNG": "1.6.48", + "LIBPNG": "1.6.49", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", From e6af31e709eb61c553d13fb8648772379211f250 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 16:09:11 +1000 Subject: [PATCH 597/628] Deprecate fromarray mode argument --- Tests/test_image_array.py | 6 ++++-- docs/deprecations.rst | 8 ++++++++ docs/releasenotes/11.3.0.rst | 7 ++++--- src/PIL/Image.py | 3 ++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index eb2309e0f..2c71dceb8 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -101,7 +101,8 @@ def test_fromarray_strides_without_tobytes() -> None: with pytest.raises(ValueError): wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) - Image.fromarray(wrapped, "L") + with pytest.warns(DeprecationWarning): + Image.fromarray(wrapped, "L") def test_fromarray_palette() -> None: @@ -110,7 +111,8 @@ def test_fromarray_palette() -> None: a = numpy.array(i) # Act - out = Image.fromarray(a, "P") + with pytest.warns(DeprecationWarning): + out = Image.fromarray(a, "P") # Assert that the Python and C palettes match assert out.palette is not None diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0490ba439..a5d89408b 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -193,6 +193,14 @@ Image.Image.get_child_images() method uses an image's file pointer, and so child images could only be retrieved from an :py:class:`PIL.ImageFile.ImageFile` instance. +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.3.0 + +The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The +mode can be automatically determined from the object's shape and type instead. + Removed features ---------------- diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index b0595def9..f0fa8c858 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -23,10 +23,11 @@ TODO Deprecations ============ -TODO -^^^^ +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The +mode can be automatically determined from the object's shape and type instead. API changes =========== diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7e9540e48..0be9e8ce4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3272,7 +3272,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: :param obj: Object with array interface :param mode: Optional mode to use when reading ``obj``. Will be determined from - type if ``None``. + type if ``None``. Deprecated. This will not be used to convert the data after reading, but will be used to change how the data is read:: @@ -3307,6 +3307,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" raise TypeError(msg) from e else: + deprecate("'mode' parameter", 13) rawmode = mode if mode in ["1", "L", "I", "P", "F"]: ndmax = 2 From 27ce12bb7a4b5e7d8f662d7eb3a6e39fe636b7c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 16:44:42 +1000 Subject: [PATCH 598/628] Added release notes for #8969 --- docs/releasenotes/11.3.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index b0595def9..bf9506a3b 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -47,6 +47,13 @@ TODO Other changes ============= +Do not build against libavif < 1 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow only supports libavif 1.0.0 or later. In order to prevent errors when building +from source, if a user happens to have an earlier libavif on their system, Pillow will +now ignore it. + Python 3.14 beta ^^^^^^^^^^^^^^^^ From 3ac1edf6dafddd26ecfae1dbf041e678d4eda97c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 17:13:02 +1000 Subject: [PATCH 599/628] Added release notes for #8912 --- docs/releasenotes/11.3.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index bf9506a3b..ea22a9c8e 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -47,6 +47,12 @@ TODO Other changes ============= +Support using grim with ImageGrab on Linux +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.ImageGrab.grab` is now able to use ``gnome-screenshot``, ``grim`` or +``spectacle`` on Linux in order to take a snapshot of the screen. + Do not build against libavif < 1 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 59667bbec54c7e37887ba1dfd0a3389c2d5bc413 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 18:39:30 +1000 Subject: [PATCH 600/628] Use *_tofile helpers --- Tests/test_file_blp.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 9f50df22d..f64a9d420 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -7,9 +7,8 @@ import pytest from PIL import BlpImagePlugin, Image from .helper import ( - assert_image_equal, assert_image_equal_tofile, - assert_image_similar, + assert_image_similar_tofile, hopper, ) @@ -52,15 +51,13 @@ def test_save(tmp_path: Path) -> None: im = hopper("P") im.save(f, blp_version=version) - with Image.open(f) as reloaded: - assert_image_equal(im.convert("RGB"), reloaded) + assert_image_equal_tofile(im.convert("RGB"), f) with Image.open("Tests/images/transparent.png") as im: f = tmp_path / "temp.blp" im.convert("P").save(f, blp_version=version) - with Image.open(f) as reloaded: - assert_image_similar(im, reloaded, 8) + assert_image_similar_tofile(im, f, 8) im = hopper() with pytest.raises(ValueError): From ce8083e0d871899294142e09c88d249409334aaf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jun 2025 18:40:03 +1000 Subject: [PATCH 601/628] Match error message --- Tests/test_file_blp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index f64a9d420..5f6b263a1 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -60,7 +60,7 @@ def test_save(tmp_path: Path) -> None: assert_image_similar_tofile(im, f, 8) im = hopper() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Unsupported BLP image mode"): im.save(f) From cb433ad00ad4ad6eeaed8e70c191ea94e8087298 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Jun 2025 08:15:08 +1000 Subject: [PATCH 602/628] Replaced ImagingError_Clear with PyErr_Clear --- src/_imaging.c | 5 ----- src/libImaging/Imaging.h | 2 -- src/libImaging/Storage.c | 2 +- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 2a7bc8d3f..2e8c8b0ad 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -369,11 +369,6 @@ ImagingError_ValueError(const char *message) { return NULL; } -void -ImagingError_Clear(void) { - PyErr_Clear(); -} - /* -------------------------------------------------------------------- */ /* HELPERS */ /* -------------------------------------------------------------------- */ diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 39ecdbff6..29e21c551 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -280,8 +280,6 @@ extern void * ImagingError_Mismatch(void); /* maps to ValueError by default */ extern void * ImagingError_ValueError(const char *message); -extern void -ImagingError_Clear(void); /* Transform callbacks */ /* ------------------- */ diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 11d6c06cc..6fe26e1bd 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -645,7 +645,7 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { return im; } - ImagingError_Clear(); + PyErr_Clear(); // Try to allocate the image once more with smallest possible block size MUTEX_LOCK(&ImagingDefaultArena.mutex); From 8309962926f8e4f77c9899c4c5b763e9f5966311 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Jun 2025 08:19:27 +1000 Subject: [PATCH 603/628] Replaced ImagingError_OSError with PyErr_SetString --- src/_imaging.c | 6 ------ src/libImaging/File.c | 2 +- src/libImaging/Imaging.h | 2 -- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 2e8c8b0ad..6241dc3ca 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -338,12 +338,6 @@ static const char *no_palette = "image has no palette"; static const char *readonly = "image is readonly"; /* static const char* no_content = "image has no content"; */ -void * -ImagingError_OSError(void) { - PyErr_SetString(PyExc_OSError, "error when accessing file"); - return NULL; -} - void * ImagingError_MemoryError(void) { return PyErr_NoMemory(); diff --git a/src/libImaging/File.c b/src/libImaging/File.c index 76d0abccc..901fe83ad 100644 --- a/src/libImaging/File.c +++ b/src/libImaging/File.c @@ -54,7 +54,7 @@ ImagingSavePPM(Imaging im, const char *outfile) { fp = fopen(outfile, "wb"); if (!fp) { - (void)ImagingError_OSError(); + PyErr_SetString(PyExc_OSError, "error when accessing file"); return 0; } diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 29e21c551..bfe67d462 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -270,8 +270,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie); /* Exceptions */ /* ---------- */ -extern void * -ImagingError_OSError(void); extern void * ImagingError_MemoryError(void); extern void * From c19afb94301de3980ea0a6fd7c26aa9ab3c05065 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:05:34 +1000 Subject: [PATCH 604/628] Use names Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/11.3.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index ea22a9c8e..45dff04de 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -50,8 +50,8 @@ Other changes Support using grim with ImageGrab on Linux ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:py:meth:`~PIL.ImageGrab.grab` is now able to use ``gnome-screenshot``, ``grim`` or -``spectacle`` on Linux in order to take a snapshot of the screen. +:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or +Spectacle on Linux in order to take a snapshot of the screen. Do not build against libavif < 1 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 7b5e11deb7441cab16df247f1f0890cd0d303372 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Jun 2025 20:06:53 +1000 Subject: [PATCH 605/628] Updated heading --- docs/releasenotes/11.3.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 45dff04de..ba091fa2c 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -47,11 +47,11 @@ TODO Other changes ============= -Support using grim with ImageGrab on Linux -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Support using more screenshot utilities with ImageGrab on Linux +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or -Spectacle on Linux in order to take a snapshot of the screen. +:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or Spectacle +on Linux in order to take a snapshot of the screen. Do not build against libavif < 1 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From d23d56e195d34734b42452995682e4a9649e5332 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 17 Jun 2025 23:10:15 +1000 Subject: [PATCH 606/628] Deprecate saving I mode images as PNG --- Tests/test_file_png.py | 14 ++++++++++++-- docs/deprecations.rst | 14 ++++++++++++++ docs/releasenotes/11.3.0.rst | 13 ++++++++++--- src/PIL/PngImagePlugin.py | 3 +++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0f0886ab8..15f67385a 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -100,11 +100,11 @@ class TestFilePng: assert im.format == "PNG" assert im.get_format_mimetype() == "image/png" - for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: + for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]: im = hopper(mode) im.save(test_file) with Image.open(test_file) as reloaded: - if mode in ("I", "I;16B"): + if mode == "I;16B": reloaded = reloaded.convert(mode) assert_image_equal(reloaded, im) @@ -801,6 +801,16 @@ class TestFilePng: with Image.open("Tests/images/truncated_end_chunk.png") as im: assert_image_equal_tofile(im, "Tests/images/hopper.png") + def test_deprecation(self, tmp_path: Path) -> None: + test_file = tmp_path / "out.png" + + im = hopper("I") + with pytest.warns(DeprecationWarning): + im.save(test_file) + + with Image.open(test_file) as reloaded: + assert_image_equal(im, reloaded.convert("I")) + @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0490ba439..a36eb4aa7 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -193,6 +193,20 @@ Image.Image.get_child_images() method uses an image's file pointer, and so child images could only be retrieved from an :py:class:`PIL.ImageFile.ImageFile` instance. +Saving I mode images as PNG +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.3.0 + +In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain +at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly +changing the data, this is now deprecated. Instead, the image can be converted to +another mode before saving:: + + from PIL import Image + im = Image.new("I", (1, 1)) + im.convert("I;16").save("out.png") + Removed features ---------------- diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index ba091fa2c..5dd151bf3 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -23,10 +23,17 @@ TODO Deprecations ============ -TODO -^^^^ +Saving I mode images as PNG +^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain +at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly +changing the data, this is now deprecated. Instead, the image can be converted to +another mode before saving:: + + from PIL import Image + im = Image.new("I", (1, 1)) + im.convert("I;16").save("out.png") API changes =========== diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index f3815a122..7999381a6 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -48,6 +48,7 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +from ._deprecate import deprecate from ._util import DeferredError TYPE_CHECKING = False @@ -1368,6 +1369,8 @@ def _save( except KeyError as e: msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e + if outmode == "I": + deprecate("Saving I mode images as PNG", 13) # # write minimal PNG file From a4e8d675b4e0eeba79d4579bca8621bea7ee68fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Jun 2025 21:59:31 +1000 Subject: [PATCH 607/628] Only check DHT marker for libjpeg-turbo --- Tests/test_file_jpeg.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 2827937cf..614044d97 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1067,10 +1067,16 @@ class TestFileJpeg: for marker in b"\xff\xd8", b"\xff\xd9": assert marker in data[1] assert marker in data[2] - # DHT, DQT - for marker in b"\xff\xc4", b"\xff\xdb": + + # DQT + markers = [b"\xff\xdb"] + if features.check_feature("libjpeg_turbo"): + # DHT + markers.append(b"\xff\xc4") + for marker in markers: assert marker in data[1] assert marker not in data[2] + # SOF0, SOS, APP0 (JFIF header) for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": assert marker not in data[1] From 79e0b0b6ada86f16bf7a8cf1c878756225d85d44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Jun 2025 22:19:20 +1000 Subject: [PATCH 608/628] Allow for custom stacklevel in deprecations --- src/PIL/PngImagePlugin.py | 2 +- src/PIL/_deprecate.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 7999381a6..1b9a89aef 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1370,7 +1370,7 @@ def _save( msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e if outmode == "I": - deprecate("Saving I mode images as PNG", 13) + deprecate("Saving I mode images as PNG", 13, stacklevel=4) # # write minimal PNG file diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 9f9d8bbc9..170d44490 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -12,6 +12,7 @@ def deprecate( *, action: str | None = None, plural: bool = False, + stacklevel: int = 3, ) -> None: """ Deprecations helper. @@ -67,5 +68,5 @@ def deprecate( warnings.warn( f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", DeprecationWarning, - stacklevel=3, + stacklevel=stacklevel, ) From 92de1db067112d5b15c034036b2216009822a3b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:12:40 +1000 Subject: [PATCH 609/628] Update dependency mypy to v1.16.1 (#9026) --- .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 a9c18ae2b..44b5badab 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.16.0 +mypy==1.16.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From ef0bab0c6579fd00987740a6a327603ff3886a38 Mon Sep 17 00:00:00 2001 From: thisismypassport <109758321+thisismypassport@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:16:26 +0300 Subject: [PATCH 610/628] Support writing QOI images (#9007) Co-authored-by: Andrew Murray --- Tests/test_file_qoi.py | 23 +++++- docs/handbook/image-file-formats.rst | 29 +++++-- docs/releasenotes/11.3.0.rst | 7 ++ src/PIL/QoiImagePlugin.py | 119 +++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index 7ce1da209..b9becb24f 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -1,10 +1,12 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, QoiImagePlugin -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal_tofile, hopper def test_sanity() -> None: @@ -34,3 +36,22 @@ def test_op_index() -> None: # QOI_OP_INDEX as the first chunk with Image.open("Tests/images/op_index.qoi") as im: assert im.getpixel((0, 0)) == (0, 0, 0, 0) + + +def test_save(tmp_path: Path) -> None: + f = tmp_path / "temp.qoi" + + im = hopper() + im.save(f, colorspace="sRGB") + + assert_image_equal_tofile(im, f) + + for path in ("Tests/images/default_font.png", "Tests/images/pil123rgba.png"): + with Image.open(path) as im: + im.save(f) + + assert_image_equal_tofile(im, f) + + im = hopper("P") + with pytest.raises(ValueError, match="Unsupported QOI image mode"): + im.save(f) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 5ca549c37..a15e84574 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1082,6 +1082,26 @@ Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. +QOI +^^^ + +.. versionadded:: 9.5.0 + +Pillow reads and writes images in Quite OK Image format using a Python codec. If you +wish to write code specifically for this format, :pypi:`qoi` is an alternative library +that uses C to decode the image and interfaces with NumPy. + +.. _qoi-saving: + +Saving +~~~~~~ + +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**colorspace** + If set to "sRGB", the colorspace will be written as sRGB with linear alpha, instead + of all channels being linear. + SGI ^^^ @@ -1578,15 +1598,6 @@ PSD Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. -QOI -^^^ - -.. versionadded:: 9.5.0 - -Pillow reads images in Quite OK Image format using a Python decoder. If you wish to -write code specifically for this format, :pypi:`qoi` is an alternative library that -uses C to decode the image and interfaces with NumPy. - SUN ^^^ diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index ba091fa2c..6bd4f7481 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -47,6 +47,13 @@ TODO Other changes ============= +Added QOI saving +^^^^^^^^^^^^^^^^ + +Support has been added for saving QOI images. ``colorspace`` can be used to specify the +colorspace as sRGB with linear alpha, e.g. ``im.save("out.qoi", colorspace="sRGB")``. +By default, all channels will be linear. + Support using more screenshot utilities with ImageGrab on Linux ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 75070abd7..dba5d809f 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -8,9 +8,12 @@ from __future__ import annotations import os +from typing import IO from . import Image, ImageFile from ._binary import i32be as i32 +from ._binary import o8 +from ._binary import o32be as o32 def _accept(prefix: bytes) -> bool: @@ -110,6 +113,122 @@ class QoiDecoder(ImageFile.PyDecoder): return -1, 0 +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.mode == "RGB": + channels = 3 + elif im.mode == "RGBA": + channels = 4 + else: + msg = "Unsupported QOI image mode" + raise ValueError(msg) + + colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1 + + fp.write(b"qoif") + fp.write(o32(im.size[0])) + fp.write(o32(im.size[1])) + fp.write(o8(channels)) + fp.write(o8(colorspace)) + + ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)]) + + +class QoiEncoder(ImageFile.PyEncoder): + _pushes_fd = True + _previous_pixel: tuple[int, int, int, int] | None = None + _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {} + _run = 0 + + def _write_run(self) -> bytes: + data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN + self._run = 0 + return data + + def _delta(self, left: int, right: int) -> int: + result = (left - right) & 255 + if result >= 128: + result -= 256 + return result + + def encode(self, bufsize: int) -> tuple[int, int, bytes]: + assert self.im is not None + + self._previously_seen_pixels = {0: (0, 0, 0, 0)} + self._previous_pixel = (0, 0, 0, 255) + + data = bytearray() + w, h = self.im.size + bands = Image.getmodebands(self.mode) + + for y in range(h): + for x in range(w): + pixel = self.im.getpixel((x, y)) + if bands == 3: + pixel = (*pixel, 255) + + if pixel == self._previous_pixel: + self._run += 1 + if self._run == 62: + data += self._write_run() + else: + if self._run: + data += self._write_run() + + r, g, b, a = pixel + hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + if self._previously_seen_pixels.get(hash_value) == pixel: + data += o8(hash_value) # QOI_OP_INDEX + elif self._previous_pixel: + self._previously_seen_pixels[hash_value] = pixel + + prev_r, prev_g, prev_b, prev_a = self._previous_pixel + if prev_a == a: + delta_r = self._delta(r, prev_r) + delta_g = self._delta(g, prev_g) + delta_b = self._delta(b, prev_b) + + if ( + -2 <= delta_r < 2 + and -2 <= delta_g < 2 + and -2 <= delta_b < 2 + ): + data += o8( + 0b01000000 + | (delta_r + 2) << 4 + | (delta_g + 2) << 2 + | (delta_b + 2) + ) # QOI_OP_DIFF + else: + delta_gr = self._delta(delta_r, delta_g) + delta_gb = self._delta(delta_b, delta_g) + if ( + -8 <= delta_gr < 8 + and -32 <= delta_g < 32 + and -8 <= delta_gb < 8 + ): + data += o8( + 0b10000000 | (delta_g + 32) + ) # QOI_OP_LUMA + data += o8((delta_gr + 8) << 4 | (delta_gb + 8)) + else: + data += o8(0b11111110) # QOI_OP_RGB + data += bytes(pixel[:3]) + else: + data += o8(0b11111111) # QOI_OP_RGBA + data += bytes(pixel) + + self._previous_pixel = pixel + + if self._run: + data += self._write_run() + data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding + + return len(data), 0, data + + Image.register_open(QoiImageFile.format, QoiImageFile, _accept) Image.register_decoder("qoi", QoiDecoder) Image.register_extension(QoiImageFile.format, ".qoi") + +Image.register_save(QoiImageFile.format, _save) +Image.register_encoder("qoi", QoiEncoder) From f937dd27cd670971b93f4bdf17abccc0338e6b4c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Jun 2025 23:44:30 +1000 Subject: [PATCH 611/628] Do not call sys.executable in PyInstaller application --- src/PIL/ImageShow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index dd240fb55..7705608e3 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -175,7 +175,9 @@ class MacViewer(Viewer): if not os.path.exists(path): raise FileNotFoundError subprocess.call(["open", "-a", "Preview.app", path]) - executable = sys.executable or shutil.which("python3") + + pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") + executable = (not pyinstaller and sys.executable) or shutil.which("python3") if executable: subprocess.Popen( [ From 216dc4ca60947b4076defa2f0565f8b74a7864e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Jun 2025 19:12:23 +1000 Subject: [PATCH 612/628] Added Python 3.14 macOS x86-64 wheels --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 72516651f..229e23ef0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -58,7 +58,7 @@ jobs: - name: "macOS 10.13 x86_64" os: macos-13 cibw_arch: x86_64 - build: "cp3{12,13}*" + build: "cp3{12,13,14}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" os: macos-13 From ae025183148a900511e7f8866ca5c28f3efc7b5f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 22 Jun 2025 22:08:51 +1000 Subject: [PATCH 613/628] Use same AVIF URL when fetching dependency (#8871) --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 098716b60..4baa5ccef 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -385,8 +385,8 @@ DEPS: dict[str, dict[str, Any]] = { "bins": [r"*.dll"], }, "libavif": { - "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", - "filename": f"libavif-{V['LIBAVIF']}.zip", + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.tar.gz", + "filename": f"libavif-{V['LIBAVIF']}.tar.gz", "license": "LICENSE", "build": [ "rustup update", From 2954964cd2b70c463d75bd7f828c7bb7f3802bf2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 23 Jun 2025 07:05:43 +1000 Subject: [PATCH 614/628] Removed ImageCmsProfile._set method (#9032) Co-authored-by: Luke Granger-Brown --- src/PIL/ImageCms.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index fdfbee789..a1584f111 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -248,6 +248,9 @@ class ImageCmsProfile: low-level profile object """ + self.filename = None + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info if isinstance(profile, str): if sys.platform == "win32": @@ -256,23 +259,18 @@ class ImageCmsProfile: profile_bytes_path.decode("ascii") except UnicodeDecodeError: with open(profile, "rb") as f: - self._set(core.profile_frombytes(f.read())) + self.profile = core.profile_frombytes(f.read()) return - self._set(core.profile_open(profile), profile) + self.filename = profile + self.profile = core.profile_open(profile) elif hasattr(profile, "read"): - self._set(core.profile_frombytes(profile.read())) + self.profile = core.profile_frombytes(profile.read()) elif isinstance(profile, core.CmsProfile): - self._set(profile) + self.profile = profile else: msg = "Invalid type for Profile" # type: ignore[unreachable] raise TypeError(msg) - def _set(self, profile: core.CmsProfile, filename: str | None = None) -> None: - self.profile = profile - self.filename = filename - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info - def tobytes(self) -> bytes: """ Returns the profile in a format suitable for embedding in From 1557585411f1f891180775dab40cfeec236d69bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Jun 2025 20:29:38 +1000 Subject: [PATCH 615/628] Use percent formatting --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3716a7b9f..256c78fc9 100644 --- a/setup.py +++ b/setup.py @@ -509,11 +509,11 @@ class pil_build_ext(build_ext): if root is None and pkg_config: if isinstance(lib_name, str): - _dbg(f"Looking for `{lib_name}` using pkg-config.") + _dbg("Looking for `%s` using pkg-config.", lib_name) root = pkg_config(lib_name) else: for lib_name2 in lib_name: - _dbg(f"Looking for `{lib_name2}` using pkg-config.") + _dbg("Looking for `%s` using pkg-config.", lib_name2) root = pkg_config(lib_name2) if root: break From 18f8af78d3f56639f91f4b788ab9d89ae573b72c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Jun 2025 20:35:09 +1000 Subject: [PATCH 616/628] Pass strings or tuples of strings to _dbg --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 256c78fc9..f05379d6d 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,6 @@ import subprocess import sys import warnings from collections.abc import Iterator -from typing import Any from setuptools import Extension, setup from setuptools.command.build_ext import build_ext @@ -148,7 +147,7 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -def _dbg(s: str, tp: Any = None) -> None: +def _dbg(s: str, tp: str | tuple[str, ...] | None = None) -> None: if DEBUG: if tp: print(s % tp) @@ -732,7 +731,7 @@ class pil_build_ext(build_ext): best_path = os.path.join(directory, name) _dbg( "Best openjpeg version %s so far in %s", - (best_version, best_path), + (str(best_version), best_path), ) if best_version and _find_library_file(self, "openjp2"): From acd8b0c2acfa1c69f91a14d5881a7f9bf436900f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:09:31 +1000 Subject: [PATCH 617/628] Fix libtiff cleanup (#9002) --- src/libImaging/TiffDecode.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 2e83fb847..e289ce405 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -1032,7 +1032,10 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - if (!clientstate->fp) { + if (clientstate->fp) { + TIFFCleanup(tiff); + clientstate->tiff = NULL; + } else { free(clientstate->data); } return -1; From e1ee8afc7d1d0a1b3b91ce4caa018e4bb0a96e71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Jun 2025 18:58:29 +1000 Subject: [PATCH 618/628] Search for libtiff library file first on Windows and macOS --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f05379d6d..354e09f85 100644 --- a/setup.py +++ b/setup.py @@ -753,12 +753,12 @@ class pil_build_ext(build_ext): if feature.want("tiff"): _dbg("Looking for tiff") if _find_include_file(self, "tiff.h"): - if _find_library_file(self, "tiff"): - feature.set("tiff", "tiff") if sys.platform in ["win32", "darwin"] and _find_library_file( self, "libtiff" ): feature.set("tiff", "libtiff") + elif _find_library_file(self, "tiff"): + feature.set("tiff", "tiff") if feature.want("freetype"): _dbg("Looking for freetype") From 3d261a210192509f2c7980f87a922b7b3f3404dc Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Thu, 26 Jun 2025 02:21:44 -0400 Subject: [PATCH 619/628] Add AVIF to wheels using only aomenc and dav1d AVIF codecs for reduced size (#8858) Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 55 ++++ .github/workflows/wheels.yml | 2 +- Tests/check_wheel.py | 6 +- docs/releasenotes/11.3.0.rst | 6 + wheels/dependency_licenses/AOM.txt | 26 ++ wheels/dependency_licenses/DAV1D.txt | 23 ++ wheels/dependency_licenses/LIBAVIF.txt | 387 +++++++++++++++++++++++ wheels/dependency_licenses/LIBYUV.txt | 29 ++ winbuild/build_prepare.py | 15 +- 9 files changed, 542 insertions(+), 7 deletions(-) create mode 100644 wheels/dependency_licenses/AOM.txt create mode 100644 wheels/dependency_licenses/DAV1D.txt create mode 100644 wheels/dependency_licenses/LIBAVIF.txt create mode 100644 wheels/dependency_licenses/LIBYUV.txt diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 996d32bc2..5384a74c0 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -51,6 +51,7 @@ LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +LIBAVIF_VERSION=1.3.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi @@ -98,6 +99,59 @@ function build_harfbuzz { touch harfbuzz-stamp } +function build_libavif { + if [ -e libavif-stamp ]; then return; fi + + python3 -m pip install meson ninja + + if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then + build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 + fi + + local build_type=MinSizeRel + local lto=ON + + local libavif_cmake_flags + + if [ -n "$IS_MACOS" ]; then + lto=OFF + libavif_cmake_flags=( + -DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ + -DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ + -DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \ + ) + else + if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then + build_type=Release + fi + libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") + fi + + local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) + # CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject + # of libavif) that disables support for encoding high bit depth images. + (cd $out_dir \ + && cmake \ + -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ + -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ + -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \ + -DBUILD_SHARED_LIBS=ON \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + -DAVIF_CODEC_AOM=LOCAL \ + -DCONFIG_AV1_HIGHBITDEPTH=0 \ + -DAVIF_CODEC_AOM_DECODE=OFF \ + -DAVIF_CODEC_DAV1D=LOCAL \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \ + -DCMAKE_C_VISIBILITY_PRESET=hidden \ + -DCMAKE_CXX_VISIBILITY_PRESET=hidden \ + -DCMAKE_BUILD_TYPE=$build_type \ + "${libavif_cmake_flags[@]}" \ + . \ + && make install) + touch libavif-stamp +} + function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then @@ -132,6 +186,7 @@ function build { build_tiff fi + build_libavif build_libpng build_lcms2 build_openjpeg diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 229e23ef0..16c350a14 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -159,7 +159,7 @@ jobs: # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images - & python.exe winbuild\build_prepare.py -v --no-imagequant --no-avif --architecture=${{ matrix.cibw_arch }} + & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} shell: pwsh - name: Build wheels diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 9602410da..a78fb09b0 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -9,7 +9,7 @@ from .helper import is_pypy def test_wheel_modules() -> None: - expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} if sys.platform == "win32": # tkinter is not available in cibuildwheel installed CPython on Windows @@ -20,6 +20,10 @@ def test_wheel_modules() -> None: except ImportError: expected_modules.remove("tkinter") + # libavif is not available on Windows for ARM64 architectures + if platform.machine() == "ARM64": + expected_modules.remove("avif") + assert set(features.get_supported_modules()) == expected_modules diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 82e4d44b6..654a7e6b6 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -80,6 +80,12 @@ Pillow only supports libavif 1.0.0 or later. In order to prevent errors when bui from source, if a user happens to have an earlier libavif on their system, Pillow will now ignore it. +AVIF support in wheels +^^^^^^^^^^^^^^^^^^^^^^ + +Support for reading and writing AVIF images is now included in Pillow's wheels, except +for Windows ARM64. libaom is available as an encoder and dav1d as a decoder. + Python 3.14 beta ^^^^^^^^^^^^^^^^ diff --git a/wheels/dependency_licenses/AOM.txt b/wheels/dependency_licenses/AOM.txt new file mode 100644 index 000000000..3a2e46c26 --- /dev/null +++ b/wheels/dependency_licenses/AOM.txt @@ -0,0 +1,26 @@ +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/DAV1D.txt b/wheels/dependency_licenses/DAV1D.txt new file mode 100644 index 000000000..875b138ec --- /dev/null +++ b/wheels/dependency_licenses/DAV1D.txt @@ -0,0 +1,23 @@ +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBAVIF.txt b/wheels/dependency_licenses/LIBAVIF.txt new file mode 100644 index 000000000..350eb9d15 --- /dev/null +++ b/wheels/dependency_licenses/LIBAVIF.txt @@ -0,0 +1,387 @@ +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wheels/dependency_licenses/LIBYUV.txt b/wheels/dependency_licenses/LIBYUV.txt new file mode 100644 index 000000000..c911747a6 --- /dev/null +++ b/wheels/dependency_licenses/LIBYUV.txt @@ -0,0 +1,29 @@ +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4baa5ccef..187d07b20 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -57,7 +57,10 @@ def cmd_nmake( def cmds_cmake( - target: str | tuple[str, ...] | list[str], *params: str, build_dir: str = "." + target: str | tuple[str, ...] | list[str], + *params: str, + build_dir: str = ".", + build_type: str = "Release", ) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -66,7 +69,7 @@ def cmds_cmake( " ".join( [ "{cmake}", - "-DCMAKE_BUILD_TYPE=Release", + f"-DCMAKE_BUILD_TYPE={build_type}", "-DCMAKE_VERBOSE_MAKEFILE=ON", "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake "-DCMAKE_C_COMPILER=cl.exe", # for Ninja @@ -397,9 +400,11 @@ DEPS: dict[str, dict[str, Any]] = { "-DAVIF_LIBSHARPYUV=LOCAL", "-DAVIF_LIBYUV=LOCAL", "-DAVIF_CODEC_AOM=LOCAL", + "-DCONFIG_AV1_HIGHBITDEPTH=0", + "-DAVIF_CODEC_AOM_DECODE=OFF", "-DAVIF_CODEC_DAV1D=LOCAL", - "-DAVIF_CODEC_RAV1E=LOCAL", - "-DAVIF_CODEC_SVT=LOCAL", + "-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON", + build_type="MinSizeRel", ), cmd_xcopy("include", "{inc_dir}"), ], @@ -755,7 +760,7 @@ def main() -> None: disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] - if args.no_avif or args.architecture != "AMD64": + if args.no_avif or args.architecture == "ARM64": disabled += ["libavif"] prefs = { From b9afe18646c5dc28efed1a74579129f8c64976e2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:00:19 +0300 Subject: [PATCH 620/628] Bump pre-commit hooks --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1a054e00..c64858299 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.11.12 + rev: v0.12.0 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.8.3 + rev: 1.8.5 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: v20.1.5 + rev: v20.1.6 hooks: - id: clang-format types: [c] @@ -51,7 +51,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.0 + rev: 0.33.1 hooks: - id: check-github-workflows - id: check-readthedocs From 234875bf9001037f7510769f83e281bfe6012441 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:01:14 +0300 Subject: [PATCH 621/628] Update Ruff hook from legacy --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c64858299..6abb732bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.12.0 hooks: - - id: ruff + - id: ruff-check args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror From d1894dcd46c9096a91dd6ab39fe1df918ba18d6c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:47:06 +0300 Subject: [PATCH 622/628] Add match parameter to pytest.warns() --- Tests/helper.py | 2 +- Tests/test_core_resources.py | 2 +- Tests/test_features.py | 13 ++++++------- Tests/test_file_apng.py | 8 ++++---- Tests/test_file_avif.py | 4 ++-- Tests/test_file_gif.py | 6 ++++-- Tests/test_file_icns.py | 4 +++- Tests/test_file_ico.py | 2 +- Tests/test_file_iptc.py | 6 +++--- Tests/test_file_jpeg.py | 13 +++++++------ Tests/test_file_png.py | 2 +- Tests/test_file_tga.py | 4 +++- Tests/test_file_tiff.py | 4 ++-- Tests/test_file_tiff_metadata.py | 4 ++-- Tests/test_file_webp.py | 4 ++-- Tests/test_image.py | 12 ++++++------ Tests/test_image_access.py | 2 +- Tests/test_image_array.py | 6 +++--- Tests/test_image_convert.py | 5 ++++- Tests/test_image_getim.py | 4 ++-- Tests/test_image_putdata.py | 2 +- Tests/test_imagecms.py | 14 +++++++------- Tests/test_imagedraw.py | 2 +- Tests/test_imagefile.py | 2 +- Tests/test_imagefont.py | 12 ++++++------ Tests/test_imagemath_lambda_eval.py | 2 +- Tests/test_imagemath_unsafe_eval.py | 4 ++-- Tests/test_lib_pack.py | 4 +++- 28 files changed, 80 insertions(+), 69 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index ec61cd263..e71b4665b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -272,7 +272,7 @@ def _cached_hopper(mode: str) -> Image.Image: else: im = hopper() if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;"): im = im.convert(mode) else: try: diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 2c1de8bc3..2a22f805d 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -188,5 +188,5 @@ class TestEnvVars: ), ) def test_warnings(self, var: dict[str, str]) -> None: - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match=list(var)[0]): Image._apply_env_variables(var) diff --git a/Tests/test_features.py b/Tests/test_features.py index f8f7f6eec..d06fb4d84 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -19,7 +19,7 @@ def test_check() -> None: assert features.check_codec(codec) == features.check(codec) for feature in features.features: if "webp" in feature: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp"): assert features.check_feature(feature) == features.check(feature) else: assert features.check_feature(feature) == features.check(feature) @@ -49,24 +49,24 @@ def test_version() -> None: test(codec, features.version_codec) for feature in features.features: if "webp" in feature: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp"): test(feature, features.version_feature) else: test(feature, features.version_feature) def test_webp_transparency() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="transp_webp"): assert (features.check("transp_webp") or False) == features.check_module("webp") def test_webp_mux() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp_mux"): assert (features.check("webp_mux") or False) == features.check_module("webp") def test_webp_anim() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="webp_anim"): assert (features.check("webp_anim") or False) == features.check_module("webp") @@ -95,10 +95,9 @@ def test_check_codecs(feature: str) -> None: def test_check_warns_on_nonexistent() -> None: - with pytest.warns(UserWarning) as cm: + with pytest.warns(UserWarning, match="Unknown feature 'typo'."): has_feature = features.check("typo") assert has_feature is False - assert str(cm[-1].message) == "Unknown feature 'typo'." def test_supported_modules() -> None: diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index a5734c202..66410b3da 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -303,7 +303,7 @@ def test_apng_chunk_errors() -> None: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() assert isinstance(im, PngImagePlugin.PngImageFile) @@ -330,14 +330,14 @@ def test_apng_chunk_errors() -> None: def test_apng_syntax_errors() -> None: - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.raises(OSError): im.load() - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated @@ -354,7 +354,7 @@ def test_apng_syntax_errors() -> None: im.seek(im.n_frames - 1) im.load() - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index b2e586637..e42e10291 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -77,8 +77,8 @@ class TestUnsupportedAvif: def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) - with pytest.warns(UserWarning): - with pytest.raises(UnidentifiedImageError): + with pytest.raises(UnidentifiedImageError): + with pytest.warns(UserWarning, match="AVIF support not installed"): with Image.open(TEST_AVIF_FILE): pass diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 20d58a9dd..29bc55ee5 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1229,7 +1229,9 @@ def test_removed_transparency(tmp_path: Path) -> None: im.putpixel((x, 0), (x, 0, 0)) im.info["transparency"] = (255, 255, 255) - with pytest.warns(UserWarning): + with pytest.warns( + UserWarning, match="Couldn't allocate palette entry for transparency" + ): im.save(out) with Image.open(out) as reloaded: @@ -1251,7 +1253,7 @@ def test_rgb_transparency(tmp_path: Path) -> None: im = Image.new("RGB", (1, 1)) im.info["transparency"] = b"" ims = [Image.new("RGB", (1, 1))] - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="should be converted to RGBA images"): im.save(out, save_all=True, append_images=ims) with Image.open(out) as reloaded: diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 2dabfd2f3..8ff59161f 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -95,7 +95,9 @@ def test_sizes() -> None: for w, h, r in im.info["sizes"]: wr = w * r hr = h * r - with pytest.warns(DeprecationWarning): + with pytest.warns( + DeprecationWarning, match=r"Setting size to \(width, height, scale\)" + ): im.size = (w, h, r) im.load() assert im.mode == "RGBA" diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 5d2ace35e..99c312ead 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -233,7 +233,7 @@ def test_save_append_images(tmp_path: Path) -> None: def test_unexpected_size() -> None: # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Image was not the expected size"): with Image.open("Tests/images/hopper_unexpected.ico") as im: assert im.size == (16, 16) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index c6c0c1aab..ffd3aad3e 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -99,7 +99,7 @@ def test_i() -> None: c = b"a" # Act - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="IptcImagePlugin.i"): ret = IptcImagePlugin.i(c) # Assert @@ -114,7 +114,7 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "stdout", mystdout) # Act - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="IptcImagePlugin.dump"): IptcImagePlugin.dump(c) # Assert @@ -122,5 +122,5 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: def test_pad_deprecation() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="IptcImagePlugin.PAD"): assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 614044d97..0ba08aaf9 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -752,10 +752,11 @@ class TestFileJpeg: # Act # Shouldn't raise error - fn = "Tests/images/sugarshack_bad_mpo_header.jpg" - with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert - assert im.format == "JPEG" + with pytest.warns(UserWarning, match="malformed MPO file"): + im = Image.open("Tests/images/sugarshack_bad_mpo_header.jpg") + + # Assert + assert im.format == "JPEG" @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) def test_save_correct_modes(self, mode: str) -> None: @@ -1103,9 +1104,9 @@ class TestFileJpeg: def test_deprecation(self) -> None: with Image.open(TEST_FILE) as im: assert isinstance(im, JpegImagePlugin.JpegImageFile) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="huffman_ac"): assert im.huffman_ac == {} - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="huffman_dc"): assert im.huffman_dc == {} diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 15f67385a..0a51fd493 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -805,7 +805,7 @@ class TestFilePng: test_file = tmp_path / "out.png" im = hopper("I") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="Saving I mode images as PNG"): im.save(test_file) with Image.open(test_file) as reloaded: diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 8b6ed3ed2..27ff4f1a4 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -190,7 +190,9 @@ def test_save_id_section(tmp_path: Path) -> None: # Save with custom id section greater than 255 characters id_section = b"Test content" * 25 - with pytest.warns(UserWarning): + with pytest.warns( + UserWarning, match="id_section has been trimmed to 255 characters" + ): im.save(out, id_section=id_section) with Image.open(out) as test_im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index e92b97c8a..046a9f1f1 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -221,7 +221,7 @@ class TestFileTiff: assert isinstance(im, JpegImagePlugin.JpegImageFile) # Should not raise struct.error. - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Corrupt EXIF data"): im._getexif() def test_save_rgba(self, tmp_path: Path) -> None: @@ -1014,7 +1014,7 @@ class TestFileTiff: @timeout_unless_slower_valgrind(2) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Corrupt EXIF data"): with Image.open(test_file): pass diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 884868345..36ad8cee9 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -300,7 +300,7 @@ def test_empty_metadata() -> None: head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) # Should not raise struct.error. - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Corrupt EXIF data"): info.load(f) @@ -481,7 +481,7 @@ def test_too_many_entries() -> None: ifd.tagtype[277] = TiffTags.SHORT # Should not raise ValueError. - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="Metadata Warning"): assert ifd[277] == 4 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f61e2c82e..916ea56fc 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -33,8 +33,8 @@ class TestUnsupportedWebp: monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False) file_path = "Tests/images/hopper.webp" - with pytest.warns(UserWarning): - with pytest.raises(OSError): + with pytest.raises(OSError): + with pytest.warns(UserWarning, match="WEBP support not installed"): with Image.open(file_path): pass diff --git a/Tests/test_image.py b/Tests/test_image.py index b018b4309..069083b19 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -53,7 +53,7 @@ except ImportError: # Deprecation helper def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;"): return Image.new(mode, size) else: return Image.new(mode, size) @@ -141,8 +141,8 @@ class TestImage: monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True) im = io.BytesIO(b"") - with pytest.warns(UserWarning): - with pytest.raises(UnidentifiedImageError): + with pytest.raises(UnidentifiedImageError): + with pytest.warns(UserWarning, match="opening failed"): with Image.open(im): pass @@ -1008,7 +1008,7 @@ class TestImage: def test_get_child_images(self) -> None: im = Image.new("RGB", (1, 1)) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"): assert im.get_child_images() == [] @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @@ -1139,7 +1139,7 @@ class TestImage: assert im.fp is None def test_deprecation(self) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="Image.isImageType"): assert not Image.isImageType(None) @@ -1150,7 +1150,7 @@ class TestImageBytes: source_bytes = im.tobytes() if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match=mode): reloaded = Image.frombytes(mode, im.size, source_bytes) else: reloaded = Image.frombytes(mode, im.size, source_bytes) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 14a5e2e7b..66412a035 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -193,7 +193,7 @@ class TestImageGetPixel: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) def test_deprecated(self, mode: str) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;"): self.check(mode) def test_list(self) -> None: diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 2c71dceb8..c27ce13d5 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -47,7 +47,7 @@ def test_toarray() -> None: with pytest.raises(OSError): numpy.array(im_truncated) else: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="__array_interface__"): numpy.array(im_truncated) @@ -101,7 +101,7 @@ def test_fromarray_strides_without_tobytes() -> None: with pytest.raises(ValueError): wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="'mode' parameter"): Image.fromarray(wrapped, "L") @@ -111,7 +111,7 @@ def test_fromarray_palette() -> None: a = numpy.array(i) # Act - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="'mode' parameter"): out = Image.fromarray(a, "P") # Assert that the Python and C palettes match diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 7d4f78c23..33f844437 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -203,7 +203,10 @@ def test_trns_RGB(tmp_path: Path) -> None: assert "transparency" not in im_rgba.info assert im_rgba.getpixel((0, 0)) == (0, 0, 0, 0) - im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE) + with pytest.warns( + UserWarning, match="Couldn't allocate palette entry for transparency" + ): + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) assert "transparency" not in im_p.info im_p.save(f) diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index fa58492fc..7b5f7a589 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -11,9 +11,9 @@ def test_sanity() -> None: type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="id property"): assert isinstance(im.im.id, int) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="unsafe_ptrs property"): ptrs = dict(im.im.unsafe_ptrs) assert ptrs.keys() == {"image8", "image32", "image"} diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 27cb7c59d..34c1763b8 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -81,7 +81,7 @@ def test_mode_F() -> None: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) def test_mode_BGR(mode: str) -> None: data = [(16, 32, 49), (32, 32, 98)] - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match=mode): im = Image.new(mode, (1, 2)) im.putdata(data) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index f062651f0..b6db0ab5c 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -54,7 +54,7 @@ def skip_missing() -> None: def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="PIL.ImageCms.versions"): v = ImageCms.versions() # should return four strings assert v[0] == "1.0.0 pil" assert list(map(type, v)) == [str, str, str, str] @@ -679,7 +679,7 @@ def test_auxiliary_channels_isolated() -> None: def test_long_modes() -> None: p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ABCDEFGHI"): ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI") @@ -703,15 +703,15 @@ def test_cmyk_lab() -> None: def test_deprecation() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageCms.DESCRIPTION"): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageCms.VERSION"): assert ImageCms.VERSION == "1.0.0 pil" - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageCms.FLAGS"): assert isinstance(ImageCms.FLAGS, dict) profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="RGBA;16B"): ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB") - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="RGBA;16B"): ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B") diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 37669a2e5..881f9c85d 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1735,5 +1735,5 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: def test_getdraw() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="'hints' parameter"): ImageDraw.getdraw(None, []) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 7622eea99..a9444c26d 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -152,7 +152,7 @@ class TestImageFile: assert reads.count(im.decodermaxblock) == 1 def test_raise_oserror(self) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="raise_oserror"): with pytest.raises(OSError): ImageFile.raise_oserror(1) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 69533c2f8..aa8bbb339 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1175,15 +1175,15 @@ def test_oom(test_file: str) -> None: def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) - with pytest.warns(UserWarning) as record: + with pytest.warns( + UserWarning, + match="Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout.", + ): font = ImageFont.truetype( FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM ) assert font.layout_engine == ImageFont.Layout.BASIC - assert str(record[-1].message) == ( - "Raqm layout was requested, but Raqm is not available. " - "Falling back to basic layout." - ) @pytest.mark.parametrize("size", [-1, 0]) @@ -1202,5 +1202,5 @@ def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(features, "version_module", fake_version_module) # Act / Assert - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="FreeType 2.9.0"): ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index 360325780..eec76118a 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -56,7 +56,7 @@ def test_sanity() -> None: def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageMath.lambda_eval options"): assert ImageMath.lambda_eval(lambda args: 1, images) == 1 diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py index b7ac84691..60ad6aafa 100644 --- a/Tests/test_imagemath_unsafe_eval.py +++ b/Tests/test_imagemath_unsafe_eval.py @@ -36,12 +36,12 @@ def test_sanity() -> None: def test_eval_deprecated() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageMath.eval"): assert ImageMath.eval("1") == 1 def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="ImageMath.unsafe_eval options"): assert ImageMath.unsafe_eval("1", images) == 1 diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index b4a300d0c..2d6af70eb 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -362,13 +362,15 @@ class TestLibUnpack: ) def test_BGR(self) -> None: - with pytest.warns(DeprecationWarning): + with pytest.warns(DeprecationWarning, match="BGR;15"): self.assert_unpack( "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8) ) + with pytest.warns(DeprecationWarning, match="BGR;16"): self.assert_unpack( "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) ) + with pytest.warns(DeprecationWarning, match="BGR;24"): self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) def test_RGBA(self) -> None: From a61a23d7ae054581db05d7eb94817685bc3ab3e4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Jun 2025 13:00:48 +1000 Subject: [PATCH 623/628] Fixed PT031 --- Tests/test_file_apng.py | 45 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 66410b3da..12204b5b7 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -303,11 +303,11 @@ def test_apng_chunk_errors() -> None: assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated - with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 - with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: - im.load() - assert isinstance(im, PngImagePlugin.PngImageFile) - assert not im.is_animated + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/chunk_multi_actl.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + im.close() with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: assert isinstance(im, PngImagePlugin.PngImageFile) @@ -330,18 +330,20 @@ def test_apng_chunk_errors() -> None: def test_apng_syntax_errors() -> None: - with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 - with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: - assert isinstance(im, PngImagePlugin.PngImageFile) - assert not im.is_animated - with pytest.raises(OSError): - im.load() + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/syntax_num_frames_zero.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + with pytest.raises(OSError): + im.load() + im.close() - with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 - with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: - assert isinstance(im, PngImagePlugin.PngImageFile) - assert not im.is_animated - im.load() + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + im.load() + im.close() # we can handle this case gracefully with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: @@ -354,11 +356,12 @@ def test_apng_syntax_errors() -> None: im.seek(im.n_frames - 1) im.load() - with pytest.warns(UserWarning, match="Invalid APNG"): # noqa: PT031 - with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: - assert isinstance(im, PngImagePlugin.PngImageFile) - assert not im.is_animated - im.load() + with pytest.warns(UserWarning, match="Invalid APNG"): + im = Image.open("Tests/images/apng/syntax_num_frames_invalid.png") + assert isinstance(im, PngImagePlugin.PngImageFile) + assert not im.is_animated + im.load() + im.close() @pytest.mark.parametrize( From e783aff6885c8b5d92f819bd9a7bc1566f1b5318 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Jun 2025 22:32:30 +1000 Subject: [PATCH 624/628] Improve SgiImagePlugin test coverage (#8896) --- Tests/test_file_sgi.py | 18 ++++++++++++++++++ src/PIL/SgiImagePlugin.py | 30 +++++++----------------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index da0965fa1..abf424dbf 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest @@ -71,6 +72,15 @@ def test_invalid_file() -> None: SgiImagePlugin.SgiImageFile(invalid_file) +def test_unsupported_image_mode() -> None: + with open("Tests/images/hopper.rgb", "rb") as fp: + data = fp.read() + data = data[:3] + b"\x03" + data[4:] + with pytest.raises(ValueError, match="Unsupported SGI image mode"): + with Image.open(BytesIO(data)): + pass + + def roundtrip(img: Image.Image, tmp_path: Path) -> None: out = tmp_path / "temp.sgi" img.save(out, format="sgi") @@ -109,3 +119,11 @@ def test_unsupported_mode(tmp_path: Path) -> None: with pytest.raises(ValueError): im.save(out, format="sgi") + + +def test_unsupported_number_of_bytes_per_pixel(tmp_path: Path) -> None: + im = hopper() + out = tmp_path / "temp.sgi" + + with pytest.raises(ValueError, match="Unsupported number of bytes per pixel"): + im.save(out, bpc=3) diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 44254b7a4..853022150 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -82,17 +82,10 @@ class SgiImageFile(ImageFile.ImageFile): # zsize : channels count zsize = i16(s, 10) - # layout - layout = bpc, dimension, zsize - # determine mode from bits/zsize - rawmode = "" try: - rawmode = MODES[layout] + rawmode = MODES[(bpc, dimension, zsize)] except KeyError: - pass - - if rawmode == "": msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -156,24 +149,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Run-Length Encoding Compression - Unsupported at this time rle = 0 - # Number of dimensions (x,y,z) - dim = 3 # X Dimension = width / Y Dimension = height x, y = im.size - if im.mode == "L" and y == 1: - dim = 1 - elif im.mode == "L": - dim = 2 # Z Dimension: Number of channels z = len(im.mode) - - if dim in {1, 2}: - z = 1 - - # assert we've got the right number of bands. - if len(im.getbands()) != z: - msg = f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" - raise ValueError(msg) + # Number of dimensions (x,y,z) + if im.mode == "L": + dimension = 1 if y == 1 else 2 + else: + dimension = 3 # Minimum Byte value pinmin = 0 @@ -188,7 +172,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(struct.pack(">h", magic_number)) fp.write(o8(rle)) fp.write(o8(bpc)) - fp.write(struct.pack(">H", dim)) + fp.write(struct.pack(">H", dimension)) fp.write(struct.pack(">H", x)) fp.write(struct.pack(">H", y)) fp.write(struct.pack(">H", z)) From 958c449b988e49aa88a412990bebdb799a97a39f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:17:20 +0300 Subject: [PATCH 625/628] Close image after assert Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_jpeg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index c0e6b467e..6dab418bf 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -752,6 +752,8 @@ class TestFileJpeg: # Assert assert im.format == "JPEG" + im.close() + @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) def test_save_correct_modes(self, mode: str) -> None: out = BytesIO() From ef98b3510e3e4f14b547762764813d7e5ca3c5a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:29:58 +1000 Subject: [PATCH 626/628] Fix buffer overflow when saving compressed DDS images (#9041) Co-authored-by: Eric Soroos --- Tests/test_file_dds.py | 17 +++++++++++++++++ src/libImaging/BcnEncode.c | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 3388fce16..5c7a943b1 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -511,3 +511,20 @@ def test_save_dx10_bc5(tmp_path: Path) -> None: im = hopper("L") with pytest.raises(OSError, match="only RGB mode can be written as BC5"): im.save(out, pixel_format="BC5") + + +@pytest.mark.parametrize( + "pixel_format, mode", + ( + ("DXT1", "RGBA"), + ("DXT3", "RGBA"), + ("DXT5", "RGBA"), + ("BC2", "RGBA"), + ("BC3", "RGBA"), + ("BC5", "RGB"), + ), +) +def test_save_large_file(tmp_path: Path, pixel_format: str, mode: str) -> None: + im = hopper(mode).resize((440, 440)) + # should not error in valgrind + im.save(tmp_path / "img.dds", pixel_format=pixel_format) diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 2bad73b92..7a5072dde 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -258,6 +258,10 @@ ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { UINT8 *dst = buf; for (;;) { + // Loop writes a max of 16 bytes per iteration + if (dst + 16 >= bytes + buf) { + break; + } if (n == 5) { encode_bc3_alpha(im, state, dst, 0); dst += 8; From d07aa6fd17d356b8f09f89a5c485fc8b1532635f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:30:22 +1000 Subject: [PATCH 627/628] Added release notes for #9041 (#9042) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/11.3.0.rst | 38 +++++++++++------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 654a7e6b6..2d35d8228 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -4,21 +4,21 @@ Security ======== -TODO -^^^^ +:cve:`2025-48379`: Write buffer overflow on BCn encoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +There is a heap buffer overflow when writing a sufficiently large (>64k encoded with +default settings) image in the DDS format due to writing into a buffer without checking +for available space. -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ +This only affects users who save untrusted data as a compressed DDS image. -TODO +* Unclear how large the potential write could be. It is likely limited by process + segfault, so it's not necessarily deterministic. It may be practically unbounded. +* Unclear if there's a restriction on the bytes that could be emitted. It's likely that + the only restriction is that the bytes would be emitted in chunks of 8 or 16. -Backwards incompatible changes -============================== - -TODO -^^^^ +This was introduced in Pillow 11.2.0 when the feature was added. Deprecations ============ @@ -41,22 +41,6 @@ another mode before saving:: im = Image.new("I", (1, 1)) im.convert("I;16").save("out.png") -API changes -=========== - -TODO -^^^^ - -TODO - -API additions -============= - -TODO -^^^^ - -TODO - Other changes ============= From 69c0c422c86b1b1a8a7f5fca5e2f464cadfcf7f2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 28 Jun 2025 10:29:01 +1000 Subject: [PATCH 628/628] Increase pytest verbosity (#9040) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .ci/test.cmd | 2 +- .ci/test.sh | 2 +- .github/workflows/wheels-test.ps1 | 4 ++-- .github/workflows/wheels-test.sh | 4 ++-- winbuild/README.md | 2 +- winbuild/build.rst | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.ci/test.cmd b/.ci/test.cmd index aafc9290c..acfac3d1a 100644 --- a/.ci/test.cmd +++ b/.ci/test.cmd @@ -1,3 +1,3 @@ python.exe -c "from PIL import Image" IF ERRORLEVEL 1 EXIT /B -python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests +python.exe -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests diff --git a/.ci/test.sh b/.ci/test.sh index 3f0ddc350..87a605d84 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -4,4 +4,4 @@ set -e python3 -c "from PIL import Image" -python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE +python3 -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index 54e7fbbfc..9f5561c46 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -23,7 +23,7 @@ cd $pillow if (!$?) { exit $LASTEXITCODE } & $venv\Scripts\$python selftest.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py +& $venv\Scripts\$python -m pytest -vv -x Tests\check_wheel.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\$python -m pytest -vx Tests +& $venv\Scripts\$python -m pytest -vv -x Tests if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index ce83a4278..94dbb4679 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -35,5 +35,5 @@ fi # Runs tests python3 selftest.py -python3 -m pytest Tests/check_wheel.py -python3 -m pytest +python3 -m pytest -vv -x Tests/check_wheel.py +python3 -m pytest -vv -x diff --git a/winbuild/README.md b/winbuild/README.md index 0d3ec8d8a..62345af60 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -24,6 +24,6 @@ cd .. %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py -%PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests +%PYTHON%\python.exe -m pytest -vv -x --cov PIL --cov Tests --cov-report term --cov-report xml Tests %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . ``` diff --git a/winbuild/build.rst b/winbuild/build.rst index 3c20c7d17..aa4677ad5 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -124,5 +124,5 @@ Here's an example script to build on Windows:: %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py - %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests + %PYTHON%\python.exe -m pytest -vv -x --cov PIL --cov Tests --cov-report term --cov-report xml Tests %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor .