Repository: googlefonts/ufo2ft
Pull request #359 (2019-12-05 20:56:46)

Proof of concept: generate COLR/CPAL from UFO layers
This PR allows glyphs in UFO layers to be used to build COLR/CPAL fonts.

I have implemented this in two steps:
1. a preprocessing filter that copies glyphs from layers to alternate glyphs in the default layer (*)
2. COLR/CPAL methods on the `BaseOutlineCompiler` class

The first step takes glyphs from any UFO layer and adds those glyphs as alternates to the default UFO layer. Selected UFO layers map to color layers by means of a lib key/value. Let's call it

- `COLOR_LAYER_MAPPING_KEY` (a symbolic name, see the code for the actual key)

The value for this key is a list of `(layerName, colorID)` pairs. The key can be in `font.lib` *or* in `glyph.lib`  (on the default UFO layer). The `glyph.lib` value has precedence over the `font.lib` value. This allows for simple configuration if all glyphs are similar in color layer structure, but still allows each glyph to have its own unique color layer configuration if needed.

The first step in the process copies all color layer glyphs to the default layer, using the layer name as a glyph name extension, for example `a.color1`, for the `a` glyph from the layer called `color1`. Additionally, this step _adds_ a key/value pair to `font.lib`: `COLOR_LAYERS_KEY`. The value for this key is a dictionary that contains all the data needed to build the `COLR` table (see below).

(The first step can be omitted if the user prefers to use the low level part of this PR (the second step), by providing color layer glyphs in the default UFO layer and providing the `COLOR_LAYERS_KEY` `font.lib` value.)

The second step looks for two `font.lib` keys, the aformentioned `COLOR_LAYERS_KEY` and `COLOR_PALETTES_KEY`. The latter contains the color palettes.

These two keys match very closely the data that goes into the `COLR` table and the `CPAL` table respectively.

The `COLOR_LAYERS_KEY` lib value should be a dict:

```python
font.lib[COLOR_LAYERS_KEY] = {
    'a': [('a.color1', 0), ('a.color2', 1)],  # [(glyphName, colorID)]
}
```

The `COLOR_PALETTES_KEY` lib value should be a lis of lists:
```python
[
    # [(red, green, blue, alpha)] values between 0..1
    [(1, 0.3, 0.1, 1), (0, 0.4, 0.8, 1)], # palette 1
    [ ... ]  # palette 2
]
```

I tried to follow the design patterns in the ufo2ft codebase, but I'm not sure I did the right thing everywhere. To use a Filter works to add the color layer glyphs, but it feels a little hack-ish.

*) The preprocessing filter needs to be activated with a `font.lib` key/value:
`font.lib["com.github.googlei18n.ufo2ft.filters"] = [{"name": "Explode Color Layer Glyphs", "pre": True}]`

