blob: aa9765e63b2cb7084aced8f8fa63ea29e5e1ac64 [file] [log] [blame]
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):
modified = fonts_to_quadratic(fonts)
assert 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)],
]