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.
295 lines
9.9 KiB
Python
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)],
|
|
]
|