Repository: googlefonts/ufo2ft
Pull request #369 (2020-03-17 18:38:44)

run ExplodeColorLayerGlyphsFilter by default
... whenever `colorPalettes` and `colorLayerMapping` lib keys are present.

Before this, to activate it one had to list "Explode Color Layer Glyphs" among the custom list of `filters`.

@justvanrossum PTAL, thanks


Patch:
diff --git a/Lib/ufo2ft/filters/explodeColorLayerGlyphs.py b/Lib/ufo2ft/filters/explodeColorLayerGlyphs.py
index be7dcb8..18321f9 100644
--- a/Lib/ufo2ft/filters/explodeColorLayerGlyphs.py
+++ b/Lib/ufo2ft/filters/explodeColorLayerGlyphs.py
@@ -15,9 +15,14 @@ class ExplodeColorLayerGlyphsFilter(BaseFilter):
         context.globalColorLayerMapping = font.lib.get(COLOR_LAYER_MAPPING_KEY)
         context.layerGlyphSets = {}
         context.colorLayerGlyphNames = set()  # glyph names that we added
-        font.lib[COLOR_LAYERS_KEY] = {}
+        if COLOR_LAYERS_KEY not in font.lib:
+            font.lib[COLOR_LAYERS_KEY] = {}
+        else:
+            # if the font already contains an explicit COLOR_LAYERS_KEY, we
+            # assume the color layers have already been 'exploded' once.
+            context.skipCurrentFont = True
         return context
-        
+
     def _getLayer(self, font, layerName):
         layer = self.context.layerGlyphSets.get(layerName)
         if layer is None:
@@ -33,19 +38,24 @@ class ExplodeColorLayerGlyphsFilter(BaseFilter):
                 # We've added this glyph already, so we're done
                 return layerGlyphName
             from ufo2ft.errors import InvalidFontData
+
             raise InvalidFontData(
                 f"a glyph named {layerGlyphName} already exists, "
                 "conflicting with a requested color layer glyph."
             )
         for component in layerGlyph.components:
             baseLayerGlyphName = self._copyGlyph(
-                layerGlyphSet, glyphSet, component.baseGlyph, layerName)
+                layerGlyphSet, glyphSet, component.baseGlyph, layerName
+            )
             component.baseGlyph = baseLayerGlyphName
         glyphSet[layerGlyphName] = layerGlyph
         self.context.colorLayerGlyphNames.add(layerGlyphName)
         return layerGlyphName
 
     def filter(self, glyph):
+        if getattr(self.context, "skipCurrentFont", False):
+            return False
+
         font = self.context.font
         glyphSet = self.context.glyphSet
         colorLayers = font.lib[COLOR_LAYERS_KEY]
@@ -54,13 +64,15 @@ class ExplodeColorLayerGlyphsFilter(BaseFilter):
             colorLayerMapping = self.context.globalColorLayerMapping
         if colorLayerMapping is None:
             # No color layer info for this glyph
-            return
+            return False
         layers = []
         for layerName, colorID in colorLayerMapping:
             layerGlyphSet = self._getLayer(font, layerName)
             if glyph.name in layerGlyphSet:
                 layerGlyphName = self._copyGlyph(
-                    layerGlyphSet, glyphSet, glyph.name, layerName)
+                    layerGlyphSet, glyphSet, glyph.name, layerName
+                )
                 layers.append((layerGlyphName, colorID))
         if layers:
             colorLayers[glyph.name] = layers
+        return True
diff --git a/Lib/ufo2ft/outlineCompiler.py b/Lib/ufo2ft/outlineCompiler.py
index 1b03f0c..c4a6020 100644
--- a/Lib/ufo2ft/outlineCompiler.py
+++ b/Lib/ufo2ft/outlineCompiler.py
@@ -893,7 +893,8 @@ class BaseOutlineCompiler(object):
         from fontTools.colorLib.builder import buildCOLR
 
         layerInfo = self.ufo.lib[COLOR_LAYERS_KEY]
-        self.otf["COLR"] = buildCOLR(layerInfo)
+        if layerInfo:
+            self.otf["COLR"] = buildCOLR(layerInfo)
 
     def setupTable_CPAL(self):
         """
diff --git a/Lib/ufo2ft/preProcessor.py b/Lib/ufo2ft/preProcessor.py
index 554b924..81f4878 100644
--- a/Lib/ufo2ft/preProcessor.py
+++ b/Lib/ufo2ft/preProcessor.py
@@ -1,6 +1,11 @@
 from __future__ import print_function, division, absolute_import, unicode_literals
 
 from fontTools.misc.py23 import basestring
+from ufo2ft.constants import (
+    COLOR_LAYERS_KEY,
+    COLOR_LAYER_MAPPING_KEY,
+    COLOR_PALETTES_KEY,
+)
 from ufo2ft.fontInfoData import getAttrWithFallback
 from ufo2ft.filters import loadFilters
 from ufo2ft.filters.decomposeComponents import DecomposeComponentsFilter
@@ -51,6 +56,25 @@ class BasePreProcessor(object):
         return glyphSet
 
 
+def _init_explode_color_layer_glyphs_filter(ufo, filters):
+    # Initialize ExplodeColorLayerGlyphsFilter, which copies color glyph layers
+    # as standalone glyphs to the default glyph set (for building COLR table), if the
+    # UFO contains the required 'colorPalettes' key, as well as 'colorLayerMapping' lib
+    # keys (in either the font's or glyph's lib).
+    # Skip doing that if an explicit 'colorLayers' key is already present.
+    if (
+        COLOR_PALETTES_KEY in ufo.lib
+        and COLOR_LAYERS_KEY not in ufo.lib
+        and (
+            COLOR_LAYER_MAPPING_KEY in ufo.lib
+            or any(COLOR_LAYER_MAPPING_KEY in g.lib for g in ufo)
+        )
+    ):
+        from ufo2ft.filters.explodeColorLayerGlyphs import ExplodeColorLayerGlyphsFilter
+
+        filters.append(ExplodeColorLayerGlyphsFilter())
+
+
 class OTFPreProcessor(BasePreProcessor):
     """Preprocessor for building CFF-flavored OpenType fonts.
 
