xref: /aosp_15_r20/external/fonttools/Tests/pens/reverseContourPen_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.pens.recordingPen import RecordingPen
2from fontTools.pens.reverseContourPen import ReverseContourPen
3import pytest
4
5
6TEST_DATA = [
7    (
8        [
9            ("moveTo", ((0, 0),)),
10            ("lineTo", ((1, 1),)),
11            ("lineTo", ((2, 2),)),
12            ("lineTo", ((3, 3),)),  # last not on move, line is implied
13            ("closePath", ()),
14        ],
15        False,  # outputImpliedClosingLine
16        [
17            ("moveTo", ((0, 0),)),
18            ("lineTo", ((3, 3),)),
19            ("lineTo", ((2, 2),)),
20            ("lineTo", ((1, 1),)),
21            ("closePath", ()),
22        ],
23    ),
24    (
25        [
26            ("moveTo", ((0, 0),)),
27            ("lineTo", ((1, 1),)),
28            ("lineTo", ((2, 2),)),
29            ("lineTo", ((3, 3),)),  # last line does not overlap move...
30            ("closePath", ()),
31        ],
32        True,  # outputImpliedClosingLine
33        [
34            ("moveTo", ((0, 0),)),
35            ("lineTo", ((3, 3),)),
36            ("lineTo", ((2, 2),)),
37            ("lineTo", ((1, 1),)),
38            ("lineTo", ((0, 0),)),  # ... but closing line is NOT implied
39            ("closePath", ()),
40        ],
41    ),
42    (
43        [
44            ("moveTo", ((0, 0),)),
45            ("lineTo", ((1, 1),)),
46            ("lineTo", ((2, 2),)),
47            ("lineTo", ((0, 0),)),  # last line overlaps move, explicit line
48            ("closePath", ()),
49        ],
50        False,
51        [
52            ("moveTo", ((0, 0),)),
53            ("lineTo", ((2, 2),)),
54            ("lineTo", ((1, 1),)),
55            ("closePath", ()),  # closing line implied
56        ],
57    ),
58    (
59        [
60            ("moveTo", ((0, 0),)),
61            ("lineTo", ((1, 1),)),
62            ("lineTo", ((2, 2),)),
63            ("lineTo", ((0, 0),)),  # last line overlaps move...
64            ("closePath", ()),
65        ],
66        True,
67        [
68            ("moveTo", ((0, 0),)),
69            ("lineTo", ((2, 2),)),
70            ("lineTo", ((1, 1),)),
71            ("lineTo", ((0, 0),)),  # ... but line is NOT implied
72            ("closePath", ()),
73        ],
74    ),
75    (
76        [
77            ("moveTo", ((0, 0),)),
78            ("lineTo", ((0, 0),)),  # duplicate lineTo following moveTo
79            ("lineTo", ((1, 1),)),
80            ("lineTo", ((2, 2),)),
81            ("closePath", ()),
82        ],
83        False,
84        [
85            ("moveTo", ((0, 0),)),
86            ("lineTo", ((2, 2),)),
87            ("lineTo", ((1, 1),)),
88            ("lineTo", ((0, 0),)),  # extra explicit lineTo is always emitted to
89            ("lineTo", ((0, 0),)),  # disambiguate from an implicit closing line
90            ("closePath", ()),
91        ],
92    ),
93    (
94        [
95            ("moveTo", ((0, 0),)),
96            ("lineTo", ((0, 0),)),  # duplicate lineTo following moveTo
97            ("lineTo", ((1, 1),)),
98            ("lineTo", ((2, 2),)),
99            ("closePath", ()),
100        ],
101        True,
102        [
103            ("moveTo", ((0, 0),)),
104            ("lineTo", ((2, 2),)),
105            ("lineTo", ((1, 1),)),
106            ("lineTo", ((0, 0),)),  # duplicate lineTo is retained also in this case,
107            ("lineTo", ((0, 0),)),  # same result as with outputImpliedClosingLine=False
108            ("closePath", ()),
109        ],
110    ),
111    (
112        [
113            ("moveTo", ((0, 0),)),
114            ("lineTo", ((1, 1),)),
115            ("closePath", ()),
116        ],
117        False,
118        [
119            ("moveTo", ((0, 0),)),
120            ("lineTo", ((1, 1),)),
121            ("closePath", ()),
122        ],
123    ),
124    (
125        [
126            ("moveTo", ((0, 0),)),
127            ("lineTo", ((1, 1),)),
128            ("closePath", ()),
129        ],
130        True,
131        [
132            ("moveTo", ((0, 0),)),
133            ("lineTo", ((1, 1),)),
134            ("lineTo", ((0, 0),)),
135            ("closePath", ()),
136        ],
137    ),
138    (
139        [
140            ("moveTo", ((0, 0),)),
141            ("curveTo", ((1, 1), (2, 2), (3, 3))),
142            ("curveTo", ((4, 4), (5, 5), (0, 0))),  # closed curveTo overlaps moveTo
143            ("closePath", ()),
144        ],
145        False,
146        [
147            ("moveTo", ((0, 0),)),  # no extra lineTo added here
148            ("curveTo", ((5, 5), (4, 4), (3, 3))),
149            ("curveTo", ((2, 2), (1, 1), (0, 0))),
150            ("closePath", ()),
151        ],
152    ),
153    (
154        [
155            ("moveTo", ((0, 0),)),
156            ("curveTo", ((1, 1), (2, 2), (3, 3))),
157            ("curveTo", ((4, 4), (5, 5), (0, 0))),  # closed curveTo overlaps moveTo
158            ("closePath", ()),
159        ],
160        True,
161        [
162            ("moveTo", ((0, 0),)),  # no extra lineTo added here, same as preceding
163            ("curveTo", ((5, 5), (4, 4), (3, 3))),
164            ("curveTo", ((2, 2), (1, 1), (0, 0))),
165            ("closePath", ()),
166        ],
167    ),
168    (
169        [
170            ("moveTo", ((0, 0),)),
171            ("curveTo", ((1, 1), (2, 2), (3, 3))),
172            ("curveTo", ((4, 4), (5, 5), (6, 6))),  # closed curve not overlapping move
173            ("closePath", ()),
174        ],
175        False,
176        [
177            ("moveTo", ((0, 0),)),
178            ("lineTo", ((6, 6),)),  # the previously implied line
179            ("curveTo", ((5, 5), (4, 4), (3, 3))),
180            ("curveTo", ((2, 2), (1, 1), (0, 0))),
181            ("closePath", ()),
182        ],
183    ),
184    (
185        [
186            ("moveTo", ((0, 0),)),
187            ("curveTo", ((1, 1), (2, 2), (3, 3))),
188            ("curveTo", ((4, 4), (5, 5), (6, 6))),  # closed curve not overlapping move
189            ("closePath", ()),
190        ],
191        True,
192        [
193            ("moveTo", ((0, 0),)),
194            ("lineTo", ((6, 6),)),  # the previously implied line (same as above)
195            ("curveTo", ((5, 5), (4, 4), (3, 3))),
196            ("curveTo", ((2, 2), (1, 1), (0, 0))),
197            ("closePath", ()),
198        ],
199    ),
200    (
201        [
202            ("moveTo", ((0, 0),)),
203            ("lineTo", ((1, 1),)),  # this line becomes implied
204            ("curveTo", ((2, 2), (3, 3), (4, 4))),
205            ("curveTo", ((5, 5), (6, 6), (7, 7))),
206            ("closePath", ()),
207        ],
208        False,
209        [
210            ("moveTo", ((0, 0),)),
211            ("lineTo", ((7, 7),)),
212            ("curveTo", ((6, 6), (5, 5), (4, 4))),
213            ("curveTo", ((3, 3), (2, 2), (1, 1))),
214            ("closePath", ()),
215        ],
216    ),
217    (
218        [
219            ("moveTo", ((0, 0),)),
220            ("lineTo", ((1, 1),)),  # this line...
221            ("curveTo", ((2, 2), (3, 3), (4, 4))),
222            ("curveTo", ((5, 5), (6, 6), (7, 7))),
223            ("closePath", ()),
224        ],
225        True,
226        [
227            ("moveTo", ((0, 0),)),
228            ("lineTo", ((7, 7),)),
229            ("curveTo", ((6, 6), (5, 5), (4, 4))),
230            ("curveTo", ((3, 3), (2, 2), (1, 1))),
231            ("lineTo", ((0, 0),)),  # ... does NOT become implied
232            ("closePath", ()),
233        ],
234    ),
235    (
236        [
237            ("moveTo", ((0, 0),)),
238            ("qCurveTo", ((1, 1), (2, 2))),
239            ("qCurveTo", ((3, 3), (0, 0))),  # closed qCurve overlaps move
240            ("closePath", ()),
241        ],
242        False,
243        [
244            ("moveTo", ((0, 0),)),  # no extra lineTo added here
245            ("qCurveTo", ((3, 3), (2, 2))),
246            ("qCurveTo", ((1, 1), (0, 0))),
247            ("closePath", ()),
248        ],
249    ),
250    (
251        [
252            ("moveTo", ((0, 0),)),
253            ("qCurveTo", ((1, 1), (2, 2))),
254            ("qCurveTo", ((3, 3), (0, 0))),  # closed qCurve overlaps move
255            ("closePath", ()),
256        ],
257        True,  # <--
258        [
259            ("moveTo", ((0, 0),)),  # no extra lineTo added here, same as above
260            ("qCurveTo", ((3, 3), (2, 2))),
261            ("qCurveTo", ((1, 1), (0, 0))),
262            ("closePath", ()),
263        ],
264    ),
265    (
266        [
267            ("moveTo", ((0, 0),)),
268            ("qCurveTo", ((1, 1), (2, 2))),
269            ("qCurveTo", ((3, 3), (4, 4))),  # closed qCurve not overlapping move
270            ("closePath", ()),
271        ],
272        False,
273        [
274            ("moveTo", ((0, 0),)),
275            ("lineTo", ((4, 4),)),  # the previously implied line
276            ("qCurveTo", ((3, 3), (2, 2))),
277            ("qCurveTo", ((1, 1), (0, 0))),
278            ("closePath", ()),
279        ],
280    ),
281    (
282        [
283            ("moveTo", ((0, 0),)),
284            ("qCurveTo", ((1, 1), (2, 2))),
285            ("qCurveTo", ((3, 3), (4, 4))),  # closed qCurve not overlapping move
286            ("closePath", ()),
287        ],
288        True,
289        [
290            ("moveTo", ((0, 0),)),
291            ("lineTo", ((4, 4),)),  # the previously implied line (same as above)
292            ("qCurveTo", ((3, 3), (2, 2))),
293            ("qCurveTo", ((1, 1), (0, 0))),
294            ("closePath", ()),
295        ],
296    ),
297    (
298        [
299            ("moveTo", ((0, 0),)),
300            ("lineTo", ((1, 1),)),
301            ("qCurveTo", ((2, 2), (3, 3))),
302            ("closePath", ()),
303        ],
304        False,
305        [
306            ("moveTo", ((0, 0),)),
307            ("lineTo", ((3, 3),)),
308            ("qCurveTo", ((2, 2), (1, 1))),
309            ("closePath", ()),
310        ],
311    ),
312    (
313        [("addComponent", ("a", (1, 0, 0, 1, 0, 0)))],
314        False,
315        [("addComponent", ("a", (1, 0, 0, 1, 0, 0)))],
316    ),
317    ([], False, []),
318    (
319        [
320            ("moveTo", ((0, 0),)),
321            ("endPath", ()),
322        ],
323        False,
324        [
325            ("moveTo", ((0, 0),)),
326            ("endPath", ()),
327        ],
328    ),
329    (
330        [
331            ("moveTo", ((0, 0),)),
332            ("closePath", ()),
333        ],
334        False,
335        [
336            ("moveTo", ((0, 0),)),
337            ("endPath", ()),  # single-point paths is always open
338        ],
339    ),
340    (
341        [("moveTo", ((0, 0),)), ("lineTo", ((1, 1),)), ("endPath", ())],
342        False,
343        [("moveTo", ((1, 1),)), ("lineTo", ((0, 0),)), ("endPath", ())],
344    ),
345    (
346        [("moveTo", ((0, 0),)), ("curveTo", ((1, 1), (2, 2), (3, 3))), ("endPath", ())],
347        False,
348        [("moveTo", ((3, 3),)), ("curveTo", ((2, 2), (1, 1), (0, 0))), ("endPath", ())],
349    ),
350    (
351        [
352            ("moveTo", ((0, 0),)),
353            ("curveTo", ((1, 1), (2, 2), (3, 3))),
354            ("lineTo", ((4, 4),)),
355            ("endPath", ()),
356        ],
357        False,
358        [
359            ("moveTo", ((4, 4),)),
360            ("lineTo", ((3, 3),)),
361            ("curveTo", ((2, 2), (1, 1), (0, 0))),
362            ("endPath", ()),
363        ],
364    ),
365    (
366        [
367            ("moveTo", ((0, 0),)),
368            ("lineTo", ((1, 1),)),
369            ("curveTo", ((2, 2), (3, 3), (4, 4))),
370            ("endPath", ()),
371        ],
372        False,
373        [
374            ("moveTo", ((4, 4),)),
375            ("curveTo", ((3, 3), (2, 2), (1, 1))),
376            ("lineTo", ((0, 0),)),
377            ("endPath", ()),
378        ],
379    ),
380    (
381        [("qCurveTo", ((0, 0), (1, 1), (2, 2), None)), ("closePath", ())],
382        False,
383        [("qCurveTo", ((0, 0), (2, 2), (1, 1), None)), ("closePath", ())],
384    ),
385    (
386        [("qCurveTo", ((0, 0), (1, 1), (2, 2), None)), ("endPath", ())],
387        False,
388        [
389            ("qCurveTo", ((0, 0), (2, 2), (1, 1), None)),
390            ("closePath", ()),  # this is always "closed"
391        ],
392    ),
393    # Test case from:
394    # https://github.com/googlei18n/cu2qu/issues/51#issue-179370514
395    (
396        [
397            ("moveTo", ((848, 348),)),
398            ("lineTo", ((848, 348),)),  # duplicate lineTo point after moveTo
399            ("qCurveTo", ((848, 526), (649, 704), (449, 704))),
400            ("qCurveTo", ((449, 704), (248, 704), (50, 526), (50, 348))),
401            ("lineTo", ((50, 348),)),
402            ("qCurveTo", ((50, 348), (50, 171), (248, -3), (449, -3))),
403            ("qCurveTo", ((449, -3), (649, -3), (848, 171), (848, 348))),
404            ("closePath", ()),
405        ],
406        False,
407        [
408            ("moveTo", ((848, 348),)),
409            ("qCurveTo", ((848, 171), (649, -3), (449, -3), (449, -3))),
410            ("qCurveTo", ((248, -3), (50, 171), (50, 348), (50, 348))),
411            ("lineTo", ((50, 348),)),
412            ("qCurveTo", ((50, 526), (248, 704), (449, 704), (449, 704))),
413            ("qCurveTo", ((649, 704), (848, 526), (848, 348))),
414            ("lineTo", ((848, 348),)),  # the duplicate point is kept
415            ("closePath", ()),
416        ],
417    ),
418    # Test case from https://github.com/googlefonts/fontmake/issues/572
419    # An additional closing lineTo is required to disambiguate a duplicate
420    # point at the end of a contour from the implied closing line.
421    (
422        [
423            ("moveTo", ((0, 651),)),
424            ("lineTo", ((0, 101),)),
425            ("lineTo", ((0, 101),)),
426            ("lineTo", ((0, 651),)),
427            ("lineTo", ((0, 651),)),
428            ("closePath", ()),
429        ],
430        False,
431        [
432            ("moveTo", ((0, 651),)),
433            ("lineTo", ((0, 651),)),
434            ("lineTo", ((0, 101),)),
435            ("lineTo", ((0, 101),)),
436            ("closePath", ()),
437        ],
438    ),
439    (
440        [
441            ("moveTo", ((0, 651),)),
442            ("lineTo", ((0, 101),)),
443            ("lineTo", ((0, 101),)),
444            ("lineTo", ((0, 651),)),
445            ("lineTo", ((0, 651),)),
446            ("closePath", ()),
447        ],
448        True,
449        [
450            ("moveTo", ((0, 651),)),
451            ("lineTo", ((0, 651),)),
452            ("lineTo", ((0, 101),)),
453            ("lineTo", ((0, 101),)),
454            ("lineTo", ((0, 651),)),  # closing line not implied
455            ("closePath", ()),
456        ],
457    ),
458]
459
460
461@pytest.mark.parametrize("contour, outputImpliedClosingLine, expected", TEST_DATA)
462def test_reverse_pen(contour, outputImpliedClosingLine, expected):
463    recpen = RecordingPen()
464    revpen = ReverseContourPen(recpen, outputImpliedClosingLine)
465    for operator, operands in contour:
466        getattr(revpen, operator)(*operands)
467    assert recpen.value == expected
468
469
470def test_reverse_pen_outputImpliedClosingLine():
471    recpen = RecordingPen()
472    revpen = ReverseContourPen(recpen)
473    revpen.moveTo((0, 0))
474    revpen.lineTo((10, 0))
475    revpen.lineTo((0, 10))
476    revpen.lineTo((0, 0))
477    revpen.closePath()
478    assert recpen.value == [
479        ("moveTo", ((0, 0),)),
480        ("lineTo", ((0, 10),)),
481        ("lineTo", ((10, 0),)),
482        # ("lineTo", ((0, 0),)),  # implied
483        ("closePath", ()),
484    ]
485
486    recpen = RecordingPen()
487    revpen = ReverseContourPen(recpen, outputImpliedClosingLine=True)
488    revpen.moveTo((0, 0))
489    revpen.lineTo((10, 0))
490    revpen.lineTo((0, 10))
491    revpen.lineTo((0, 0))
492    revpen.closePath()
493    assert recpen.value == [
494        ("moveTo", ((0, 0),)),
495        ("lineTo", ((0, 10),)),
496        ("lineTo", ((10, 0),)),
497        ("lineTo", ((0, 0),)),  # not implied
498        ("closePath", ()),
499    ]
500
501
502@pytest.mark.parametrize("contour, outputImpliedClosingLine, expected", TEST_DATA)
503def test_reverse_point_pen(contour, outputImpliedClosingLine, expected):
504    from fontTools.pens.pointPen import (
505        ReverseContourPointPen,
506        PointToSegmentPen,
507        SegmentToPointPen,
508    )
509
510    recpen = RecordingPen()
511    pt2seg = PointToSegmentPen(recpen, outputImpliedClosingLine)
512    revpen = ReverseContourPointPen(pt2seg)
513    seg2pt = SegmentToPointPen(revpen)
514    for operator, operands in contour:
515        getattr(seg2pt, operator)(*operands)
516
517    assert recpen.value == expected
518