xref: /aosp_15_r20/external/fonttools/Tests/pens/ttGlyphPen_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1import os
2import pytest
3import struct
4
5from fontTools import ttLib
6from fontTools.pens.basePen import PenError
7from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen
8from fontTools.pens.ttGlyphPen import TTGlyphPen, TTGlyphPointPen, MAX_F2DOT14
9
10
11class TTGlyphPenTestBase:
12    def runEndToEnd(self, filename):
13        font = ttLib.TTFont()
14        ttx_path = os.path.join(
15            os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
16            "..",
17            "ttLib",
18            "data",
19            filename,
20        )
21        font.importXML(ttx_path)
22
23        glyphSet = font.getGlyphSet()
24        glyfTable = font["glyf"]
25        pen = self.penClass(glyphSet)
26
27        for name in font.getGlyphOrder():
28            getattr(glyphSet[name], self.drawMethod)(pen)
29            oldGlyph = glyfTable[name]
30            newGlyph = pen.glyph()
31
32            if hasattr(oldGlyph, "program"):
33                newGlyph.program = oldGlyph.program
34
35            assert oldGlyph.compile(glyfTable) == newGlyph.compile(glyfTable)
36
37    def test_e2e_linesAndSimpleComponents(self):
38        self.runEndToEnd("TestTTF-Regular.ttx")
39
40    def test_e2e_curvesAndComponentTransforms(self):
41        self.runEndToEnd("TestTTFComplex-Regular.ttx")
42
43
44class TTGlyphPenTest(TTGlyphPenTestBase):
45    penClass = TTGlyphPen
46    drawMethod = "draw"
47
48    def test_moveTo_errorWithinContour(self):
49        pen = TTGlyphPen(None)
50        pen.moveTo((0, 0))
51        with pytest.raises(PenError):
52            pen.moveTo((1, 0))
53
54    def test_closePath_ignoresAnchors(self):
55        pen = TTGlyphPen(None)
56        pen.moveTo((0, 0))
57        pen.closePath()
58        assert not pen.points
59        assert not pen.types
60        assert not pen.endPts
61
62    def test_endPath_sameAsClosePath(self):
63        pen = TTGlyphPen(None)
64
65        pen.moveTo((0, 0))
66        pen.lineTo((0, 1))
67        pen.lineTo((1, 0))
68        pen.closePath()
69        closePathGlyph = pen.glyph()
70
71        pen.moveTo((0, 0))
72        pen.lineTo((0, 1))
73        pen.lineTo((1, 0))
74        pen.endPath()
75        endPathGlyph = pen.glyph()
76
77        assert closePathGlyph == endPathGlyph
78
79    def test_glyph_errorOnUnendedContour(self):
80        pen = TTGlyphPen(None)
81        pen.moveTo((0, 0))
82        with pytest.raises(PenError):
83            pen.glyph()
84
85    def test_glyph_decomposes(self):
86        componentName = "a"
87        glyphSet = {}
88        pen = TTGlyphPen(glyphSet)
89
90        pen.moveTo((0, 0))
91        pen.lineTo((0, 1))
92        pen.lineTo((1, 0))
93        pen.closePath()
94        glyphSet[componentName] = _TestGlyph(pen.glyph())
95
96        pen.moveTo((0, 0))
97        pen.lineTo((0, 1))
98        pen.lineTo((1, 0))
99        pen.closePath()
100        pen.addComponent(componentName, (1, 0, 0, 1, 2, 0))
101        pen.addComponent("missing", (1, 0, 0, 1, 0, 0))  # skipped
102        compositeGlyph = pen.glyph()
103
104        pen.moveTo((0, 0))
105        pen.lineTo((0, 1))
106        pen.lineTo((1, 0))
107        pen.closePath()
108        pen.moveTo((2, 0))
109        pen.lineTo((2, 1))
110        pen.lineTo((3, 0))
111        pen.closePath()
112        plainGlyph = pen.glyph()
113
114        assert plainGlyph == compositeGlyph
115
116    def test_remove_extra_move_points(self):
117        pen = TTGlyphPen(None)
118        pen.moveTo((0, 0))
119        pen.lineTo((100, 0))
120        pen.qCurveTo((100, 50), (50, 100), (0, 0))
121        pen.closePath()
122        assert len(pen.points) == 4
123        assert pen.points[0] == (0, 0)
124
125    def test_keep_move_point(self):
126        pen = TTGlyphPen(None)
127        pen.moveTo((0, 0))
128        pen.lineTo((100, 0))
129        pen.qCurveTo((100, 50), (50, 100), (30, 30))
130        # when last and move pts are different, closePath() implies a lineTo
131        pen.closePath()
132        assert len(pen.points) == 5
133        assert pen.points[0] == (0, 0)
134
135    def test_keep_duplicate_end_point(self):
136        pen = TTGlyphPen(None)
137        pen.moveTo((0, 0))
138        pen.lineTo((100, 0))
139        pen.qCurveTo((100, 50), (50, 100), (0, 0))
140        pen.lineTo((0, 0))  # the duplicate point is not removed
141        pen.closePath()
142        assert len(pen.points) == 5
143        assert pen.points[0] == (0, 0)
144
145    def test_within_range_component_transform(self):
146        componentName = "a"
147        glyphSet = {}
148        pen = TTGlyphPen(glyphSet)
149
150        pen.moveTo((0, 0))
151        pen.lineTo((0, 1))
152        pen.lineTo((1, 0))
153        pen.closePath()
154        glyphSet[componentName] = _TestGlyph(pen.glyph())
155
156        pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0))
157        pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0))
158        compositeGlyph = pen.glyph()
159
160        pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0))
161        pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0))
162        expectedGlyph = pen.glyph()
163
164        assert expectedGlyph == compositeGlyph
165
166    def test_clamp_to_almost_2_component_transform(self):
167        componentName = "a"
168        glyphSet = {}
169        pen = TTGlyphPen(glyphSet)
170
171        pen.moveTo((0, 0))
172        pen.lineTo((0, 1))
173        pen.lineTo((1, 0))
174        pen.closePath()
175        glyphSet[componentName] = _TestGlyph(pen.glyph())
176
177        pen.addComponent(componentName, (1.99999, 0, 0, 1, 0, 0))
178        pen.addComponent(componentName, (1, 2, 0, 1, 0, 0))
179        pen.addComponent(componentName, (1, 0, 2, 1, 0, 0))
180        pen.addComponent(componentName, (1, 0, 0, 2, 0, 0))
181        pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0))
182        compositeGlyph = pen.glyph()
183
184        almost2 = MAX_F2DOT14  # 0b1.11111111111111
185        pen.addComponent(componentName, (almost2, 0, 0, 1, 0, 0))
186        pen.addComponent(componentName, (1, almost2, 0, 1, 0, 0))
187        pen.addComponent(componentName, (1, 0, almost2, 1, 0, 0))
188        pen.addComponent(componentName, (1, 0, 0, almost2, 0, 0))
189        pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0))
190        expectedGlyph = pen.glyph()
191
192        assert expectedGlyph == compositeGlyph
193
194    def test_out_of_range_transform_decomposed(self):
195        componentName = "a"
196        glyphSet = {}
197        pen = TTGlyphPen(glyphSet)
198
199        pen.moveTo((0, 0))
200        pen.lineTo((0, 1))
201        pen.lineTo((1, 0))
202        pen.closePath()
203        glyphSet[componentName] = _TestGlyph(pen.glyph())
204
205        pen.addComponent(componentName, (3, 0, 0, 2, 0, 0))
206        pen.addComponent(componentName, (1, 0, 0, 1, -1, 2))
207        pen.addComponent(componentName, (2, 0, 0, -3, 0, 0))
208        compositeGlyph = pen.glyph()
209
210        pen.moveTo((0, 0))
211        pen.lineTo((0, 2))
212        pen.lineTo((3, 0))
213        pen.closePath()
214        pen.moveTo((-1, 2))
215        pen.lineTo((-1, 3))
216        pen.lineTo((0, 2))
217        pen.closePath()
218        pen.moveTo((0, 0))
219        pen.lineTo((0, -3))
220        pen.lineTo((2, 0))
221        pen.closePath()
222        expectedGlyph = pen.glyph()
223
224        assert expectedGlyph == compositeGlyph
225
226    def test_no_handle_overflowing_transform(self):
227        componentName = "a"
228        glyphSet = {}
229        pen = TTGlyphPen(glyphSet, handleOverflowingTransforms=False)
230
231        pen.moveTo((0, 0))
232        pen.lineTo((0, 1))
233        pen.lineTo((1, 0))
234        pen.closePath()
235        baseGlyph = pen.glyph()
236        glyphSet[componentName] = _TestGlyph(baseGlyph)
237
238        pen.addComponent(componentName, (3, 0, 0, 1, 0, 0))
239        compositeGlyph = pen.glyph()
240
241        assert compositeGlyph.components[0].transform == ((3, 0), (0, 1))
242
243        with pytest.raises(struct.error):
244            compositeGlyph.compile({"a": baseGlyph})
245
246    def assertGlyphBoundsEqual(self, glyph, bounds):
247        assert (glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax) == bounds
248
249    def test_round_float_coordinates_and_component_offsets(self):
250        glyphSet = {}
251        pen = TTGlyphPen(glyphSet)
252
253        pen.moveTo((0, 0))
254        pen.lineTo((0, 1))
255        pen.lineTo((367.6, 0))
256        pen.closePath()
257        simpleGlyph = pen.glyph()
258
259        simpleGlyph.recalcBounds(glyphSet)
260        self.assertGlyphBoundsEqual(simpleGlyph, (0, 0, 368, 1))
261
262        componentName = "a"
263        glyphSet[componentName] = simpleGlyph
264
265        pen.addComponent(componentName, (1, 0, 0, 1, -86.4, 0))
266        compositeGlyph = pen.glyph()
267
268        compositeGlyph.recalcBounds(glyphSet)
269        self.assertGlyphBoundsEqual(compositeGlyph, (-86, 0, 282, 1))
270
271    def test_scaled_component_bounds(self):
272        glyphSet = {}
273
274        pen = TTGlyphPen(glyphSet)
275        pen.moveTo((-231, 939))
276        pen.lineTo((-55, 939))
277        pen.lineTo((-55, 745))
278        pen.lineTo((-231, 745))
279        pen.closePath()
280        glyphSet["gravecomb"] = pen.glyph()
281
282        pen = TTGlyphPen(glyphSet)
283        pen.moveTo((-278, 939))
284        pen.lineTo((8, 939))
285        pen.lineTo((8, 745))
286        pen.lineTo((-278, 745))
287        pen.closePath()
288        glyphSet["circumflexcomb"] = pen.glyph()
289
290        pen = TTGlyphPen(glyphSet)
291        pen.addComponent("circumflexcomb", (1, 0, 0, 1, 0, 0))
292        pen.addComponent("gravecomb", (0.9, 0, 0, 0.9, 198, 180))
293        glyphSet["uni0302_uni0300"] = uni0302_uni0300 = pen.glyph()
294
295        uni0302_uni0300.recalcBounds(glyphSet)
296        self.assertGlyphBoundsEqual(uni0302_uni0300, (-278, 745, 148, 1025))
297
298    def test_outputImpliedClosingLine(self):
299        glyphSet = {}
300
301        pen = TTGlyphPen(glyphSet)
302        pen.moveTo((0, 0))
303        pen.lineTo((10, 0))
304        pen.lineTo((0, 10))
305        pen.lineTo((0, 0))
306        pen.closePath()
307        glyph = pen.glyph()
308        assert len(glyph.coordinates) == 3
309
310        pen = TTGlyphPen(glyphSet, outputImpliedClosingLine=True)
311        pen.moveTo((0, 0))
312        pen.lineTo((10, 0))
313        pen.lineTo((0, 10))
314        pen.lineTo((0, 0))
315        pen.closePath()
316        glyph = pen.glyph()
317        assert len(glyph.coordinates) == 4
318
319
320class TTGlyphPointPenTest(TTGlyphPenTestBase):
321    penClass = TTGlyphPointPen
322    drawMethod = "drawPoints"
323
324    def test_glyph_simple(self):
325        pen = TTGlyphPointPen(None)
326        pen.beginPath()
327        pen.addPoint((50, 0), "line")
328        pen.addPoint((450, 0), "line")
329        pen.addPoint((450, 700), "line")
330        pen.addPoint((50, 700), "line")
331        pen.endPath()
332        glyph = pen.glyph()
333        assert glyph.numberOfContours == 1
334        assert glyph.endPtsOfContours == [3]
335
336    def test_addPoint_noErrorOnCurve(self):
337        pen = TTGlyphPointPen(None)
338        pen.beginPath()
339        pen.addPoint((0, 0), "curve")
340        pen.endPath()
341
342    def test_beginPath_beginPathOnOpenPath(self):
343        pen = TTGlyphPointPen(None)
344        pen.beginPath()
345        pen.addPoint((0, 0))
346        with pytest.raises(PenError):
347            pen.beginPath()
348
349    def test_glyph_errorOnUnendedContour(self):
350        pen = TTGlyphPointPen(None)
351        pen.beginPath()
352        pen.addPoint((0, 0))
353        with pytest.raises(PenError):
354            pen.glyph()
355
356    def test_glyph_decomposes(self):
357        componentName = "a"
358        glyphSet = {}
359        pen = TTGlyphPointPen(glyphSet)
360
361        pen.beginPath()
362        pen.addPoint((0, 0), "line")
363        pen.addPoint((0, 1), "line")
364        pen.addPoint((1, 0), "line")
365        pen.endPath()
366        glyphSet[componentName] = _TestGlyph(pen.glyph())
367
368        pen.beginPath()
369        pen.addPoint((0, 0), "line")
370        pen.addPoint((0, 1), "line")
371        pen.addPoint((1, 0), "line")
372        pen.endPath()
373        pen.addComponent(componentName, (1, 0, 0, 1, 2, 0))
374        pen.addComponent("missing", (1, 0, 0, 1, 0, 0))  # skipped
375        compositeGlyph = pen.glyph()
376
377        pen.beginPath()
378        pen.addPoint((0, 0), "line")
379        pen.addPoint((0, 1), "line")
380        pen.addPoint((1, 0), "line")
381        pen.endPath()
382        pen.beginPath()
383        pen.addPoint((2, 0), "line")
384        pen.addPoint((2, 1), "line")
385        pen.addPoint((3, 0), "line")
386        pen.endPath()
387        plainGlyph = pen.glyph()
388
389        assert plainGlyph == compositeGlyph
390
391    def test_keep_duplicate_end_point(self):
392        pen = TTGlyphPointPen(None)
393        pen.beginPath()
394        pen.addPoint((0, 0), "line")
395        pen.addPoint((100, 0), "line")
396        pen.addPoint((100, 50))
397        pen.addPoint((50, 100))
398        pen.addPoint((0, 0), "qcurve")
399        pen.addPoint((0, 0), "line")  # the duplicate point is not removed
400        pen.endPath()
401        assert len(pen.points) == 6
402        assert pen.points[0] == (0, 0)
403
404    def test_within_range_component_transform(self):
405        componentName = "a"
406        glyphSet = {}
407        pen = TTGlyphPointPen(glyphSet)
408
409        pen.beginPath()
410        pen.addPoint((0, 0), "line")
411        pen.addPoint((0, 1), "line")
412        pen.addPoint((1, 0), "line")
413        pen.endPath()
414        glyphSet[componentName] = _TestGlyph(pen.glyph())
415
416        pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0))
417        pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0))
418        compositeGlyph = pen.glyph()
419
420        pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0))
421        pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0))
422        expectedGlyph = pen.glyph()
423
424        assert expectedGlyph == compositeGlyph
425
426    def test_clamp_to_almost_2_component_transform(self):
427        componentName = "a"
428        glyphSet = {}
429        pen = TTGlyphPointPen(glyphSet)
430
431        pen.beginPath()
432        pen.addPoint((0, 0), "line")
433        pen.addPoint((0, 1), "line")
434        pen.addPoint((1, 0), "line")
435        pen.endPath()
436        glyphSet[componentName] = _TestGlyph(pen.glyph())
437
438        pen.addComponent(componentName, (1.99999, 0, 0, 1, 0, 0))
439        pen.addComponent(componentName, (1, 2, 0, 1, 0, 0))
440        pen.addComponent(componentName, (1, 0, 2, 1, 0, 0))
441        pen.addComponent(componentName, (1, 0, 0, 2, 0, 0))
442        pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0))
443        compositeGlyph = pen.glyph()
444
445        almost2 = MAX_F2DOT14  # 0b1.11111111111111
446        pen.addComponent(componentName, (almost2, 0, 0, 1, 0, 0))
447        pen.addComponent(componentName, (1, almost2, 0, 1, 0, 0))
448        pen.addComponent(componentName, (1, 0, almost2, 1, 0, 0))
449        pen.addComponent(componentName, (1, 0, 0, almost2, 0, 0))
450        pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0))
451        expectedGlyph = pen.glyph()
452
453        assert expectedGlyph == compositeGlyph
454
455    def test_out_of_range_transform_decomposed(self):
456        componentName = "a"
457        glyphSet = {}
458        pen = TTGlyphPointPen(glyphSet)
459
460        pen.beginPath()
461        pen.addPoint((0, 0), "line")
462        pen.addPoint((0, 1), "line")
463        pen.addPoint((1, 0), "line")
464        pen.endPath()
465        glyphSet[componentName] = _TestGlyph(pen.glyph())
466
467        pen.addComponent(componentName, (3, 0, 0, 2, 0, 0))
468        pen.addComponent(componentName, (1, 0, 0, 1, -1, 2))
469        pen.addComponent(componentName, (2, 0, 0, -3, 0, 0))
470        compositeGlyph = pen.glyph()
471
472        pen.beginPath()
473        pen.addPoint((0, 0), "line")
474        pen.addPoint((0, 2), "line")
475        pen.addPoint((3, 0), "line")
476        pen.endPath()
477        pen.beginPath()
478        pen.addPoint((-1, 2), "line")
479        pen.addPoint((-1, 3), "line")
480        pen.addPoint((0, 2), "line")
481        pen.endPath()
482        pen.beginPath()
483        pen.addPoint((0, 0), "line")
484        pen.addPoint((0, -3), "line")
485        pen.addPoint((2, 0), "line")
486        pen.endPath()
487        expectedGlyph = pen.glyph()
488
489        assert expectedGlyph == compositeGlyph
490
491    def test_no_handle_overflowing_transform(self):
492        componentName = "a"
493        glyphSet = {}
494        pen = TTGlyphPointPen(glyphSet, handleOverflowingTransforms=False)
495
496        pen.beginPath()
497        pen.addPoint((0, 0), "line")
498        pen.addPoint((0, 1), "line")
499        pen.addPoint((1, 0), "line")
500        pen.endPath()
501        baseGlyph = pen.glyph()
502        glyphSet[componentName] = _TestGlyph(baseGlyph)
503
504        pen.addComponent(componentName, (3, 0, 0, 1, 0, 0))
505        compositeGlyph = pen.glyph()
506
507        assert compositeGlyph.components[0].transform == ((3, 0), (0, 1))
508
509        with pytest.raises(struct.error):
510            compositeGlyph.compile({"a": baseGlyph})
511
512    def assertGlyphBoundsEqual(self, glyph, bounds):
513        assert (glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax) == bounds
514
515    def test_round_float_coordinates_and_component_offsets(self):
516        glyphSet = {}
517        pen = TTGlyphPointPen(glyphSet)
518
519        pen.beginPath()
520        pen.addPoint((0, 0), "line")
521        pen.addPoint((0, 1), "line")
522        pen.addPoint((367.6, 0), "line")
523        pen.endPath()
524        simpleGlyph = pen.glyph()
525
526        simpleGlyph.recalcBounds(glyphSet)
527        self.assertGlyphBoundsEqual(simpleGlyph, (0, 0, 368, 1))
528
529        componentName = "a"
530        glyphSet[componentName] = simpleGlyph
531
532        pen.addComponent(componentName, (1, 0, 0, 1, -86.4, 0))
533        compositeGlyph = pen.glyph()
534
535        compositeGlyph.recalcBounds(glyphSet)
536        self.assertGlyphBoundsEqual(compositeGlyph, (-86, 0, 282, 1))
537
538    def test_scaled_component_bounds(self):
539        glyphSet = {}
540
541        pen = TTGlyphPointPen(glyphSet)
542        pen.beginPath()
543        pen.addPoint((-231, 939), "line")
544        pen.addPoint((-55, 939), "line")
545        pen.addPoint((-55, 745), "line")
546        pen.addPoint((-231, 745), "line")
547        pen.endPath()
548        glyphSet["gravecomb"] = pen.glyph()
549
550        pen = TTGlyphPointPen(glyphSet)
551        pen.beginPath()
552        pen.addPoint((-278, 939), "line")
553        pen.addPoint((8, 939), "line")
554        pen.addPoint((8, 745), "line")
555        pen.addPoint((-278, 745), "line")
556        pen.endPath()
557        glyphSet["circumflexcomb"] = pen.glyph()
558
559        pen = TTGlyphPointPen(glyphSet)
560        pen.addComponent("circumflexcomb", (1, 0, 0, 1, 0, 0))
561        pen.addComponent("gravecomb", (0.9, 0, 0, 0.9, 198, 180))
562        glyphSet["uni0302_uni0300"] = uni0302_uni0300 = pen.glyph()
563
564        uni0302_uni0300.recalcBounds(glyphSet)
565        self.assertGlyphBoundsEqual(uni0302_uni0300, (-278, 745, 148, 1025))
566
567    def test_open_path_starting_with_move(self):
568        # when a contour starts with a 'move' point, it signifies the beginnig
569        # of an open contour.
570        # https://unifiedfontobject.org/versions/ufo3/glyphs/glif/#point-types
571        pen1 = TTGlyphPointPen(None)
572        pen1.beginPath()
573        pen1.addPoint((0, 0), "move")  # contour is open
574        pen1.addPoint((10, 10), "line")
575        pen1.addPoint((20, 20))
576        pen1.addPoint((20, 0), "qcurve")
577        pen1.endPath()
578
579        pen2 = TTGlyphPointPen(None)
580        pen2.beginPath()
581        pen2.addPoint((0, 0), "line")  # contour is closed
582        pen2.addPoint((10, 10), "line")
583        pen2.addPoint((20, 20))
584        pen2.addPoint((20, 0), "qcurve")
585        pen2.endPath()
586
587        # Since TrueType contours are always implicitly closed, the pen will
588        # interpret both these paths as equivalent
589        assert pen1.points == pen2.points == [(0, 0), (10, 10), (20, 20), (20, 0)]
590        assert pen1.types == pen2.types == [1, 1, 0, 1]
591
592    def test_skip_empty_contours(self):
593        pen = TTGlyphPointPen(None)
594        pen.beginPath()
595        pen.endPath()
596        pen.beginPath()
597        pen.endPath()
598        glyph = pen.glyph()
599        assert glyph.numberOfContours == 0
600
601
602class CubicGlyfTest:
603    def test_cubic_simple(self):
604        spen = TTGlyphPen(None)
605        spen.moveTo((0, 0))
606        spen.curveTo((0, 1), (1, 1), (1, 0))
607        spen.closePath()
608
609        ppen = TTGlyphPointPen(None)
610        ppen.beginPath()
611        ppen.addPoint((0, 0), "line")
612        ppen.addPoint((0, 1))
613        ppen.addPoint((1, 1))
614        ppen.addPoint((1, 0), "curve")
615        ppen.endPath()
616
617        for pen in (spen, ppen):
618            glyph = pen.glyph()
619
620            for i in range(2):
621                if i == 1:
622                    glyph.compile(None)
623
624                assert list(glyph.coordinates) == [(0, 0), (0, 1), (1, 1), (1, 0)]
625                assert list(glyph.flags) == [0x01, 0x80, 0x80, 0x01]
626
627                rpen = RecordingPen()
628                glyph.draw(rpen, None)
629                assert rpen.value == [
630                    ("moveTo", ((0, 0),)),
631                    (
632                        "curveTo",
633                        (
634                            (0, 1),
635                            (1, 1),
636                            (1, 0),
637                        ),
638                    ),
639                    ("closePath", ()),
640                ]
641
642    @pytest.mark.parametrize(
643        "dropImpliedOnCurves, segment_pen_commands, point_pen_commands, expected_coordinates, expected_flags, expected_endPts",
644        [
645            (  # Two curves that do NOT merge; request merging
646                True,
647                [
648                    ("moveTo", ((0, 0),)),
649                    ("curveTo", ((0, 1), (1, 2), (2, 2))),
650                    ("curveTo", ((3, 3), (4, 1), (4, 0))),
651                    ("closePath", ()),
652                ],
653                [
654                    ("beginPath", (), {}),
655                    ("addPoint", ((0, 0), "line", None, None), {}),
656                    ("addPoint", ((0, 1), None, None, None), {}),
657                    ("addPoint", ((1, 2), None, None, None), {}),
658                    ("addPoint", ((2, 2), "curve", None, None), {}),
659                    ("addPoint", ((3, 3), None, None, None), {}),
660                    ("addPoint", ((4, 1), None, None, None), {}),
661                    ("addPoint", ((4, 0), "curve", None, None), {}),
662                    ("endPath", (), {}),
663                ],
664                [(0, 0), (0, 1), (1, 2), (2, 2), (3, 3), (4, 1), (4, 0)],
665                [0x01, 0x80, 0x80, 0x01, 0x80, 0x80, 0x01],
666                [6],
667            ),
668            (  # Two curves that merge; request merging
669                True,
670                [
671                    ("moveTo", ((0, 0),)),
672                    ("curveTo", ((0, 1), (1, 2), (2, 2))),
673                    ("curveTo", ((3, 2), (4, 1), (4, 0))),
674                    ("closePath", ()),
675                ],
676                [
677                    ("beginPath", (), {}),
678                    ("addPoint", ((0, 0), "line", None, None), {}),
679                    ("addPoint", ((0, 1), None, None, None), {}),
680                    ("addPoint", ((1, 2), None, None, None), {}),
681                    ("addPoint", ((2, 2), "curve", None, None), {}),
682                    ("addPoint", ((3, 2), None, None, None), {}),
683                    ("addPoint", ((4, 1), None, None, None), {}),
684                    ("addPoint", ((4, 0), "curve", None, None), {}),
685                    ("endPath", (), {}),
686                ],
687                [(0, 0), (0, 1), (1, 2), (3, 2), (4, 1), (4, 0)],
688                [0x01, 0x80, 0x80, 0x80, 0x80, 0x01],
689                [5],
690            ),
691            (  # Two curves that merge; request NOT merging
692                False,
693                [
694                    ("moveTo", ((0, 0),)),
695                    ("curveTo", ((0, 1), (1, 2), (2, 2))),
696                    ("curveTo", ((3, 2), (4, 1), (4, 0))),
697                    ("closePath", ()),
698                ],
699                [
700                    ("beginPath", (), {}),
701                    ("addPoint", ((0, 0), "line", None, None), {}),
702                    ("addPoint", ((0, 1), None, None, None), {}),
703                    ("addPoint", ((1, 2), None, None, None), {}),
704                    ("addPoint", ((2, 2), "curve", None, None), {}),
705                    ("addPoint", ((3, 2), None, None, None), {}),
706                    ("addPoint", ((4, 1), None, None, None), {}),
707                    ("addPoint", ((4, 0), "curve", None, None), {}),
708                    ("endPath", (), {}),
709                ],
710                [(0, 0), (0, 1), (1, 2), (2, 2), (3, 2), (4, 1), (4, 0)],
711                [0x01, 0x80, 0x80, 0x01, 0x80, 0x80, 0x01],
712                [6],
713            ),
714            (  # Two (duplicate) contours
715                True,
716                [
717                    ("moveTo", ((0, 0),)),
718                    ("curveTo", ((0, 1), (1, 2), (2, 2))),
719                    ("curveTo", ((3, 2), (4, 1), (4, 0))),
720                    ("closePath", ()),
721                    ("moveTo", ((0, 0),)),
722                    ("curveTo", ((0, 1), (1, 2), (2, 2))),
723                    ("curveTo", ((3, 2), (4, 1), (4, 0))),
724                    ("closePath", ()),
725                ],
726                [
727                    ("beginPath", (), {}),
728                    ("addPoint", ((0, 0), "line", None, None), {}),
729                    ("addPoint", ((0, 1), None, None, None), {}),
730                    ("addPoint", ((1, 2), None, None, None), {}),
731                    ("addPoint", ((2, 2), "curve", None, None), {}),
732                    ("addPoint", ((3, 2), None, None, None), {}),
733                    ("addPoint", ((4, 1), None, None, None), {}),
734                    ("addPoint", ((4, 0), "curve", None, None), {}),
735                    ("endPath", (), {}),
736                    ("beginPath", (), {}),
737                    ("addPoint", ((0, 0), "line", None, None), {}),
738                    ("addPoint", ((0, 1), None, None, None), {}),
739                    ("addPoint", ((1, 2), None, None, None), {}),
740                    ("addPoint", ((2, 2), "curve", None, None), {}),
741                    ("addPoint", ((3, 2), None, None, None), {}),
742                    ("addPoint", ((4, 1), None, None, None), {}),
743                    ("addPoint", ((4, 0), "curve", None, None), {}),
744                    ("endPath", (), {}),
745                ],
746                [
747                    (0, 0),
748                    (0, 1),
749                    (1, 2),
750                    (3, 2),
751                    (4, 1),
752                    (4, 0),
753                    (0, 0),
754                    (0, 1),
755                    (1, 2),
756                    (3, 2),
757                    (4, 1),
758                    (4, 0),
759                ],
760                [
761                    0x01,
762                    0x80,
763                    0x80,
764                    0x80,
765                    0x80,
766                    0x01,
767                    0x01,
768                    0x80,
769                    0x80,
770                    0x80,
771                    0x80,
772                    0x01,
773                ],
774                [5, 11],
775            ),
776        ],
777    )
778    def test_cubic_topology(
779        self,
780        dropImpliedOnCurves,
781        segment_pen_commands,
782        point_pen_commands,
783        expected_coordinates,
784        expected_flags,
785        expected_endPts,
786    ):
787        spen = TTGlyphPen(None)
788        rpen = RecordingPen()
789        rpen.value = segment_pen_commands
790        rpen.replay(spen)
791
792        ppen = TTGlyphPointPen(None)
793        rpen = RecordingPointPen()
794        rpen.value = point_pen_commands
795        rpen.replay(ppen)
796
797        for pen in (spen, ppen):
798            glyph = pen.glyph(dropImpliedOnCurves=dropImpliedOnCurves)
799
800            assert list(glyph.coordinates) == expected_coordinates
801            assert list(glyph.flags) == expected_flags
802            assert list(glyph.endPtsOfContours) == expected_endPts
803
804            rpen = RecordingPen()
805            glyph.draw(rpen, None)
806            assert rpen.value == segment_pen_commands
807
808
809class _TestGlyph(object):
810    def __init__(self, glyph):
811        self.coordinates = glyph.coordinates
812
813    def draw(self, pen):
814        pen.moveTo(self.coordinates[0])
815        for point in self.coordinates[1:]:
816            pen.lineTo(point)
817        pen.closePath()
818
819    def drawPoints(self, pen):
820        pen.beginPath()
821        for point in self.coordinates:
822            pen.addPoint(point, "line")
823        pen.endPath()
824