Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added "justify" align for multiline text #8721

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Tests/images/multiline_text_justify.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion Tests/test_imagefont.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 12 additions & 8 deletions docs/reference/ImageDraw.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
210 changes: 112 additions & 98 deletions src/PIL/ImageDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,21 +557,6 @@

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],
Expand Down Expand Up @@ -697,29 +682,30 @@
# Only draw normal text
draw_text(ink)

def multiline_text(
def _prepare_multiline_text(

Check warning on line 685 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L685

Added line #L685 was not covered by tests
self,
xy: tuple[float, float],
text: AnyStr,
fill: _Ink | None = None,
font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
anchor: str | None = None,
spacing: float = 4,
align: str = "left",
direction: str | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
stroke_fill: _Ink | None = None,
embedded_color: bool = False,
*,
font_size: float | None = None,
) -> 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)
Expand All @@ -738,11 +724,21 @@

widths = []
max_width: float = 0
lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
lines = text.split("\n" if isinstance(text, str) else b"\n")
line_spacing = (

Check warning on line 728 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L727-L728

Added lines #L727 - L728 were not covered by tests
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
line,
font,
direction=direction,
features=features,
language=language,
embedded_color=embedded_color,
)
widths.append(line_width)
max_width = max(max_width, line_width)
Expand All @@ -753,6 +749,7 @@
elif anchor[1] == "d":
top -= (len(lines) - 1) * line_spacing

parts = []

Check warning on line 752 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L752

Added line #L752 was not covered by tests
for idx, line in enumerate(lines):
left = xy[0]
width_difference = max_width - widths[idx]
Expand All @@ -764,18 +761,81 @@
left -= width_difference

# then align by align parameter
if align == "left":
if align in ("left", "justify"):

Check warning on line 764 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L764

Added line #L764 was not covered by tests
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"'

Check warning on line 771 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L771

Added line #L771 was not covered by tests
raise ValueError(msg)

if align == "justify" and width_difference != 0:
words = line.split(" " if isinstance(text, str) else b" ")
word_widths = [

Check warning on line 776 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L774-L776

Added lines #L774 - L776 were not covered by tests
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)

Check warning on line 790 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L787-L790

Added lines #L787 - L790 were not covered by tests
else:
parts.append(((left, top), line))

Check warning on line 792 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L792

Added line #L792 was not covered by tests

top += line_spacing

Check warning on line 794 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L794

Added line #L794 was not covered by tests

return font, anchor, parts

Check warning on line 796 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L796

Added line #L796 was not covered by tests

def multiline_text(

Check warning on line 798 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L798

Added line #L798 was not covered by tests
self,
xy: tuple[float, float],
text: AnyStr,
fill: _Ink | None = None,
font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
anchor: str | None = None,
spacing: float = 4,
align: str = "left",
direction: str | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
stroke_fill: _Ink | None = None,
embedded_color: bool = False,
*,
font_size: float | None = None,
) -> None:
font, anchor, lines = self._prepare_multiline_text(

Check warning on line 821 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L821

Added line #L821 was not covered by tests
xy,
text,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
embedded_color,
font_size,
)

for xy, line in lines:

Check warning on line 836 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L836

Added line #L836 was not covered by tests
self.text(
(left, top),
xy,
line,
fill,
font,
Expand All @@ -787,7 +847,6 @@
stroke_fill=stroke_fill,
embedded_color=embedded_color,
)
top += line_spacing

def textlength(
self,
Expand Down Expand Up @@ -889,69 +948,26 @@
*,
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(

Check warning on line 951 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L951

Added line #L951 was not covered by tests
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:

Check warning on line 968 in src/PIL/ImageDraw.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageDraw.py#L968

Added line #L968 was not covered by tests
bbox_line = self.textbbox(
(left, top),
xy,
line,
font,
anchor,
Expand All @@ -971,8 +987,6 @@
max(bbox[3], bbox_line[3]),
)

top += line_spacing

if bbox is None:
return xy[0], xy[1], xy[0], xy[1]
return bbox
Expand Down
Loading