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