Support ttb multiline text

This commit is contained in:
Andrew Murray 2025-02-04 21:07:05 +11:00
parent b57b4e5f2c
commit f056c259a7
3 changed files with 88 additions and 71 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -4,7 +4,11 @@ import pytest
from PIL import Image, ImageDraw, ImageFont
from .helper import assert_image_similar_tofile, skip_unless_feature
from .helper import (
assert_image_equal_tofile,
assert_image_similar_tofile,
skip_unless_feature,
)
FONT_SIZE = 20
FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
@ -354,11 +358,27 @@ def test_combine_multiline(anchor: str, align: str) -> None:
d.line(((200, 0), (200, 400)), "gray")
bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align)
d.rectangle(bbox, outline="red")
d.multiline_text((200, 200), text, fill="black", anchor=anchor, font=f, align=align)
d.multiline_text((200, 200), text, "black", anchor=anchor, font=f, align=align)
assert_image_similar_tofile(im, path, 0.015)
def test_combine_multiline_ttb() -> None:
path = "Tests/images/test_combine_multiline_ttb.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
text = "te\nxt"
im = Image.new("RGB", (400, 400), "white")
d = ImageDraw.Draw(im)
d.line(((0, 200), (400, 200)), "gray")
d.line(((200, 0), (200, 400)), "gray")
bbox = d.multiline_textbbox((200, 200), text, f, direction="ttb")
d.rectangle(bbox, outline="red")
d.multiline_text((200, 200), text, "black", f, direction="ttb")
assert_image_equal_tofile(im, path)
def test_anchor_invalid_ttb() -> None:
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new("RGB", (100, 100), "white")
@ -378,8 +398,3 @@ def test_anchor_invalid_ttb() -> None:
d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
with pytest.raises(ValueError):
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
# ttb multiline text does not support anchors at all
with pytest.raises(ValueError):
d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb")
with pytest.raises(ValueError):
d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb")

View File

@ -708,24 +708,18 @@ class ImageDraw:
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"
anchor = "lt" if direction == "ttb" else "la"
elif len(anchor) != 2:
msg = "anchor must be a 2 character string"
raise ValueError(msg)
elif anchor[1] in "tb":
elif anchor[1] in "tb" and direction != "ttb":
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]
@ -733,67 +727,75 @@ class ImageDraw:
+ 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):
if direction == "ttb":
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:
for line in lines:
parts.append(((left, top), line))
left += line_spacing
else:
widths = []
max_width: float = 0
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 += line_spacing
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 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