Repository: googlefonts/ufo2ft
Pull request #770 (2023-07-28 18:13:05)

allow to reverseDirection without convertCubics
previously reverseDirection option was handled exclusively within cu2qu, but since convertCubics=False allows one to skip cu2qu altogether, it'd become impossible to do the reversing independently from the cu2qu conversion. So I added a separate filter that does the reversing (using the fontTools ReverseContourPointPen) used only when convertCubics=False and reverseDirection=True.


Patch:
diff --git a/Lib/ufo2ft/filters/__init__.py b/Lib/ufo2ft/filters/__init__.py
index ce4e405..841c3cc 100644
--- a/Lib/ufo2ft/filters/__init__.py
+++ b/Lib/ufo2ft/filters/__init__.py
@@ -14,6 +14,7 @@ from .explodeColorLayerGlyphs import ExplodeColorLayerGlyphsFilter
 from .flattenComponents import FlattenComponentsFilter
 from .propagateAnchors import PropagateAnchorsFilter
 from .removeOverlaps import RemoveOverlapsFilter
+from .reverseContourDirection import ReverseContourDirectionFilter
 from .sortContours import SortContoursFilter
 from .transformations import TransformationsFilter
 
@@ -27,6 +28,7 @@ __all__ = [
     "FlattenComponentsFilter",
     "PropagateAnchorsFilter",
     "RemoveOverlapsFilter",
+    "ReverseContourDirectionFilter",
     "SortContoursFilter",
     "TransformationsFilter",
     "loadFilters",
diff --git a/Lib/ufo2ft/filters/reverseContourDirection.py b/Lib/ufo2ft/filters/reverseContourDirection.py
new file mode 100644
index 0000000..3d5d47d
--- /dev/null
+++ b/Lib/ufo2ft/filters/reverseContourDirection.py
@@ -0,0 +1,15 @@
+from fontTools.pens.pointPen import ReverseContourPointPen
+
+from ufo2ft.filters import BaseFilter
+
+
+class ReverseContourDirectionFilter(BaseFilter):
+    def filter(self, glyph):
+        if not len(glyph):
+            return False
+        pen = ReverseContourPointPen(glyph.getPointPen())
+        contours = list(glyph)
+        glyph.clearContours()
+        for contour in contours:
+            contour.drawPoints(pen)
+        return True
diff --git a/Lib/ufo2ft/preProcessor.py b/Lib/ufo2ft/preProcessor.py
index 532e77b..84a7fe9 100644
--- a/Lib/ufo2ft/preProcessor.py
+++ b/Lib/ufo2ft/preProcessor.py
@@ -231,6 +231,12 @@ class TTFPreProcessor(OTFPreProcessor):
                     allQuadratic=allQuadratic,
                 )
             )
+        elif reverseDirection:
+            from ufo2ft.filters.reverseContourDirection import (
+                ReverseContourDirectionFilter,
+            )
+
+            filters.append(ReverseContourDirectionFilter(include=lambda g: len(g)))
         return filters
 
 
@@ -345,6 +351,14 @@ class TTFInterpolatablePreProcessor:
                 remember_curve_type=self._rememberCurveType and self.inplace,
                 all_quadratic=self.allQuadratic,
             )
+        elif self._reverseDirection:
+            from ufo2ft.filters.reverseContourDirection import (
+                ReverseContourDirectionFilter,
+            )
+
+            reverseDirection = ReverseContourDirectionFilter(include=lambda g: len(g))
+            for ufo, glyphSet in zip(self.ufos, self.glyphSets):
+                reverseDirection(ufo, glyphSet)
 
         # TrueType fonts cannot mix contours and components, so pick out all glyphs
         # that have contours (`bool(len(g)) == True`) and decompose their


Test patch:
diff --git a/tests/preProcessor_test.py b/tests/preProcessor_test.py
index fb59984..eec9ec5 100644
--- a/tests/preProcessor_test.py
+++ b/tests/preProcessor_test.py
@@ -131,6 +131,20 @@ class TTFPreProcessorTest:
         a = glyphSet["a"]
         assert (a[0][0].x, a[0][0].y) == (ufo["a"][0][0].x + 10, ufo["a"][0][0].y - 10)
 
+    def test_no_convertCubics_reverseDirection(self, FontClass):
+        ufo = FontClass(getpath("TestFont.ufo"))
+
+        glyphSet = TTFPreProcessor(
+            ufo, convertCubics=False, reverseDirection=True
+        ).process()
+
+        contours = [contour for contour in glyphSet["c"]]
+        points = [point for point in contours[0]]
+        assert points[0].segmentType == "line"
+        assert points[1].segmentType is None
+        assert points[2].segmentType is None
+        assert points[3].segmentType == "curve"
+
 
 class TTFInterpolatablePreProcessorTest:
     def test_no_inplace(self, FontClass):
@@ -248,6 +262,22 @@ class TTFInterpolatablePreProcessorTest:
             ufo2["a"][0][0].y - 10,
         )
 
+    def test_no_convertCubics_reverseDirection(self, FontClass):
+        ufo1 = FontClass(getpath("TestFont.ufo"))
+        ufo2 = FontClass(getpath("TestFont.ufo"))
+
+        glyphSets = TTFInterpolatablePreProcessor(
+            [ufo1, ufo2], convertCubics=False, reverseDirection=True
+        ).process()
+
+        for glyphSet in glyphSets:
+            contours = [contour for contour in glyphSet["c"]]
+            points = [point for point in contours[0]]
+            assert points[0].segmentType == "line"
+            assert points[1].segmentType is None
+            assert points[2].segmentType is None
+            assert points[3].segmentType == "curve"
+
 
 class SkipExportGlyphsTest:
     def test_skip_export_glyphs_filter(self, FontClass):