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