Repository: googlefonts/ufo2ft

Issue:
decomposeTransformedComponents does not account for variable fonts
I have been getting errors on my build project where it is being reported that some glyphs in the font have a different number of segments. In reviewing the glyphs, I realized that what is happening is that in the lighter weights, there are components that have been transformed, but in the heaviest weight, they are not transformed. So the decomposeTransformedComponents filter is decomposing them in the lighter weights, but not the heaviest weight. 

In a situation where compileVariableTTF is being run, it would make sense for decomposeTransformedComponents to check the components across all UFOs and decompose all of them if one is transformed in order to maintain compatibility.  

Discussion:
anthrotype: makes sense that DecomposeTransformedComponentsFilter decomposes a glyph fully when any of the components has transforms, not only that single component. Not sure why it was done that way to begin with.
Please add a test, otherwise LGTM - thanks!

Patch:
diff --git a/Lib/ufo2ft/filters/decomposeTransformedComponents.py b/Lib/ufo2ft/filters/decomposeTransformedComponents.py
index ed03e12..0c39769 100644
--- a/Lib/ufo2ft/filters/decomposeTransformedComponents.py
+++ b/Lib/ufo2ft/filters/decomposeTransformedComponents.py
@@ -8,20 +8,13 @@ class DecomposeTransformedComponentsFilter(BaseFilter):
     def filter(self, glyph):
         if not glyph.components:
             return False
-        transformedComponents = []
+        needs_decomposition = False
         for component in glyph.components:
             if component.transformation[:4] != Identity[:4]:
-                transformedComponents.append(component)
-        if not transformedComponents:
+                needs_decomposition = True
+                break
+        if not needs_decomposition:
             return False
-        specificComponents = [c.baseGlyph for c in transformedComponents]
-        ufo2ft.util.deepCopyContours(
-            self.context.glyphSet,
-            glyph,
-            glyph,
-            Transform(),
-            specificComponents=specificComponents,
-        )
-        for component in transformedComponents:
-            glyph.removeComponent(component)
+        ufo2ft.util.deepCopyContours(self.context.glyphSet, glyph, glyph, Transform())
+        glyph.clearComponents()
         return True
diff --git a/Lib/ufo2ft/preProcessor.py b/Lib/ufo2ft/preProcessor.py
index c0d476e..6906a8d 100644
--- a/Lib/ufo2ft/preProcessor.py
+++ b/Lib/ufo2ft/preProcessor.py
@@ -7,6 +7,9 @@ from ufo2ft.constants import (
 )
 from ufo2ft.filters import isValidFilter, loadFilters
 from ufo2ft.filters.decomposeComponents import DecomposeComponentsFilter
+from ufo2ft.filters.decomposeTransformedComponents import (
+    DecomposeTransformedComponentsFilter,
+)
 from ufo2ft.fontInfoData import getAttrWithFallback
 from ufo2ft.util import _GlyphSet
 
@@ -303,10 +306,20 @@ class TTFInterpolatablePreProcessor:
     def process(self):
         from cu2qu.ufo import fonts_to_quadratic
 
+        needs_decomposition = set()
+
         # first apply all custom pre-filters
         for funcs, ufo, glyphSet in zip(self.preFilters, self.ufos, self.glyphSets):
             for func in funcs:
-                func(ufo, glyphSet)
+                if isinstance(func, DecomposeTransformedComponentsFilter):
+                    needs_decomposition |= func(ufo, glyphSet)
+                else:
+                    func(ufo, glyphSet)
+        # If we decomposed a glyph in some masters, we must ensure it is decomposed in
+        # all masters. (https://github.com/googlefonts/ufo2ft/issues/507)
+        decompose = DecomposeComponentsFilter(include=needs_decomposition)
+        for ufo, glyphSet in zip(self.ufos, self.glyphSets):
+            decompose(ufo, glyphSet)
 
         # then apply all default filters
         for funcs, ufo, glyphSet in zip(self.defaultFilters, self.ufos, self.glyphSets):


Test patch:
diff --git a/tests/filters/decomposeTransformedComponents_test.py b/tests/filters/decomposeTransformedComponents_test.py
index 9489a6a..0cbf260 100644
--- a/tests/filters/decomposeTransformedComponents_test.py
+++ b/tests/filters/decomposeTransformedComponents_test.py
@@ -1,6 +1,7 @@
 from ufo2ft.filters.decomposeTransformedComponents import (
     DecomposeTransformedComponentsFilter,
 )
+from ufo2ft.preProcessor import TTFInterpolatablePreProcessor
 
 
 class DecomposeTransformedComponentsFilterTest:
@@ -56,3 +57,39 @@ class DecomposeTransformedComponentsFilterTest:
         # nine.of has no outline and one component, it was not decomposed
         assert len(ufo["nine.of"]) == 0
         assert len(ufo["nine.of"].components) == 1
+
+    def test_decompose_compatibly(self, FontClass):
+        ufo1 = FontClass()
+        c = ufo1.newGlyph("comp")
+        c.width = 300
+        pen = c.getPen()
+        pen.moveTo((0, 0))
+        pen.lineTo((300, 0))
+        pen.lineTo((150, 300))
+        pen.closePath()
+
+        b = ufo1.newGlyph("base")
+        b.width = 300
+        pen = b.getPen()
+        pen.addComponent("comp", (0.5, 0, 0, 0.5, 0, 0))
+
+        ufo2 = FontClass()
+        c = ufo2.newGlyph("comp")
+        c.width = 600
+        pen = c.getPen()
+        pen.moveTo((0, 0))
+        pen.lineTo((600, 0))
+        pen.lineTo((300, 600))
+        pen.closePath()
+
+        b = ufo2.newGlyph("base")
+        b.width = 600
+        pen = b.getPen()
+        pen.addComponent("comp", (1, 0, 0, 1, 0, 0))
+
+        # Because ufo1.base needs decomposing, so should ufo2.base
+        glyphsets = TTFInterpolatablePreProcessor(
+            [ufo1, ufo2], filters=[DecomposeTransformedComponentsFilter(pre=True)]
+        ).process()
+        assert len(glyphsets[0]["base"]) == 1
+        assert len(glyphsets[1]["base"]) == 1