xref: /aosp_15_r20/external/fonttools/Tests/cu2qu/ufo_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughesimport os
2*e1fe3e4aSElliott Hughes
3*e1fe3e4aSElliott Hughesfrom fontTools.misc.loggingTools import CapturingLogHandler
4*e1fe3e4aSElliott Hughesfrom fontTools.cu2qu.ufo import (
5*e1fe3e4aSElliott Hughes    fonts_to_quadratic,
6*e1fe3e4aSElliott Hughes    font_to_quadratic,
7*e1fe3e4aSElliott Hughes    glyphs_to_quadratic,
8*e1fe3e4aSElliott Hughes    glyph_to_quadratic,
9*e1fe3e4aSElliott Hughes    logger,
10*e1fe3e4aSElliott Hughes    CURVE_TYPE_LIB_KEY,
11*e1fe3e4aSElliott Hughes)
12*e1fe3e4aSElliott Hughesfrom fontTools.cu2qu.errors import (
13*e1fe3e4aSElliott Hughes    IncompatibleSegmentNumberError,
14*e1fe3e4aSElliott Hughes    IncompatibleSegmentTypesError,
15*e1fe3e4aSElliott Hughes    IncompatibleFontsError,
16*e1fe3e4aSElliott Hughes)
17*e1fe3e4aSElliott Hughes
18*e1fe3e4aSElliott Hughesimport pytest
19*e1fe3e4aSElliott Hughes
20*e1fe3e4aSElliott Hughes
21*e1fe3e4aSElliott HughesufoLib2 = pytest.importorskip("ufoLib2")
22*e1fe3e4aSElliott Hughes
23*e1fe3e4aSElliott HughesDATADIR = os.path.join(os.path.dirname(__file__), "data")
24*e1fe3e4aSElliott Hughes
25*e1fe3e4aSElliott HughesTEST_UFOS = [
26*e1fe3e4aSElliott Hughes    os.path.join(DATADIR, "RobotoSubset-Regular.ufo"),
27*e1fe3e4aSElliott Hughes    os.path.join(DATADIR, "RobotoSubset-Bold.ufo"),
28*e1fe3e4aSElliott Hughes]
29*e1fe3e4aSElliott Hughes
30*e1fe3e4aSElliott Hughes
31*e1fe3e4aSElliott Hughes@pytest.fixture
32*e1fe3e4aSElliott Hughesdef fonts():
33*e1fe3e4aSElliott Hughes    return [ufoLib2.Font.open(ufo) for ufo in TEST_UFOS]
34*e1fe3e4aSElliott Hughes
35*e1fe3e4aSElliott Hughes
36*e1fe3e4aSElliott Hughesclass FontsToQuadraticTest(object):
37*e1fe3e4aSElliott Hughes    def test_modified(self, fonts):
38*e1fe3e4aSElliott Hughes        modified = fonts_to_quadratic(fonts)
39*e1fe3e4aSElliott Hughes        assert modified
40*e1fe3e4aSElliott Hughes
41*e1fe3e4aSElliott Hughes    def test_stats(self, fonts):
42*e1fe3e4aSElliott Hughes        stats = {}
43*e1fe3e4aSElliott Hughes        fonts_to_quadratic(fonts, stats=stats)
44*e1fe3e4aSElliott Hughes        assert stats == {"1": 1, "2": 79, "3": 130, "4": 2}
45*e1fe3e4aSElliott Hughes
46*e1fe3e4aSElliott Hughes    def test_dump_stats(self, fonts):
47*e1fe3e4aSElliott Hughes        with CapturingLogHandler(logger, "INFO") as captor:
48*e1fe3e4aSElliott Hughes            fonts_to_quadratic(fonts, dump_stats=True)
49*e1fe3e4aSElliott Hughes        assert captor.assertRegex("New spline lengths:")
50*e1fe3e4aSElliott Hughes
51*e1fe3e4aSElliott Hughes    def test_remember_curve_type_quadratic(self, fonts):
52*e1fe3e4aSElliott Hughes        fonts_to_quadratic(fonts, remember_curve_type=True)
53*e1fe3e4aSElliott Hughes        assert fonts[0].lib[CURVE_TYPE_LIB_KEY] == "quadratic"
54*e1fe3e4aSElliott Hughes        with CapturingLogHandler(logger, "INFO") as captor:
55*e1fe3e4aSElliott Hughes            fonts_to_quadratic(fonts, remember_curve_type=True)
56*e1fe3e4aSElliott Hughes        assert captor.assertRegex("already converted")
57*e1fe3e4aSElliott Hughes
58*e1fe3e4aSElliott Hughes    def test_remember_curve_type_mixed(self, fonts):
59*e1fe3e4aSElliott Hughes        fonts_to_quadratic(fonts, remember_curve_type=True, all_quadratic=False)
60*e1fe3e4aSElliott Hughes        assert fonts[0].lib[CURVE_TYPE_LIB_KEY] == "mixed"
61*e1fe3e4aSElliott Hughes        with CapturingLogHandler(logger, "INFO") as captor:
62*e1fe3e4aSElliott Hughes            fonts_to_quadratic(fonts, remember_curve_type=True)
63*e1fe3e4aSElliott Hughes        assert captor.assertRegex("already converted")
64*e1fe3e4aSElliott Hughes
65*e1fe3e4aSElliott Hughes    def test_no_remember_curve_type(self, fonts):
66*e1fe3e4aSElliott Hughes        assert CURVE_TYPE_LIB_KEY not in fonts[0].lib
67*e1fe3e4aSElliott Hughes        fonts_to_quadratic(fonts, remember_curve_type=False)
68*e1fe3e4aSElliott Hughes        assert CURVE_TYPE_LIB_KEY not in fonts[0].lib
69*e1fe3e4aSElliott Hughes
70*e1fe3e4aSElliott Hughes    def test_different_glyphsets(self, fonts):
71*e1fe3e4aSElliott Hughes        del fonts[0]["a"]
72*e1fe3e4aSElliott Hughes        assert "a" not in fonts[0]
73*e1fe3e4aSElliott Hughes        assert "a" in fonts[1]
74*e1fe3e4aSElliott Hughes        assert fonts_to_quadratic(fonts)
75*e1fe3e4aSElliott Hughes
76*e1fe3e4aSElliott Hughes    def test_max_err_em_float(self, fonts):
77*e1fe3e4aSElliott Hughes        stats = {}
78*e1fe3e4aSElliott Hughes        fonts_to_quadratic(fonts, max_err_em=0.002, stats=stats)
79*e1fe3e4aSElliott Hughes        assert stats == {"1": 5, "2": 193, "3": 14}
80*e1fe3e4aSElliott Hughes
81*e1fe3e4aSElliott Hughes    def test_max_err_em_list(self, fonts):
82*e1fe3e4aSElliott Hughes        stats = {}
83*e1fe3e4aSElliott Hughes        fonts_to_quadratic(fonts, max_err_em=[0.002, 0.002], stats=stats)
84*e1fe3e4aSElliott Hughes        assert stats == {"1": 5, "2": 193, "3": 14}
85*e1fe3e4aSElliott Hughes
86*e1fe3e4aSElliott Hughes    def test_max_err_float(self, fonts):
87*e1fe3e4aSElliott Hughes        stats = {}
88*e1fe3e4aSElliott Hughes        fonts_to_quadratic(fonts, max_err=4.096, stats=stats)
89*e1fe3e4aSElliott Hughes        assert stats == {"1": 5, "2": 193, "3": 14}
90*e1fe3e4aSElliott Hughes
91*e1fe3e4aSElliott Hughes    def test_max_err_list(self, fonts):
92*e1fe3e4aSElliott Hughes        stats = {}
93*e1fe3e4aSElliott Hughes        fonts_to_quadratic(fonts, max_err=[4.096, 4.096], stats=stats)
94*e1fe3e4aSElliott Hughes        assert stats == {"1": 5, "2": 193, "3": 14}
95*e1fe3e4aSElliott Hughes
96*e1fe3e4aSElliott Hughes    def test_both_max_err_and_max_err_em(self, fonts):
97*e1fe3e4aSElliott Hughes        with pytest.raises(TypeError, match="Only one .* can be specified"):
98*e1fe3e4aSElliott Hughes            fonts_to_quadratic(fonts, max_err=1.000, max_err_em=0.001)
99*e1fe3e4aSElliott Hughes
100*e1fe3e4aSElliott Hughes    def test_single_font(self, fonts):
101*e1fe3e4aSElliott Hughes        assert font_to_quadratic(fonts[0], max_err_em=0.002, reverse_direction=True)
102*e1fe3e4aSElliott Hughes        assert font_to_quadratic(
103*e1fe3e4aSElliott Hughes            fonts[1], max_err_em=0.002, reverse_direction=True, all_quadratic=False
104*e1fe3e4aSElliott Hughes        )
105*e1fe3e4aSElliott Hughes
106*e1fe3e4aSElliott Hughes
107*e1fe3e4aSElliott Hughesclass GlyphsToQuadraticTest(object):
108*e1fe3e4aSElliott Hughes    @pytest.mark.parametrize(
109*e1fe3e4aSElliott Hughes        ["glyph", "expected"],
110*e1fe3e4aSElliott Hughes        [("A", False), ("a", True)],  # contains no curves, it is not modified
111*e1fe3e4aSElliott Hughes        ids=["lines-only", "has-curves"],
112*e1fe3e4aSElliott Hughes    )
113*e1fe3e4aSElliott Hughes    def test_modified(self, fonts, glyph, expected):
114*e1fe3e4aSElliott Hughes        glyphs = [f[glyph] for f in fonts]
115*e1fe3e4aSElliott Hughes        assert glyphs_to_quadratic(glyphs) == expected
116*e1fe3e4aSElliott Hughes
117*e1fe3e4aSElliott Hughes    def test_stats(self, fonts):
118*e1fe3e4aSElliott Hughes        stats = {}
119*e1fe3e4aSElliott Hughes        glyphs_to_quadratic([f["a"] for f in fonts], stats=stats)
120*e1fe3e4aSElliott Hughes        assert stats == {"2": 1, "3": 7, "4": 3, "5": 1}
121*e1fe3e4aSElliott Hughes
122*e1fe3e4aSElliott Hughes    def test_max_err_float(self, fonts):
123*e1fe3e4aSElliott Hughes        glyphs = [f["a"] for f in fonts]
124*e1fe3e4aSElliott Hughes        stats = {}
125*e1fe3e4aSElliott Hughes        glyphs_to_quadratic(glyphs, max_err=4.096, stats=stats)
126*e1fe3e4aSElliott Hughes        assert stats == {"2": 11, "3": 1}
127*e1fe3e4aSElliott Hughes
128*e1fe3e4aSElliott Hughes    def test_max_err_list(self, fonts):
129*e1fe3e4aSElliott Hughes        glyphs = [f["a"] for f in fonts]
130*e1fe3e4aSElliott Hughes        stats = {}
131*e1fe3e4aSElliott Hughes        glyphs_to_quadratic(glyphs, max_err=[4.096, 4.096], stats=stats)
132*e1fe3e4aSElliott Hughes        assert stats == {"2": 11, "3": 1}
133*e1fe3e4aSElliott Hughes
134*e1fe3e4aSElliott Hughes    def test_reverse_direction(self, fonts):
135*e1fe3e4aSElliott Hughes        glyphs = [f["A"] for f in fonts]
136*e1fe3e4aSElliott Hughes        assert glyphs_to_quadratic(glyphs, reverse_direction=True)
137*e1fe3e4aSElliott Hughes
138*e1fe3e4aSElliott Hughes    def test_single_glyph(self, fonts):
139*e1fe3e4aSElliott Hughes        assert glyph_to_quadratic(fonts[0]["a"], max_err=4.096, reverse_direction=True)
140*e1fe3e4aSElliott Hughes
141*e1fe3e4aSElliott Hughes    @pytest.mark.parametrize(
142*e1fe3e4aSElliott Hughes        ["outlines", "exception", "message"],
143*e1fe3e4aSElliott Hughes        [
144*e1fe3e4aSElliott Hughes            [
145*e1fe3e4aSElliott Hughes                [
146*e1fe3e4aSElliott Hughes                    [
147*e1fe3e4aSElliott Hughes                        ("moveTo", ((0, 0),)),
148*e1fe3e4aSElliott Hughes                        ("curveTo", ((1, 1), (2, 2), (3, 3))),
149*e1fe3e4aSElliott Hughes                        ("curveTo", ((4, 4), (5, 5), (6, 6))),
150*e1fe3e4aSElliott Hughes                        ("closePath", ()),
151*e1fe3e4aSElliott Hughes                    ],
152*e1fe3e4aSElliott Hughes                    [
153*e1fe3e4aSElliott Hughes                        ("moveTo", ((7, 7),)),
154*e1fe3e4aSElliott Hughes                        ("curveTo", ((8, 8), (9, 9), (10, 10))),
155*e1fe3e4aSElliott Hughes                        ("closePath", ()),
156*e1fe3e4aSElliott Hughes                    ],
157*e1fe3e4aSElliott Hughes                ],
158*e1fe3e4aSElliott Hughes                IncompatibleSegmentNumberError,
159*e1fe3e4aSElliott Hughes                "have different number of segments",
160*e1fe3e4aSElliott Hughes            ],
161*e1fe3e4aSElliott Hughes            [
162*e1fe3e4aSElliott Hughes                [
163*e1fe3e4aSElliott Hughes                    [
164*e1fe3e4aSElliott Hughes                        ("moveTo", ((0, 0),)),
165*e1fe3e4aSElliott Hughes                        ("curveTo", ((1, 1), (2, 2), (3, 3))),
166*e1fe3e4aSElliott Hughes                        ("closePath", ()),
167*e1fe3e4aSElliott Hughes                    ],
168*e1fe3e4aSElliott Hughes                    [
169*e1fe3e4aSElliott Hughes                        ("moveTo", ((4, 4),)),
170*e1fe3e4aSElliott Hughes                        ("lineTo", ((5, 5),)),
171*e1fe3e4aSElliott Hughes                        ("closePath", ()),
172*e1fe3e4aSElliott Hughes                    ],
173*e1fe3e4aSElliott Hughes                ],
174*e1fe3e4aSElliott Hughes                IncompatibleSegmentTypesError,
175*e1fe3e4aSElliott Hughes                "have incompatible segment types",
176*e1fe3e4aSElliott Hughes            ],
177*e1fe3e4aSElliott Hughes        ],
178*e1fe3e4aSElliott Hughes        ids=[
179*e1fe3e4aSElliott Hughes            "unequal-length",
180*e1fe3e4aSElliott Hughes            "different-segment-types",
181*e1fe3e4aSElliott Hughes        ],
182*e1fe3e4aSElliott Hughes    )
183*e1fe3e4aSElliott Hughes    def test_incompatible_glyphs(self, outlines, exception, message):
184*e1fe3e4aSElliott Hughes        glyphs = []
185*e1fe3e4aSElliott Hughes        for i, outline in enumerate(outlines):
186*e1fe3e4aSElliott Hughes            glyph = ufoLib2.objects.Glyph("glyph%d" % i)
187*e1fe3e4aSElliott Hughes            pen = glyph.getPen()
188*e1fe3e4aSElliott Hughes            for operator, args in outline:
189*e1fe3e4aSElliott Hughes                getattr(pen, operator)(*args)
190*e1fe3e4aSElliott Hughes            glyphs.append(glyph)
191*e1fe3e4aSElliott Hughes        with pytest.raises(exception) as excinfo:
192*e1fe3e4aSElliott Hughes            glyphs_to_quadratic(glyphs)
193*e1fe3e4aSElliott Hughes        assert excinfo.match(message)
194*e1fe3e4aSElliott Hughes
195*e1fe3e4aSElliott Hughes    def test_incompatible_fonts(self):
196*e1fe3e4aSElliott Hughes        font1 = ufoLib2.Font()
197*e1fe3e4aSElliott Hughes        font1.info.unitsPerEm = 1000
198*e1fe3e4aSElliott Hughes        glyph1 = font1.newGlyph("a")
199*e1fe3e4aSElliott Hughes        pen1 = glyph1.getPen()
200*e1fe3e4aSElliott Hughes        for operator, args in [
201*e1fe3e4aSElliott Hughes            ("moveTo", ((0, 0),)),
202*e1fe3e4aSElliott Hughes            ("lineTo", ((1, 1),)),
203*e1fe3e4aSElliott Hughes            ("endPath", ()),
204*e1fe3e4aSElliott Hughes        ]:
205*e1fe3e4aSElliott Hughes            getattr(pen1, operator)(*args)
206*e1fe3e4aSElliott Hughes
207*e1fe3e4aSElliott Hughes        font2 = ufoLib2.Font()
208*e1fe3e4aSElliott Hughes        font2.info.unitsPerEm = 1000
209*e1fe3e4aSElliott Hughes        glyph2 = font2.newGlyph("a")
210*e1fe3e4aSElliott Hughes        pen2 = glyph2.getPen()
211*e1fe3e4aSElliott Hughes        for operator, args in [
212*e1fe3e4aSElliott Hughes            ("moveTo", ((0, 0),)),
213*e1fe3e4aSElliott Hughes            ("curveTo", ((1, 1), (2, 2), (3, 3))),
214*e1fe3e4aSElliott Hughes            ("endPath", ()),
215*e1fe3e4aSElliott Hughes        ]:
216*e1fe3e4aSElliott Hughes            getattr(pen2, operator)(*args)
217*e1fe3e4aSElliott Hughes
218*e1fe3e4aSElliott Hughes        with pytest.raises(IncompatibleFontsError) as excinfo:
219*e1fe3e4aSElliott Hughes            fonts_to_quadratic([font1, font2])
220*e1fe3e4aSElliott Hughes        assert excinfo.match("fonts contains incompatible glyphs: 'a'")
221*e1fe3e4aSElliott Hughes
222*e1fe3e4aSElliott Hughes        assert hasattr(excinfo.value, "glyph_errors")
223*e1fe3e4aSElliott Hughes        error = excinfo.value.glyph_errors["a"]
224*e1fe3e4aSElliott Hughes        assert isinstance(error, IncompatibleSegmentTypesError)
225*e1fe3e4aSElliott Hughes        assert error.segments == {1: ["line", "curve"]}
226*e1fe3e4aSElliott Hughes
227*e1fe3e4aSElliott Hughes    def test_already_quadratic(self):
228*e1fe3e4aSElliott Hughes        glyph = ufoLib2.objects.Glyph()
229*e1fe3e4aSElliott Hughes        pen = glyph.getPen()
230*e1fe3e4aSElliott Hughes        pen.moveTo((0, 0))
231*e1fe3e4aSElliott Hughes        pen.qCurveTo((1, 1), (2, 2))
232*e1fe3e4aSElliott Hughes        pen.closePath()
233*e1fe3e4aSElliott Hughes        assert not glyph_to_quadratic(glyph)
234*e1fe3e4aSElliott Hughes
235*e1fe3e4aSElliott Hughes    def test_open_paths(self):
236*e1fe3e4aSElliott Hughes        glyph = ufoLib2.objects.Glyph()
237*e1fe3e4aSElliott Hughes        pen = glyph.getPen()
238*e1fe3e4aSElliott Hughes        pen.moveTo((0, 0))
239*e1fe3e4aSElliott Hughes        pen.lineTo((1, 1))
240*e1fe3e4aSElliott Hughes        pen.curveTo((2, 2), (3, 3), (4, 4))
241*e1fe3e4aSElliott Hughes        pen.endPath()
242*e1fe3e4aSElliott Hughes        assert glyph_to_quadratic(glyph)
243*e1fe3e4aSElliott Hughes        # open contour is still open
244*e1fe3e4aSElliott Hughes        assert glyph[-1][0].segmentType == "move"
245*e1fe3e4aSElliott Hughes
246*e1fe3e4aSElliott Hughes    def test_ignore_components(self):
247*e1fe3e4aSElliott Hughes        glyph = ufoLib2.objects.Glyph()
248*e1fe3e4aSElliott Hughes        pen = glyph.getPen()
249*e1fe3e4aSElliott Hughes        pen.addComponent("a", (1, 0, 0, 1, 0, 0))
250*e1fe3e4aSElliott Hughes        pen.moveTo((0, 0))
251*e1fe3e4aSElliott Hughes        pen.curveTo((1, 1), (2, 2), (3, 3))
252*e1fe3e4aSElliott Hughes        pen.closePath()
253*e1fe3e4aSElliott Hughes        assert glyph_to_quadratic(glyph)
254*e1fe3e4aSElliott Hughes        assert len(glyph.components) == 1
255*e1fe3e4aSElliott Hughes
256*e1fe3e4aSElliott Hughes    def test_overlapping_start_end_points(self):
257*e1fe3e4aSElliott Hughes        # https://github.com/googlefonts/fontmake/issues/572
258*e1fe3e4aSElliott Hughes        glyph1 = ufoLib2.objects.Glyph()
259*e1fe3e4aSElliott Hughes        pen = glyph1.getPointPen()
260*e1fe3e4aSElliott Hughes        pen.beginPath()
261*e1fe3e4aSElliott Hughes        pen.addPoint((0, 651), segmentType="line")
262*e1fe3e4aSElliott Hughes        pen.addPoint((0, 101), segmentType="line")
263*e1fe3e4aSElliott Hughes        pen.addPoint((0, 101), segmentType="line")
264*e1fe3e4aSElliott Hughes        pen.addPoint((0, 651), segmentType="line")
265*e1fe3e4aSElliott Hughes        pen.endPath()
266*e1fe3e4aSElliott Hughes
267*e1fe3e4aSElliott Hughes        glyph2 = ufoLib2.objects.Glyph()
268*e1fe3e4aSElliott Hughes        pen = glyph2.getPointPen()
269*e1fe3e4aSElliott Hughes        pen.beginPath()
270*e1fe3e4aSElliott Hughes        pen.addPoint((1, 651), segmentType="line")
271*e1fe3e4aSElliott Hughes        pen.addPoint((2, 101), segmentType="line")
272*e1fe3e4aSElliott Hughes        pen.addPoint((3, 101), segmentType="line")
273*e1fe3e4aSElliott Hughes        pen.addPoint((4, 651), segmentType="line")
274*e1fe3e4aSElliott Hughes        pen.endPath()
275*e1fe3e4aSElliott Hughes
276*e1fe3e4aSElliott Hughes        glyphs = [glyph1, glyph2]
277*e1fe3e4aSElliott Hughes
278*e1fe3e4aSElliott Hughes        assert glyphs_to_quadratic(glyphs, reverse_direction=True)
279*e1fe3e4aSElliott Hughes
280*e1fe3e4aSElliott Hughes        assert [[(p.x, p.y) for p in glyph[0]] for glyph in glyphs] == [
281*e1fe3e4aSElliott Hughes            [
282*e1fe3e4aSElliott Hughes                (0, 651),
283*e1fe3e4aSElliott Hughes                (0, 651),
284*e1fe3e4aSElliott Hughes                (0, 101),
285*e1fe3e4aSElliott Hughes                (0, 101),
286*e1fe3e4aSElliott Hughes            ],
287*e1fe3e4aSElliott Hughes            [(1, 651), (4, 651), (3, 101), (2, 101)],
288*e1fe3e4aSElliott Hughes        ]
289