Skip to content

Commit

Permalink
add getbbox and getlength, with tests
Browse files Browse the repository at this point in the history
(cherry picked from commit c5f6373)
  • Loading branch information
nulano committed Oct 7, 2020
1 parent 877831b commit 0ffd51a
Show file tree
Hide file tree
Showing 16 changed files with 296 additions and 13 deletions.
1 change: 1 addition & 0 deletions Tests/fonts/LICENSE.txt
Original file line number Diff line number Diff line change
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
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_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_combine_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.
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_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_combine_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_combine_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_combine_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_combine_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.
63 changes: 53 additions & 10 deletions Tests/test_imagefont.py
Original file line number Diff line number Diff line change
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,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 @@ -754,27 +785,39 @@ 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_raqm,top",
(
# 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),
),
)
def test_anchor(self, anchor):
def test_anchor(self, anchor, left, left_raqm, top):
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
freetype = parse_version(features.version_module("freetype2"))

if self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM or freetype < parse_version("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")
Expand Down
82 changes: 82 additions & 0 deletions Tests/test_imagefontctl.py
Original file line number Diff line number Diff line change
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 parse_version(features.version_module("freetype2")) < parse_version("2.5.1"):
Expand Down Expand Up @@ -298,6 +349,37 @@ def test_combine(name, text, dir, anchor, epsilon):
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)


def test_anchor_invalid_ttb():
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new("RGB", (100, 100), "white")
Expand Down
8 changes: 5 additions & 3 deletions src/PIL/ImageDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,20 +385,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
110 changes: 110 additions & 0 deletions src/PIL/ImageFont.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,123 @@ def getmetrics(self):
"""
return self.font.ascent, self.font.descent

def getlength(self, text, mode="", direction=None, features=None, language=None):
"""
Returns length (in pixels) of given text if rendered in font with
provided direction, features, and language.
This is the amount by which following text should be offset.
Text bounding box may extend past the length in some fonts,
e.g. when using italics or accents.
The result is returned as a float; it is a whole number if using basic layout.
:param text: Text to measure.
:param mode: Used by some graphics drivers to indicate what mode the
driver prefers; if empty, the renderer may return either
mode. Note that the mode is always a string, to simplify
C-level implementations.
:param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right) or 'ttb' (top to bottom).
Requires libraqm.
:param features: A list of OpenType font features to be used during text
layout. This is usually used to turn on optional
font features that are not enabled by default,
for example 'dlig' or 'ss01', but can be also
used to turn off default font features for
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP 47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
:return: Width for horizontal, height for vertical text.
"""
return (
self.font.getlength(text, mode == "1", direction, features, language) / 64
)

def getbbox(
self,
text,
mode="",
direction=None,
features=None,
language=None,
stroke_width=0,
anchor=None,
):
"""
Returns bounding box (in pixels) of given text relative to given anchor
if rendered in font with provided direction, features, and language.
Use :py:func`getlength()` to get the offset of following text. The bounding
box includes extra margins for some fonts, e.g. italics or accents.
:param text: Text to render.
:param mode: Used by some graphics drivers to indicate what mode the
driver prefers; if empty, the renderer may return either
mode. Note that the mode is always a string, to simplify
C-level implementations.
:param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right) or 'ttb' (top to bottom).
Requires libraqm.
:param features: A list of OpenType font features to be used during text
layout. This is usually used to turn on optional
font features that are not enabled by default,
for example 'dlig' or 'ss01', but can be also
used to turn off default font features for
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP 47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
:param stroke_width: The width of the text stroke.
:param anchor: The text anchor alignment. Determines the relative location of
the anchor to the text. The default alignment is top left.
See :ref:`text-anchors` for valid values.
:return: ``(left, top, right, bottom)`` bounding box
"""
size, offset = self.font.getsize(
text, mode == "1", direction, features, language, anchor
)
left, top = offset[0] - stroke_width, offset[1] - stroke_width
width, height = size[0] + stroke_width, size[1] + stroke_width
return left, top, left + width, top + height

def getsize(
self, text, direction=None, features=None, language=None, stroke_width=0
):
"""
Returns width and height (in pixels) of given text if rendered in font with
provided direction, features, and language.
Use :py:func:`getlength()` to measure the offset of following text.
Use :py:func:`getbbox()` to get the exact bounding box based on an anchor.
:param text: Text to measure.
:param direction: Direction of the text. It can be 'rtl' (right to
Expand Down
Loading

0 comments on commit 0ffd51a

Please sign in to comment.