add textlength and textbbox to ImageDraw

This commit is contained in:
nulano 2020-10-07 22:43:29 +01:00
parent 395aa946a9
commit 1551e120ae
12 changed files with 185 additions and 18 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -221,12 +221,15 @@ class TestImageFont:
"Tests/fonts/" + font, size, layout_engine=self.LAYOUT_ENGINE
)
im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im)
if self.LAYOUT_ENGINE == ImageFont.LAYOUT_BASIC:
length = f.getlength(text, mode)
length = d.textlength(text, f)
assert length == self.metrics["getlength"][length_basic_index]
else:
# disable kerning, kerning metrics changed
length = f.getlength(text, mode, features=["-kern"])
length = d.textlength(text, f, features=["-kern"])
assert length == length_raqm
def test_render_multiline(self):
@ -813,20 +816,20 @@ class TestImageFont:
else:
width, height = (128, 44)
bbox_expected = (left, top, left + width, top + height)
f = ImageFont.truetype(
"Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE
)
# test getbbox
assert f.getbbox(text, anchor=anchor) == (left, top, left + width, top + height)
# test render
im = Image.new("RGB", (200, 200), "white")
d = ImageDraw.Draw(im)
d.line(((0, 100), (200, 100)), "gray")
d.line(((100, 0), (100, 200)), "gray")
d.text((100, 100), text, fill="black", anchor=anchor, font=f)
assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected
with Image.open(path) as expected:
assert_image_similar(im, expected, 7)
@ -879,13 +882,24 @@ class TestImageFont:
pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor))
pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor))
pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor))
pytest.raises(
ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)
)
pytest.raises(
ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
)
pytest.raises(
ValueError,
lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
)
for anchor in ["lt", "lb"]:
pytest.raises(
ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
)
pytest.raises(
ValueError,
lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
)
@skip_unless_feature("raqm")

View File

@ -222,10 +222,12 @@ def test_language():
ids=("None", "ltr", "rtl2", "rtl", "ttb"),
)
def test_getlength(mode, text, direction, expected):
try:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im)
assert ttf.getlength(text, mode, direction) == expected
try:
assert d.textlength(text, ttf, direction) == expected
except ValueError as ex:
if (
direction == "ttb"
@ -374,6 +376,8 @@ def test_combine_multiline(anchor, align):
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, 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)
with Image.open(path) as expected:
@ -396,14 +400,28 @@ def test_anchor_invalid_ttb():
pytest.raises(
ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb")
)
pytest.raises(
ValueError,
lambda: d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb"),
)
pytest.raises(
ValueError,
lambda: d.multiline_text(
(0, 0), "foo\nbar", anchor=anchor, direction="ttb"
),
)
pytest.raises(
ValueError,
lambda: d.multiline_textbbox(
(0, 0), "foo\nbar", anchor=anchor, direction="ttb"
),
)
# ttb multiline text does not support anchors at all
pytest.raises(
ValueError,
lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"),
)
pytest.raises(
ValueError,
lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb"),
)

View File

@ -385,9 +385,6 @@ class ImageDraw:
elif anchor[1] in "tb":
raise ValueError("anchor not supported for multiline text")
if font is None:
font = self.getfont()
widths = []
max_width = 0
lines = self._multiline_split(text)
@ -395,12 +392,8 @@ class ImageDraw:
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
)
for line in lines:
line_width = font.getlength(
line,
self.fontmode,
direction=direction,
features=features,
language=language,
line_width = self.textlength(
line, font, direction=direction, features=features, language=language
)
widths.append(line_width)
max_width = max(max_width, line_width)
@ -487,6 +480,148 @@ class ImageDraw:
max_width = max(max_width, line_width)
return max_width, len(lines) * line_spacing - spacing
def textlength(self, text, font=None, direction=None, features=None, language=None):
"""Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text):
raise ValueError("can't measure length of multiline text")
if font is None:
font = self.getfont()
try:
return font.getlength(text, self.fontmode, direction, features, language)
except AttributeError:
size = self.textsize(
text, font, direction=direction, features=features, language=language
)
if direction == "ttb":
return size[1]
return size[0]
def textbbox(
self,
xy,
text,
font=None,
anchor=None,
spacing=4,
align="left",
direction=None,
features=None,
language=None,
stroke_width=0,
):
"""Get the bounding box of a given string, in pixels."""
if self._multiline_check(text):
return self.multiline_textbbox(
xy,
text,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
)
if font is None:
font = self.getfont()
bbox = font.getbbox(
text, self.fontmode, direction, features, language, stroke_width, anchor
)
return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1]
def multiline_textbbox(
self,
xy,
text,
font=None,
anchor=None,
spacing=4,
align="left",
direction=None,
features=None,
language=None,
stroke_width=0,
):
if direction == "ttb":
raise ValueError("ttb direction is unsupported for multiline text")
if anchor is None:
anchor = "la"
elif len(anchor) != 2:
raise ValueError("anchor must be a 2 character string")
elif anchor[1] in "tb":
raise ValueError("anchor not supported for multiline text")
widths = []
max_width = 0
lines = self._multiline_split(text)
line_spacing = (
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
)
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
bbox = 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:
raise ValueError('align must be "left", "center" or "right"')
bbox_line = self.textbbox(
(left, top),
line,
font,
anchor,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
)
if bbox is None:
bbox = bbox_line
else:
bbox = (
min(bbox[0], bbox_line[0]),
min(bbox[1], bbox_line[1]),
max(bbox[2], bbox_line[2]),
max(bbox[3], bbox_line[3]),
)
top += line_spacing
if bbox is None:
return xy[0], xy[1], xy[0], xy[1]
return bbox
def Draw(im, mode=None):
"""