Repository: googlefonts/ufo2ft
Pull request #399 (2020-08-15 14:52:18)

Decompose scaled or rotated components
This adds a filter to decomposed rotated or scaled components. Some TrueType implementations don’t deal well with those.

madig: > Some TrueType implementations don’t deal well with those.

Which ones? :O
anthrotype: Shouldn't this filter class live in its own separate module?

I think that's what is expectated for the getFilterClass function used by loadFilters to load the filters from the lib.plist:

https://github.com/googlefonts/ufo2ft/blob/e1c08f6efb07a2fb5202d65a90af94dbd599e7e8/Lib/ufo2ft/filters/__init__.py#L14-L26
anthrotype: I believe Denis is referring to those transformations (like flipping horizontally/vertically) that reverse the component's contour direction, and thus may create white gaps if such transformed components overlap with other components. Am I right?

There's code in ufo2ft.util.deepCopyContours that checks for those transforms (a negative determinant is the trigger):

https://github.com/googlefonts/ufo2ft/blob/e1c08f6efb07a2fb5202d65a90af94dbd599e7e8/Lib/ufo2ft/util.py#L192-L197

Perhaps the issue is that by default we only decomposing glyphs that are "mixed" with both contour and components; whereas Denis would like to trigger this for such problematic composite glyphs as well.

There's also a related fontmake issue: https://github.com/googlefonts/fontmake/issues/253#issuecomment-335535293
anthrotype: I don't think there's anything wrong with merely having a component scaled or rotated, I've never come across an implementation that doesn't render such components properly. It's when they overlap other components and have transforms that flip their direction that you get rendering issues (as exemplified in the linked fontmake issue)
moyogo: > >  Some TrueType implementations don’t deal well with those.
>
> Which ones? :O

Any implementation of the TrueType implementation that doesn’t understand the component flags SCALED_COMPONENT_OFFSET or UNSCALED_COMPONENT_OFFSET for example, or any implementation that expects the component flag WE_HAVE_A_SCALE to apply scaling.

We probably should set more flags from https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#composite-glyph-description.
But even then, there’s still the issue of implementations that are not aware of the SCALED_COMPONENT_OFFSET or UNSCALED_COMPONENT_OFFSET flags, for example if they follow only https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6glyf.html which doesn’t mention those flags.
moyogo: > Shouldn't this filter class live in its own separate module?

I moved the filter to its own module.

http://pfaedit.sourceforge.net/Composites/ (pfaedit is the super old name of Fontforge) also documents the issue about how Apple’s and Microsoft’s understanding of scaled components differs.
khaledhosny: I have been using scaled and rotated components for almost 10 years, if there is a problem I would have known by now.

The only issue I had with components was Mac OS X not liking nested components (they get displaced), so for a long time I had code to de-nest them, but I dropped this recently because I can no longer reproduce it, no complaints so far.
khaledhosny: (nested and transformed components, that is it)
moyogo: There are several printers that have issues with nested components. See for example https://github.com/JulietaUla/Montserrat/issues/108 which I suspect is the issue, since Udieresis has a nested component.
Dalton Maag had clients that had the issue a couple of times ; in fact one of the printers from this decade at their office even has the nested component issue (or at least when I was there).
The flattenComponentsFilter can be used to flatten nested components to basic components.

For scaled components, this merge request is for a module that is optional, like the flattenComponentsFilter one is. I have no intention in making fontmake/ufo2ft using it be default.
There are a few workflows where it can be useful:
- VF with components scaled by different 2x2 transforms in different masters, see https://github.com/googlefonts/fontmake/issues/595#issuecomment-554420850
- When working with VTT https://github.com/TypeNetwork/opensans-1/issues/36#issuecomment-645267424 (which is probably the same issue as the first)
- When working with ttfautohint https://github.com/googlefonts/fontbakery/issues/2011

anthrotype: Yes, makes sense to have such an optional filter available.
Can we name it "Decompose Transformed Components" instead of just "Scaled"? 

Note that Glyphs.app has a custom parameter "Keep Transformed Components" which off by default; so by default Glyphs.app will decompose "distorted and mirrored components", unless that parameter is set. See https://glyphsapp.com/blog/new-features-in-glyphs-2-5



Patch:
diff --git a/Lib/ufo2ft/filters/decomposeTransformedComponents.py b/Lib/ufo2ft/filters/decomposeTransformedComponents.py
new file mode 100644
index 0000000..95d220d
--- /dev/null
+++ b/Lib/ufo2ft/filters/decomposeTransformedComponents.py
@@ -0,0 +1,27 @@
+from fontTools.misc.transform import Transform, Identity
+
+import ufo2ft.util
+from ufo2ft.filters import BaseFilter
+
+
+class DecomposeTransformedComponentsFilter(BaseFilter):
+    def filter(self, glyph):
+        if not glyph.components:
+            return False
+        transformedComponents = []
+        for component in glyph.components:
+            if component.transformation[:4] != Identity[:4]:
+                transformedComponents.append(component)
+        if not transformedComponents:
+            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)
+        return True


Test patch:
diff --git a/tests/filters/decomposeTransformedComponents_test.py b/tests/filters/decomposeTransformedComponents_test.py
new file mode 100644
index 0000000..ca88f38
--- /dev/null
+++ b/tests/filters/decomposeTransformedComponents_test.py
@@ -0,0 +1,58 @@
+from ufo2ft.filters.decomposeTransformedComponents import DecomposeTransformedComponentsFilter
+from ufo2ft.util import logger
+import logging
+
+
+class DecomposeTransformedComponentsFilterTest(object):
+    def test_transformed_components(self, FontClass):
+        ufo = FontClass()
+        a = ufo.newGlyph("six.lf")
+        a.width = 300
+        pen = a.getPen()
+        pen.moveTo((0, 0))
+        pen.lineTo((300, 0))
+        pen.lineTo((150, 300))
+        pen.closePath()
+
+        # six has one component
+        c = ufo.newGlyph("six")
+        c.width = 300
+        pen = c.getPen()
+        pen.addComponent("six.lf", (1, 0, 0, 1, 0, 0))
+
+        # nine.lf has one transformed component of a componenent
+        b = ufo.newGlyph("nine.lf")
+        b.width = 300
+        pen = b.getPen()
+        pen.addComponent("six.lf", (-1, 0, 0, -1, 0, 0))
+
+        # nine has one transformed component
+        c = ufo.newGlyph("nine")
+        c.width = 300
+        pen = c.getPen()
+        pen.addComponent("six", (-1, 0, 0, -1, 0, 0))
+
+        # nine.of has one component of a transformed component
+        d = ufo.newGlyph("nine.of")
+        d.width = 300
+        pen = d.getPen()
+        pen.addComponent("nine", (1, 0, 0, 1, 0, -80))
+
+        filter_ = DecomposeTransformedComponentsFilter()
+
+        assert filter_(ufo)
+        # six.lf has one outline and no component
+        assert len(ufo["six.lf"]) == 1
+        assert not ufo["six.lf"].components
+        # six has no outline and one component
+        assert len(ufo["six"]) == 0
+        assert len(ufo["six"].components) == 1
+        # nine.lf has one outline and no component, it was decomposed
+        assert len(ufo["nine.lf"]) == 1
+        assert not ufo["nine.lf"].components
+        # nine has one outline and no component, it was decomposed
+        assert len(ufo["nine"]) == 1
+        assert not ufo["nine"].components
+        # 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