xref: /aosp_15_r20/external/fonttools/Tests/pens/cu2quPen_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1# Copyright 2016 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import sys
16import unittest
17
18from fontTools.pens.cu2quPen import Cu2QuPen, Cu2QuPointPen, Cu2QuMultiPen
19from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen
20from fontTools.misc.loggingTools import CapturingLogHandler
21from textwrap import dedent
22import logging
23import pytest
24
25try:
26    from .utils import CUBIC_GLYPHS, QUAD_GLYPHS
27    from .utils import DummyGlyph, DummyPointGlyph
28    from .utils import DummyPen, DummyPointPen
29except ImportError as e:
30    pytest.skip(str(e), allow_module_level=True)
31
32
33MAX_ERR = 1.0
34
35
36class _TestPenMixin(object):
37    """Collection of tests that are shared by both the SegmentPen and the
38    PointPen test cases, plus some helper methods.
39    """
40
41    maxDiff = None
42
43    def diff(self, expected, actual):
44        import difflib
45
46        expected = str(self.Glyph(expected)).splitlines(True)
47        actual = str(self.Glyph(actual)).splitlines(True)
48        diff = difflib.unified_diff(
49            expected, actual, fromfile="expected", tofile="actual"
50        )
51        return "".join(diff)
52
53    def convert_glyph(self, glyph, **kwargs):
54        # draw source glyph onto a new glyph using a Cu2Qu pen and return it
55        converted = self.Glyph()
56        pen = getattr(converted, self.pen_getter_name)()
57        quadpen = self.Cu2QuPen(pen, MAX_ERR, **kwargs)
58        getattr(glyph, self.draw_method_name)(quadpen)
59        return converted
60
61    def expect_glyph(self, source, expected):
62        converted = self.convert_glyph(source)
63        self.assertNotEqual(converted, source)
64        if not converted.approx(expected):
65            print(self.diff(expected, converted))
66            self.fail("converted glyph is different from expected")
67
68    def test_convert_simple_glyph(self):
69        self.expect_glyph(CUBIC_GLYPHS["a"], QUAD_GLYPHS["a"])
70        self.expect_glyph(CUBIC_GLYPHS["A"], QUAD_GLYPHS["A"])
71
72    def test_convert_composite_glyph(self):
73        source = CUBIC_GLYPHS["Aacute"]
74        converted = self.convert_glyph(source)
75        # components don't change after quadratic conversion
76        self.assertEqual(converted, source)
77
78    def test_convert_mixed_glyph(self):
79        # this contains a mix of contours and components
80        self.expect_glyph(CUBIC_GLYPHS["Eacute"], QUAD_GLYPHS["Eacute"])
81
82    def test_reverse_direction(self):
83        for name in ("a", "A", "Eacute"):
84            source = CUBIC_GLYPHS[name]
85            normal_glyph = self.convert_glyph(source)
86            reversed_glyph = self.convert_glyph(source, reverse_direction=True)
87
88            # the number of commands is the same, just their order is iverted
89            self.assertTrue(len(normal_glyph.outline), len(reversed_glyph.outline))
90            self.assertNotEqual(normal_glyph, reversed_glyph)
91
92    def test_stats(self):
93        stats = {}
94        for name in CUBIC_GLYPHS.keys():
95            source = CUBIC_GLYPHS[name]
96            self.convert_glyph(source, stats=stats)
97
98        self.assertTrue(stats)
99        self.assertTrue("1" in stats)
100        self.assertEqual(type(stats["1"]), int)
101
102    def test_addComponent(self):
103        pen = self.Pen()
104        quadpen = self.Cu2QuPen(pen, MAX_ERR)
105        quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0))
106
107        # components are passed through without changes
108        self.assertEqual(
109            str(pen).splitlines(),
110            [
111                "pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))",
112            ],
113        )
114
115
116class TestCu2QuPen(unittest.TestCase, _TestPenMixin):
117    def __init__(self, *args, **kwargs):
118        super(TestCu2QuPen, self).__init__(*args, **kwargs)
119        self.Glyph = DummyGlyph
120        self.Pen = DummyPen
121        self.Cu2QuPen = Cu2QuPen
122        self.pen_getter_name = "getPen"
123        self.draw_method_name = "draw"
124
125    def test_qCurveTo_1_point(self):
126        pen = DummyPen()
127        quadpen = Cu2QuPen(pen, MAX_ERR)
128        quadpen.moveTo((0, 0))
129        quadpen.qCurveTo((1, 1))
130
131        self.assertEqual(
132            str(pen).splitlines(),
133            [
134                "pen.moveTo((0, 0))",
135                "pen.qCurveTo((1, 1))",
136            ],
137        )
138
139    def test_qCurveTo_more_than_1_point(self):
140        pen = DummyPen()
141        quadpen = Cu2QuPen(pen, MAX_ERR)
142        quadpen.moveTo((0, 0))
143        quadpen.qCurveTo((1, 1), (2, 2))
144
145        self.assertEqual(
146            str(pen).splitlines(),
147            [
148                "pen.moveTo((0, 0))",
149                "pen.qCurveTo((1, 1), (2, 2))",
150            ],
151        )
152
153    def test_curveTo_1_point(self):
154        pen = DummyPen()
155        quadpen = Cu2QuPen(pen, MAX_ERR)
156        quadpen.moveTo((0, 0))
157        quadpen.curveTo((1, 1))
158
159        self.assertEqual(
160            str(pen).splitlines(),
161            [
162                "pen.moveTo((0, 0))",
163                "pen.qCurveTo((1, 1))",
164            ],
165        )
166
167    def test_curveTo_2_points(self):
168        pen = DummyPen()
169        quadpen = Cu2QuPen(pen, MAX_ERR)
170        quadpen.moveTo((0, 0))
171        quadpen.curveTo((1, 1), (2, 2))
172
173        self.assertEqual(
174            str(pen).splitlines(),
175            [
176                "pen.moveTo((0, 0))",
177                "pen.qCurveTo((1, 1), (2, 2))",
178            ],
179        )
180
181    def test_curveTo_3_points(self):
182        pen = DummyPen()
183        quadpen = Cu2QuPen(pen, MAX_ERR)
184        quadpen.moveTo((0, 0))
185        quadpen.curveTo((1, 1), (2, 2), (3, 3))
186
187        self.assertEqual(
188            str(pen).splitlines(),
189            [
190                "pen.moveTo((0, 0))",
191                "pen.qCurveTo((0.75, 0.75), (2.25, 2.25), (3, 3))",
192            ],
193        )
194
195    def test_curveTo_more_than_3_points(self):
196        # a 'SuperBezier' as described in fontTools.basePen.AbstractPen
197        pen = DummyPen()
198        quadpen = Cu2QuPen(pen, MAX_ERR)
199        quadpen.moveTo((0, 0))
200        quadpen.curveTo((1, 1), (2, 2), (3, 3), (4, 4))
201
202        self.assertEqual(
203            str(pen).splitlines(),
204            [
205                "pen.moveTo((0, 0))",
206                "pen.qCurveTo((0.75, 0.75), (1.625, 1.625), (2, 2))",
207                "pen.qCurveTo((2.375, 2.375), (3.25, 3.25), (4, 4))",
208            ],
209        )
210
211
212class TestCu2QuPointPen(unittest.TestCase, _TestPenMixin):
213    def __init__(self, *args, **kwargs):
214        super(TestCu2QuPointPen, self).__init__(*args, **kwargs)
215        self.Glyph = DummyPointGlyph
216        self.Pen = DummyPointPen
217        self.Cu2QuPen = Cu2QuPointPen
218        self.pen_getter_name = "getPointPen"
219        self.draw_method_name = "drawPoints"
220
221    def test_super_bezier_curve(self):
222        pen = DummyPointPen()
223        quadpen = Cu2QuPointPen(pen, MAX_ERR)
224        quadpen.beginPath()
225        quadpen.addPoint((0, 0), segmentType="move")
226        quadpen.addPoint((1, 1))
227        quadpen.addPoint((2, 2))
228        quadpen.addPoint((3, 3))
229        quadpen.addPoint(
230            (4, 4), segmentType="curve", smooth=False, name="up", selected=1
231        )
232        quadpen.endPath()
233
234        self.assertEqual(
235            str(pen).splitlines(),
236            """\
237pen.beginPath()
238pen.addPoint((0, 0), name=None, segmentType='move', smooth=False)
239pen.addPoint((0.75, 0.75), name=None, segmentType=None, smooth=False)
240pen.addPoint((1.625, 1.625), name=None, segmentType=None, smooth=False)
241pen.addPoint((2, 2), name=None, segmentType='qcurve', smooth=True)
242pen.addPoint((2.375, 2.375), name=None, segmentType=None, smooth=False)
243pen.addPoint((3.25, 3.25), name=None, segmentType=None, smooth=False)
244pen.addPoint((4, 4), name='up', segmentType='qcurve', selected=1, smooth=False)
245pen.endPath()""".splitlines(),
246        )
247
248    def test__flushContour_restore_starting_point(self):
249        pen = DummyPointPen()
250        quadpen = Cu2QuPointPen(pen, MAX_ERR)
251
252        # collect the output of _flushContour before it's sent to _drawPoints
253        new_segments = []
254
255        def _drawPoints(segments):
256            new_segments.extend(segments)
257            Cu2QuPointPen._drawPoints(quadpen, segments)
258
259        quadpen._drawPoints = _drawPoints
260
261        # a closed path (ie. no "move" segmentType)
262        quadpen._flushContour(
263            [
264                (
265                    "curve",
266                    [
267                        ((2, 2), False, None, {}),
268                        ((1, 1), False, None, {}),
269                        ((0, 0), False, None, {}),
270                    ],
271                ),
272                (
273                    "curve",
274                    [
275                        ((1, 1), False, None, {}),
276                        ((2, 2), False, None, {}),
277                        ((3, 3), False, None, {}),
278                    ],
279                ),
280            ]
281        )
282
283        # the original starting point is restored: the last segment has become
284        # the first
285        self.assertEqual(new_segments[0][1][-1][0], (3, 3))
286        self.assertEqual(new_segments[-1][1][-1][0], (0, 0))
287
288        new_segments = []
289        # an open path (ie. starting with "move")
290        quadpen._flushContour(
291            [
292                (
293                    "move",
294                    [
295                        ((0, 0), False, None, {}),
296                    ],
297                ),
298                (
299                    "curve",
300                    [
301                        ((1, 1), False, None, {}),
302                        ((2, 2), False, None, {}),
303                        ((3, 3), False, None, {}),
304                    ],
305                ),
306            ]
307        )
308
309        # the segment order stays the same before and after _flushContour
310        self.assertEqual(new_segments[0][1][-1][0], (0, 0))
311        self.assertEqual(new_segments[-1][1][-1][0], (3, 3))
312
313    def test_quad_no_oncurve(self):
314        """When passed a contour which has no on-curve points, the
315        Cu2QuPointPen will treat it as a special quadratic contour whose
316        first point has 'None' coordinates.
317        """
318        self.maxDiff = None
319        pen = DummyPointPen()
320        quadpen = Cu2QuPointPen(pen, MAX_ERR)
321        quadpen.beginPath()
322        quadpen.addPoint((1, 1))
323        quadpen.addPoint((2, 2))
324        quadpen.addPoint((3, 3))
325        quadpen.endPath()
326
327        self.assertEqual(
328            str(pen),
329            dedent(
330                """\
331                pen.beginPath()
332                pen.addPoint((1, 1), name=None, segmentType=None, smooth=False)
333                pen.addPoint((2, 2), name=None, segmentType=None, smooth=False)
334                pen.addPoint((3, 3), name=None, segmentType=None, smooth=False)
335                pen.endPath()"""
336            ),
337        )
338
339
340class TestCu2QuMultiPen(unittest.TestCase):
341    def test_multi_pen(self):
342        pens = [RecordingPen(), RecordingPen()]
343        pen = Cu2QuMultiPen(pens, 0.1)
344        pen.moveTo([((0, 0),), ((0, 0),)])
345        pen.lineTo([((0, 1),), ((0, 1),)])
346        pen.qCurveTo([((0, 2),), ((0, 2),)])
347        pen.qCurveTo([((0, 3), (1, 3)), ((0, 3), (1, 4))])
348        pen.curveTo([((2, 3), (0, 3), (0, 0)), ((1.1, 4), (0, 4), (0, 0))])
349        pen.closePath()
350
351        assert len(pens[0].value) == 6
352        assert len(pens[1].value) == 6
353
354        for op0, op1 in zip(pens[0].value, pens[1].value):
355            assert op0[0] == op0[0]
356            assert op0[0] != "curveTo"
357
358
359class TestAllQuadraticFalse(unittest.TestCase):
360    def test_segment_pen_cubic(self):
361        rpen = RecordingPen()
362        pen = Cu2QuPen(rpen, 0.1, all_quadratic=False)
363
364        pen.moveTo((0, 0))
365        pen.curveTo((0, 1), (2, 1), (2, 0))
366        pen.closePath()
367
368        assert rpen.value == [
369            ("moveTo", ((0, 0),)),
370            ("curveTo", ((0, 1), (2, 1), (2, 0))),
371            ("closePath", ()),
372        ]
373
374    def test_segment_pen_quadratic(self):
375        rpen = RecordingPen()
376        pen = Cu2QuPen(rpen, 0.1, all_quadratic=False)
377
378        pen.moveTo((0, 0))
379        pen.curveTo((2, 2), (4, 2), (6, 0))
380        pen.closePath()
381
382        assert rpen.value == [
383            ("moveTo", ((0, 0),)),
384            ("qCurveTo", ((3, 3), (6, 0))),
385            ("closePath", ()),
386        ]
387
388    def test_point_pen_cubic(self):
389        rpen = RecordingPointPen()
390        pen = Cu2QuPointPen(rpen, 0.1, all_quadratic=False)
391
392        pen.beginPath()
393        pen.addPoint((0, 0), "move")
394        pen.addPoint((0, 1))
395        pen.addPoint((2, 1))
396        pen.addPoint((2, 0), "curve")
397        pen.endPath()
398
399        assert rpen.value == [
400            ("beginPath", (), {}),
401            ("addPoint", ((0, 0), "move", False, None), {}),
402            ("addPoint", ((0, 1), None, False, None), {}),
403            ("addPoint", ((2, 1), None, False, None), {}),
404            ("addPoint", ((2, 0), "curve", False, None), {}),
405            ("endPath", (), {}),
406        ]
407
408    def test_point_pen_quadratic(self):
409        rpen = RecordingPointPen()
410        pen = Cu2QuPointPen(rpen, 0.1, all_quadratic=False)
411
412        pen.beginPath()
413        pen.addPoint((0, 0), "move")
414        pen.addPoint((2, 2))
415        pen.addPoint((4, 2))
416        pen.addPoint((6, 0), "curve")
417        pen.endPath()
418
419        assert rpen.value == [
420            ("beginPath", (), {}),
421            ("addPoint", ((0, 0), "move", False, None), {}),
422            ("addPoint", ((3, 3), None, False, None), {}),
423            ("addPoint", ((6, 0), "qcurve", False, None), {}),
424            ("endPath", (), {}),
425        ]
426
427
428if __name__ == "__main__":
429    unittest.main()
430