mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-06-06 06:03:14 +03:00
Support ttb multiline text
This commit is contained in:
parent
b57b4e5f2c
commit
f056c259a7
BIN
Tests/images/test_combine_multiline_ttb.png
Normal file
BIN
Tests/images/test_combine_multiline_ttb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
|
@ -4,7 +4,11 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
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_SIZE = 20
|
||||||
FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
|
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")
|
d.line(((200, 0), (200, 400)), "gray")
|
||||||
bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align)
|
bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align)
|
||||||
d.rectangle(bbox, outline="red")
|
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)
|
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:
|
def test_anchor_invalid_ttb() -> None:
|
||||||
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
||||||
im = Image.new("RGB", (100, 100), "white")
|
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")
|
d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
|
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")
|
|
||||||
|
|
|
@ -708,24 +708,18 @@ class ImageDraw:
|
||||||
str,
|
str,
|
||||||
list[tuple[tuple[float, float], AnyStr]],
|
list[tuple[tuple[float, float], AnyStr]],
|
||||||
]:
|
]:
|
||||||
if direction == "ttb":
|
|
||||||
msg = "ttb direction is unsupported for multiline text"
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
if anchor is None:
|
if anchor is None:
|
||||||
anchor = "la"
|
anchor = "lt" if direction == "ttb" else "la"
|
||||||
elif len(anchor) != 2:
|
elif len(anchor) != 2:
|
||||||
msg = "anchor must be a 2 character string"
|
msg = "anchor must be a 2 character string"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
elif anchor[1] in "tb":
|
elif anchor[1] in "tb" and direction != "ttb":
|
||||||
msg = "anchor not supported for multiline text"
|
msg = "anchor not supported for multiline text"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
if font is None:
|
if font is None:
|
||||||
font = self._getfont(font_size)
|
font = self._getfont(font_size)
|
||||||
|
|
||||||
widths = []
|
|
||||||
max_width: float = 0
|
|
||||||
lines = text.split("\n" if isinstance(text, str) else b"\n")
|
lines = text.split("\n" if isinstance(text, str) else b"\n")
|
||||||
line_spacing = (
|
line_spacing = (
|
||||||
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
|
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
|
||||||
|
@ -733,67 +727,75 @@ class ImageDraw:
|
||||||
+ spacing
|
+ 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]
|
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 = []
|
parts = []
|
||||||
for idx, line in enumerate(lines):
|
if direction == "ttb":
|
||||||
left = xy[0]
|
left = xy[0]
|
||||||
width_difference = max_width - widths[idx]
|
for line in lines:
|
||||||
|
|
||||||
# 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))
|
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
|
return font, anchor, parts
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user