From 9a4f39588dc082f6a6fbab3354b2a55fc588c195 Mon Sep 17 00:00:00 2001
From: Andrew Murray <radarhere@users.noreply.github.com>
Date: Wed, 29 Jan 2025 18:58:53 +1100
Subject: [PATCH 01/19] 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 <radarhere@users.noreply.github.com>
Date: Wed, 29 Jan 2025 19:42:25 +1100
Subject: [PATCH 02/19] 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 <radarhere@users.noreply.github.com>
Date: Wed, 29 Jan 2025 20:12:45 +1100
Subject: [PATCH 03/19] 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
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
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 <radarhere@users.noreply.github.com>
Date: Fri, 31 Jan 2025 19:09:03 +1100
Subject: [PATCH 04/19] 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 05/19] 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 <radarhere@users.noreply.github.com>
Date: Sat, 1 Feb 2025 00:47:21 +1100
Subject: [PATCH 06/19] 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 <radarhere@users.noreply.github.com>
Date: Sat, 1 Feb 2025 01:13:04 +1100
Subject: [PATCH 07/19] 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 08/19] 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 <radarhere@users.noreply.github.com>
Date: Sat, 1 Feb 2025 10:54:18 +1100
Subject: [PATCH 09/19] 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 <radarhere@users.noreply.github.com>
Date: Sat, 1 Feb 2025 12:21:15 +1100
Subject: [PATCH 10/19] 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 <radarhere@users.noreply.github.com>
Date: Mon, 3 Feb 2025 19:32:59 +1100
Subject: [PATCH 11/19] 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 12/19] [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 13/19] [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 14/19] 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 <radarhere@users.noreply.github.com>
Date: Wed, 5 Feb 2025 18:42:35 +1100
Subject: [PATCH 15/19] 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 <radarhere@users.noreply.github.com>
Date: Wed, 5 Feb 2025 19:12:50 +1100
Subject: [PATCH 16/19] 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 <radarhere@users.noreply.github.com>
Date: Wed, 5 Feb 2025 18:36:14 +1100
Subject: [PATCH 17/19] 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 <radarhere@users.noreply.github.com>
Date: Wed, 5 Feb 2025 20:20:57 +1100
Subject: [PATCH 18/19] 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 <radarhere@users.noreply.github.com>
Date: Thu, 6 Feb 2025 19:26:49 +1100
Subject: [PATCH 19/19] 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