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
|
"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:
|
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]
|
assert length == self.metrics["getlength"][length_basic_index]
|
||||||
else:
|
else:
|
||||||
# disable kerning, kerning metrics changed
|
# disable kerning, kerning metrics changed
|
||||||
length = f.getlength(text, mode, features=["-kern"])
|
length = d.textlength(text, f, features=["-kern"])
|
||||||
assert length == length_raqm
|
assert length == length_raqm
|
||||||
|
|
||||||
def test_render_multiline(self):
|
def test_render_multiline(self):
|
||||||
|
@ -813,20 +816,20 @@ class TestImageFont:
|
||||||
else:
|
else:
|
||||||
width, height = (128, 44)
|
width, height = (128, 44)
|
||||||
|
|
||||||
|
bbox_expected = (left, top, left + width, top + height)
|
||||||
|
|
||||||
f = ImageFont.truetype(
|
f = ImageFont.truetype(
|
||||||
"Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE
|
"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")
|
im = Image.new("RGB", (200, 200), "white")
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
d.line(((0, 100), (200, 100)), "gray")
|
d.line(((0, 100), (200, 100)), "gray")
|
||||||
d.line(((100, 0), (100, 200)), "gray")
|
d.line(((100, 0), (100, 200)), "gray")
|
||||||
d.text((100, 100), text, fill="black", anchor=anchor, font=f)
|
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:
|
with Image.open(path) as expected:
|
||||||
assert_image_similar(im, expected, 7)
|
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.getmask2("hello", anchor=anchor))
|
||||||
pytest.raises(ValueError, lambda: font.getbbox("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.text((0, 0), "hello", anchor=anchor))
|
||||||
|
pytest.raises(
|
||||||
|
ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)
|
||||||
|
)
|
||||||
pytest.raises(
|
pytest.raises(
|
||||||
ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
|
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"]:
|
for anchor in ["lt", "lb"]:
|
||||||
pytest.raises(
|
pytest.raises(
|
||||||
ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
|
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")
|
@skip_unless_feature("raqm")
|
||||||
|
|
|
@ -222,10 +222,12 @@ def test_language():
|
||||||
ids=("None", "ltr", "rtl2", "rtl", "ttb"),
|
ids=("None", "ltr", "rtl2", "rtl", "ttb"),
|
||||||
)
|
)
|
||||||
def test_getlength(mode, text, direction, expected):
|
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:
|
except ValueError as ex:
|
||||||
if (
|
if (
|
||||||
direction == "ttb"
|
direction == "ttb"
|
||||||
|
@ -374,6 +376,8 @@ def test_combine_multiline(anchor, align):
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
d.line(((0, 200), (400, 200)), "gray")
|
d.line(((0, 200), (400, 200)), "gray")
|
||||||
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)
|
||||||
|
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, fill="black", anchor=anchor, font=f, align=align)
|
||||||
|
|
||||||
with Image.open(path) as expected:
|
with Image.open(path) as expected:
|
||||||
|
@ -396,14 +400,28 @@ def test_anchor_invalid_ttb():
|
||||||
pytest.raises(
|
pytest.raises(
|
||||||
ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb")
|
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(
|
pytest.raises(
|
||||||
ValueError,
|
ValueError,
|
||||||
lambda: d.multiline_text(
|
lambda: d.multiline_text(
|
||||||
(0, 0), "foo\nbar", anchor=anchor, direction="ttb"
|
(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
|
# ttb multiline text does not support anchors at all
|
||||||
pytest.raises(
|
pytest.raises(
|
||||||
ValueError,
|
ValueError,
|
||||||
lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"),
|
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":
|
elif anchor[1] in "tb":
|
||||||
raise ValueError("anchor not supported for multiline text")
|
raise ValueError("anchor not supported for multiline text")
|
||||||
|
|
||||||
if font is None:
|
|
||||||
font = self.getfont()
|
|
||||||
|
|
||||||
widths = []
|
widths = []
|
||||||
max_width = 0
|
max_width = 0
|
||||||
lines = self._multiline_split(text)
|
lines = self._multiline_split(text)
|
||||||
|
@ -395,12 +392,8 @@ class ImageDraw:
|
||||||
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
|
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
|
||||||
)
|
)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line_width = font.getlength(
|
line_width = self.textlength(
|
||||||
line,
|
line, font, direction=direction, features=features, language=language
|
||||||
self.fontmode,
|
|
||||||
direction=direction,
|
|
||||||
features=features,
|
|
||||||
language=language,
|
|
||||||
)
|
)
|
||||||
widths.append(line_width)
|
widths.append(line_width)
|
||||||
max_width = max(max_width, line_width)
|
max_width = max(max_width, line_width)
|
||||||
|
@ -487,6 +480,148 @@ class ImageDraw:
|
||||||
max_width = max(max_width, line_width)
|
max_width = max(max_width, line_width)
|
||||||
return max_width, len(lines) * line_spacing - spacing
|
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):
|
def Draw(im, mode=None):
|
||||||
"""
|
"""
|
||||||
|
|