I have added the built fonts from the test here, so people can inspect them and play with them.
[ColorTestFonts.zip](https://github.com/googlefonts/ufo2ft/files/3929066/ColorTestFonts.zip)

The same source data could be used to build an SVG table, but that's for another time. A tool that takes a built COLR/CPAL font and adds the SVG table based on that should be feasible, if it doesn't already exist. The same goes for all the pixel color font formats, using a renderer such as cairo.

(Needless to say that all the UFO-modifying that is done is temporary, as per how ufo2ft works, and does not affect UFO data on-disk.)


Patch:
diff --git a/Lib/ufo2ft/constants.py b/Lib/ufo2ft/constants.py
index 51e2d9e..b1336c7 100644
--- a/Lib/ufo2ft/constants.py
+++ b/Lib/ufo2ft/constants.py
@@ -10,3 +10,7 @@ GLYPHS_PREFIX = "com.schriftgestaltung."
 
 USE_PRODUCTION_NAMES = UFO2FT_PREFIX + "useProductionNames"
 GLYPHS_DONT_USE_PRODUCTION_NAMES = GLYPHS_PREFIX + "Don't use Production Names"
+
+COLOR_LAYERS_KEY = "com.github.googlei18n.ufo2ft.colorLayers"
+COLOR_PALETTES_KEY = "com.github.googlei18n.ufo2ft.colorPalettes"
+COLOR_LAYER_MAPPING_KEY = "com.github.googlei18n.ufo2ft.colorLayerMapping"
diff --git a/Lib/ufo2ft/filters/explodeColorLayerGlyphs.py b/Lib/ufo2ft/filters/explodeColorLayerGlyphs.py
new file mode 100644
index 0000000..be7dcb8
--- /dev/null
+++ b/Lib/ufo2ft/filters/explodeColorLayerGlyphs.py
@@ -0,0 +1,66 @@
+from ufo2ft.filters import BaseFilter
+from ufo2ft.util import _GlyphSet
+from ufo2ft.constants import COLOR_LAYERS_KEY, COLOR_LAYER_MAPPING_KEY
+
+
+class ExplodeColorLayerGlyphsFilter(BaseFilter):
+
+    """ This filter doesn't really filter glyphs, but copies glyphs
+    from UFO layers to alternate glyphs in the default layer, for use
+    in the COLR table.
+    """
+
+    def set_context(self, font, glyphSet):
+        context = super().set_context(font, glyphSet)
+        context.globalColorLayerMapping = font.lib.get(COLOR_LAYER_MAPPING_KEY)
+        context.layerGlyphSets = {}
+        context.colorLayerGlyphNames = set()  # glyph names that we added
+        font.lib[COLOR_LAYERS_KEY] = {}
+        return context
+        
+    def _getLayer(self, font, layerName):
+        layer = self.context.layerGlyphSets.get(layerName)
+        if layer is None:
+            layer = _GlyphSet.from_layer(font, layerName)
+            self.context.layerGlyphSets[layerName] = layer
+        return layer
+
+    def _copyGlyph(self, layerGlyphSet, glyphSet, glyphName, layerName):
+        layerGlyph = layerGlyphSet[glyphName]
+        layerGlyphName = f"{glyphName}.{layerName}"
+        if layerGlyphName in glyphSet:
+            if layerGlyphName in self.context.colorLayerGlyphNames:
+                # 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)
+            component.baseGlyph = baseLayerGlyphName
+        glyphSet[layerGlyphName] = layerGlyph
+        self.context.colorLayerGlyphNames.add(layerGlyphName)
+        return layerGlyphName
+
+    def filter(self, glyph):
+        font = self.context.font
+        glyphSet = self.context.glyphSet
+        colorLayers = font.lib[COLOR_LAYERS_KEY]
+        colorLayerMapping = glyph.lib.get(COLOR_LAYER_MAPPING_KEY)
+        if colorLayerMapping is None:
+            colorLayerMapping = self.context.globalColorLayerMapping
+        if colorLayerMapping is None:
+            # No color layer info for this glyph
+            return
+        layers = []
+        for layerName, colorID in colorLayerMapping:
+            layerGlyphSet = self._getLayer(font, layerName)
+            if glyph.name in layerGlyphSet:
+                layerGlyphName = self._copyGlyph(
+                    layerGlyphSet, glyphSet, glyph.name, layerName)
+                layers.append((layerGlyphName, colorID))
+        if layers:
+            colorLayers[glyph.name] = layers
diff --git a/Lib/ufo2ft/outlineCompiler.py b/Lib/ufo2ft/outlineCompiler.py
index 90ba5be..26b1932 100644
--- a/Lib/ufo2ft/outlineCompiler.py
+++ b/Lib/ufo2ft/outlineCompiler.py
@@ -38,6 +38,7 @@ from ufo2ft.util import (
     makeUnicodeToGlyphNameMapping,
     calcCodePageRanges,
 )
+from ufo2ft.constants import COLOR_LAYERS_KEY, COLOR_PALETTES_KEY
 
 
 logger = logging.getLogger(__name__)
@@ -69,7 +70,8 @@ class BaseOutlineCompiler(object):
 
     sfntVersion = None
     tables = frozenset(
-        ["head", "hmtx", "hhea", "name", "maxp", "cmap", "OS/2", "post", "vmtx", "vhea"]
+        ["head", "hmtx", "hhea", "name", "maxp", "cmap", "OS/2", "post", "vmtx", "vhea",
+         "COLR", "CPAL"]
     )
 
     def __init__(self, font, glyphSet=None, glyphOrder=None, tables=None):
@@ -108,6 +110,8 @@ class BaseOutlineCompiler(object):
             getAttrWithFallback(self.ufo.info, metric) is not None
             for metric in vertical_metrics
         )
