Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add getlength and getbbox functions for TrueType fonts #4959

Merged
merged 12 commits into from Oct 12, 2020
1 change: 1 addition & 0 deletions Tests/fonts/LICENSE.txt
Expand Up @@ -9,6 +9,7 @@ ter-x20b.pcf, from http://terminus-font.sourceforge.net/

All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to.

OpenSansCondensed-LightItalic.tt, from https://fonts.google.com/specimen/Open+Sans, under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)

10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base

Expand Down
Binary file added Tests/fonts/OpenSansCondensed-LightItalic.ttf
Binary file not shown.
Binary file added Tests/images/test_combine_multiline_lm_center.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/test_combine_multiline_lm_left.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/test_combine_multiline_lm_right.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/test_combine_multiline_mm_center.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/test_combine_multiline_mm_left.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/test_combine_multiline_mm_right.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/test_combine_multiline_rm_center.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/test_combine_multiline_rm_left.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/test_combine_multiline_rm_right.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 71 additions & 10 deletions Tests/test_imagefont.py
Expand Up @@ -41,20 +41,23 @@ class TestImageFont:
"getters": (13, 16),
"mask": (107, 13),
"multiline-anchor": 6,
"getlength": (36, 27, 27, 33),
},
(">=2.7",): {
"multiline": 6.2,
"textsize": 2.5,
"getters": (12, 16),
"mask": (108, 13),
"multiline-anchor": 4,
"getlength": (36, 21, 24, 33),
},
"Default": {
"multiline": 0.5,
"textsize": 0.5,
"getters": (12, 16),
"mask": (108, 13),
"multiline-anchor": 4,
"getlength": (36, 24, 24, 33),
},
}

Expand Down Expand Up @@ -198,6 +201,37 @@ def test_textsize_equal(self):
# Epsilon ~.5 fails with FreeType 2.7
assert_image_similar(im, target_img, self.metrics["textsize"])

@pytest.mark.parametrize(
"text,mode,font,size,length_basic_index,length_raqm",
hugovk marked this conversation as resolved.
Show resolved Hide resolved
(
# basic test
("text", "L", "FreeMono.ttf", 15, 0, 36),
("text", "1", "FreeMono.ttf", 15, 0, 36),
# issue 4177
("rrr", "L", "DejaVuSans.ttf", 18, 1, 22.21875),
("rrr", "1", "DejaVuSans.ttf", 18, 2, 22.21875),
# test 'l' not including extra margin
# using exact value 2047 / 64 for raqm, checked with debugger
("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 3, 31.984375),
("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 3, 31.984375),
),
)
def test_getlength(self, text, mode, font, size, length_basic_index, length_raqm):
f = ImageFont.truetype(
"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 = d.textlength(text, f)
assert length == self.metrics["getlength"][length_basic_index]
else:
# disable kerning, kerning metrics changed
length = d.textlength(text, f, features=["-kern"])
assert length == length_raqm

def test_render_multiline(self):
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
Expand Down Expand Up @@ -754,23 +788,36 @@ def test_variation_set_by_axes(self):
self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)

@pytest.mark.parametrize(
"anchor",
"anchor,left,left_old,top",
nulano marked this conversation as resolved.
Show resolved Hide resolved
(
# test horizontal anchors
"ls",
"ms",
"rs",
("ls", 0, 0, -36),
("ms", -64, -65, -36),
("rs", -128, -129, -36),
# test vertical anchors
"ma",
"mt",
"mm",
"mb",
"md",
("ma", -64, -65, 16),
("mt", -64, -65, 0),
("mm", -64, -65, -17),
("mb", -64, -65, -44),
("md", -64, -65, -51),
),
ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
)
def test_anchor(self, anchor):
def test_anchor(self, anchor, left, left_old, top):
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"

freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.4"):
width, height = (129, 44)
left = left_old
elif self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM:
width, height = (129, 44)
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
)
Expand All @@ -781,6 +828,8 @@ def test_anchor(self, anchor):
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)

Expand Down Expand Up @@ -831,14 +880,26 @@ def test_anchor_invalid(self):

for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]:
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")
Expand Down
103 changes: 103 additions & 0 deletions Tests/test_imagefontctl.py
Expand Up @@ -209,6 +209,59 @@ def test_language():
assert_image_similar(im, target_img, 0.5)


@pytest.mark.parametrize("mode", ("L", "1"))
@pytest.mark.parametrize(
"text,direction,expected",
nulano marked this conversation as resolved.
Show resolved Hide resolved
(
("سلطنة عمان Oman", None, 173.703125),
("سلطنة عمان Oman", "ltr", 173.703125),
("Oman سلطنة عمان", "rtl", 173.703125),
("English عربي", "rtl", 123.796875),
("test", "ttb", 80.0),
),
ids=("None", "ltr", "rtl2", "rtl", "ttb"),
)
def test_getlength(mode, text, direction, expected):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im)

try:
assert d.textlength(text, ttf, direction) == expected
except ValueError as ex:
if (
direction == "ttb"
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
):
pytest.skip("libraqm 0.7 or greater not available")


@pytest.mark.parametrize("mode", ("L", "1"))
@pytest.mark.parametrize("direction", ("ltr", "ttb"))
@pytest.mark.parametrize(
"text",
("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"),
ids=("caron-above", "caron-below", "double-breve", "overline"),
)
def test_getlength_combine(mode, direction, text):
if text == "i\u0305i" and direction == "ttb":
pytest.skip("fails with this font")

ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)

try:
target = ttf.getlength("ii", mode, direction)
actual = ttf.getlength(text, mode, direction)

assert actual == target
except ValueError as ex:
if (
direction == "ttb"
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
):
pytest.skip("libraqm 0.7 or greater not available")


@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
def test_anchor_ttb(anchor):
if parse_version(features.version_module("freetype2")) < parse_version("2.5.1"):
Expand Down Expand Up @@ -298,6 +351,39 @@ def test_combine(name, text, dir, anchor, epsilon):
assert_image_similar(im, expected, epsilon)


@pytest.mark.parametrize(
"anchor,align",
nulano marked this conversation as resolved.
Show resolved Hide resolved
(
("lm", "left"), # pass with getsize
("lm", "center"), # fail at 2.12
("lm", "right"), # fail at 2.57
("mm", "left"), # fail at 2.12
("mm", "center"), # pass with getsize
("mm", "right"), # fail at 2.12
("rm", "left"), # fail at 2.57
("rm", "center"), # fail at 2.12
("rm", "right"), # pass with getsize
),
)
def test_combine_multiline(anchor, align):
# test that multiline text uses getlength, not getsize or getbbox

path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word

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, 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:
assert_image_similar(im, expected, 0.015)


def test_anchor_invalid_ttb():
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new("RGB", (100, 100), "white")
Expand All @@ -308,17 +394,34 @@ def test_anchor_invalid_ttb():
pytest.raises(
ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb")
)
pytest.raises(
ValueError, lambda: font.getbbox("hello", anchor=anchor, direction="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"),
)