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

Implement anchor for truetype text functions #4724

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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_anchor_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_anchor_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_anchor_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_anchor_multiline_ma_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_anchor_multiline_md_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_anchor_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_anchor_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_anchor_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_anchor_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_anchor_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_anchor_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.
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.
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.
127 changes: 111 additions & 16 deletions Tests/test_imagefont.py
Expand Up @@ -34,9 +34,24 @@ class TestImageFont:
# Freetype has different metrics depending on the version.
# (and, other things, but first things first)
METRICS = {
(">=2.3", "<2.4"): {"multiline": 30, "textsize": 12, "getters": (12, 16)},
(">=2.7",): {"multiline": 6.2, "textsize": 2.5, "getters": (12, 16)},
"Default": {"multiline": 0.5, "textsize": 0.5, "getters": (12, 16)},
(">=2.3", "<2.4"): {
"multiline": 30,
"textsize": 12,
"multiline-anchor": 6,
"getlength": (36, 27, 27, 33),
},
(">=2.7",): {
"multiline": 6.2,
"textsize": 2.5,
"multiline-anchor": 4,
"getlength": (36, 21, 24, 33),
},
"Default": {
"multiline": 0.5,
"textsize": 0.5,
"multiline-anchor": 4,
"getlength": (36, 24, 24, 33),
},
}

@classmethod
Expand Down Expand Up @@ -179,6 +194,34 @@ 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",
(
# 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
)

if self.LAYOUT_ENGINE == ImageFont.LAYOUT_BASIC:
length = f.getlength(text, mode)
assert length == self.metrics["getlength"][length_basic_index]
else:
# disable kerning, kerning metrics changed
length = f.getlength(text, mode, 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 @@ -581,7 +624,7 @@ def test_imagefont_getters(self):
assert t.font.glyphs == 4177
assert t.getsize("A") == (12, 16)
assert t.getsize("AB") == (24, 16)
assert t.getsize("M") == self.metrics["getters"]
assert t.getsize("M") == (12, 16)
assert t.getsize("y") == (12, 20)
assert t.getsize("a") == (12, 16)
assert t.getsize_multiline("A") == (12, 16)
Expand Down Expand Up @@ -735,35 +778,87 @@ def test_variation_set_by_axes(self):
self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)

@pytest.mark.parametrize(
"name,text,anchor",
"anchor,left,left_raqm,top",
(
# test horizontal anchors
("quick", "Quick", "ls"),
("quick", "Quick", "ms"),
("quick", "Quick", "rs"),
("ls", 0, 0, -36),
("ms", -64, -65, -36),
("rs", -128, -129, -36),
# test vertical anchors
("quick", "Quick", "ma"),
("quick", "Quick", "mt"),
("quick", "Quick", "mm"),
("quick", "Quick", "mb"),
("quick", "Quick", "md"),
("ma", -64, -65, 16),
("mt", -64, -65, 0),
("mm", -64, -65, -17),
("mb", -64, -65, -44),
("md", -64, -65, -51),
),
)
def test_anchor(self, name, text, anchor):
path = "Tests/images/test_anchor_%s_%s.png" % (name, anchor)
def test_anchor(self, anchor, left, left_raqm, top):
name, text = "quick", "Quick"
target = "Tests/images/test_anchor_%s_%s.png" % (name, anchor)
freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)

if self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM or freetype < "2.4":
width, height = (129, 44)
left = left_raqm
else:
width, height = (128, 44)

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)

with Image.open(path) as expected:
with Image.open(target) as expected:
assert_image_similar(im, expected, 7)

@pytest.mark.parametrize(
"anchor,align",
(
# test horizontal anchors
("lm", "left"),
("lm", "center"),
("lm", "right"),
("mm", "left"),
("mm", "center"),
("mm", "right"),
("rm", "left"),
("rm", "center"),
("rm", "right"),
# test vertical anchors
("ma", "center"),
# ("mm", "center"),
("md", "center"),
),
)
def test_anchor_multiline(self, anchor, align):
target = "Tests/images/test_anchor_multiline_%s_%s.png" % (anchor, align)
text = "a\nlong\ntext sample"

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

# test render
im = Image.new("RGB", (600, 400), "white")
d = ImageDraw.Draw(im)
d.line(((0, 200), (600, 200)), "gray")
d.line(((300, 0), (300, 400)), "gray")
d.multiline_text(
(300, 200), text, fill="black", anchor=anchor, font=f, align=align
)

with Image.open(target) as expected:
assert_image_similar(im, expected, self.metrics["multiline-anchor"])


@skip_unless_feature("raqm")
class TestImageFont_RaqmLayout(TestImageFont):
Expand Down
87 changes: 86 additions & 1 deletion Tests/test_imagefontctl.py
Expand Up @@ -209,6 +209,57 @@ def test_language():
assert_image_similar(im, target_img, 0.5)


@pytest.mark.parametrize("mode", ("L", "1"))
@pytest.mark.parametrize(
"text,direction,expected",
(
("سلطنة عمان 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):
try:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)

assert ttf.getlength(text, mode, 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 distutils.version.StrictVersion(ImageFont.core.freetype2_version) < "2.5.1":
Expand Down Expand Up @@ -291,8 +342,42 @@ def test_combine(name, text, dir, anchor, epsilon):
try:
d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f)
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
if (
dir == "ttb"
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
):
pytest.skip("libraqm 0.7 or greater not available")

with Image.open(path) as expected:
assert_image_similar(im, expected, epsilon)


@pytest.mark.parametrize(
"anchor,align",
(
("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 getline, not getsize or getbbox

path = "Tests/images/test_combine_multiline_%s_%s.png" % (anchor, align)
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")
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)
8 changes: 5 additions & 3 deletions src/PIL/ImageDraw.py
Expand Up @@ -379,20 +379,22 @@ def multiline_text(
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)
line_spacing = (
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
)
for line in lines:
line_width, line_height = self.textsize(
line_width = font.getlength(
line,
font,
self.fontmode,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
)
widths.append(line_width)
max_width = max(max_width, line_width)
Expand Down