Skip to content

Commit f056c25

Browse files
committed
Support ttb multiline text
1 parent b57b4e5 commit f056c25

File tree

3 files changed

+88
-71
lines changed

3 files changed

+88
-71
lines changed
3.95 KB
Loading

Tests/test_imagefontctl.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
from PIL import Image, ImageDraw, ImageFont
66

7-
from .helper import assert_image_similar_tofile, skip_unless_feature
7+
from .helper import (
8+
assert_image_equal_tofile,
9+
assert_image_similar_tofile,
10+
skip_unless_feature,
11+
)
812

913
FONT_SIZE = 20
1014
FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
@@ -354,11 +358,27 @@ def test_combine_multiline(anchor: str, align: str) -> None:
354358
d.line(((200, 0), (200, 400)), "gray")
355359
bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align)
356360
d.rectangle(bbox, outline="red")
357-
d.multiline_text((200, 200), text, fill="black", anchor=anchor, font=f, align=align)
361+
d.multiline_text((200, 200), text, "black", anchor=anchor, font=f, align=align)
358362

359363
assert_image_similar_tofile(im, path, 0.015)
360364

361365

366+
def test_combine_multiline_ttb() -> None:
367+
path = "Tests/images/test_combine_multiline_ttb.png"
368+
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
369+
text = "te\nxt"
370+
371+
im = Image.new("RGB", (400, 400), "white")
372+
d = ImageDraw.Draw(im)
373+
d.line(((0, 200), (400, 200)), "gray")
374+
d.line(((200, 0), (200, 400)), "gray")
375+
bbox = d.multiline_textbbox((200, 200), text, f, direction="ttb")
376+
d.rectangle(bbox, outline="red")
377+
d.multiline_text((200, 200), text, "black", f, direction="ttb")
378+
379+
assert_image_equal_tofile(im, path)
380+
381+
362382
def test_anchor_invalid_ttb() -> None:
363383
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
364384
im = Image.new("RGB", (100, 100), "white")
@@ -378,8 +398,3 @@ def test_anchor_invalid_ttb() -> None:
378398
d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
379399
with pytest.raises(ValueError):
380400
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
381-
# ttb multiline text does not support anchors at all
382-
with pytest.raises(ValueError):
383-
d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb")
384-
with pytest.raises(ValueError):
385-
d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb")

src/PIL/ImageDraw.py

Lines changed: 66 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -708,92 +708,94 @@ def _prepare_multiline_text(
708708
str,
709709
list[tuple[tuple[float, float], AnyStr]],
710710
]:
711-
if direction == "ttb":
712-
msg = "ttb direction is unsupported for multiline text"
713-
raise ValueError(msg)
714-
715711
if anchor is None:
716-
anchor = "la"
712+
anchor = "lt" if direction == "ttb" else "la"
717713
elif len(anchor) != 2:
718714
msg = "anchor must be a 2 character string"
719715
raise ValueError(msg)
720-
elif anchor[1] in "tb":
716+
elif anchor[1] in "tb" and direction != "ttb":
721717
msg = "anchor not supported for multiline text"
722718
raise ValueError(msg)
723719

724720
if font is None:
725721
font = self._getfont(font_size)
726722

727-
widths = []
728-
max_width: float = 0
729723
lines = text.split("\n" if isinstance(text, str) else b"\n")
730724
line_spacing = (
731725
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
732726
+ stroke_width
733727
+ spacing
734728
)
735729

736-
for line in lines:
737-
line_width = self.textlength(
738-
line,
739-
font,
740-
direction=direction,
741-
features=features,
742-
language=language,
743-
embedded_color=embedded_color,
744-
)
745-
widths.append(line_width)
746-
max_width = max(max_width, line_width)
747-
748730
top = xy[1]
749-
if anchor[1] == "m":
750-
top -= (len(lines) - 1) * line_spacing / 2.0
751-
elif anchor[1] == "d":
752-
top -= (len(lines) - 1) * line_spacing
753-
754731
parts = []
755-
for idx, line in enumerate(lines):
732+
if direction == "ttb":
756733
left = xy[0]
757-
width_difference = max_width - widths[idx]
758-
759-
# first align left by anchor
760-
if anchor[0] == "m":
761-
left -= width_difference / 2.0
762-
elif anchor[0] == "r":
763-
left -= width_difference
764-
765-
# then align by align parameter
766-
if align in ("left", "justify"):
767-
pass
768-
elif align == "center":
769-
left += width_difference / 2.0
770-
elif align == "right":
771-
left += width_difference
772-
else:
773-
msg = 'align must be "left", "center", "right" or "justify"'
774-
raise ValueError(msg)
775-
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:
734+
for line in lines:
794735
parts.append(((left, top), line))
736+
left += line_spacing
737+
else:
738+
widths = []
739+
max_width: float = 0
740+
for line in lines:
741+
line_width = self.textlength(
742+
line,
743+
font,
744+
direction=direction,
745+
features=features,
746+
language=language,
747+
embedded_color=embedded_color,
748+
)
749+
widths.append(line_width)
750+
max_width = max(max_width, line_width)
751+
752+
if anchor[1] == "m":
753+
top -= (len(lines) - 1) * line_spacing / 2.0
754+
elif anchor[1] == "d":
755+
top -= (len(lines) - 1) * line_spacing
756+
757+
for idx, line in enumerate(lines):
758+
left = xy[0]
759+
width_difference = max_width - widths[idx]
760+
761+
# first align left by anchor
762+
if anchor[0] == "m":
763+
left -= width_difference / 2.0
764+
elif anchor[0] == "r":
765+
left -= width_difference
766+
767+
# then align by align parameter
768+
if align in ("left", "justify"):
769+
pass
770+
elif align == "center":
771+
left += width_difference / 2.0
772+
elif align == "right":
773+
left += width_difference
774+
else:
775+
msg = 'align must be "left", "center", "right" or "justify"'
776+
raise ValueError(msg)
777+
778+
if align == "justify" and width_difference != 0:
779+
words = line.split(" " if isinstance(text, str) else b" ")
780+
word_widths = [
781+
self.textlength(
782+
word,
783+
font,
784+
direction=direction,
785+
features=features,
786+
language=language,
787+
embedded_color=embedded_color,
788+
)
789+
for word in words
790+
]
791+
width_difference = max_width - sum(word_widths)
792+
for i, word in enumerate(words):
793+
parts.append(((left, top), word))
794+
left += word_widths[i] + width_difference / (len(words) - 1)
795+
else:
796+
parts.append(((left, top), line))
795797

796-
top += line_spacing
798+
top += line_spacing
797799

798800
return font, anchor, parts
799801

0 commit comments

Comments
 (0)