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 9 commits
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
2 changes: 2 additions & 0 deletions Tests/fonts/LICENSE.txt
@@ -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 All @@ -8,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/NotoSans-Regular.ttf
Binary file not shown.
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_anchor_quick_ls.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_quick_ma.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_quick_mb.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_quick_md.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_quick_mm.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_quick_ms.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_quick_mt.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_quick_rs.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_ttb_f_lt.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_ttb_f_mm.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_ttb_f_rb.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_ttb_f_sm.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Tests/images/test_arabictext_features.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_caron.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_caron_below.png
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_multiline_lm_center.png
Binary file added Tests/images/test_combine_multiline_lm_left.png
Binary file added Tests/images/test_combine_multiline_lm_right.png
Binary file added Tests/images/test_combine_multiline_mm_left.png
Binary file added Tests/images/test_combine_multiline_mm_right.png
Binary file added Tests/images/test_combine_multiline_rm_center.png
Binary file added Tests/images/test_combine_multiline_rm_left.png
Binary file added Tests/images/test_combine_multiline_rm_right.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
Binary file modified Tests/images/test_complex_unicode_text.png
Binary file modified Tests/images/test_complex_unicode_text2.png
Binary file modified Tests/images/test_direction_ltr.png
Binary file modified Tests/images/test_direction_rtl.png
Binary file modified Tests/images/test_direction_ttb.png
Binary file modified Tests/images/test_direction_ttb_stroke.png
Binary file modified Tests/images/test_kerning_features.png
Binary file modified Tests/images/test_language.png
Binary file modified Tests/images/test_ligature_features.png
Binary file modified Tests/images/test_text.png
Binary file modified Tests/images/test_x_max_and_y_offset.png
Binary file modified Tests/images/test_y_offset.png
6 changes: 3 additions & 3 deletions Tests/test_font_leaks.py
Expand Up @@ -5,15 +5,15 @@

class TestTTypeFontLeak(PillowLeakTestCase):
# fails at iteration 3 in master
iterations = 10
iterations = 40
mem_limit = 4096 # k

def _test_font(self, font):
im = Image.new("RGB", (255, 255), "white")
draw = ImageDraw.ImageDraw(im)
self._test_leak(
lambda: draw.text(
(0, 0), "some text " * 1024, font=font, fill="black" # ~10k
(0, 0), "some text " * 256, font=font, fill="black" # ~10k
)
)

Expand All @@ -25,7 +25,7 @@ def test_leak(self):

class TestDefaultFontLeak(TestTTypeFontLeak):
# fails at iteration 37 in master
iterations = 100
iterations = 400
mem_limit = 1024 # k

def test_leak(self):
Expand Down
8 changes: 4 additions & 4 deletions Tests/test_imagedraw.py
Expand Up @@ -1015,7 +1015,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(
Expand All @@ -1031,11 +1031,11 @@ 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(
im, Image.open("Tests/images/imagedraw_stroke_descender.png"), 6.76
im, Image.open("Tests/images/imagedraw_stroke_descender.png"), 6.78
)


Expand All @@ -1048,7 +1048,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
148 changes: 137 additions & 11 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": (13, 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 @@ -342,7 +385,7 @@ def test_rotated_transposed_font_get_mask(self):
mask = transposed_font.getmask(text)

# Assert
assert mask.size == (13, 108)
assert mask.size in ((13, 107), (13, 108))

def test_unrotated_transposed_font_get_mask(self):
# Arrange
Expand All @@ -355,7 +398,7 @@ def test_unrotated_transposed_font_get_mask(self):
mask = transposed_font.getmask(text)

# Assert
assert mask.size == (108, 13)
assert mask.size in ((107, 13), (108, 13))

def test_free_type_font_get_name(self):
# Arrange
Expand Down Expand Up @@ -399,7 +442,7 @@ def test_free_type_font_get_mask(self):
mask = font.getmask(text)

# Assert
assert mask.size == (108, 13)
assert mask.size in ((107, 13), (108, 13))

def test_load_path_not_found(self):
# Arrange
Expand Down Expand Up @@ -470,7 +513,8 @@ def test_unicode_extended(self):
d = ImageDraw.Draw(img)
d.text((10, 10), text, font=ttf)

assert_image_similar_tofile(img, target, self.metrics["multiline"])
# fails with 14.7
assert_image_similar_tofile(img, target, 6.2)

def _test_fake_loading_font(self, monkeypatch, path_to_fake, fontname):
# Make a copy of FreeTypeFont so we can patch the original
Expand Down Expand Up @@ -580,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 @@ -702,10 +746,10 @@ def test_variation_set_by_name(self):
font.set_variation_by_name("Bold")

font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
self._check_text(font, "Tests/images/variation_adobe.png", 11)
self._check_text(font, "Tests/images/variation_adobe.png", 11.2)
for name in ["Bold", b"Bold"]:
font.set_variation_by_name(name)
self._check_text(font, "Tests/images/variation_adobe_name.png", 11)
self._check_text(font, "Tests/images/variation_adobe_name.png", 11.3)

font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
self._check_text(font, "Tests/images/variation_tiny.png", 40)
Expand All @@ -727,12 +771,94 @@ def test_variation_set_by_axes(self):

font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
font.set_variation_by_axes([500, 50])
self._check_text(font, "Tests/images/variation_adobe_axes.png", 5.1)
self._check_text(font, "Tests/images/variation_adobe_axes.png", 5.8)

font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
font.set_variation_by_axes([100])
self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)

@pytest.mark.parametrize(
"anchor,left,left_raqm,top",
(
# test horizontal anchors
("ls", 0, 0, -36),
("ms", -64, -65, -36),
("rs", -128, -129, -36),
# test vertical anchors
("ma", -64, -65, 16),
("mt", -64, -65, 0),
("mm", -64, -65, -17),
("mb", -64, -65, -44),
("md", -64, -65, -51),
),
)
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(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