@@ -65,7 +89,12 @@ class OTFPreProcessor(BasePreProcessor):
     """
 
     def initDefaultFilters(self, removeOverlaps=False, overlapsBackend=None):
-        filters = [DecomposeComponentsFilter()]
+        filters = []
+
+        _init_explode_color_layer_glyphs_filter(self.ufo, filters)
+
+        filters.append(DecomposeComponentsFilter())
+
         if removeOverlaps:
             from ufo2ft.filters.removeOverlaps import RemoveOverlapsFilter
 
@@ -120,9 +149,13 @@ class TTFPreProcessor(OTFPreProcessor):
         reverseDirection=True,
         rememberCurveType=True,
     ):
+        filters = []
+
+        _init_explode_color_layer_glyphs_filter(self.ufo, filters)
+
         # len(g) is the number of contours, so we include the all glyphs
         # that have both components and at least one contour
-        filters = [DecomposeComponentsFilter(include=lambda g: len(g))]
+        filters.append(DecomposeComponentsFilter(include=lambda g: len(g)))
 
         if removeOverlaps:
             from ufo2ft.filters.removeOverlaps import RemoveOverlapsFilter


Test patch:
diff --git a/tests/data/ColorTest.ufo/lib.plist b/tests/data/ColorTest.ufo/lib.plist
index a43f8f0..f01f5b0 100644
--- a/tests/data/ColorTest.ufo/lib.plist
+++ b/tests/data/ColorTest.ufo/lib.plist
@@ -39,15 +39,6 @@
         </array>
       </array>
     </array>
-    <key>com.github.googlei18n.ufo2ft.filters</key>
-    <array>
-      <dict>
-        <key>name</key>
-        <string>Explode Color Layer Glyphs</string>
-        <key>pre</key>
-        <true/>
-      </dict>
-    </array>
     <key>public.glyphOrder</key>
     <array>
       <string>space</string>
diff --git a/tests/preProcessor_test.py b/tests/preProcessor_test.py
index a5a11fa..f8d0966 100644
--- a/tests/preProcessor_test.py
+++ b/tests/preProcessor_test.py
@@ -2,10 +2,21 @@ from __future__ import print_function, division, absolute_import, unicode_litera
 import os
 import logging
 import ufo2ft
-from ufo2ft.preProcessor import TTFPreProcessor, TTFInterpolatablePreProcessor
+from ufo2ft.preProcessor import (
+    TTFPreProcessor,
+    TTFInterpolatablePreProcessor,
+    _init_explode_color_layer_glyphs_filter,
+)
 from ufo2ft.filters import UFO2FT_FILTERS_KEY
+from ufo2ft.filters.explodeColorLayerGlyphs import ExplodeColorLayerGlyphsFilter
 from cu2qu.ufo import CURVE_TYPE_LIB_KEY
 from fontTools import designspaceLib
+from ufo2ft.constants import (
+    COLOR_LAYERS_KEY,
+    COLOR_LAYER_MAPPING_KEY,
+    COLOR_PALETTES_KEY,
+)
+import pytest
 
 
 def getpath(filename):
@@ -262,3 +273,41 @@ class SkipExportGlyphsTest(object):
         assert not hasattr(glyphs["c"], "components")
         assert glyphs["e"].numberOfContours == 13
         assert not hasattr(glyphs["e"], "components")
+
+
+@pytest.fixture
+def color_ufo(FontClass):
+    ufo = FontClass()
+    ufo.lib[COLOR_PALETTES_KEY] = [[(1, 0.3, 0.1, 1), (0, 0.4, 0.8, 1)]]
+    return ufo
+
+
+class InitExplodeColorLayerGlyphsFilterTest(object):
+    def test_no_color_palettes(self, FontClass):
+        ufo = FontClass()
+        filters = []
+        _init_explode_color_layer_glyphs_filter(ufo, filters)
+        assert not filters
+
+    def test_no_color_layer_mapping(self, color_ufo):
+        filters = []
+        _init_explode_color_layer_glyphs_filter(color_ufo, filters)
+        assert not filters
+
+    def test_explicit_color_layers(self, color_ufo):
+        color_ufo.lib[COLOR_LAYERS_KEY] = {"a": [("a.z_0", 1), ("a.z_1", 0)]}
+        filters = []
+        _init_explode_color_layer_glyphs_filter(color_ufo, filters)
+        assert not filters
+
+    def test_font_color_layer_mapping(self, color_ufo):
+        color_ufo.lib[COLOR_LAYER_MAPPING_KEY] = [("z_0", 1), ("z_1", 0)]
+        filters = []
+        _init_explode_color_layer_glyphs_filter(color_ufo, filters)
+        assert isinstance(filters[0], ExplodeColorLayerGlyphsFilter)
+
+    def test_glyph_color_layer_mapping(self, color_ufo):
+        color_ufo.newGlyph("a").lib[COLOR_LAYER_MAPPING_KEY] = [("z_0", 0), ("z_1", 1)]
+        filters = []
+        _init_explode_color_layer_glyphs_filter(color_ufo, filters)
+        assert isinstance(filters[0], ExplodeColorLayerGlyphsFilter)