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(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_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) class GlyphsToQuadraticTest(object): @pytest.mark.parametrize( ["glyph", "expected"], [('A', False), # contains no curves, it is not modified ('a', True)], 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) ], ]