mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-23 15:20:33 +03:00
Merge pull request #8721 from radarhere/justify
Added "justify" align for multiline text
This commit is contained in:
commit
2810d7c6ba
BIN
Tests/images/multiline_text_justify.png
Normal file
BIN
Tests/images/multiline_text_justify.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
|
@ -254,7 +254,8 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@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(
|
def test_render_multiline_text_align(
|
||||||
font: ImageFont.FreeTypeFont, align: str, ext: str
|
font: ImageFont.FreeTypeFont, align: str, ext: str
|
||||||
|
|
|
@ -387,8 +387,11 @@ Methods
|
||||||
the number of pixels between lines.
|
the number of pixels between lines.
|
||||||
:param align: If the text is passed on to
|
:param align: If the text is passed on to
|
||||||
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`,
|
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`,
|
||||||
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
|
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
|
||||||
Use the ``anchor`` parameter to specify the alignment to ``xy``.
|
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
|
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||||
Requires libraqm.
|
Requires libraqm.
|
||||||
|
@ -455,8 +458,11 @@ Methods
|
||||||
of Pillow, but implemented only in version 8.0.0.
|
of Pillow, but implemented only in version 8.0.0.
|
||||||
|
|
||||||
:param spacing: The number of pixels between lines.
|
:param spacing: The number of pixels between lines.
|
||||||
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
|
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
|
||||||
Use the ``anchor`` parameter to specify the alignment to ``xy``.
|
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
|
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||||
Requires libraqm.
|
Requires libraqm.
|
||||||
|
@ -599,8 +605,11 @@ Methods
|
||||||
the number of pixels between lines.
|
the number of pixels between lines.
|
||||||
:param align: If the text is passed on to
|
:param align: If the text is passed on to
|
||||||
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
|
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
|
||||||
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
|
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
|
||||||
Use the ``anchor`` parameter to specify the alignment to ``xy``.
|
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
|
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||||
Requires libraqm.
|
Requires libraqm.
|
||||||
|
@ -650,8 +659,11 @@ Methods
|
||||||
vertical text. See :ref:`text-anchors` for details.
|
vertical text. See :ref:`text-anchors` for details.
|
||||||
This parameter is ignored for non-TrueType fonts.
|
This parameter is ignored for non-TrueType fonts.
|
||||||
:param spacing: The number of pixels between lines.
|
:param spacing: The number of pixels between lines.
|
||||||
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
|
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
|
||||||
Use the ``anchor`` parameter to specify the alignment to ``xy``.
|
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
|
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||||
Requires libraqm.
|
Requires libraqm.
|
||||||
|
|
|
@ -44,6 +44,18 @@ TODO
|
||||||
API Additions
|
API Additions
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
``"justify"`` multiline text alignment
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
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))
|
||||||
|
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
|
Check for MozJPEG
|
||||||
^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -557,21 +557,6 @@ class ImageDraw:
|
||||||
|
|
||||||
return split_character in text
|
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(
|
def text(
|
||||||
self,
|
self,
|
||||||
xy: tuple[float, float],
|
xy: tuple[float, float],
|
||||||
|
@ -699,6 +684,119 @@ class ImageDraw:
|
||||||
# Only draw normal text
|
# Only draw normal text
|
||||||
draw_text(ink)
|
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 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", "right" or "justify"'
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
return font, anchor, parts
|
||||||
|
|
||||||
def multiline_text(
|
def multiline_text(
|
||||||
self,
|
self,
|
||||||
xy: tuple[float, float],
|
xy: tuple[float, float],
|
||||||
|
@ -722,62 +820,24 @@ class ImageDraw:
|
||||||
*,
|
*,
|
||||||
font_size: float | None = None,
|
font_size: float | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if direction == "ttb":
|
font, anchor, lines = self._prepare_multiline_text(
|
||||||
msg = "ttb direction is unsupported for multiline text"
|
xy,
|
||||||
raise ValueError(msg)
|
text,
|
||||||
|
font,
|
||||||
if anchor is None:
|
anchor,
|
||||||
anchor = "la"
|
spacing,
|
||||||
elif len(anchor) != 2:
|
align,
|
||||||
msg = "anchor must be a 2 character string"
|
direction,
|
||||||
raise ValueError(msg)
|
features,
|
||||||
elif anchor[1] in "tb":
|
language,
|
||||||
msg = "anchor not supported for multiline text"
|
stroke_width,
|
||||||
raise ValueError(msg)
|
embedded_color,
|
||||||
|
font_size,
|
||||||
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
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
for xy, line in lines:
|
||||||
self.text(
|
self.text(
|
||||||
(left, top),
|
xy,
|
||||||
line,
|
line,
|
||||||
fill,
|
fill,
|
||||||
font,
|
font,
|
||||||
|
@ -789,7 +849,6 @@ class ImageDraw:
|
||||||
stroke_fill=stroke_fill,
|
stroke_fill=stroke_fill,
|
||||||
embedded_color=embedded_color,
|
embedded_color=embedded_color,
|
||||||
)
|
)
|
||||||
top += line_spacing
|
|
||||||
|
|
||||||
def textlength(
|
def textlength(
|
||||||
self,
|
self,
|
||||||
|
@ -891,69 +950,26 @@ class ImageDraw:
|
||||||
*,
|
*,
|
||||||
font_size: float | None = None,
|
font_size: float | None = None,
|
||||||
) -> tuple[float, float, float, float]:
|
) -> tuple[float, float, float, float]:
|
||||||
if direction == "ttb":
|
font, anchor, lines = self._prepare_multiline_text(
|
||||||
msg = "ttb direction is unsupported for multiline text"
|
xy,
|
||||||
raise ValueError(msg)
|
text,
|
||||||
|
font,
|
||||||
if anchor is None:
|
anchor,
|
||||||
anchor = "la"
|
spacing,
|
||||||
elif len(anchor) != 2:
|
align,
|
||||||
msg = "anchor must be a 2 character string"
|
direction,
|
||||||
raise ValueError(msg)
|
features,
|
||||||
elif anchor[1] in "tb":
|
language,
|
||||||
msg = "anchor not supported for multiline text"
|
stroke_width,
|
||||||
raise ValueError(msg)
|
embedded_color,
|
||||||
|
font_size,
|
||||||
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
|
|
||||||
|
|
||||||
bbox: tuple[float, float, float, float] | None = None
|
bbox: tuple[float, float, float, float] | None = None
|
||||||
|
|
||||||
for idx, line in enumerate(lines):
|
for xy, line in 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)
|
|
||||||
|
|
||||||
bbox_line = self.textbbox(
|
bbox_line = self.textbbox(
|
||||||
(left, top),
|
xy,
|
||||||
line,
|
line,
|
||||||
font,
|
font,
|
||||||
anchor,
|
anchor,
|
||||||
|
@ -973,8 +989,6 @@ class ImageDraw:
|
||||||
max(bbox[3], bbox_line[3]),
|
max(bbox[3], bbox_line[3]),
|
||||||
)
|
)
|
||||||
|
|
||||||
top += line_spacing
|
|
||||||
|
|
||||||
if bbox is None:
|
if bbox is None:
|
||||||
return xy[0], xy[1], xy[0], xy[1]
|
return xy[0], xy[1], xy[0], xy[1]
|
||||||
return bbox
|
return bbox
|
||||||
|
|
Loading…
Reference in New Issue
Block a user