From 10eaff8ac7548ff50cefc003b27e2ba1a46ed71b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jan 2025 20:12:45 +1100 Subject: [PATCH] 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