+        self.colorLayers = COLOR_LAYERS_KEY in self.ufo.lib and \
+                           COLOR_PALETTES_KEY in self.ufo.lib
 
         # write the glyph order
         self.otf.setGlyphOrder(self.glyphOrder)
@@ -124,6 +128,9 @@ class BaseOutlineCompiler(object):
         if self.vertical:
             self.setupTable_vmtx()
             self.setupTable_vhea()
+        if self.colorLayers:
+            self.setupTable_COLR()
+            self.setupTable_CPAL()
         self.setupOtherTables()
         self.importTTX()
 
@@ -873,6 +880,46 @@ class BaseOutlineCompiler(object):
         post.minMemType1 = 0
         post.maxMemType1 = 0
 
+    def setupTable_COLR(self):
+        """
+        Compile the COLR table.
+
+        **This should not be called externally.**
+        """
+        if "COLR" not in self.tables:
+            return
+
+        from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord
+        layerInfo = self.ufo.lib[COLOR_LAYERS_KEY]
+        colorLayerLists = {}
+        for baseGlyphName, layers in layerInfo.items():
+            colorLayerLists[baseGlyphName] = [
+                    LayerRecord(layerGlyphName, colorID)
+                        for layerGlyphName, colorID in layers]
+        self.otf["COLR"] = colr = newTable("COLR")
+        colr.version = 0
+        colr.ColorLayers = colorLayerLists
+
+    def setupTable_CPAL(self):
+        """
+        Compile the CPAL table.
+
+        **This should not be called externally.**
+        """
+        if "CPAL" not in self.tables:
+            return
+
+        from fontTools.ttLib.tables.C_P_A_L_ import Color
+        palettes = self.ufo.lib[COLOR_PALETTES_KEY]
+        if len({len(p) for p in palettes}) != 1:
+            from ufo2ft.errors import InvalidFontData
+            raise InvalidFontData("color palettes have different lengths")
+        self.otf["CPAL"] = cpal = newTable("CPAL")
+        cpal.version = 0
+        cpal.numPaletteEntries = len(palettes[0])
+        cpal.palettes = [[Color(*(round(v*255) for v in (blue, green, red, alpha)))
+                for red, green, blue, alpha in palette] for palette in palettes]
+
     def setupOtherTables(self):
         """
         Make the other tables. The default implementation does nothing.


Test patch:
diff --git a/tests/data/ColorTest.ufo/fontinfo.plist b/tests/data/ColorTest.ufo/fontinfo.plist
new file mode 100644
index 0000000..42b76d7
--- /dev/null
+++ b/tests/data/ColorTest.ufo/fontinfo.plist
@@ -0,0 +1,34 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>ascender</key>
+    <integer>750</integer>
+    <key>capHeight</key>
+    <integer>750</integer>
+    <key>descender</key>
+    <integer>-250</integer>
+    <key>familyName</key>
+    <string>ColorTest</string>
+    <key>guidelines</key>
+    <array/>
+    <key>postscriptBlueValues</key>
+    <array/>
+    <key>postscriptFamilyBlues</key>
+    <array/>
+    <key>postscriptFamilyOtherBlues</key>
+    <array/>
+    <key>postscriptOtherBlues</key>
+    <array/>
+    <key>postscriptStemSnapH</key>
+    <array/>
+    <key>postscriptStemSnapV</key>
+    <array/>
+    <key>styleName</key>
+    <string>Regular</string>
+    <key>unitsPerEm</key>
+    <integer>1000</integer>
+    <key>xHeight</key>
+    <integer>500</integer>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTest.ufo/glyphs.color1/a.glif b/tests/data/ColorTest.ufo/glyphs.color1/a.glif
new file mode 100644
index 0000000..ab6576d
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color1/a.glif
@@ -0,0 +1,21 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="a" format="2">
+  <advance width="500"/>
+  <unicode hex="0061"/>
+  <outline>
+    <contour>
+      <point x="260" y="82" type="curve" smooth="yes"/>
+      <point x="342" y="82"/>
+      <point x="408" y="189"/>
+      <point x="408" y="321" type="curve" smooth="yes"/>
+      <point x="408" y="452"/>
+      <point x="342" y="560"/>
+      <point x="260" y="560" type="curve" smooth="yes"/>
+      <point x="178" y="560"/>
+      <point x="111" y="452"/>
+      <point x="111" y="321" type="curve" smooth="yes"/>
+      <point x="111" y="189"/>
+      <point x="178" y="82"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTest.ufo/glyphs.color1/b.glif b/tests/data/ColorTest.ufo/glyphs.color1/b.glif
new file mode 100644
index 0000000..6360389
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color1/b.glif
@@ -0,0 +1,20 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="b" format="2">
+  <advance width="500"/>
+  <outline>
+    <contour>
+      <point x="260" y="82" type="curve" smooth="yes"/>
+      <point x="342" y="82"/>
+      <point x="408" y="189"/>
+      <point x="408" y="321" type="curve" smooth="yes"/>
+      <point x="408" y="452"/>
+      <point x="342" y="560"/>
+      <point x="260" y="560" type="curve" smooth="yes"/>
+      <point x="178" y="560"/>
+      <point x="111" y="452"/>
+      <point x="111" y="321" type="curve" smooth="yes"/>
+      <point x="111" y="189"/>
+      <point x="178" y="82"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTest.ufo/glyphs.color1/c.glif b/tests/data/ColorTest.ufo/glyphs.color1/c.glif
new file mode 100644
index 0000000..9c1d6f2
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color1/c.glif
@@ -0,0 +1,20 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="c" format="2">
+  <advance width="500"/>
+  <outline>
+    <contour>
+      <point x="260" y="82" type="curve" smooth="yes"/>
+      <point x="342" y="82"/>
+      <point x="408" y="189"/>
+      <point x="408" y="321" type="curve" smooth="yes"/>
+      <point x="408" y="452"/>
+      <point x="342" y="560"/>
+      <point x="260" y="560" type="curve" smooth="yes"/>
+      <point x="178" y="560"/>
+      <point x="111" y="452"/>
+      <point x="111" y="321" type="curve" smooth="yes"/>
+      <point x="111" y="189"/>
+      <point x="178" y="82"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTest.ufo/glyphs.color1/contents.plist b/tests/data/ColorTest.ufo/glyphs.color1/contents.plist
new file mode 100644
index 0000000..3a55d6a
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color1/contents.plist
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>a</key>
+    <string>a.glif</string>
+    <key>b</key>
+    <string>b.glif</string>
+    <key>c</key>
+    <string>c.glif</string>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTest.ufo/glyphs.color1/layerinfo.plist b/tests/data/ColorTest.ufo/glyphs.color1/layerinfo.plist
new file mode 100644
index 0000000..321e50a
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color1/layerinfo.plist
@@ -0,0 +1,8 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>color</key>
+    <string>0,1,0.25,0.7</string>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTest.ufo/glyphs.color2/a.glif b/tests/data/ColorTest.ufo/glyphs.color2/a.glif
new file mode 100644
index 0000000..4d93657
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color2/a.glif
@@ -0,0 +1,13 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="a" format="2">
+  <advance width="500"/>
+  <unicode hex="0061"/>
+  <outline>
+    <contour>
+      <point x="195" y="230" type="line"/>
+      <point x="485" y="230" type="line"/>
+      <point x="485" y="626" type="line"/>
+      <point x="195" y="626" type="line"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTest.ufo/glyphs.color2/b.glif b/tests/data/ColorTest.ufo/glyphs.color2/b.glif
new file mode 100644
index 0000000..1833b9e
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color2/b.glif
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="b" format="2">
+  <advance width="500"/>
+  <outline>
+    <component base="c"/>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTest.ufo/glyphs.color2/c.glif b/tests/data/ColorTest.ufo/glyphs.color2/c.glif
new file mode 100644
index 0000000..a078b8b
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color2/c.glif
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="c" format="2">
+  <advance width="500"/>
+  <outline>
+    <contour>
+      <point x="195" y="230" type="line"/>
+      <point x="485" y="230" type="line"/>
+      <point x="485" y="626" type="line"/>
+      <point x="195" y="626" type="line"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTest.ufo/glyphs.color2/contents.plist b/tests/data/ColorTest.ufo/glyphs.color2/contents.plist
new file mode 100644
index 0000000..3a55d6a
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color2/contents.plist
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>a</key>
+    <string>a.glif</string>
+    <key>b</key>
+    <string>b.glif</string>
+    <key>c</key>
+    <string>c.glif</string>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTest.ufo/glyphs.color2/layerinfo.plist b/tests/data/ColorTest.ufo/glyphs.color2/layerinfo.plist
new file mode 100644
index 0000000..f3d8fae
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs.color2/layerinfo.plist
@@ -0,0 +1,8 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>color</key>
+    <string>0,1,1,0.7</string>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTest.ufo/glyphs/a.glif b/tests/data/ColorTest.ufo/glyphs/a.glif
new file mode 100644
index 0000000..8553e0b
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs/a.glif
@@ -0,0 +1,13 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="a" format="2">
+  <advance width="500"/>
+  <unicode hex="0061"/>
+  <outline>
+    <contour>
+      <point x="36" y="77" type="line"/>
+      <point x="318" y="77" type="line"/>
+      <point x="318" y="458" type="line"/>
+      <point x="36" y="458" type="line"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTest.ufo/glyphs/b.glif b/tests/data/ColorTest.ufo/glyphs/b.glif
new file mode 100644
index 0000000..03f0451
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs/b.glif
@@ -0,0 +1,28 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="b" format="2">
+  <advance width="500"/>
+  <unicode hex="0062"/>
+  <outline>
+    <contour>
+      <point x="36" y="77" type="line"/>
+      <point x="318" y="77" type="line"/>
+      <point x="318" y="458" type="line"/>
+      <point x="36" y="458" type="line"/>
+    </contour>
+  </outline>
+  <lib>
+    <dict>
+      <key>com.github.googlei18n.ufo2ft.colorLayerMapping</key>
+      <array>
+        <array>
+          <string>color1</string>
+          <integer>1</integer>
+        </array>
+        <array>
+          <string>color2</string>
+          <integer>0</integer>
+        </array>
+      </array>
+    </dict>
+  </lib>
+</glyph>
diff --git a/tests/data/ColorTest.ufo/glyphs/c.glif b/tests/data/ColorTest.ufo/glyphs/c.glif
new file mode 100644
index 0000000..81a8775
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs/c.glif
@@ -0,0 +1,28 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="c" format="2">
+  <advance width="500"/>
+  <unicode hex="0063"/>
+  <outline>
+    <contour>
+      <point x="36" y="77" type="line"/>
+      <point x="318" y="77" type="line"/>
+      <point x="318" y="458" type="line"/>
+      <point x="36" y="458" type="line"/>
+    </contour>
+  </outline>
+  <lib>
+    <dict>
+      <key>com.github.googlei18n.ufo2ft.colorLayerMapping</key>
+      <array>
+        <array>
+          <string>color2</string>
+          <integer>1</integer>
+        </array>
+        <array>
+          <string>color1</string>
+          <integer>0</integer>
+        </array>
+      </array>
+    </dict>
+  </lib>
+</glyph>
diff --git a/tests/data/ColorTest.ufo/glyphs/contents.plist b/tests/data/ColorTest.ufo/glyphs/contents.plist
new file mode 100644
index 0000000..3a55d6a
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs/contents.plist
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>a</key>
+    <string>a.glif</string>
+    <key>b</key>
+    <string>b.glif</string>
+    <key>c</key>
+    <string>c.glif</string>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTest.ufo/glyphs/layerinfo.plist b/tests/data/ColorTest.ufo/glyphs/layerinfo.plist
new file mode 100644
index 0000000..3cf39b4
--- /dev/null
+++ b/tests/data/ColorTest.ufo/glyphs/layerinfo.plist
@@ -0,0 +1,8 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>color</key>
+    <string>1,0.75,0,0.7</string>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTest.ufo/layercontents.plist b/tests/data/ColorTest.ufo/layercontents.plist
new file mode 100644
index 0000000..42fa06b
--- /dev/null
+++ b/tests/data/ColorTest.ufo/layercontents.plist
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <array>
+    <array>
+      <string>foreground</string>
+      <string>glyphs</string>
+    </array>
+    <array>
+      <string>color1</string>
+      <string>glyphs.color1</string>
+    </array>
+    <array>
+      <string>color2</string>
+      <string>glyphs.color2</string>
+    </array>
+  </array>
+</plist>
diff --git a/tests/data/ColorTest.ufo/lib.plist b/tests/data/ColorTest.ufo/lib.plist
new file mode 100644
index 0000000..a43f8f0
--- /dev/null
+++ b/tests/data/ColorTest.ufo/lib.plist
@@ -0,0 +1,59 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>com.defcon.sortDescriptor</key>
+    <array>
+      <dict>
+        <key>ascending</key>
+        <string>Latin-1</string>
+        <key>type</key>
+        <string>characterSet</string>
+      </dict>
+    </array>
+    <key>com.github.googlei18n.ufo2ft.colorLayerMapping</key>
+    <array>
+      <array>
+        <string>color1</string>
+        <integer>0</integer>
+      </array>
+      <array>
+        <string>color2</string>
+        <integer>1</integer>
+      </array>
+    </array>
+    <key>com.github.googlei18n.ufo2ft.colorPalettes</key>
+    <array>
+      <array>
+        <array>
+          <integer>1</integer>
+          <real>0.3</real>
+          <real>0.1</real>
+          <integer>1</integer>
+        </array>
+        <array>
+          <integer>0</integer>
+          <real>0.4</real>
+          <real>0.8</real>
+          <integer>1</integer>
+        </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>
+      <string>a</string>
+      <string>b</string>
+      <string>c</string>
+    </array>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTest.ufo/metainfo.plist b/tests/data/ColorTest.ufo/metainfo.plist
new file mode 100644
index 0000000..7b8b34a
--- /dev/null
+++ b/tests/data/ColorTest.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>creator</key>
+    <string>com.github.fonttools.ufoLib</string>
+    <key>formatVersion</key>
+    <integer>3</integer>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTestRaw.ufo/fontinfo.plist b/tests/data/ColorTestRaw.ufo/fontinfo.plist
new file mode 100644
index 0000000..42b76d7
--- /dev/null
+++ b/tests/data/ColorTestRaw.ufo/fontinfo.plist
@@ -0,0 +1,34 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>ascender</key>
+    <integer>750</integer>
+    <key>capHeight</key>
+    <integer>750</integer>
+    <key>descender</key>
+    <integer>-250</integer>
+    <key>familyName</key>
+    <string>ColorTest</string>
+    <key>guidelines</key>
+    <array/>
+    <key>postscriptBlueValues</key>
+    <array/>
+    <key>postscriptFamilyBlues</key>
+    <array/>
+    <key>postscriptFamilyOtherBlues</key>
+    <array/>
+    <key>postscriptOtherBlues</key>
+    <array/>
+    <key>postscriptStemSnapH</key>
+    <array/>
+    <key>postscriptStemSnapV</key>
+    <array/>
+    <key>styleName</key>
+    <string>Regular</string>
+    <key>unitsPerEm</key>
+    <integer>1000</integer>
+    <key>xHeight</key>
+    <integer>500</integer>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTestRaw.ufo/glyphs/a.color1.glif b/tests/data/ColorTestRaw.ufo/glyphs/a.color1.glif
new file mode 100644
index 0000000..96bf9d0
--- /dev/null
+++ b/tests/data/ColorTestRaw.ufo/glyphs/a.color1.glif
@@ -0,0 +1,20 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="a.color1" format="2">
+  <advance width="500"/>
+  <outline>
+    <contour>
+      <point x="260" y="82" type="curve" smooth="yes"/>
+      <point x="342" y="82"/>
+      <point x="408" y="189"/>
+      <point x="408" y="321" type="curve" smooth="yes"/>
+      <point x="408" y="452"/>
+      <point x="342" y="560"/>
+      <point x="260" y="560" type="curve" smooth="yes"/>
+      <point x="178" y="560"/>
+      <point x="111" y="452"/>
+      <point x="111" y="321" type="curve" smooth="yes"/>
+      <point x="111" y="189"/>
+      <point x="178" y="82"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTestRaw.ufo/glyphs/a.color2.glif b/tests/data/ColorTestRaw.ufo/glyphs/a.color2.glif
new file mode 100644
index 0000000..1c08976
--- /dev/null
+++ b/tests/data/ColorTestRaw.ufo/glyphs/a.color2.glif
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="a.color2" format="2">
+  <advance width="500"/>
+  <outline>
+    <contour>
+      <point x="195" y="230" type="line"/>
+      <point x="485" y="230" type="line"/>
+      <point x="485" y="626" type="line"/>
+      <point x="195" y="626" type="line"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTestRaw.ufo/glyphs/a.glif b/tests/data/ColorTestRaw.ufo/glyphs/a.glif
new file mode 100644
index 0000000..8553e0b
--- /dev/null
+++ b/tests/data/ColorTestRaw.ufo/glyphs/a.glif
@@ -0,0 +1,13 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="a" format="2">
+  <advance width="500"/>
+  <unicode hex="0061"/>
+  <outline>
+    <contour>
+      <point x="36" y="77" type="line"/>
+      <point x="318" y="77" type="line"/>
+      <point x="318" y="458" type="line"/>
+      <point x="36" y="458" type="line"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/ColorTestRaw.ufo/glyphs/contents.plist b/tests/data/ColorTestRaw.ufo/glyphs/contents.plist
new file mode 100644
index 0000000..0d46f08
--- /dev/null
+++ b/tests/data/ColorTestRaw.ufo/glyphs/contents.plist
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>a</key>
+    <string>a.glif</string>
+    <key>a.color1</key>
+    <string>a.color1.glif</string>
+    <key>a.color2</key>
+    <string>a.color2.glif</string>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTestRaw.ufo/glyphs/layerinfo.plist b/tests/data/ColorTestRaw.ufo/glyphs/layerinfo.plist
new file mode 100644
index 0000000..3cf39b4
--- /dev/null
+++ b/tests/data/ColorTestRaw.ufo/glyphs/layerinfo.plist
@@ -0,0 +1,8 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>color</key>
+    <string>1,0.75,0,0.7</string>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTestRaw.ufo/layercontents.plist b/tests/data/ColorTestRaw.ufo/layercontents.plist
new file mode 100644
index 0000000..047e152
--- /dev/null
+++ b/tests/data/ColorTestRaw.ufo/layercontents.plist
@@ -0,0 +1,10 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <array>
+    <array>
+      <string>foreground</string>
+      <string>glyphs</string>
+    </array>
+  </array>
+</plist>
diff --git a/tests/data/ColorTestRaw.ufo/lib.plist b/tests/data/ColorTestRaw.ufo/lib.plist
new file mode 100644
index 0000000..1f65212
--- /dev/null
+++ b/tests/data/ColorTestRaw.ufo/lib.plist
@@ -0,0 +1,53 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>com.defcon.sortDescriptor</key>
+    <array>
+      <dict>
+        <key>ascending</key>
+        <string>Latin-1</string>
+        <key>type</key>
+        <string>characterSet</string>
+      </dict>
+    </array>
+    <key>com.github.googlei18n.ufo2ft.colorLayers</key>
+    <dict>
+      <key>a</key>
+      <array>
+        <array>
+          <string>a.color1</string>
+          <integer>0</integer>
+        </array>
+        <array>
+          <string>a.color2</string>
+          <integer>1</integer>
+        </array>
+      </array>
+    </dict>
+    <key>com.github.googlei18n.ufo2ft.colorPalettes</key>
+    <array>
+      <array>
+        <array>
+          <integer>1</integer>
+          <real>0.3</real>
+          <real>0.1</real>
+          <integer>1</integer>
+        </array>
+        <array>
+          <integer>0</integer>
+          <real>0.4</real>
+          <real>0.8</real>
+          <integer>1</integer>
+        </array>
+      </array>
+    </array>
+    <key>public.glyphOrder</key>
+    <array>
+      <string>space</string>
+      <string>a</string>
+      <string>a.color1</string>
+      <string>a.color2</string>
+    </array>
+  </dict>
+</plist>
diff --git a/tests/data/ColorTestRaw.ufo/metainfo.plist b/tests/data/ColorTestRaw.ufo/metainfo.plist
new file mode 100644
index 0000000..7b8b34a
--- /dev/null
+++ b/tests/data/ColorTestRaw.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>creator</key>
+    <string>com.github.fonttools.ufoLib</string>
+    <key>formatVersion</key>
+    <integer>3</integer>
+  </dict>
+</plist>
diff --git a/tests/outlineCompiler_test.py b/tests/outlineCompiler_test.py
index b0eabf4..747cdc1 100644
--- a/tests/outlineCompiler_test.py
+++ b/tests/outlineCompiler_test.py
@@ -770,6 +770,33 @@ class NamesTest(object):
         assert name in result
 
 
+class ColrCpalTest:
+
+    def test_colr_cpal(self, FontClass):
+        testufo = FontClass(getpath("ColorTest.ufo"))
+        assert "com.github.googlei18n.ufo2ft.colorLayerMapping" in testufo.lib
+        assert "com.github.googlei18n.ufo2ft.colorPalettes" in testufo.lib
+        result = compileTTF(testufo)
+        assert "COLR" in result
+        assert "CPAL" in result
+        layers = {gn: [(layer.name, layer.colorID) for layer in layers]
+                      for gn, layers in result["COLR"].ColorLayers.items()}
+        assert layers == {'a': [('a.color1', 0), ('a.color2', 1)],
+                          'b': [('b.color1', 1), ('b.color2', 0)],
+                          'c': [('c.color2', 1), ('c.color1', 0)]}
+
+    def test_colr_cpal_raw(self, FontClass):
+        testufo = FontClass(getpath("ColorTestRaw.ufo"))
+        assert "com.github.googlei18n.ufo2ft.colorLayers" in testufo.lib
+        assert "com.github.googlei18n.ufo2ft.colorPalettes" in testufo.lib
+        result = compileTTF(testufo)
+        palettes = [[(c.red, c.green, c.blue, c.alpha) for c in p] for p in result["CPAL"].palettes]
+        assert palettes == [[(255, 76, 26, 255), (0, 102, 204, 255)]]
+        layers = {gn: [(layer.name, layer.colorID) for layer in layers]
+                      for gn, layers in result["COLR"].ColorLayers.items()}
+        assert layers == {"a": [('a.color1', 0), ('a.color2', 1)]}
+
+
 ASCII = [unichr(c) for c in range(0x20, 0x7E)]