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