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 fonts #4930

Merged
merged 8 commits into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Tests/fonts/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
Expand Down
Binary file added Tests/fonts/NotoSans-Regular.ttf
Binary file not shown.
Binary file added Tests/images/test_anchor_multiline_lm_center.png
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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_quick_ls.png
Loading
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_quick_ma.png
Loading
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_quick_mb.png
Loading
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_quick_md.png
Loading
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_quick_mm.png
Loading
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_quick_ms.png
Loading
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_quick_mt.png
Loading
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_quick_rs.png
Loading
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_ttb_f_lt.png
Loading
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_ttb_f_mm.png
Loading
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_ttb_f_rb.png
Loading
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_ttb_f_sm.png
Loading
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_caron.png
Loading
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_caron_below.png
Loading
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_caron_below_lb.png
Binary file added Tests/images/test_combine_caron_below_ld.png
Binary file added Tests/images/test_combine_caron_below_ls.png
Binary file added Tests/images/test_combine_caron_below_ttb.png
Binary file added Tests/images/test_combine_caron_below_ttb_lb.png
Binary file added Tests/images/test_combine_caron_la.png
Binary file added Tests/images/test_combine_caron_ls.png
Binary file added Tests/images/test_combine_caron_lt.png
Binary file added Tests/images/test_combine_caron_ttb.png
Binary file added Tests/images/test_combine_caron_ttb_lt.png
Binary file added Tests/images/test_combine_double_breve_below.png
Binary file added Tests/images/test_combine_overline.png
Binary file added Tests/images/test_combine_overline_la.png
Binary file added Tests/images/test_combine_overline_ra.png
Binary file added Tests/images/test_combine_overline_ttb.png
Binary file added Tests/images/test_combine_overline_ttb_mt.png
Binary file added Tests/images/test_combine_overline_ttb_rt.png
Binary file added Tests/images/test_combine_overline_ttb_st.png
6 changes: 3 additions & 3 deletions Tests/test_imagedraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,7 @@ def test_stroke():
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)

# Act
draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill)
draw.text((12, 12), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill)

# Assert
assert_image_similar_tofile(
Expand All @@ -1036,7 +1036,7 @@ def test_stroke_descender():
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)

# Act
draw.text((10, 0), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0")
draw.text((12, 2), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0")

# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76)
Expand All @@ -1051,7 +1051,7 @@ def test_stroke_multiline():

# Act
draw.multiline_text(
(10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0"
(12, 12), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0"
)

# Assert
Expand Down
90 changes: 90 additions & 0 deletions Tests/test_imagefont.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,21 @@ class TestImageFont:
"textsize": 12,
"getters": (13, 16),
"mask": (107, 13),
"multiline-anchor": 6,
},
(">=2.7",): {
"multiline": 6.2,
"textsize": 2.5,
"getters": (12, 16),
"mask": (108, 13),
"multiline-anchor": 4,
},
"Default": {
"multiline": 0.5,
"textsize": 0.5,
"getters": (12, 16),
"mask": (108, 13),
"multiline-anchor": 4,
},
}

Expand Down Expand Up @@ -750,6 +753,93 @@ def test_variation_set_by_axes(self):
font.set_variation_by_axes([100])
self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)

@pytest.mark.parametrize(
"anchor",
(
# test horizontal anchors
"ls",
"ms",
"rs",
# test vertical anchors
"ma",
"mt",
"mm",
"mb",
"md",
),
)
def test_anchor(self, anchor):
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
f = ImageFont.truetype(
"Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE
)

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:
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"), # duplicate
("md", "center"),
),
)
def test_anchor_multiline(self, anchor, align):
target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
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"])

def test_anchor_invalid(self):
font = self.get_font()
im = Image.new("RGB", (100, 100), "white")
d = ImageDraw.Draw(im)
d.font = font

for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]:
pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor))
pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor))
pytest.raises(
ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
)
for anchor in ["lt", "lb"]:
pytest.raises(
ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
)


@skip_unless_feature("raqm")
class TestImageFont_RaqmLayout(TestImageFont):
Expand Down
120 changes: 118 additions & 2 deletions Tests/test_imagefontctl.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from packaging.version import parse as parse_version

