add textlength and textbbox to ImageDraw
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
|
@ -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")
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|