xref: /aosp_15_r20/external/fonttools/Tests/pens/pointPen_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1import unittest
2
3from fontTools.pens.basePen import AbstractPen
4from fontTools.pens.pointPen import (
5    AbstractPointPen,
6    PointToSegmentPen,
7    SegmentToPointPen,
8    GuessSmoothPointPen,
9    ReverseContourPointPen,
10)
11
12
13class _TestSegmentPen(AbstractPen):
14    def __init__(self):
15        self._commands = []
16
17    def __repr__(self):
18        return " ".join(self._commands)
19
20    def moveTo(self, pt):
21        self._commands.append("%s %s moveto" % (pt[0], pt[1]))
22
23    def lineTo(self, pt):
24        self._commands.append("%s %s lineto" % (pt[0], pt[1]))
25
26    def curveTo(self, *pts):
27        pts = ["%s %s" % pt for pt in pts]
28        self._commands.append("%s curveto" % " ".join(pts))
29
30    def qCurveTo(self, *pts):
31        pts = ["%s %s" % pt if pt is not None else "None" for pt in pts]
32        self._commands.append("%s qcurveto" % " ".join(pts))
33
34    def closePath(self):
35        self._commands.append("closepath")
36
37    def endPath(self):
38        self._commands.append("endpath")
39
40    def addComponent(self, glyphName, transformation):
41        self._commands.append("'%s' %s addcomponent" % (glyphName, transformation))
42
43
44def _reprKwargs(kwargs):
45    items = []
46    for key in sorted(kwargs):
47        value = kwargs[key]
48        if isinstance(value, str):
49            items.append("%s='%s'" % (key, value))
50        else:
51            items.append("%s=%s" % (key, value))
52    return items
53
54
55class _TestPointPen(AbstractPointPen):
56    def __init__(self):
57        self._commands = []
58
59    def __repr__(self):
60        return " ".join(self._commands)
61
62    def beginPath(self, identifier=None, **kwargs):
63        items = []
64        if identifier is not None:
65            items.append("identifier='%s'" % identifier)
66        items.extend(_reprKwargs(kwargs))
67        self._commands.append("beginPath(%s)" % ", ".join(items))
68
69    def addPoint(
70        self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
71    ):
72        items = ["%s" % (pt,)]
73        if segmentType is not None:
74            items.append("segmentType='%s'" % segmentType)
75        if smooth:
76            items.append("smooth=True")
77        if name is not None:
78            items.append("name='%s'" % name)
79        if identifier is not None:
80            items.append("identifier='%s'" % identifier)
81        items.extend(_reprKwargs(kwargs))
82        self._commands.append("addPoint(%s)" % ", ".join(items))
83
84    def endPath(self):
85        self._commands.append("endPath()")
86
87    def addComponent(self, glyphName, transform, identifier=None, **kwargs):
88        items = ["'%s'" % glyphName, "%s" % transform]
89        if identifier is not None:
90            items.append("identifier='%s'" % identifier)
91        items.extend(_reprKwargs(kwargs))
92        self._commands.append("addComponent(%s)" % ", ".join(items))
93
94
95class PointToSegmentPenTest(unittest.TestCase):
96    def test_open(self):
97        pen = _TestSegmentPen()
98        ppen = PointToSegmentPen(pen)
99        ppen.beginPath()
100        ppen.addPoint((10, 10), "move")
101        ppen.addPoint((10, 20), "line")
102        ppen.endPath()
103        self.assertEqual("10 10 moveto 10 20 lineto endpath", repr(pen))
104
105    def test_closed(self):
106        pen = _TestSegmentPen()
107        ppen = PointToSegmentPen(pen)
108        ppen.beginPath()
109        ppen.addPoint((10, 10), "line")
110        ppen.addPoint((10, 20), "line")
111        ppen.addPoint((20, 20), "line")
112        ppen.endPath()
113        self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(pen))
114
115    def test_cubic(self):
116        pen = _TestSegmentPen()
117        ppen = PointToSegmentPen(pen)
118        ppen.beginPath()
119        ppen.addPoint((10, 10), "line")
120        ppen.addPoint((10, 20))
121        ppen.addPoint((20, 20))
122        ppen.addPoint((20, 40), "curve")
123        ppen.endPath()
124        self.assertEqual("10 10 moveto 10 20 20 20 20 40 curveto closepath", repr(pen))
125
126    def test_quad(self):
127        pen = _TestSegmentPen()
128        ppen = PointToSegmentPen(pen)
129        ppen.beginPath(identifier="foo")
130        ppen.addPoint((10, 10), "line")
131        ppen.addPoint((10, 40))
132        ppen.addPoint((40, 40))
133        ppen.addPoint((10, 40), "qcurve")
134        ppen.endPath()
135        self.assertEqual("10 10 moveto 10 40 40 40 10 40 qcurveto closepath", repr(pen))
136
137    def test_quad_onlyOffCurvePoints(self):
138        pen = _TestSegmentPen()
139        ppen = PointToSegmentPen(pen)
140        ppen.beginPath()
141        ppen.addPoint((10, 10))
142        ppen.addPoint((10, 40))
143        ppen.addPoint((40, 40))
144        ppen.endPath()
145        self.assertEqual("10 10 10 40 40 40 None qcurveto closepath", repr(pen))
146
147    def test_roundTrip1(self):
148        tpen = _TestPointPen()
149        ppen = PointToSegmentPen(SegmentToPointPen(tpen))
150        ppen.beginPath()
151        ppen.addPoint((10, 10), "line")
152        ppen.addPoint((10, 20))
153        ppen.addPoint((20, 20))
154        ppen.addPoint((20, 40), "curve")
155        ppen.endPath()
156        self.assertEqual(
157            "beginPath() addPoint((10, 10), segmentType='line') addPoint((10, 20)) "
158            "addPoint((20, 20)) addPoint((20, 40), segmentType='curve') endPath()",
159            repr(tpen),
160        )
161
162    def test_closed_outputImpliedClosingLine(self):
163        tpen = _TestSegmentPen()
164        ppen = PointToSegmentPen(tpen, outputImpliedClosingLine=True)
165        ppen.beginPath()
166        ppen.addPoint((10, 10), "line")
167        ppen.addPoint((10, 20), "line")
168        ppen.addPoint((20, 20), "line")
169        ppen.endPath()
170        self.assertEqual(
171            "10 10 moveto "
172            "10 20 lineto "
173            "20 20 lineto "
174            "10 10 lineto "  # explicit closing line
175            "closepath",
176            repr(tpen),
177        )
178
179    def test_closed_line_overlapping_start_end_points(self):
180        # Test case from https://github.com/googlefonts/fontmake/issues/572.
181        tpen = _TestSegmentPen()
182        ppen = PointToSegmentPen(tpen, outputImpliedClosingLine=False)
183        # The last oncurve point on this closed contour is a "line" segment and has
184        # same coordinates as the starting point.
185        ppen.beginPath()
186        ppen.addPoint((0, 651), segmentType="line")
187        ppen.addPoint((0, 101), segmentType="line")
188        ppen.addPoint((0, 101), segmentType="line")
189        ppen.addPoint((0, 651), segmentType="line")
190        ppen.endPath()
191        # Check that we always output an explicit 'lineTo' segment at the end,
192        # regardless of the value of 'outputImpliedClosingLine', to disambiguate
193        # the duplicate point from the implied closing line.
194        self.assertEqual(
195            "0 651 moveto "
196            "0 101 lineto "
197            "0 101 lineto "
198            "0 651 lineto "
199            "0 651 lineto "
200            "closepath",
201            repr(tpen),
202        )
203
204    def test_roundTrip2(self):
205        tpen = _TestPointPen()
206        ppen = PointToSegmentPen(SegmentToPointPen(tpen))
207        ppen.beginPath()
208        ppen.addPoint((0, 651), segmentType="line")
209        ppen.addPoint((0, 101), segmentType="line")
210        ppen.addPoint((0, 101), segmentType="line")
211        ppen.addPoint((0, 651), segmentType="line")
212        ppen.endPath()
213        self.assertEqual(
214            "beginPath() "
215            "addPoint((0, 651), segmentType='line') "
216            "addPoint((0, 101), segmentType='line') "
217            "addPoint((0, 101), segmentType='line') "
218            "addPoint((0, 651), segmentType='line') "
219            "endPath()",
220            repr(tpen),
221        )
222
223
224class TestSegmentToPointPen(unittest.TestCase):
225    def test_move(self):
226        tpen = _TestPointPen()
227        pen = SegmentToPointPen(tpen)
228        pen.moveTo((10, 10))
229        pen.endPath()
230        self.assertEqual(
231            "beginPath() addPoint((10, 10), segmentType='move') endPath()", repr(tpen)
232        )
233
234    def test_poly(self):
235        tpen = _TestPointPen()
236        pen = SegmentToPointPen(tpen)
237        pen.moveTo((10, 10))
238        pen.lineTo((10, 20))
239        pen.lineTo((20, 20))
240        pen.closePath()
241        self.assertEqual(
242            "beginPath() addPoint((10, 10), segmentType='line') "
243            "addPoint((10, 20), segmentType='line') "
244            "addPoint((20, 20), segmentType='line') endPath()",
245            repr(tpen),
246        )
247
248    def test_cubic(self):
249        tpen = _TestPointPen()
250        pen = SegmentToPointPen(tpen)
251        pen.moveTo((10, 10))
252        pen.curveTo((10, 20), (20, 20), (20, 10))
253        pen.closePath()
254        self.assertEqual(
255            "beginPath() addPoint((10, 10), segmentType='line') "
256            "addPoint((10, 20)) addPoint((20, 20)) addPoint((20, 10), "
257            "segmentType='curve') endPath()",
258            repr(tpen),
259        )
260
261    def test_quad(self):
262        tpen = _TestPointPen()
263        pen = SegmentToPointPen(tpen)
264        pen.moveTo((10, 10))
265        pen.qCurveTo((10, 20), (20, 20), (20, 10))
266        pen.closePath()
267        self.assertEqual(
268            "beginPath() addPoint((10, 10), segmentType='line') "
269            "addPoint((10, 20)) addPoint((20, 20)) "
270            "addPoint((20, 10), segmentType='qcurve') endPath()",
271            repr(tpen),
272        )
273
274    def test_quad2(self):
275        tpen = _TestPointPen()
276        pen = SegmentToPointPen(tpen)
277        pen.qCurveTo((10, 20), (20, 20), (20, 10), (10, 10), None)
278        pen.closePath()
279        self.assertEqual(
280            "beginPath() addPoint((10, 20)) addPoint((20, 20)) "
281            "addPoint((20, 10)) addPoint((10, 10)) endPath()",
282            repr(tpen),
283        )
284
285    def test_roundTrip1(self):
286        spen = _TestSegmentPen()
287        pen = SegmentToPointPen(PointToSegmentPen(spen))
288        pen.moveTo((10, 10))
289        pen.lineTo((10, 20))
290        pen.lineTo((20, 20))
291        pen.closePath()
292        self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(spen))
293
294    def test_roundTrip2(self):
295        spen = _TestSegmentPen()
296        pen = SegmentToPointPen(PointToSegmentPen(spen))
297        pen.qCurveTo((10, 20), (20, 20), (20, 10), (10, 10), None)
298        pen.closePath()
299        pen.addComponent("base", [1, 0, 0, 1, 0, 0])
300        self.assertEqual(
301            "10 20 20 20 20 10 10 10 None qcurveto closepath "
302            "'base' [1, 0, 0, 1, 0, 0] addcomponent",
303            repr(spen),
304        )
305
306
307class TestGuessSmoothPointPen(unittest.TestCase):
308    def test_guessSmooth_exact(self):
309        tpen = _TestPointPen()
310        pen = GuessSmoothPointPen(tpen)
311        pen.beginPath(identifier="foo")
312        pen.addPoint((0, 100), segmentType="curve")
313        pen.addPoint((0, 200))
314        pen.addPoint((400, 200), identifier="bar")
315        pen.addPoint((400, 100), segmentType="curve")
316        pen.addPoint((400, 0))
317        pen.addPoint((0, 0))
318        pen.endPath()
319        self.assertEqual(
320            "beginPath(identifier='foo') "
321            "addPoint((0, 100), segmentType='curve', smooth=True) "
322            "addPoint((0, 200)) addPoint((400, 200), identifier='bar') "
323            "addPoint((400, 100), segmentType='curve', smooth=True) "
324            "addPoint((400, 0)) addPoint((0, 0)) endPath()",
325            repr(tpen),
326        )
327
328    def test_guessSmooth_almost(self):
329        tpen = _TestPointPen()
330        pen = GuessSmoothPointPen(tpen)
331        pen.beginPath()
332        pen.addPoint((0, 100), segmentType="curve")
333        pen.addPoint((1, 200))
334        pen.addPoint((395, 200))
335        pen.addPoint((400, 100), segmentType="curve")
336        pen.addPoint((400, 0))
337        pen.addPoint((0, 0))
338        pen.endPath()
339        self.assertEqual(
340            "beginPath() addPoint((0, 100), segmentType='curve', smooth=True) "
341            "addPoint((1, 200)) addPoint((395, 200)) "
342            "addPoint((400, 100), segmentType='curve', smooth=True) "
343            "addPoint((400, 0)) addPoint((0, 0)) endPath()",
344            repr(tpen),
345        )
346
347    def test_guessSmooth_tangent(self):
348        tpen = _TestPointPen()
349        pen = GuessSmoothPointPen(tpen)
350        pen.beginPath()
351        pen.addPoint((0, 0), segmentType="move")
352        pen.addPoint((0, 100), segmentType="line")
353        pen.addPoint((3, 200))
354        pen.addPoint((300, 200))
355        pen.addPoint((400, 200), segmentType="curve")
356        pen.endPath()
357        self.assertEqual(
358            "beginPath() addPoint((0, 0), segmentType='move') "
359            "addPoint((0, 100), segmentType='line', smooth=True) "
360            "addPoint((3, 200)) addPoint((300, 200)) "
361            "addPoint((400, 200), segmentType='curve') endPath()",
362            repr(tpen),
363        )
364
365
366class TestReverseContourPointPen(unittest.TestCase):
367    def test_singlePoint(self):
368        tpen = _TestPointPen()
369        pen = ReverseContourPointPen(tpen)
370        pen.beginPath()
371        pen.addPoint((0, 0), segmentType="move")
372        pen.endPath()
373        self.assertEqual(
374            "beginPath() " "addPoint((0, 0), segmentType='move') " "endPath()",
375            repr(tpen),
376        )
377
378    def test_line(self):
379        tpen = _TestPointPen()
380        pen = ReverseContourPointPen(tpen)
381        pen.beginPath()
382        pen.addPoint((0, 0), segmentType="move")
383        pen.addPoint((0, 100), segmentType="line")
384        pen.endPath()
385        self.assertEqual(
386            "beginPath() "
387            "addPoint((0, 100), segmentType='move') "
388            "addPoint((0, 0), segmentType='line') "
389            "endPath()",
390            repr(tpen),
391        )
392
393    def test_triangle(self):
394        tpen = _TestPointPen()
395        pen = ReverseContourPointPen(tpen)
396        pen.beginPath()
397        pen.addPoint((0, 0), segmentType="line")
398        pen.addPoint((0, 100), segmentType="line")
399        pen.addPoint((100, 100), segmentType="line")
400        pen.endPath()
401        self.assertEqual(
402            "beginPath() "
403            "addPoint((0, 0), segmentType='line') "
404            "addPoint((100, 100), segmentType='line') "
405            "addPoint((0, 100), segmentType='line') "
406            "endPath()",
407            repr(tpen),
408        )
409
410    def test_cubicOpen(self):
411        tpen = _TestPointPen()
412        pen = ReverseContourPointPen(tpen)
413        pen.beginPath()
414        pen.addPoint((0, 0), segmentType="move")
415        pen.addPoint((0, 100))
416        pen.addPoint((100, 200))
417        pen.addPoint((200, 200), segmentType="curve")
418        pen.endPath()
419        self.assertEqual(
420            "beginPath() "
421            "addPoint((200, 200), segmentType='move') "
422            "addPoint((100, 200)) "
423            "addPoint((0, 100)) "
424            "addPoint((0, 0), segmentType='curve') "
425            "endPath()",
426            repr(tpen),
427        )
428
429    def test_quadOpen(self):
430        tpen = _TestPointPen()
431        pen = ReverseContourPointPen(tpen)
432        pen.beginPath()
433        pen.addPoint((0, 0), segmentType="move")
434        pen.addPoint((0, 100))
435        pen.addPoint((100, 200))
436        pen.addPoint((200, 200), segmentType="qcurve")
437        pen.endPath()
438        self.assertEqual(
439            "beginPath() "
440            "addPoint((200, 200), segmentType='move') "
441            "addPoint((100, 200)) "
442            "addPoint((0, 100)) "
443            "addPoint((0, 0), segmentType='qcurve') "
444            "endPath()",
445            repr(tpen),
446        )
447
448    def test_cubicClosed(self):
449        tpen = _TestPointPen()
450        pen = ReverseContourPointPen(tpen)
451        pen.beginPath()
452        pen.addPoint((0, 0), segmentType="line")
453        pen.addPoint((0, 100))
454        pen.addPoint((100, 200))
455        pen.addPoint((200, 200), segmentType="curve")
456        pen.endPath()
457        self.assertEqual(
458            "beginPath() "
459            "addPoint((0, 0), segmentType='curve') "
460            "addPoint((200, 200), segmentType='line') "
461            "addPoint((100, 200)) "
462            "addPoint((0, 100)) "
463            "endPath()",
464            repr(tpen),
465        )
466
467    def test_quadClosedOffCurveStart(self):
468        tpen = _TestPointPen()
469        pen = ReverseContourPointPen(tpen)
470        pen.beginPath()
471        pen.addPoint((100, 200))
472        pen.addPoint((200, 200), segmentType="qcurve")
473        pen.addPoint((0, 0), segmentType="line")
474        pen.addPoint((0, 100))
475        pen.endPath()
476        self.assertEqual(
477            "beginPath() "
478            "addPoint((100, 200)) "
479            "addPoint((0, 100)) "
480            "addPoint((0, 0), segmentType='qcurve') "
481            "addPoint((200, 200), segmentType='line') "
482            "endPath()",
483            repr(tpen),
484        )
485
486    def test_quadNoOnCurve(self):
487        tpen = _TestPointPen()
488        pen = ReverseContourPointPen(tpen)
489        pen.beginPath(identifier="bar")
490        pen.addPoint((0, 0))
491        pen.addPoint((0, 100), identifier="foo", arbitrary="foo")
492        pen.addPoint((100, 200), arbitrary=123)
493        pen.addPoint((200, 200))
494        pen.endPath()
495        pen.addComponent("base", [1, 0, 0, 1, 0, 0], identifier="foo")
496        self.assertEqual(
497            "beginPath(identifier='bar') "
498            "addPoint((0, 0)) "
499            "addPoint((200, 200)) "
500            "addPoint((100, 200), arbitrary=123) "
501            "addPoint((0, 100), identifier='foo', arbitrary='foo') "
502            "endPath() "
503            "addComponent('base', [1, 0, 0, 1, 0, 0], identifier='foo')",
504            repr(tpen),
505        )
506
507    def test_closed_line_overlapping_start_end_points(self):
508        # Test case from https://github.com/googlefonts/fontmake/issues/572
509        tpen = _TestPointPen()
510        pen = ReverseContourPointPen(tpen)
511        pen.beginPath()
512        pen.addPoint((0, 651), segmentType="line")
513        pen.addPoint((0, 101), segmentType="line")
514        pen.addPoint((0, 101), segmentType="line")
515        pen.addPoint((0, 651), segmentType="line")
516        pen.endPath()
517        self.assertEqual(
518            "beginPath() "
519            "addPoint((0, 651), segmentType='line') "
520            "addPoint((0, 651), segmentType='line') "
521            "addPoint((0, 101), segmentType='line') "
522            "addPoint((0, 101), segmentType='line') "
523            "endPath()",
524            repr(tpen),
525        )
526