from PIL import Image, ImageDraw, ImageFont
from PIL import Image, ImageDraw, ImageFont, features

from .helper import assert_image_similar, skip_unless_feature

Expand Down Expand Up @@ -123,7 +124,7 @@ def test_text_direction_ttb_stroke():
draw = ImageDraw.Draw(im)
try:
draw.text(
(25, 25),
(27, 27),
"あい",
font=ttf,
fill=500,
Expand Down Expand Up @@ -206,3 +207,118 @@ def test_language():
target = "Tests/images/test_language.png"
with Image.open(target) as target_img:
assert_image_similar(im, target_img, 0.5)


@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"):
# FreeType 2.5.1 README: Miscellaneous Changes:
# Improved computation of emulated vertical metrics for TrueType fonts.
pytest.skip("FreeType <2.5.1 has incompatible ttb metrics")

text = "f"
path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120)

im = Image.new("RGB", (200, 400), "white")
d = ImageDraw.Draw(im)
d.line(((0, 200), (200, 200)), "gray")
d.line(((100, 0), (100, 400)), "gray")
try:
d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
except ValueError as ex:
if 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, 1) # fails at 5


combine_tests = (
# extends above (e.g. issue #4553)
("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08),
("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08),
("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08),
("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08),
("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3),
("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3),
# extends below
("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02),
("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02),
("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02),
("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02),
("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03),
("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03),
# extends to the right (e.g. issue #3745)
("double_breve_below", "a\u035Ci", None, None, 0.02),
("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02),
("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02),
("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02),
("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02),
("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02),
("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02),
# extends to the left (fail=0.064)
("overline", "i\u0305", None, None, 0.02),
("overline_la", "i\u0305", "la", None, 0.02),
("overline_ra", "i\u0305", "ra", None, 0.02),
("overline_ttb", "i\u0305", None, "ttb", 0.02),
("overline_ttb_rt", "i\u0305", "rt", "ttb", 0.02),
("overline_ttb_mt", "i\u0305", "mt", "ttb", 0.02),
("overline_ttb_st", "i\u0305", "st", "ttb", 0.02),
)


# this tests various combining characters for anchor alignment and clipping
@pytest.mark.parametrize(
"name,text,anchor,dir,epsilon", combine_tests, ids=[r[0] for r in combine_tests]
)
def test_combine(name, text, dir, anchor, epsilon):
if (
parse_version(features.version_module("freetype2")) < parse_version("2.5.1")
and dir == "ttb"
):
# FreeType 2.5.1 README: Miscellaneous Changes:
# Improved computation of emulated vertical metrics for TrueType fonts.
pytest.skip("FreeType <2.5.1 has incompatible ttb metrics")

path = f"Tests/images/test_combine_{name}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)

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")
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":
pytest.skip("libraqm 0.7 or greater not available")

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


def test_anchor_invalid_ttb():
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new("RGB", (100, 100), "white")
d = ImageDraw.Draw(im)
d.font = font

for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]:
pytest.raises(
ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb")
)
pytest.raises(
ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb")
)
pytest.raises(
ValueError,
lambda: d.multiline_text(
(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"),
)
Binary file added docs/example/anchors.png
28 changes: 28 additions & 0 deletions docs/example/anchors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from PIL import Image, ImageDraw, ImageFont

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


def test(anchor):
im = Image.new("RGBA", (200, 100), "white")
d = ImageDraw.Draw(im)
d.line(((100, 0), (100, 100)), "gray")
d.line(((0, 50), (200, 50)), "gray")
d.text((100, 50), f"{anchor} example", "black", font, anchor)
return im


if __name__ == "__main__":
im = Image.new("RGBA", (600, 300), "white")
d = ImageDraw.Draw(im)
for y, row in enumerate(
(("ma", "mt", "mm"), ("ms", "mb", "md"), ("ls", "ms", "rs"))
):
for x, anchor in enumerate(row):
im.paste(test(anchor), (x * 200, y * 100))
if x != 0:
d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
if y != 0:
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
im.save("docs/example/anchors.png")
im.show()
1 change: 1 addition & 0 deletions docs/handbook/appendices.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ Appendices
:maxdepth: 2

image-file-formats
text-anchors
writing-your-own-file-decoder
Loading