fonttools/Tests/cu2qu/ufo_test.py
Cosimo Lupo f02813bd54
[cu2qu/ufo] return set of modified glyph names from fonts_to_quadratic
In ufo2ft preProcessor, we need to know which specific glyphs were actually modified (converted to quadratic), in order to do other things while processing filters, not simply if the fonts were modified as a whole; thus, here I changed fonts_to_quadratic to return the set of modified glyph names instead of just True/False. The change is backward compatible because code that checks whether the returned value is True/False will continue to work since bool(set) is True for non-empty set, False for empty ones.
2024-03-04 12:11:09 +00:00

295 lines
9.9 KiB
Python

import os
from fontTools.misc.loggingTools import CapturingLogHandler
from fontTools.cu2qu.ufo import (
fonts_to_quadratic,
font_to_quadratic,
glyphs_to_quadratic,
glyph_to_quadratic,
logger,
CURVE_TYPE_LIB_KEY,
)
from fontTools.cu2qu.errors import (
IncompatibleSegmentNumberError,
IncompatibleSegmentTypesError,
IncompatibleFontsError,
)
import pytest
ufoLib2 = pytest.importorskip("ufoLib2")
DATADIR = os.path.join(os.path.dirname(__file__), "data")
TEST_UFOS = [
os.path.join(DATADIR, "RobotoSubset-Regular.ufo"),
os.path.join(DATADIR, "RobotoSubset-Bold.ufo"),
]
@pytest.fixture
def fonts():
return [ufoLib2.Font.open(ufo) for ufo in TEST_UFOS]
class FontsToQuadraticTest(object):
def test_modified(self, fonts):
# previously this method returned True/False, now it returns a set of modified
# glyph names.
modified = fonts_to_quadratic(fonts)
# the first assertion continues to work whether the return value is a bool/set
# so the change is backward compatible
assert modified
assert len(modified) > 0
assert "B" in modified
def test_stats(self, fonts):
stats = {}
fonts_to_quadratic(fonts, stats=stats)
assert stats == {"1": 1, "2": 79, "3": 130, "4": 2}
def test_dump_stats(self, fonts):
with CapturingLogHandler(logger, "INFO") as captor:
fonts_to_quadratic(fonts, dump_stats=True)
assert captor.assertRegex("New spline lengths:")
def test_remember_curve_type_quadratic(self, fonts):
fonts_to_quadratic(fonts, remember_curve_type=True)
assert fonts[0].lib[CURVE_TYPE_LIB_KEY] == "quadratic"
with CapturingLogHandler(logger, "INFO") as captor:
fonts_to_quadratic(fonts, remember_curve_type=True)
assert captor.assertRegex("already converted")
def test_remember_curve_type_mixed(self, fonts):
fonts_to_quadratic(fonts, remember_curve_type=True, all_quadratic=False)
assert fonts[0].lib[CURVE_TYPE_LIB_KEY] == "mixed"
with CapturingLogHandler(logger, "INFO") as captor:
fonts_to_quadratic(fonts, remember_curve_type=True)
assert captor.assertRegex("already converted")
def test_no_remember_curve_type(self, fonts):
assert CURVE_TYPE_LIB_KEY not in fonts[0].lib
fonts_to_quadratic(fonts, remember_curve_type=False)
assert CURVE_TYPE_LIB_KEY not in fonts[0].lib
def test_different_glyphsets(self, fonts):
del fonts[0]["a"]
assert "a" not in fonts[0]
assert "a" in fonts[1]
assert fonts_to_quadratic(fonts)
def test_max_err_em_float(self, fonts):
stats = {}
fonts_to_quadratic(fonts, max_err_em=0.002, stats=stats)
assert stats == {"1": 5, "2": 193, "3": 14}
def test_max_err_em_list(self, fonts):
stats = {}
fonts_to_quadratic(fonts, max_err_em=[0.002, 0.002], stats=stats)
assert stats == {"1": 5, "2": 193, "3": 14}
def test_max_err_float(self, fonts):
stats = {}
fonts_to_quadratic(fonts, max_err=4.096, stats=stats)
assert stats == {"1": 5, "2": 193, "3": 14}
def test_max_err_list(self, fonts):
stats = {}
fonts_to_quadratic(fonts, max_err=[4.096, 4.096], stats=stats)
assert stats == {"1": 5, "2": 193, "3": 14}
def test_both_max_err_and_max_err_em(self, fonts):
with pytest.raises(TypeError, match="Only one .* can be specified"):
fonts_to_quadratic(fonts, max_err=1.000, max_err_em=0.001)
def test_single_font(self, fonts):
assert font_to_quadratic(fonts[0], max_err_em=0.002, reverse_direction=True)
assert font_to_quadratic(
fonts[1], max_err_em=0.002, reverse_direction=True, all_quadratic=False
)
class GlyphsToQuadraticTest(object):
@pytest.mark.parametrize(
["glyph", "expected"],
[("A", False), ("a", True)], # contains no curves, it is not modified
ids=["lines-only", "has-curves"],
)
def test_modified(self, fonts, glyph, expected):
glyphs = [f[glyph] for f in fonts]
assert glyphs_to_quadratic(glyphs) == expected
def test_stats(self, fonts):
stats = {}
glyphs_to_quadratic([f["a"] for f in fonts], stats=stats)
assert stats == {"2": 1, "3": 7, "4": 3, "5": 1}
def test_max_err_float(self, fonts):
glyphs = [f["a"] for f in fonts]
stats = {}
glyphs_to_quadratic(glyphs, max_err=4.096, stats=stats)
assert stats == {"2": 11, "3": 1}
def test_max_err_list(self, fonts):
glyphs = [f["a"] for f in fonts]
stats = {}
glyphs_to_quadratic(glyphs, max_err=[4.096, 4.096], stats=stats)
assert stats == {"2": 11, "3": 1}
def test_reverse_direction(self, fonts):
glyphs = [f["A"] for f in fonts]
assert glyphs_to_quadratic(glyphs, reverse_direction=True)
def test_single_glyph(self, fonts):
assert glyph_to_quadratic(fonts[0]["a"], max_err=4.096, reverse_direction=True)
@pytest.mark.parametrize(
["outlines", "exception", "message"],
[
[
[
[
("moveTo", ((0, 0),)),
("curveTo", ((1, 1), (2, 2), (3, 3))),
("curveTo", ((4, 4), (5, 5), (6, 6))),
("closePath", ()),
],
[
("moveTo", ((7, 7),)),
("curveTo", ((8, 8), (9, 9), (10, 10))),
("closePath", ()),
],
],
IncompatibleSegmentNumberError,
"have different number of segments",
],
[
[
[
("moveTo", ((0, 0),)),
("curveTo", ((1, 1), (2, 2), (3, 3))),
("closePath", ()),
],
[
("moveTo", ((4, 4),)),
("lineTo", ((5, 5),)),
("closePath", ()),
],
],
IncompatibleSegmentTypesError,
"have incompatible segment types",
],
],
ids=[
"unequal-length",
"different-segment-types",
],
)
def test_incompatible_glyphs(self, outlines, exception, message):
glyphs = []
for i, outline in enumerate(outlines):
glyph = ufoLib2.objects.Glyph("glyph%d" % i)
pen = glyph.getPen()
for operator, args in outline:
getattr(pen, operator)(*args)
glyphs.append(glyph)
with pytest.raises(exception) as excinfo:
glyphs_to_quadratic(glyphs)
assert excinfo.match(message)
def test_incompatible_fonts(self):
font1 = ufoLib2.Font()
font1.info.unitsPerEm = 1000
glyph1 = font1.newGlyph("a")
pen1 = glyph1.getPen()
for operator, args in [
("moveTo", ((0, 0),)),
("lineTo", ((1, 1),)),
("endPath", ()),
]:
getattr(pen1, operator)(*args)
font2 = ufoLib2.Font()
font2.info.unitsPerEm = 1000
glyph2 = font2.newGlyph("a")
pen2 = glyph2.getPen()
for operator, args in [
("moveTo", ((0, 0),)),
("curveTo", ((1, 1), (2, 2), (3, 3))),
("endPath", ()),
]:
getattr(pen2, operator)(*args)
with pytest.raises(IncompatibleFontsError) as excinfo:
fonts_to_quadratic([font1, font2])
assert excinfo.match("fonts contains incompatible glyphs: 'a'")
assert hasattr(excinfo.value, "glyph_errors")
error = excinfo.value.glyph_errors["a"]
assert isinstance(error, IncompatibleSegmentTypesError)
assert error.segments == {1: ["line", "curve"]}
def test_already_quadratic(self):
glyph = ufoLib2.objects.Glyph()
pen = glyph.getPen()
pen.moveTo((0, 0))
pen.qCurveTo((1, 1), (2, 2))
pen.closePath()
assert not glyph_to_quadratic(glyph)
def test_open_paths(self):
glyph = ufoLib2.objects.Glyph()
pen = glyph.getPen()
pen.moveTo((0, 0))
pen.lineTo((1, 1))
pen.curveTo((2, 2), (3, 3), (4, 4))
pen.endPath()
assert glyph_to_quadratic(glyph)
# open contour is still open
assert glyph[-1][0].segmentType == "move"
def test_ignore_components(self):
glyph = ufoLib2.objects.Glyph()
pen = glyph.getPen()
pen.addComponent("a", (1, 0, 0, 1, 0, 0))
pen.moveTo((0, 0))
pen.curveTo((1, 1), (2, 2), (3, 3))
pen.closePath()
assert glyph_to_quadratic(glyph)
assert len(glyph.components) == 1
def test_overlapping_start_end_points(self):
# https://github.com/googlefonts/fontmake/issues/572
glyph1 = ufoLib2.objects.Glyph()
pen = glyph1.getPointPen()
pen.beginPath()
pen.addPoint((0, 651), segmentType="line")
pen.addPoint((0, 101), segmentType="line")
pen.addPoint((0, 101), segmentType="line")
pen.addPoint((0, 651), segmentType="line")
pen.endPath()
glyph2 = ufoLib2.objects.Glyph()
pen = glyph2.getPointPen()
pen.beginPath()
pen.addPoint((1, 651), segmentType="line")
pen.addPoint((2, 101), segmentType="line")
pen.addPoint((3, 101), segmentType="line")
pen.addPoint((4, 651), segmentType="line")
pen.endPath()
glyphs = [glyph1, glyph2]
assert glyphs_to_quadratic(glyphs, reverse_direction=True)
assert [[(p.x, p.y) for p in glyph[0]] for glyph in glyphs] == [
[
(0, 651),
(0, 651),
(0, 101),
(0, 101),
],
[(1, 651), (4, 651), (3, 101), (2, 101)],
]