Skip to content

Commit 2810d7c

Browse files
authored
Merge pull request #8721 from radarhere/justify
Added "justify" align for multiline text
2 parents 92eb11e + 69c9572 commit 2810d7c

File tree

5 files changed

+146
-107
lines changed

5 files changed

+146
-107
lines changed
3.17 KB
Loading

Tests/test_imagefont.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,8 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
254254

255255

256256
@pytest.mark.parametrize(
257-
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
257+
"align, ext",
258+
(("left", ""), ("center", "_center"), ("right", "_right"), ("justify", "_justify")),
258259
)
259260
def test_render_multiline_text_align(
260261
font: ImageFont.FreeTypeFont, align: str, ext: str

docs/reference/ImageDraw.rst

+20-8
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,11 @@ Methods
387387
the number of pixels between lines.
388388
:param align: If the text is passed on to
389389
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`,
390-
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
391-
Use the ``anchor`` parameter to specify the alignment to ``xy``.
390+
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
391+
the relative alignment of lines. Use the ``anchor`` parameter to
392+
specify the alignment to ``xy``.
393+
394+
.. versionadded:: 11.2.0 ``"justify"``
392395
:param direction: Direction of the text. It can be ``"rtl"`` (right to
393396
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
394397
Requires libraqm.
@@ -455,8 +458,11 @@ Methods
455458
of Pillow, but implemented only in version 8.0.0.
456459

457460
:param spacing: The number of pixels between lines.
458-
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
459-
Use the ``anchor`` parameter to specify the alignment to ``xy``.
461+
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
462+
the relative alignment of lines. Use the ``anchor`` parameter to
463+
specify the alignment to ``xy``.
464+
465+
.. versionadded:: 11.2.0 ``"justify"``
460466
:param direction: Direction of the text. It can be ``"rtl"`` (right to
461467
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
462468
Requires libraqm.
@@ -599,8 +605,11 @@ Methods
599605
the number of pixels between lines.
600606
:param align: If the text is passed on to
601607
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
602-
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
603-
Use the ``anchor`` parameter to specify the alignment to ``xy``.
608+
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
609+
the relative alignment of lines. Use the ``anchor`` parameter to
610+
specify the alignment to ``xy``.
611+
612+
.. versionadded:: 11.2.0 ``"justify"``
604613
:param direction: Direction of the text. It can be ``"rtl"`` (right to
605614
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
606615
Requires libraqm.
@@ -650,8 +659,11 @@ Methods
650659
vertical text. See :ref:`text-anchors` for details.
651660
This parameter is ignored for non-TrueType fonts.
652661
:param spacing: The number of pixels between lines.
653-
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
654-
Use the ``anchor`` parameter to specify the alignment to ``xy``.
662+
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
663+
the relative alignment of lines. Use the ``anchor`` parameter to
664+
specify the alignment to ``xy``.
665+
666+
.. versionadded:: 11.2.0 ``"justify"``
655667
:param direction: Direction of the text. It can be ``"rtl"`` (right to
656668
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
657669
Requires libraqm.

docs/releasenotes/11.2.0.rst

+12
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ TODO
4444
API Additions
4545
=============
4646

47+
``"justify"`` multiline text alignment
48+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49+
50+
In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be
51+
aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`::
52+
53+
from PIL import Image, ImageDraw
54+
im = Image.new("RGB", (50, 25))
55+
draw = ImageDraw.Draw(im)
56+
draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify")
57+
draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify")
58+
4759
Check for MozJPEG
4860
^^^^^^^^^^^^^^^^^
4961

src/PIL/ImageDraw.py

+112-98
Original file line numberDiff line numberDiff line change
@@ -557,21 +557,6 @@ def _multiline_check(self, text: AnyStr) -> bool:
557557

558558
return split_character in text
559559

560-
def _multiline_split(self, text: AnyStr) -> list[AnyStr]:
561-
return text.split("\n" if isinstance(text, str) else b"\n")
562-
563-
def _multiline_spacing(
564-
self,
565-
font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont,
566-
spacing: float,
567-
stroke_width: float,
568-
) -> float:
569-
return (
570-
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
571-
+ stroke_width
572-
+ spacing
573-
)
574-
575560
def text(
576561
self,
577562
xy: tuple[float, float],
@@ -699,29 +684,30 @@ def draw_text(ink: int, stroke_width: float = 0) -> None:
699684
# Only draw normal text
700685
draw_text(ink)
701686

702-
def multiline_text(
687+
def _prepare_multiline_text(
703688
self,
704689
xy: tuple[float, float],
705690
text: AnyStr,
706-
fill: _Ink | None = None,
707691
font: (
708692
ImageFont.ImageFont
709693
| ImageFont.FreeTypeFont
710694
| ImageFont.TransposedFont
711695
| None
712-
) = None,
713-
anchor: str | None = None,
714-
spacing: float = 4,
715-
align: str = "left",
716-
direction: str | None = None,
717-
features: list[str] | None = None,
718-
language: str | None = None,
719-
stroke_width: float = 0,
720-
stroke_fill: _Ink | None = None,
721-
embedded_color: bool = False,
722-
*,
723-
font_size: float | None = None,
724-
) -> None:
696+
),
697+
anchor: str | None,
698+
spacing: float,
699+
align: str,
700+
direction: str | None,
701+
features: list[str] | None,
702+
language: str | None,
703+
stroke_width: float,
704+
embedded_color: bool,
705+
font_size: float | None,
706+
) -> tuple[
707+
ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont,
708+
str,
709+
list[tuple[tuple[float, float], AnyStr]],
710+
]:
725711
if direction == "ttb":
726712
msg = "ttb direction is unsupported for multiline text"
727713
raise ValueError(msg)
@@ -740,11 +726,21 @@ def multiline_text(
740726

741727
widths = []
742728
max_width: float = 0
743-
lines = self._multiline_split(text)
744-
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
729+
lines = text.split("\n" if isinstance(text, str) else b"\n")
730+
line_spacing = (
731+
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
732+
+ stroke_width
733+
+ spacing
734+
)
735+
745736
for line in lines:
746737
line_width = self.textlength(
747-
line, font, direction=direction, features=features, language=language
738+
line,
739+
font,
740+
direction=direction,
741+
features=features,
742+
language=language,
743+
embedded_color=embedded_color,
748744
)
749745
widths.append(line_width)
750746
max_width = max(max_width, line_width)
@@ -755,6 +751,7 @@ def multiline_text(
755751
elif anchor[1] == "d":
756752
top -= (len(lines) - 1) * line_spacing
757753

754+
parts = []
758755
for idx, line in enumerate(lines):
759756
left = xy[0]
760757
width_difference = max_width - widths[idx]
@@ -766,18 +763,81 @@ def multiline_text(
766763
left -= width_difference
767764

768765
# then align by align parameter
769-
if align == "left":
766+
if align in ("left", "justify"):
770767
pass
771768
elif align == "center":
772769
left += width_difference / 2.0
773770
elif align == "right":
774771
left += width_difference
775772
else:
776-
msg = 'align must be "left", "center" or "right"'
773+
msg = 'align must be "left", "center", "right" or "justify"'
777774
raise ValueError(msg)
778775

776+
if align == "justify" and width_difference != 0:
777+
words = line.split(" " if isinstance(text, str) else b" ")
778+
word_widths = [
779+
self.textlength(
780+
word,
781+
font,
782+
direction=direction,
783+
features=features,
784+
language=language,
785+
embedded_color=embedded_color,
786+
)
787+
for word in words
788+
]
789+
width_difference = max_width - sum(word_widths)
790+
for i, word in enumerate(words):
791+
parts.append(((left, top), word))
792+
left += word_widths[i] + width_difference / (len(words) - 1)
793+
else:
794+
parts.append(((left, top), line))
795+
796+
top += line_spacing
797+
798+
return font, anchor, parts
799+
800+
def multiline_text(
801+
self,
802+
xy: tuple[float, float],
803+
text: AnyStr,
804+
fill: _Ink | None = None,
805+
font: (
806+
ImageFont.ImageFont
807+
| ImageFont.FreeTypeFont
808+
| ImageFont.TransposedFont
809+
| None
810+
) = None,
811+
anchor: str | None = None,
812+
spacing: float = 4,
813+
align: str = "left",
814+
direction: str | None = None,
815+
features: list[str] | None = None,
816+
language: str | None = None,
817+
stroke_width: float = 0,
818+
stroke_fill: _Ink | None = None,
819+
embedded_color: bool = False,
820+
*,
821+
font_size: float | None = None,
822+
) -> None:
823+
font, anchor, lines = self._prepare_multiline_text(
824+
xy,
825+
text,
826+
font,
827+
anchor,
828+
spacing,
829+
align,
830+
direction,
831+
features,
832+
language,
833+
stroke_width,
834+
embedded_color,
835+
font_size,
836+
)
837+
838+
for xy, line in lines:
779839
self.text(
780-
(left, top),
840+
xy,
781841
line,
782842
fill,
783843
font,
@@ -789,7 +849,6 @@ def multiline_text(
789849
stroke_fill=stroke_fill,
790850
embedded_color=embedded_color,
791851
)
792-
top += line_spacing
793852

794853
def textlength(
795854
self,
@@ -891,69 +950,26 @@ def multiline_textbbox(
891950
*,
892951
font_size: float | None = None,
893952
) -> tuple[float, float, float, float]:
894-
if direction == "ttb":
895-
msg = "ttb direction is unsupported for multiline text"
896-
raise ValueError(msg)
897-
898-
if anchor is None:
899-
anchor = "la"
900-
elif len(anchor) != 2:
901-
msg = "anchor must be a 2 character string"
902-
raise ValueError(msg)
903-
elif anchor[1] in "tb":
904-
msg = "anchor not supported for multiline text"
905-
raise ValueError(msg)
906-
907-
if font is None:
908-
font = self._getfont(font_size)
909-
910-
widths = []
911-
max_width: float = 0
912-
lines = self._multiline_split(text)
913-
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
914-
for line in lines:
915-
line_width = self.textlength(
916-
line,
917-
font,
918-
direction=direction,
919-
features=features,
920-
language=language,
921-
embedded_color=embedded_color,
922-
)
923-
widths.append(line_width)
924-
max_width = max(max_width, line_width)
925-
926-
top = xy[1]
927-
if anchor[1] == "m":
928-
top -= (len(lines) - 1) * line_spacing / 2.0
929-
elif anchor[1] == "d":
930-
top -= (len(lines) - 1) * line_spacing
953+
font, anchor, lines = self._prepare_multiline_text(
954+
xy,
955+
text,
956+
font,
957+
anchor,
958+
spacing,
959+
align,
960+
direction,
961+
features,
962+
language,
963+
stroke_width,
964+
embedded_color,
965+
font_size,
966+
)
931967

932968
bbox: tuple[float, float, float, float] | None = None
933969

934-
for idx, line in enumerate(lines):
935-
left = xy[0]
936-
width_difference = max_width - widths[idx]
937-
938-
# first align left by anchor
939-
if anchor[0] == "m":
940-
left -= width_difference / 2.0
941-
elif anchor[0] == "r":
942-
left -= width_difference
943-
944-
# then align by align parameter
945-
if align == "left":
946-
pass
947-
elif align == "center":
948-
left += width_difference / 2.0
949-
elif align == "right":
950-
left += width_difference
951-
else:
952-
msg = 'align must be "left", "center" or "right"'
953-
raise ValueError(msg)
954-
970+
for xy, line in lines:
955971
bbox_line = self.textbbox(
956-
(left, top),
972+
xy,
957973
line,
958974
font,
959975
anchor,
@@ -973,8 +989,6 @@ def multiline_textbbox(
973989
max(bbox[3], bbox_line[3]),
974990
)
975991

976-
top += line_spacing
977-
978992
if bbox is None:
979993
return xy[0], xy[1], xy[0], xy[1]
980994
return bbox

0 commit comments

Comments
 (0)