Repository: fonttools/fonttools

Issue:
TTGlyphPointPen doesn't handle open contours (starting with 'move')
in the PointPen protocol used by UFO glyphs, open contours start with a point that has segmentType="move", see https://unifiedfontobject.org/versions/ufo3/glyphs/glif/#point-types

Open contours are valid in UFO, and the original TTGlyphPen treats them as closed for the sake of building TrueType glyphs (where all paths are implicilty closed and filled with black).

But if one tries to construct an open contour with the TTGlyphPointPen, one gets a NotImplementedError:

```python
>>> from fontTools.pens.ttGlyphPen import TTGlyphPointPen, TTGlyphPen
>>> pen = TTGlyphPointPen(None)
>>> pen.beginPath()
>>> pen.addPoint((0, 0), "move")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/clupo/Github/fonttools/Lib/fontTools/pens/ttGlyphPen.py", line 282, in addPoint
    raise NotImplementedError
NotImplementedError
```

Whereas this works fine using `TTGlyphPen`, where `endPath` (the method that in segment pens signals an open contour) is aliased to `closePath`, e.g.:

```python
pen = TTGlyphPen(None)
pen.moveTo((0, 0))
pen.lineTo((10, 10))
pen.lineTo((20, 0))
pen.endPath()  # or pen.closePath()
```

The current code in TTGlyphPointPen is incorrectly assuming that the only segment types for point pens are: None (for an off-curve point), "line", "qcurve" and "curve"; it's completely ignoring the explicit "move" case. Actually, an inline comment above the NotImplementedError signals that the author incorrectly assumed that when the segment type is neither None, "line" nor "qcurve" then it must be a (cubic) "curve" -- which is in fact not supported and should error with the NotImplementedError --, but "move" is a valid segment type and should not fall through the `else:` block.

https://github.com/fonttools/fonttools/blob/aae11e45c29f4d07770574ce8577e6e61bc2b15c/Lib/fontTools/pens/ttGlyphPen.py#L276-L282

"move" should be treated like "line" or "qcurve", i.e. like a on-curve point.
This way the TTGlyphPointPen will treat incoming open contours the same way as the TTGlyphPen, that is, as implicitly closed.







Discussion:

Resolving patch:
diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py
--- a/Lib/fontTools/pens/ttGlyphPen.py
+++ b/Lib/fontTools/pens/ttGlyphPen.py
@@ -275,10 +275,11 @@ def addPoint(
             raise PenError("Can't add a point to a closed contour.")
         if segmentType is None:
             self.types.append(0)  # offcurve
-        elif segmentType in ("qcurve", "line"):
+        elif segmentType in ("qcurve", "line", "move"):
             self.types.append(1)  # oncurve
+        elif segmentType == "curve":
+            raise NotImplementedError("cubic curves are not supported")
         else:
-            # cubic curves are not supported
-            raise NotImplementedError
+            raise AssertionError(segmentType)
 
         self.points.append(pt)


Test patch:
diff --git a/Tests/pens/ttGlyphPen_test.py b/Tests/pens/ttGlyphPen_test.py
--- a/Tests/pens/ttGlyphPen_test.py
+++ b/Tests/pens/ttGlyphPen_test.py
@@ -549,6 +549,32 @@ def test_scaled_component_bounds(self):
         uni0302_uni0300.recalcBounds(glyphSet)
         self.assertGlyphBoundsEqual(uni0302_uni0300, (-278, 745, 148, 1025))
 
+    def test_open_path_starting_with_move(self):
+        # when a contour starts with a 'move' point, it signifies the beginnig
+        # of an open contour.
+        # https://unifiedfontobject.org/versions/ufo3/glyphs/glif/#point-types
+        pen1 = TTGlyphPointPen(None)
+        pen1.beginPath()
+        pen1.addPoint((0, 0), "move")  # contour is open
+        pen1.addPoint((10, 10), "line")
+        pen1.addPoint((20, 20))
+        pen1.addPoint((20, 0), "qcurve")
+        pen1.endPath()
+
+        pen2 = TTGlyphPointPen(None)
+        pen2.beginPath()
+        pen2.addPoint((0, 0), "line")  # contour is closed
+        pen2.addPoint((10, 10), "line")
+        pen2.addPoint((20, 20))
+        pen2.addPoint((20, 0), "qcurve")
+        pen2.endPath()
+
+        # Since TrueType contours are always implicitly closed, the pen will
+        # interpret both these paths as equivalent
+        assert pen1.points == pen2.points == [(0, 0), (10, 10), (20, 20), (20, 0)]
+        assert pen1.types == pen2.types == [1, 1, 0, 1]
+
+
 
 class _TestGlyph(object):
     def __init__(self, glyph):