UW@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 018/187] 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 019/187] 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 020/187] 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 021/187] 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 022/187] 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 023/187] 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 024/187] 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 025/187] 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 026/187] 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 027/187] 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 028/187] 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 029/187] 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 030/187] 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 031/187] 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 032/187] 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 033/187] 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 034/187] 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 035/187] 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 036/187] 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 037/187] 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 038/187] 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 039/187] 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 040/187] 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 041/187] 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 042/187] 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 043/187] 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 044/187] 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 045/187] 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 046/187] 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 047/187] 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 048/187] 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 049/187] 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 050/187] 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 051/187] 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 052/187] 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 053/187] 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 054/187] 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 055/187] [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 056/187] 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 057/187] 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 058/187] 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 059/187] 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 060/187] 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 061/187] 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 062/187] 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 063/187] 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 064/187] 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 065/187] 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 066/187] 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 067/187] 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 068/187] 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 069/187] 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 070/187] 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 071/187] 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 072/187] 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 073/187] 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 074/187] 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 075/187] 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 076/187] 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 077/187] 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 078/187] 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 079/187] 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 080/187] 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 081/187] 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 082/187] 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 083/187] 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 084/187] 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 085/187] 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 086/187] 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 087/187] 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 088/187] 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 089/187] 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 090/187] 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 091/187] 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 092/187] 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 093/187] 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 094/187] 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 095/187] 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 096/187] 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 097/187] 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 098/187] 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 099/187] 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 100/187] 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 101/187] 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 102/187] 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 103/187] 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 104/187] 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 105/187] 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 106/187] 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 107/187] 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 108/187] 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 109/187] 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 110/187] 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)|3OCopCL9jU8gPyR
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 111/187] 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 112/187] 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 113/187] 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 114/187] 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 115/187] 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 116/187] 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 117/187] 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 118/187] 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 119/187] [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 120/187] [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 121/187] 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 122/187] 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 123/187] 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 124/187] 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 125/187] 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 126/187] 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 127/187] 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 128/187] 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 129/187] 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 130/187] 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 131/187] 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 132/187] 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 133/187] 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 134/187] 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 135/187] 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 136/187] 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 137/187] 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 138/187] 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 139/187] 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 140/187] 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 141/187] 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 142/187] 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 143/187] 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 144/187] 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 145/187] 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 146/187] 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 147/187] 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 148/187] 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 149/187] 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 150/187] 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 151/187] 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 152/187] 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 153/187] 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 154/187] 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 155/187] 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 156/187] 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 157/187] 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 158/187] 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 159/187] 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 160/187] 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 161/187] 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 162/187] _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 163/187] 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 164/187] 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 165/187] 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 166/187] 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 167/187] 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 168/187] 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 169/187] 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 170/187] 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 171/187] 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 172/187] 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 173/187] 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 174/187] 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 175/187] 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 176/187] 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 177/187] 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 178/187] [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 179/187] 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 180/187] 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 181/187] 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 182/187] 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 183/187] 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 184/187] 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 185/187] 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 186/187] 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 187/187] 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;
+ }
}
}