Upgrade fonttools to 4.8.0

Test: None
Change-Id: I089b1e10b6820bcaf96d9b17827944d8f5d97e4e
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 469a650..7759df7 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -4,6 +4,6 @@
 
 log = logging.getLogger(__name__)
 
-version = __version__ = "4.7.0"
+version = __version__ = "4.8.0"
 
 __all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py
index 486909e..9fe7f20 100644
--- a/Lib/fontTools/colorLib/builder.py
+++ b/Lib/fontTools/colorLib/builder.py
@@ -1,31 +1,148 @@
+import collections
+import copy
 import enum
-from typing import Dict, Iterable, List, Optional, Tuple, Union
-from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord, table_C_O_L_R_
-from fontTools.ttLib.tables.C_P_A_L_ import Color, table_C_P_A_L_
-from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e
+from functools import partial
+from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
+from fontTools.ttLib.tables import C_O_L_R_
+from fontTools.ttLib.tables import C_P_A_L_
+from fontTools.ttLib.tables import _n_a_m_e
+from fontTools.ttLib.tables import otTables as ot
+from fontTools.ttLib.tables.otTables import (
+    ExtendMode,
+    VariableValue,
+    VariableFloat,
+    VariableInt,
+)
 from .errors import ColorLibError
 
 
-def buildCOLR(colorLayers: Dict[str, List[Tuple[str, int]]]) -> table_C_O_L_R_:
+# TODO move type aliases to colorLib.types?
+_Kwargs = Mapping[str, Any]
+_PaintInput = Union[int, _Kwargs, ot.Paint]
+_LayerTuple = Tuple[str, _PaintInput]
+_LayersList = Sequence[_LayerTuple]
+_ColorGlyphsDict = Dict[str, _LayersList]
+_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
+_Number = Union[int, float]
+_ScalarInput = Union[_Number, VariableValue, Tuple[_Number, int]]
+_ColorStopTuple = Tuple[_ScalarInput, int]
+_ColorStopInput = Union[_ColorStopTuple, _Kwargs, ot.ColorStop]
+_ColorStopsList = Sequence[_ColorStopInput]
+_ExtendInput = Union[int, str, ExtendMode]
+_ColorLineInput = Union[_Kwargs, ot.ColorLine]
+_PointTuple = Tuple[_ScalarInput, _ScalarInput]
+_PointInput = Union[_PointTuple, ot.Point]
+_AffineTuple = Tuple[_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput]
+_AffineInput = Union[_AffineTuple, ot.Affine2x2]
+
+
+def populateCOLRv0(
+    table: ot.COLR,
+    colorGlyphsV0: _ColorGlyphsV0Dict,
+    glyphMap: Optional[Mapping[str, int]] = None,
+):
+    """Build v0 color layers and add to existing COLR table.
+
+    Args:
+        table: a raw otTables.COLR() object (not ttLib's table_C_O_L_R_).
+        colorGlyphsV0: map of base glyph names to lists of (layer glyph names,
+            color palette index) tuples.
+        glyphMap: a map from glyph names to glyph indices, as returned from
+            TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
+    """
+    if glyphMap is not None:
+        colorGlyphItems = sorted(
+            colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
+        )
+    else:
+        colorGlyphItems = colorGlyphsV0.items()
+    baseGlyphRecords = []
+    layerRecords = []
+    for baseGlyph, layers in colorGlyphItems:
+        baseRec = ot.BaseGlyphRecord()
+        baseRec.BaseGlyph = baseGlyph
+        baseRec.FirstLayerIndex = len(layerRecords)
+        baseRec.NumLayers = len(layers)
+        baseGlyphRecords.append(baseRec)
+
+        for layerGlyph, paletteIndex in layers:
+            layerRec = ot.LayerRecord()
+            layerRec.LayerGlyph = layerGlyph
+            layerRec.PaletteIndex = paletteIndex
+            layerRecords.append(layerRec)
+
+    table.BaseGlyphRecordCount = len(baseGlyphRecords)
+    table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray()
+    table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords
+    table.LayerRecordArray = ot.LayerRecordArray()
+    table.LayerRecordArray.LayerRecord = layerRecords
+    table.LayerRecordCount = len(layerRecords)
+
+
+def buildCOLR(
+    colorGlyphs: _ColorGlyphsDict,
+    version: Optional[int] = None,
+    glyphMap: Optional[Mapping[str, int]] = None,
+    varStore: Optional[ot.VarStore] = None,
+) -> C_O_L_R_.table_C_O_L_R_:
     """Build COLR table from color layers mapping.
 
     Args:
-        colorLayers: : map of base glyph names to lists of (layer glyph names,
-            palette indices) tuples.
+        colorGlyphs: map of base glyph names to lists of (layer glyph names,
+            Paint) tuples. For COLRv0, a paint is simply the color palette index
+            (int); for COLRv1, paint can be either solid colors (with variable
+            opacity), linear gradients or radial gradients.
+        version: the version of COLR table. If None, the version is determined
+            by the presence of gradients or variation data (varStore), which
+            require version 1; otherwise, if there are only simple colors, version
+            0 is used.
+        glyphMap: a map from glyph names to glyph indices, as returned from
+            TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
+        varStore: Optional ItemVarationStore for deltas associated with v1 layer.
 
     Return:
-        A new COLRv0 table.
+        A new COLR table.
     """
-    colorLayerLists = {}
-    for baseGlyphName, layers in colorLayers.items():
-        colorLayerLists[baseGlyphName] = [
-            LayerRecord(layerGlyphName, colorID) for layerGlyphName, colorID in layers
-        ]
+    self = C_O_L_R_.table_C_O_L_R_()
 
-    colr = table_C_O_L_R_()
-    colr.version = 0
-    colr.ColorLayers = colorLayerLists
-    return colr
+    if varStore is not None and version == 0:
+        raise ValueError("Can't add VarStore to COLRv0")
+
+    if version in (None, 0) and not varStore:
+        # split color glyphs into v0 and v1 and encode separately
+        colorGlyphsV0, colorGlyphsV1 = _splitSolidAndGradientGlyphs(colorGlyphs)
+        if version == 0 and colorGlyphsV1:
+            # TODO Derive "average" solid color from gradients?
+            raise ValueError("Can't encode gradients in COLRv0")
+    else:
+        # unless explicitly requested for v1 or have variations, in which case
+        # we encode all color glyph as v1
+        colorGlyphsV0, colorGlyphsV1 = None, colorGlyphs
+
+    colr = ot.COLR()
+
+    if colorGlyphsV0:
+        populateCOLRv0(colr, colorGlyphsV0, glyphMap)
+    else:
+        colr.BaseGlyphRecordCount = colr.LayerRecordCount = 0
+        colr.BaseGlyphRecordArray = colr.LayerRecordArray = None
+
+    if colorGlyphsV1:
+        colr.BaseGlyphV1Array = buildBaseGlyphV1Array(colorGlyphsV1, glyphMap)
+
+    if version is None:
+        version = 1 if (varStore or colorGlyphsV1) else 0
+    elif version not in (0, 1):
+        raise NotImplementedError(version)
+    self.version = colr.Version = version
+
+    if version == 0:
+        self._fromOTTable(colr)
+    else:
+        colr.VarStore = varStore
+        self.table = colr
+
+    return self
 
 
 class ColorPaletteType(enum.IntFlag):
@@ -45,12 +162,12 @@
 
 
 def buildPaletteLabels(
-    labels: List[_OptionalLocalizedString], nameTable: table__n_a_m_e
+    labels: Iterable[_OptionalLocalizedString], nameTable: _n_a_m_e.table__n_a_m_e
 ) -> List[Optional[int]]:
     return [
         nameTable.addMultilingualName(l, mac=False)
         if isinstance(l, dict)
-        else table_C_P_A_L_.NO_NAME_ID
+        else C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
         if l is None
         else nameTable.addMultilingualName({"en": l}, mac=False)
         for l in labels
@@ -58,12 +175,12 @@
 
 
 def buildCPAL(
-    palettes: List[List[Tuple[float, float, float, float]]],
-    paletteTypes: Optional[List[ColorPaletteType]] = None,
-    paletteLabels: Optional[List[_OptionalLocalizedString]] = None,
-    paletteEntryLabels: Optional[List[_OptionalLocalizedString]] = None,
-    nameTable: Optional[table__n_a_m_e] = None,
-) -> table_C_P_A_L_:
+    palettes: Sequence[Sequence[Tuple[float, float, float, float]]],
+    paletteTypes: Optional[Sequence[ColorPaletteType]] = None,
+    paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
+    paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
+    nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None,
+) -> C_P_A_L_.table_C_P_A_L_:
     """Build CPAL table from list of color palettes.
 
     Args:
@@ -89,7 +206,7 @@
             "nameTable is required if palette or palette entries have labels"
         )
 
-    cpal = table_C_P_A_L_()
+    cpal = C_P_A_L_.table_C_P_A_L_()
     cpal.numPaletteEntries = len(palettes[0])
 
     cpal.palettes = []
@@ -106,7 +223,9 @@
                 )
             # input colors are RGBA, CPAL encodes them as BGRA
             red, green, blue, alpha = color
-            colors.append(Color(*(round(v * 255) for v in (blue, green, red, alpha))))
+            colors.append(
+                C_P_A_L_.Color(*(round(v * 255) for v in (blue, green, red, alpha)))
+            )
         cpal.palettes.append(colors)
 
     if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
@@ -119,7 +238,9 @@
                 )
             cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes]
         else:
-            cpal.paletteTypes = [table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(palettes)
+            cpal.paletteTypes = [C_P_A_L_.table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(
+                palettes
+            )
 
         if paletteLabels is not None:
             if len(paletteLabels) != len(palettes):
@@ -128,7 +249,7 @@
                 )
             cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
         else:
-            cpal.paletteLabels = [table_C_P_A_L_.NO_NAME_ID] * len(palettes)
+            cpal.paletteLabels = [C_P_A_L_.table_C_P_A_L_.NO_NAME_ID] * len(palettes)
 
         if paletteEntryLabels is not None:
             if len(paletteEntryLabels) != cpal.numPaletteEntries:
@@ -139,9 +260,275 @@
             cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
         else:
             cpal.paletteEntryLabels = [
-                table_C_P_A_L_.NO_NAME_ID
+                C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
             ] * cpal.numPaletteEntries
     else:
         cpal.version = 0
 
     return cpal
+
+
+# COLR v1 tables
+# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
+
+_DEFAULT_TRANSPARENCY = VariableFloat(0.0)
+
+
+def _splitSolidAndGradientGlyphs(
+    colorGlyphs: _ColorGlyphsDict,
+) -> Tuple[Dict[str, List[Tuple[str, int]]], Dict[str, List[Tuple[str, ot.Paint]]]]:
+    colorGlyphsV0 = {}
+    colorGlyphsV1 = {}
+    for baseGlyph, layers in colorGlyphs.items():
+        newLayers = []
+        allSolidColors = True
+        for layerGlyph, paint in layers:
+            paint = _to_ot_paint(paint)
+            if (
+                paint.Format != 1
+                or paint.Color.Transparency.value != _DEFAULT_TRANSPARENCY.value
+            ):
+                allSolidColors = False
+            newLayers.append((layerGlyph, paint))
+        if allSolidColors:
+            colorGlyphsV0[baseGlyph] = [
+                (layerGlyph, paint.Color.PaletteIndex)
+                for layerGlyph, paint in newLayers
+            ]
+        else:
+            colorGlyphsV1[baseGlyph] = newLayers
+
+    # sanity check
+    assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
+
+    return colorGlyphsV0, colorGlyphsV1
+
+
+def _to_variable_value(value: _ScalarInput, cls=VariableValue) -> VariableValue:
+    if isinstance(value, cls):
+        return value
+    try:
+        it = iter(value)
+    except TypeError:  # not iterable
+        return cls(value)
+    else:
+        return cls._make(it)
+
+
+_to_variable_float = partial(_to_variable_value, cls=VariableFloat)
+_to_variable_int = partial(_to_variable_value, cls=VariableInt)
+
+
+def buildColor(
+    paletteIndex: int, transparency: _ScalarInput = _DEFAULT_TRANSPARENCY
+) -> ot.Color:
+    self = ot.Color()
+    self.PaletteIndex = int(paletteIndex)
+    self.Transparency = _to_variable_float(transparency)
+    return self
+
+
+def buildSolidColorPaint(
+    paletteIndex: int, transparency: _ScalarInput = _DEFAULT_TRANSPARENCY
+) -> ot.Paint:
+    self = ot.Paint()
+    self.Format = 1
+    self.Color = buildColor(paletteIndex, transparency)
+    return self
+
+
+def buildColorStop(
+    offset: _ScalarInput,
+    paletteIndex: int,
+    transparency: _ScalarInput = _DEFAULT_TRANSPARENCY,
+) -> ot.ColorStop:
+    self = ot.ColorStop()
+    self.StopOffset = _to_variable_float(offset)
+    self.Color = buildColor(paletteIndex, transparency)
+    return self
+
+
+def _to_extend_mode(v: _ExtendInput) -> ExtendMode:
+    if isinstance(v, ExtendMode):
+        return v
+    elif isinstance(v, str):
+        try:
+            return getattr(ExtendMode, v.upper())
+        except AttributeError:
+            raise ValueError(f"{v!r} is not a valid ExtendMode")
+    return ExtendMode(v)
+
+
+def buildColorLine(
+    stops: _ColorStopsList, extend: _ExtendInput = ExtendMode.PAD
+) -> ot.ColorLine:
+    self = ot.ColorLine()
+    self.Extend = _to_extend_mode(extend)
+    self.StopCount = len(stops)
+    self.ColorStop = [
+        stop
+        if isinstance(stop, ot.ColorStop)
+        else buildColorStop(**stop)
+        if isinstance(stop, collections.abc.Mapping)
+        else buildColorStop(*stop)
+        for stop in stops
+    ]
+    return self
+
+
+def buildPoint(x: _ScalarInput, y: _ScalarInput) -> ot.Point:
+    self = ot.Point()
+    # positions are encoded as Int16 so round to int
+    self.x = _to_variable_int(x)
+    self.y = _to_variable_int(y)
+    return self
+
+
+def _to_variable_point(pt: _PointInput) -> ot.Point:
+    if isinstance(pt, ot.Point):
+        return pt
+    return buildPoint(*pt)
+
+
+def _to_color_line(obj):
+    if isinstance(obj, ot.ColorLine):
+        return obj
+    elif isinstance(obj, collections.abc.Mapping):
+        return buildColorLine(**obj)
+    raise TypeError(obj)
+
+
+def buildLinearGradientPaint(
+    colorLine: _ColorLineInput,
+    p0: _PointInput,
+    p1: _PointInput,
+    p2: Optional[_PointInput] = None,
+) -> ot.Paint:
+    self = ot.Paint()
+    self.Format = 2
+    self.ColorLine = _to_color_line(colorLine)
+
+    if p2 is None:
+        p2 = copy.copy(p1)
+    for i, pt in enumerate((p0, p1, p2)):
+        setattr(self, f"p{i}", _to_variable_point(pt))
+
+    return self
+
+
+def buildAffine2x2(
+    xx: _ScalarInput, xy: _ScalarInput, yx: _ScalarInput, yy: _ScalarInput
+) -> ot.Affine2x2:
+    self = ot.Affine2x2()
+    locs = locals()
+    for attr in ("xx", "xy", "yx", "yy"):
+        value = locs[attr]
+        setattr(self, attr, _to_variable_float(value))
+    return self
+
+
+def buildRadialGradientPaint(
+    colorLine: _ColorLineInput,
+    c0: _PointInput,
+    c1: _PointInput,
+    r0: _ScalarInput,
+    r1: _ScalarInput,
+    affine: Optional[_AffineInput] = None,
+) -> ot.Paint:
+
+    self = ot.Paint()
+    self.Format = 3
+    self.ColorLine = _to_color_line(colorLine)
+
+    for i, pt in [(0, c0), (1, c1)]:
+        setattr(self, f"c{i}", _to_variable_point(pt))
+
+    for i, r in [(0, r0), (1, r1)]:
+        # distances are encoded as UShort so we round to int
+        setattr(self, f"r{i}", _to_variable_int(r))
+
+    if affine is not None and not isinstance(affine, ot.Affine2x2):
+        affine = buildAffine2x2(*affine)
+    self.Affine = affine
+
+    return self
+
+
+def _to_ot_paint(paint: _PaintInput) -> ot.Paint:
+    if isinstance(paint, ot.Paint):
+        return paint
+    elif isinstance(paint, int):
+        paletteIndex = paint
+        return buildSolidColorPaint(paletteIndex)
+    elif isinstance(paint, collections.abc.Mapping):
+        return buildPaint(**paint)
+    raise TypeError(f"expected int, Mapping or ot.Paint, found {type(paint.__name__)}")
+
+
+def buildLayerV1Record(layerGlyph: str, paint: _PaintInput) -> ot.LayerV1Record:
+    self = ot.LayerV1Record()
+    self.LayerGlyph = layerGlyph
+    self.Paint = _to_ot_paint(paint)
+    return self
+
+
+def buildLayerV1Array(
+    layers: Sequence[Union[_LayerTuple, ot.LayerV1Record]]
+) -> ot.LayerV1Array:
+    self = ot.LayerV1Array()
+    self.LayerCount = len(layers)
+    records = []
+    for layer in layers:
+        if isinstance(layer, ot.LayerV1Record):
+            record = layer
+        else:
+            layerGlyph, paint = layer
+            record = buildLayerV1Record(layerGlyph, paint)
+        records.append(record)
+    self.LayerV1Record = records
+    return self
+
+
+def buildBaseGlyphV1Record(
+    baseGlyph: str, layers: Union[_LayersList, ot.LayerV1Array]
+) -> ot.BaseGlyphV1Array:
+    self = ot.BaseGlyphV1Record()
+    self.BaseGlyph = baseGlyph
+    if not isinstance(layers, ot.LayerV1Array):
+        layers = buildLayerV1Array(layers)
+    self.LayerV1Array = layers
+    return self
+
+
+def buildBaseGlyphV1Array(
+    colorGlyphs: Union[_ColorGlyphsDict, Dict[str, ot.LayerV1Array]],
+    glyphMap: Optional[Mapping[str, int]] = None,
+) -> ot.BaseGlyphV1Array:
+    if glyphMap is not None:
+        colorGlyphItems = sorted(
+            colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
+        )
+    else:
+        colorGlyphItems = colorGlyphs.items()
+    records = [
+        buildBaseGlyphV1Record(baseGlyph, layers)
+        for baseGlyph, layers in colorGlyphItems
+    ]
+    self = ot.BaseGlyphV1Array()
+    self.BaseGlyphCount = len(records)
+    self.BaseGlyphV1Record = records
+    return self
+
+
+_PAINT_BUILDERS = {
+    1: buildSolidColorPaint,
+    2: buildLinearGradientPaint,
+    3: buildRadialGradientPaint,
+}
+
+
+def buildPaint(format: int, **kwargs) -> ot.Paint:
+    try:
+        return _PAINT_BUILDERS[format](**kwargs)
+    except KeyError:
+        raise NotImplementedError(format)
diff --git a/Lib/fontTools/cu2qu/ufo.py b/Lib/fontTools/cu2qu/ufo.py
index 63d5799..447de7b 100644
--- a/Lib/fontTools/cu2qu/ufo.py
+++ b/Lib/fontTools/cu2qu/ufo.py
@@ -37,6 +37,9 @@
 
 __all__ = ['fonts_to_quadratic', 'font_to_quadratic']
 
+# The default approximation error below is a relative value (1/1000 of the EM square).
+# Later on, we convert it to absolute font units by multiplying it by a font's UPEM
+# (see fonts_to_quadratic).
 DEFAULT_MAX_ERR = 0.001
 CURVE_TYPE_LIB_KEY = "com.github.googlei18n.cu2qu.curve_type"
 
diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index b0f805a..e416c0c 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -109,7 +109,7 @@
     if hasattr(g, 'asFea'):
         return g.asFea()
     elif isinstance(g, tuple) and len(g) == 2:
-        return asFea(g[0]) + "-" + asFea(g[1])   # a range
+        return asFea(g[0]) + " - " + asFea(g[1])   # a range
     elif g.lower() in fea_keywords:
         return "\\" + g
     else:
@@ -197,7 +197,7 @@
     def add_cid_range(self, start, end, glyphs):
         if self.curr < len(self.glyphs):
             self.original.extend(self.glyphs[self.curr:])
-        self.original.append(("cid{:05d}".format(start), "cid{:05d}".format(end)))
+        self.original.append(("\\{}".format(start), "\\{}".format(end)))
         self.glyphs.extend(glyphs)
         self.curr = len(self.glyphs)
 
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index bda34e2..a3eaf62 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -299,7 +299,7 @@
             if self.next_token_type_ is Lexer.NAME:
                 glyph = self.expect_glyph_()
                 location = self.cur_token_location_
-                if '-' in glyph and glyph not in self.glyphNames_:
+                if '-' in glyph and self.glyphNames_ and glyph not in self.glyphNames_:
                     start, limit = self.split_glyph_range_(glyph, location)
                     self.check_glyph_name_in_glyph_set(start, limit)
                     glyphs.add_range(
@@ -314,6 +314,11 @@
                         start, limit,
                         self.make_glyph_range_(location, start, limit))
                 else:
+                    if '-' in glyph and not self.glyphNames_:
+                        log.warning(str(FeatureLibError(
+                            f"Ambiguous glyph name that looks like a range: {glyph!r}",
+                            location
+                        )))
                     self.check_glyph_name_in_glyph_set(glyph)
                     glyphs.append(glyph)
             elif self.next_token_type_ is Lexer.CID:
@@ -351,7 +356,7 @@
             else:
                 raise FeatureLibError(
                     "Expected glyph name, glyph range, "
-                    "or glyph class reference",
+                    f"or glyph class reference, found {self.next_token_!r}",
                     self.next_token_location_)
         self.expect_symbol_("]")
         return glyphs
@@ -826,8 +831,14 @@
                 'is not supported',
                 location)
 
+        # If there are remaining glyphs to parse, this is an invalid GSUB statement
+        if len(new) != 0:
+            raise FeatureLibError(
+                'Invalid substitution statement',
+                location
+            )
+
         # GSUB lookup type 6: Chaining contextual substitution.
-        assert len(new) == 0, new
         rule = self.ast.ChainContextSubstStatement(
             old_prefix, old, old_suffix, lookups, location=location)
         return rule
diff --git a/Lib/fontTools/pens/cu2quPen.py b/Lib/fontTools/pens/cu2quPen.py
index cf98b22..497585b 100644
--- a/Lib/fontTools/pens/cu2quPen.py
+++ b/Lib/fontTools/pens/cu2quPen.py
@@ -24,7 +24,9 @@
     using the FontTools SegmentPen protocol.
 
     other_pen: another SegmentPen used to draw the transformed outline.
-    max_err: maximum approximation error in font units.
+    max_err: maximum approximation error in font units. For optimal results,
+        if you know the UPEM of the font, we recommend setting this to a
+        value equal, or close to UPEM / 1000.
     reverse_direction: flip the contours' direction but keep starting point.
     stats: a dictionary counting the point numbers of quadratic segments.
     ignore_single_points: don't emit contours containing only a single point
@@ -137,7 +139,9 @@
     using the RoboFab PointPen protocol.
 
     other_point_pen: another PointPen used to draw the transformed outline.
-    max_err: maximum approximation error in font units.
+    max_err: maximum approximation error in font units. For optimal results,
+        if you know the UPEM of the font, we recommend setting this to a
+        value equal, or close to UPEM / 1000.
     reverse_direction: reverse the winding direction of all contours.
     stats: a dictionary counting the point numbers of quadratic segments.
     """
diff --git a/Lib/fontTools/ttLib/tables/C_O_L_R_.py b/Lib/fontTools/ttLib/tables/C_O_L_R_.py
index 064aa27..2366c5e 100644
--- a/Lib/fontTools/ttLib/tables/C_O_L_R_.py
+++ b/Lib/fontTools/ttLib/tables/C_O_L_R_.py
@@ -5,7 +5,6 @@
 from fontTools.misc.py23 import *
 from fontTools.misc.textTools import safeEval
 from . import DefaultTable
-import struct
 
 
 class table_C_O_L_R_(DefaultTable.DefaultTable):
@@ -15,91 +14,91 @@
 	ttFont['COLR'][<glyphName>] = <value> will set the color layers for any glyph.
 	"""
 
-	def decompile(self, data, ttFont):
-		self.getGlyphName = ttFont.getGlyphName # for use in get/set item functions, for access by GID
-		self.version, numBaseGlyphRecords, offsetBaseGlyphRecord, offsetLayerRecord, numLayerRecords = struct.unpack(">HHLLH", data[:14])
-		assert (self.version == 0), "Version of COLR table is higher than I know how to handle"
-		glyphOrder = ttFont.getGlyphOrder()
-		gids = []
-		layerLists = []
-		glyphPos = offsetBaseGlyphRecord
-		for i in range(numBaseGlyphRecords):
-			gid, firstLayerIndex, numLayers = struct.unpack(">HHH", data[glyphPos:glyphPos+6])
-			glyphPos += 6
-			gids.append(gid)
-			assert (firstLayerIndex + numLayers <= numLayerRecords)
-			layerPos = offsetLayerRecord + firstLayerIndex * 4
-			layers = []
-			for j in range(numLayers):
-				layerGid, colorID = struct.unpack(">HH", data[layerPos:layerPos+4])
-				try:
-					layerName = glyphOrder[layerGid]
-				except IndexError:
-					layerName = self.getGlyphName(layerGid)
-				layerPos += 4
-				layers.append(LayerRecord(layerName, colorID))
-			layerLists.append(layers)
-
+	def _fromOTTable(self, table):
+		self.version = 0
 		self.ColorLayers = colorLayerLists = {}
-		try:
-			names = [glyphOrder[gid] for gid in gids]
-		except IndexError:
-			getGlyphName = self.getGlyphName
-			names = map(getGlyphName, gids)
+		layerRecords = table.LayerRecordArray.LayerRecord
+		numLayerRecords = len(layerRecords)
+		for baseRec in table.BaseGlyphRecordArray.BaseGlyphRecord:
+			baseGlyph = baseRec.BaseGlyph
+			firstLayerIndex = baseRec.FirstLayerIndex
+			numLayers = baseRec.NumLayers
+			assert (firstLayerIndex + numLayers <= numLayerRecords)
+			layers = []
+			for i in range(firstLayerIndex, firstLayerIndex+numLayers):
+				layerRec = layerRecords[i]
+				layers.append(
+					LayerRecord(layerRec.LayerGlyph, layerRec.PaletteIndex)
+				)
+			colorLayerLists[baseGlyph] = layers
 
-		for name, layerList in zip(names, layerLists):
-			colorLayerLists[name] = layerList
+	def _toOTTable(self, ttFont):
+		from . import otTables
+		from fontTools.colorLib.builder import populateCOLRv0
+
+		tableClass = getattr(otTables, self.tableTag)
+		table = tableClass()
+		table.Version = self.version
+
+		populateCOLRv0(
+			table,
+			{
+				baseGlyph: [(layer.name, layer.colorID) for layer in layers]
+				for baseGlyph, layers in self.ColorLayers.items()
+			},
+			glyphMap=ttFont.getReverseGlyphMap(rebuild=True),
+		)
+		return table
+
+	def decompile(self, data, ttFont):
+		from .otBase import OTTableReader
+		from . import otTables
+
+		# We use otData to decompile, but we adapt the decompiled otTables to the
+		# existing COLR v0 API for backward compatibility.
+		reader = OTTableReader(data, tableTag=self.tableTag)
+		tableClass = getattr(otTables, self.tableTag)
+		table = tableClass()
+		table.decompile(reader, ttFont)
+
+		if table.Version == 0:
+			self._fromOTTable(table)
+		else:
+			# for new versions, keep the raw otTables around
+			self.table = table
 
 	def compile(self, ttFont):
-		ordered = []
-		ttFont.getReverseGlyphMap(rebuild=True)
-		glyphNames = self.ColorLayers.keys()
-		for glyphName in glyphNames:
-			try:
-				gid = ttFont.getGlyphID(glyphName)
-			except:
-				assert 0, "COLR table contains a glyph name not in ttFont.getGlyphNames(): " + str(glyphName)
-			ordered.append([gid, glyphName, self.ColorLayers[glyphName]])
-		ordered.sort()
+		from .otBase import OTTableWriter
 
-		glyphMap = []
-		layerMap = []
-		for (gid, glyphName, layers) in ordered:
-			glyphMap.append(struct.pack(">HHH", gid, len(layerMap), len(layers)))
-			for layer in layers:
-				layerMap.append(struct.pack(">HH", ttFont.getGlyphID(layer.name), layer.colorID))
+		if hasattr(self, "table"):
+			table = self.table
+		else:
+			table = self._toOTTable(ttFont)
 
-		dataList = [struct.pack(">HHLLH", self.version, len(glyphMap), 14, 14+6*len(glyphMap), len(layerMap))]
-		dataList.extend(glyphMap)
-		dataList.extend(layerMap)
-		data = bytesjoin(dataList)
-		return data
+		writer = OTTableWriter(tableTag=self.tableTag)
+		table.compile(writer, ttFont)
+		return writer.getAllData()
 
 	def toXML(self, writer, ttFont):
-		writer.simpletag("version", value=self.version)
-		writer.newline()
-		ordered = []
-		glyphNames = self.ColorLayers.keys()
-		for glyphName in glyphNames:
-			try:
-				gid = ttFont.getGlyphID(glyphName)
-			except:
-				assert 0, "COLR table contains a glyph name not in ttFont.getGlyphNames(): " + str(glyphName)
-			ordered.append([gid, glyphName, self.ColorLayers[glyphName]])
-		ordered.sort()
-		for entry in ordered:
-			writer.begintag("ColorGlyph", name=entry[1])
+		if hasattr(self, "table"):
+			self.table.toXML2(writer, ttFont)
+		else:
+			writer.simpletag("version", value=self.version)
 			writer.newline()
-			for layer in entry[2]:
-				layer.toXML(writer, ttFont)
-			writer.endtag("ColorGlyph")
-			writer.newline()
+			for baseGlyph in sorted(self.ColorLayers.keys(), key=ttFont.getGlyphID):
+				writer.begintag("ColorGlyph", name=baseGlyph)
+				writer.newline()
+				for layer in self.ColorLayers[baseGlyph]:
+					layer.toXML(writer, ttFont)
+				writer.endtag("ColorGlyph")
+				writer.newline()
 
 	def fromXML(self, name, attrs, content, ttFont):
-		if not hasattr(self, "ColorLayers"):
-			self.ColorLayers = {}
-		self.getGlyphName = ttFont.getGlyphName # for use in get/set item functions, for access by GID
-		if name == "ColorGlyph":
+		if name == "version":  # old COLR v0 API
+			setattr(self, name, safeEval(attrs["value"]))
+		elif name == "ColorGlyph":
+			if not hasattr(self, "ColorLayers"):
+				self.ColorLayers = {}
 			glyphName = attrs["name"]
 			for element in content:
 				if isinstance(element, basestring):
@@ -111,32 +110,31 @@
 				layer = LayerRecord()
 				layer.fromXML(element[0], element[1], element[2], ttFont)
 				layers.append (layer)
-			self[glyphName] = layers
-		elif "value" in attrs:
-			setattr(self, name, safeEval(attrs["value"]))
+			self.ColorLayers[glyphName] = layers
+		else:  # new COLR v1 API
+			from . import otTables
 
-	def __getitem__(self, glyphSelector):
-		if isinstance(glyphSelector, int):
-			# its a gid, convert to glyph name
-			glyphSelector = self.getGlyphName(glyphSelector)
+			if not hasattr(self, "table"):
+				tableClass = getattr(otTables, self.tableTag)
+				self.table = tableClass()
+			self.table.fromXML(name, attrs, content, ttFont)
+			self.table.populateDefaults()
 
-		if glyphSelector not in self.ColorLayers:
-			return None
+	def __getitem__(self, glyphName):
+		if not isinstance(glyphName, str):
+			raise TypeError(f"expected str, found {type(glyphName).__name__}")
+		return self.ColorLayers[glyphName]
 
-		return self.ColorLayers[glyphSelector]
+	def __setitem__(self, glyphName, value):
+		if not isinstance(glyphName, str):
+			raise TypeError(f"expected str, found {type(glyphName).__name__}")
+		if value is not None:
+			self.ColorLayers[glyphName] = value
+		elif glyphName in self.ColorLayers:
+			del self.ColorLayers[glyphName]
 
-	def __setitem__(self, glyphSelector, value):
-		if isinstance(glyphSelector, int):
-			# its a gid, convert to glyph name
-			glyphSelector = self.getGlyphName(glyphSelector)
-
-		if  value:
-			self.ColorLayers[glyphSelector] = value
-		elif glyphSelector in self.ColorLayers:
-			del self.ColorLayers[glyphSelector]
-
-	def __delitem__(self, glyphSelector):
-		del self.ColorLayers[glyphSelector]
+	def __delitem__(self, glyphName):
+		del self.ColorLayers[glyphName]
 
 class LayerRecord(object):
 
@@ -151,8 +149,6 @@
 	def fromXML(self, eltname, attrs, content, ttFont):
 		for (name, value) in attrs.items():
 			if name == "name":
-				if isinstance(value, int):
-					value = ttFont.getGlyphName(value)
 				setattr(self, name, value)
 			else:
 				setattr(self, name, safeEval(value))
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
index 6182134..786238a 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -652,6 +652,15 @@
 		else:
 			table = self.__dict__.copy()
 
+		# some count references may have been initialized in a custom preWrite; we set
+		# these in the writer's state beforehand (instead of sequentially) so they will
+		# be propagated to all nested subtables even if the count appears in the current
+		# table only *after* the offset to the subtable that it is counting.
+		for conv in self.getConverters():
+			if conv.isCount and conv.isPropagated:
+				value = table.get(conv.name)
+				if isinstance(value, CountReference):
+					writer[conv.name] = value
 
 		if hasattr(self, 'sortCoverageLast'):
 			writer.sortCoverageLast = 1
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
index 6f02050..b737e5c 100644
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -13,7 +13,9 @@
                      OTTableReader, OTTableWriter, ValueRecordFactory)
 from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
                        ContextualMorphAction, LigatureMorphAction,
-                       InsertionMorphAction, MorxSubtable)
+                       InsertionMorphAction, MorxSubtable, VariableFloat,
+                       VariableInt, ExtendMode as _ExtendMode)
+from itertools import zip_longest
 from functools import partial
 import struct
 import logging
@@ -134,7 +136,22 @@
 		self.tableClass = tableClass
 		self.isCount = name.endswith("Count") or name in ['DesignAxisRecordSize', 'ValueRecordSize']
 		self.isLookupType = name.endswith("LookupType") or name == "MorphType"
-		self.isPropagated = name in ["ClassCount", "Class2Count", "FeatureTag", "SettingsCount", "VarRegionCount", "MappingCount", "RegionAxisCount", 'DesignAxisCount', 'DesignAxisRecordSize', 'AxisValueCount', 'ValueRecordSize', 'AxisCount']
+		self.isPropagated = name in [
+			"ClassCount",
+			"Class2Count",
+			"FeatureTag",
+			"SettingsCount",
+			"VarRegionCount",
+			"MappingCount",
+			"RegionAxisCount",
+			"DesignAxisCount",
+			"DesignAxisRecordSize",
+			"AxisValueCount",
+			"ValueRecordSize",
+			"AxisCount",
+			"BaseGlyphRecordCount",
+			"LayerRecordCount",
+		]
 
 	def readArray(self, reader, font, tableDict, count):
 		"""Read an array of values from the reader."""
@@ -185,15 +202,22 @@
 
 
 class SimpleValue(BaseConverter):
+	@staticmethod
+	def toString(value):
+		return value
+	@staticmethod
+	def fromString(value):
+		return value
 	def xmlWrite(self, xmlWriter, font, value, name, attrs):
-		xmlWriter.simpletag(name, attrs + [("value", value)])
+		xmlWriter.simpletag(name, attrs + [("value", self.toString(value))])
 		xmlWriter.newline()
 	def xmlRead(self, attrs, content, font):
-		return attrs["value"]
+		return self.fromString(attrs["value"])
 
 class IntValue(SimpleValue):
-	def xmlRead(self, attrs, content, font):
-		return int(attrs["value"], 0)
+	@staticmethod
+	def fromString(value):
+		return int(value, 0)
 
 class Long(IntValue):
 	staticSize = 4
@@ -210,9 +234,9 @@
 		writer.writeULong(value)
 
 class Flags32(ULong):
-	def xmlWrite(self, xmlWriter, font, value, name, attrs):
-		xmlWriter.simpletag(name, attrs + [("value", "0x%08X" % value)])
-		xmlWriter.newline()
+	@staticmethod
+	def toString(value):
+		return "0x%08X" % value
 
 class Short(IntValue):
 	staticSize = 2
@@ -303,8 +327,9 @@
 
 
 class FloatValue(SimpleValue):
-	def xmlRead(self, attrs, content, font):
-		return float(attrs["value"])
+	@staticmethod
+	def fromString(value):
+		return float(value)
 
 class DeciPoints(FloatValue):
 	staticSize = 2
@@ -320,11 +345,12 @@
 		return  fi2fl(reader.readLong(), 16)
 	def write(self, writer, font, tableDict, value, repeatIndex=None):
 		writer.writeLong(fl2fi(value, 16))
-	def xmlRead(self, attrs, content, font):
-		return str2fl(attrs["value"], 16)
-	def xmlWrite(self, xmlWriter, font, value, name, attrs):
-		xmlWriter.simpletag(name, attrs + [("value", fl2str(value, 16))])
-		xmlWriter.newline()
+	@staticmethod
+	def fromString(value):
+		return str2fl(value, 16)
+	@staticmethod
+	def toString(value):
+		return fl2str(value, 16)
 
 class F2Dot14(FloatValue):
 	staticSize = 2
@@ -332,13 +358,14 @@
 		return  fi2fl(reader.readShort(), 14)
 	def write(self, writer, font, tableDict, value, repeatIndex=None):
 		writer.writeShort(fl2fi(value, 14))
-	def xmlRead(self, attrs, content, font):
-		return str2fl(attrs["value"], 14)
-	def xmlWrite(self, xmlWriter, font, value, name, attrs):
-		xmlWriter.simpletag(name, attrs + [("value", fl2str(value, 14))])
-		xmlWriter.newline()
+	@staticmethod
+	def fromString(value):
+		return str2fl(value, 14)
+	@staticmethod
+	def toString(value):
+		return fl2str(value, 14)
 
-class Version(BaseConverter):
+class Version(SimpleValue):
 	staticSize = 4
 	def read(self, reader, font, tableDict):
 		value = reader.readLong()
@@ -348,16 +375,12 @@
 		value = fi2ve(value)
 		assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
 		writer.writeLong(value)
-	def xmlRead(self, attrs, content, font):
-		value = attrs["value"]
-		value = ve2fi(value)
-		return value
-	def xmlWrite(self, xmlWriter, font, value, name, attrs):
-		value = fi2ve(value)
-		value = "0x%08x" % value
-		xmlWriter.simpletag(name, attrs + [("value", value)])
-		xmlWriter.newline()
-
+	@staticmethod
+	def fromString(value):
+		return ve2fi(value)
+	@staticmethod
+	def toString(value):
+		return "0x%08x" % value
 	@staticmethod
 	def fromFloat(v):
 		return fl2fi(v, 16)
@@ -1583,6 +1606,111 @@
 			xmlWriter.comment(" ".join(flags))
 		xmlWriter.newline()
 
+def _issubclass_namedtuple(x):
+	return (
+		issubclass(x, tuple)
+		and getattr(x, "_fields", None) is not None
+	)
+
+
+class _NamedTupleConverter(BaseConverter):
+	# subclasses must override this
+	tupleClass = NotImplemented
+	# List[SimpleValue]
+	converterClasses = NotImplemented
+
+	def __init__(self, name, repeat, aux, tableClass=None):
+		# we expect all converters to be subclasses of SimpleValue
+		assert all(issubclass(klass, SimpleValue) for klass in self.converterClasses)
+		assert _issubclass_namedtuple(self.tupleClass), repr(self.tupleClass)
+		assert len(self.tupleClass._fields) == len(self.converterClasses)
+		assert tableClass is None  # tableClass is unused by SimplValues
+		BaseConverter.__init__(self, name, repeat, aux)
+		self.converters = [
+			klass(name=name, repeat=None, aux=None)
+			for name, klass in zip(self.tupleClass._fields, self.converterClasses)
+		]
+		self.convertersByName = {conv.name: conv for conv in self.converters}
+		# returned by getRecordSize method
+		self.staticSize = sum(c.staticSize for c in self.converters)
+
+	def read(self, reader, font, tableDict):
+		kwargs = {
+			conv.name: conv.read(reader, font, tableDict)
+			for conv in self.converters
+		}
+		return self.tupleClass(**kwargs)
+
+	def write(self, writer, font, tableDict, value, repeatIndex=None):
+		for conv in self.converters:
+			v = getattr(value, conv.name)
+			# repeatIndex is unused for SimpleValues
+			conv.write(writer, font, tableDict, v, repeatIndex=None)
+
+	def xmlWrite(self, xmlWriter, font, value, name, attrs):
+		assert value is not None
+		defaults = value.__new__.__defaults__ or ()
+		assert len(self.converters) >= len(defaults)
+		values = {}
+		required = object()
+		for conv, default in zip_longest(
+			reversed(self.converters),
+			reversed(defaults),
+			fillvalue=required,
+		):
+			v = getattr(value, conv.name)
+			if default is required or v != default:
+				values[conv.name] = conv.toString(v)
+		if attrs is None:
+			attrs = []
+		attrs.extend(
+			(conv.name, values[conv.name])
+			for conv in self.converters
+			if conv.name in values
+		)
+		xmlWriter.simpletag(name, attrs)
+		xmlWriter.newline()
+
+	def xmlRead(self, attrs, content, font):
+		converters = self.convertersByName
+		kwargs = {
+			k: converters[k].fromString(v)
+			for k, v in attrs.items()
+		}
+		return self.tupleClass(**kwargs)
+
+
+class VariableScalar(_NamedTupleConverter):
+	tupleClass = VariableFloat
+	converterClasses = [Fixed, ULong]
+
+
+class VariableNormalizedScalar(_NamedTupleConverter):
+	tupleClass = VariableFloat
+	converterClasses = [F2Dot14, ULong]
+
+
+class VariablePosition(_NamedTupleConverter):
+	tupleClass = VariableInt
+	converterClasses = [Short, ULong]
+
+
+class VariableDistance(_NamedTupleConverter):
+	tupleClass = VariableInt
+	converterClasses = [UShort, ULong]
+
+
+class ExtendMode(UShort):
+	def read(self, reader, font, tableDict):
+		return _ExtendMode(super().read(reader, font, tableDict))
+	@staticmethod
+	def fromString(value):
+		return getattr(_ExtendMode, value.upper())
+	@staticmethod
+	def toString(value):
+		return _ExtendMode(value).name.lower()
+
+
 converterMapping = {
 	# type		class
 	"int8":		Int8,
@@ -1609,6 +1737,7 @@
 	"VarIdxMapValue":	VarIdxMapValue,
 	"VarDataValue":	VarDataValue,
 	"LookupFlag": LookupFlag,
+	"ExtendMode": ExtendMode,
 
 	# AAT
 	"CIDGlyphMap":	CIDGlyphMap,
@@ -1624,4 +1753,10 @@
 	"STXHeader":	lambda C: partial(STXHeader, tableClass=C),
 	"OffsetTo":	lambda C: partial(Table, tableClass=C),
 	"LOffsetTo":	lambda C: partial(LTable, tableClass=C),
+
+	# Variable types
+	"VariableScalar": VariableScalar,
+	"VariableNormalizedScalar": VariableNormalizedScalar,
+	"VariablePosition": VariablePosition,
+	"VariableDistance": VariableDistance,
 }
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index 1fa9156..b893a7b 100755
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
@@ -1539,4 +1539,107 @@
 		('int16', 'CVTValueArray', 'NumCVTEntries', 0, 'CVT value'),
 	]),
 
+	#
+	# COLR
+	#
+
+	('COLR', [
+		('uint16', 'Version', None, None, 'Table version number (starts at 0).'),
+		('uint16', 'BaseGlyphRecordCount', None, None, 'Number of Base Glyph Records.'),
+		('LOffset', 'BaseGlyphRecordArray', None, None, 'Offset (from beginning of COLR table) to Base Glyph records.'),
+		('LOffset', 'LayerRecordArray', None, None, 'Offset (from beginning of COLR table) to Layer Records.'),
+		('uint16', 'LayerRecordCount', None, None, 'Number of Layer Records.'),
+		('LOffset', 'BaseGlyphV1Array', None, 'Version >= 1', 'Offset (from beginning of COLR table) to array of Version-1 Base Glyph records.'),
+		('LOffset', 'VarStore', None, 'Version >= 1', 'Offset to variation store (may be NULL)'),
+	]),
+
+	('BaseGlyphRecordArray', [
+		('BaseGlyphRecord', 'BaseGlyphRecord', 'BaseGlyphRecordCount', 0, 'Base Glyph records.'),
+	]),
+
+	('BaseGlyphRecord', [
+		('GlyphID', 'BaseGlyph', None, None, 'Glyph ID of reference glyph. This glyph is for reference only and is not rendered for color.'),
+		('uint16', 'FirstLayerIndex', None, None, 'Index (from beginning of the Layer Records) to the layer record. There will be numLayers consecutive entries for this base glyph.'),
+		('uint16', 'NumLayers', None, None, 'Number of color layers associated with this glyph.'),
+	]),
+
+	('LayerRecordArray', [
+		('LayerRecord', 'LayerRecord', 'LayerRecordCount', 0, 'Layer records.'),
+	]),
+
+	('LayerRecord', [
+		('GlyphID', 'LayerGlyph', None, None, 'Glyph ID of layer glyph (must be in z-order from bottom to top).'),
+		('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
+	]),
+
+	('BaseGlyphV1Array', [
+		('uint32', 'BaseGlyphCount', None, None, 'Number of Version-1 Base Glyph records'),
+		('struct', 'BaseGlyphV1Record', 'BaseGlyphCount', 0, 'Array of Version-1 Base Glyph records'),
+	]),
+
+	('BaseGlyphV1Record', [
+		('GlyphID', 'BaseGlyph', None, None, 'Glyph ID of reference glyph.'),
+		('LOffset', 'LayerV1Array', None, None, 'Offset (from beginning of BaseGlyphV1Array) to LayerV1Array.'),
+	]),
+
+	('LayerV1Array', [
+		('uint32', 'LayerCount', None, None, 'Number of Version-1 Layer records'),
+		('struct', 'LayerV1Record', 'LayerCount', 0, 'Array of Version-1 Layer records'),
+	]),
+
+	('LayerV1Record', [
+		('GlyphID', 'LayerGlyph', None, None, 'Glyph ID of layer glyph (must be in z-order from bottom to top).'),
+		('LOffset', 'Paint', None, None, 'Offset (from beginning of LayerV1Array) to Paint subtable.'),
+	]),
+
+	('Affine2x2', [
+		('VariableScalar', 'xx', None, None, ''),
+		('VariableScalar', 'xy', None, None, ''),
+		('VariableScalar', 'yx', None, None, ''),
+		('VariableScalar', 'yy', None, None, ''),
+	]),
+
+	('Point', [
+		('VariablePosition', 'x', None, None, ''),
+		('VariablePosition', 'y', None, None, ''),
+	]),
+
+	('Color', [
+		('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
+		('VariableNormalizedScalar', 'Transparency', None, None, 'Values outsided [0.,1.] reserved'),
+	]),
+
+	('ColorStop', [
+		('VariableNormalizedScalar', 'StopOffset', None, None, ''),
+		('Color', 'Color', None, None, ''),
+	]),
+
+	('ColorLine', [
+		('ExtendMode', 'Extend', None, None, 'Enum {PAD = 0, REPEAT = 1, REFLECT = 2}'),
+		('uint16', 'StopCount', None, None, 'Number of Color stops.'),
+		('ColorStop', 'ColorStop', 'StopCount', 0, 'Array of Color stops.'),
+	]),
+
+	('PaintFormat1', [
+		('uint16', 'PaintFormat', None, None, 'Format identifier-format = 1'),
+		('Color', 'Color', None, None, 'A solid color paint.'),
+	]),
+
+	('PaintFormat2', [
+		('uint16', 'PaintFormat', None, None, 'Format identifier-format = 2'),
+		('LOffset', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
+		('Point', 'p0', None, None, ''),
+		('Point', 'p1', None, None, ''),
+		('Point', 'p2', None, None, 'Normal; equal to p1 in simple cases.'),
+	]),
+
+	('PaintFormat3', [
+		('uint16', 'PaintFormat', None, None, 'Format identifier-format = 3'),
+		('LOffset', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
+		('Point', 'c0', None, None, ''),
+		('Point', 'c1', None, None, ''),
+		('VariableDistance', 'r0', None, None, ''),
+		('VariableDistance', 'r1', None, None, ''),
+		('LOffsetTo(Affine2x2)', 'Affine', None, None, 'Offset (from beginning of Paint table) to Affine2x2 subtable.'),
+	]),
 ]
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index 302c9b2..fa8638a 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -5,7 +5,11 @@
 Most are constructed upon import from data in otData.py, all are populated with
 converter objects from otConverters.py.
 """
+from enum import IntEnum
+import itertools
+from collections import namedtuple
 from fontTools.misc.py23 import *
+from fontTools.misc.fixedTools import otRound
 from fontTools.misc.textTools import pad, safeEval
 from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference
 import logging
@@ -1152,6 +1156,100 @@
 			ligs.append(lig)
 
 
+class COLR(BaseTable):
+
+	def decompile(self, reader, font):
+		# COLRv0 is exceptional in that LayerRecordCount appears *after* the
+		# LayerRecordArray it counts, but the parser logic expects Count fields
+		# to always precede the arrays. Here we work around this by parsing the
+		# LayerRecordCount before the rest of the table, and storing it in
+		# the reader's local state.
+		subReader = reader.getSubReader(offset=0)
+		for conv in self.getConverters():
+			if conv.name != "LayerRecordCount":
+				subReader.advance(conv.staticSize)
+				continue
+			conv = self.getConverterByName("LayerRecordCount")
+			reader[conv.name] = conv.read(subReader, font, tableDict={})
+			break
+		else:
+			raise AssertionError("LayerRecordCount converter not found")
+		return BaseTable.decompile(self, reader, font)
+
+	def preWrite(self, font):
+		# The writer similarly assumes Count values precede the things counted,
+		# thus here we pre-initialize a CountReference; the actual count value
+		# will be set to the lenght of the array by the time this is assembled.
+		self.LayerRecordCount = None
+		return {
+			**self.__dict__,
+			"LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount")
+		}
+
+
+class BaseGlyphRecordArray(BaseTable):
+
+	def preWrite(self, font):
+		self.BaseGlyphRecord = sorted(
+			self.BaseGlyphRecord,
+			key=lambda rec: font.getGlyphID(rec.BaseGlyph)
+		)
+		return self.__dict__.copy()
+
+
+class BaseGlyphV1Array(BaseTable):
+
+	def preWrite(self, font):
+		self.BaseGlyphV1Record = sorted(
+			self.BaseGlyphV1Record,
+			key=lambda rec: font.getGlyphID(rec.BaseGlyph)
+		)
+		return self.__dict__.copy()
+
+
+
+class VariableValue(namedtuple("VariableValue", ["value", "varIdx"])):
+	__slots__ = ()
+
+	_value_mapper = None
+
+	def __new__(cls, value, varIdx=0):
+		return super().__new__(
+			cls,
+			cls._value_mapper(value) if cls._value_mapper else value,
+			varIdx
+		)
+
+	@classmethod
+	def _make(cls, iterable):
+		if cls._value_mapper:
+			it = iter(iterable)
+			try:
+				value = next(it)
+			except StopIteration:
+				pass
+			else:
+				value = cls._value_mapper(value)
+				iterable = itertools.chain((value,), it)
+		return super()._make(iterable)
+
+
+class VariableFloat(VariableValue):
+	__slots__ = ()
+	_value_mapper = float
+
+
+class VariableInt(VariableValue):
+	__slots__ = ()
+	_value_mapper = otRound
+
+
+class ExtendMode(IntEnum):
+	PAD = 0
+	REPEAT = 1
+	REFLECT = 2
+
+
 # For each subtable format there is a class. However, we don't really distinguish
 # between "field name" and "format name": often these are the same. Yet there's
 # a whole bunch of fields with different names. The following dict is a mapping
diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO
index 59a6af8..ba60e7c 100644
--- a/Lib/fonttools.egg-info/PKG-INFO
+++ b/Lib/fonttools.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: fonttools
-Version: 4.7.0
+Version: 4.8.0
 Summary: Tools to manipulate font files
 Home-page: http://github.com/fonttools/fonttools
 Author: Just van Rossum
@@ -193,7 +193,7 @@
         
           The module exports a ElementTree-like API for reading/writing XML files, and
           allows to use as the backend either the built-in ``xml.etree`` module or
-          `lxml <https://http://lxml.de>`__. The latter is preferred whenever present,
+          `lxml <https://lxml.de>`__. The latter is preferred whenever present,
           as it is generally faster and more secure.
         
           *Extra:* ``lxml``
@@ -414,13 +414,29 @@
         Changelog
         ~~~~~~~~~
         
+        4.8.0 (released 2020-04-16)
+        ---------------------------
+        
+        - [feaLib] If Parser is initialized without a ``glyphNames`` parameter, it cannot
+          distinguish between a glyph name containing an hyphen, or a range of glyph names;
+          instead of raising an error, it now interprets them as literal glyph names, while
+          also outputting a logging warning to alert user about the ambiguity (#1768, #1870).
+        - [feaLib] When serializing AST to string, emit spaces around hyphens that denote
+          ranges. Also, fixed an issue with CID ranges when round-tripping AST->string->AST
+          (#1872).
+        - [Snippets/otf2ttf] In otf2ttf.py script update LSB in hmtx to match xMin (#1873).
+        - [colorLib] Added experimental support for building ``COLR`` v1 tables as per
+          the `colr-gradients-spec <https://github.com/googlefonts/colr-gradients-spec/blob/master/colr-gradients-spec.md>`__
+          draft proposal. **NOTE**: both the API and the XML dump of ``COLR`` v1 are
+          susceptible to change while the proposal is being discussed and formalized (#1822).
+        
         4.7.0 (released 2020-04-03)
         ---------------------------
         
         - [cu2qu] Added ``fontTools.cu2qu`` package, imported from the original
           `cu2qu <https://github.com/googlefonts/cu2qu>`__ project. The ``cu2qu.pens`` module
           was moved to ``fontTools.pens.cu2quPen``. The optional cu2qu extension module
-          can be compiled by installing `Cython <https://cython.org/>` before installing
+          can be compiled by installing `Cython <https://cython.org/>`__ before installing
           fonttools from source (i.e. git repo or sdist tarball). The wheel package that
           is published on PyPI (i.e. the one ``pip`` downloads, unless ``--no-binary``
           option is used), will continue to be pure-Python for now (#1868).
@@ -2031,13 +2047,13 @@
 Classifier: Topic :: Multimedia :: Graphics
 Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
 Requires-Python: >=3.6
-Provides-Extra: unicode
-Provides-Extra: ufo
-Provides-Extra: woff
-Provides-Extra: type1
 Provides-Extra: graphite
+Provides-Extra: interpolatable
 Provides-Extra: all
+Provides-Extra: unicode
+Provides-Extra: symfont
 Provides-Extra: lxml
 Provides-Extra: plot
-Provides-Extra: symfont
-Provides-Extra: interpolatable
+Provides-Extra: type1
+Provides-Extra: ufo
+Provides-Extra: woff
diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt
index 2924cc2..acf50be 100644
--- a/Lib/fonttools.egg-info/SOURCES.txt
+++ b/Lib/fonttools.egg-info/SOURCES.txt
@@ -519,6 +519,7 @@
 Tests/feaLib/data/GSUB_6.ttx
 Tests/feaLib/data/GSUB_8.fea
 Tests/feaLib/data/GSUB_8.ttx
+Tests/feaLib/data/GSUB_error.fea
 Tests/feaLib/data/GlyphClassDef.fea
 Tests/feaLib/data/GlyphClassDef.ttx
 Tests/feaLib/data/LigatureCaretByIndex.fea
@@ -577,6 +578,8 @@
 Tests/feaLib/data/bug568.ttx
 Tests/feaLib/data/bug633.fea
 Tests/feaLib/data/bug633.ttx
+Tests/feaLib/data/cid_range.fea
+Tests/feaLib/data/cid_range.ttx
 Tests/feaLib/data/enum.fea
 Tests/feaLib/data/enum.ttx
 Tests/feaLib/data/feature_aalt.fea
@@ -873,6 +876,7 @@
 Tests/ttLib/tables/C_B_L_C_test.py
 Tests/ttLib/tables/C_F_F__2_test.py
 Tests/ttLib/tables/C_F_F_test.py
+Tests/ttLib/tables/C_O_L_R_test.py
 Tests/ttLib/tables/C_P_A_L_test.py
 Tests/ttLib/tables/M_V_A_R_test.py
 Tests/ttLib/tables/O_S_2f_2_test.py
diff --git a/METADATA b/METADATA
index 639b948..70ad5b8 100644
--- a/METADATA
+++ b/METADATA
@@ -7,12 +7,12 @@
   }
   url {
     type: ARCHIVE
-    value: "https://github.com/fonttools/fonttools/releases/download/4.7.0/fonttools-4.7.0.zip"
+    value: "https://github.com/fonttools/fonttools/releases/download/4.8.0/fonttools-4.8.0.zip"
   }
-  version: "4.7.0"
+  version: "4.8.0"
   last_upgrade_date {
     year: 2020
     month: 4
-    day: 3
+    day: 16
   }
 }
diff --git a/NEWS.rst b/NEWS.rst
index f292a61..f771591 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,10 +1,26 @@
+4.8.0 (released 2020-04-16)
+---------------------------
+
+- [feaLib] If Parser is initialized without a ``glyphNames`` parameter, it cannot
+  distinguish between a glyph name containing an hyphen, or a range of glyph names;
+  instead of raising an error, it now interprets them as literal glyph names, while
+  also outputting a logging warning to alert user about the ambiguity (#1768, #1870).
+- [feaLib] When serializing AST to string, emit spaces around hyphens that denote
+  ranges. Also, fixed an issue with CID ranges when round-tripping AST->string->AST
+  (#1872).
+- [Snippets/otf2ttf] In otf2ttf.py script update LSB in hmtx to match xMin (#1873).
+- [colorLib] Added experimental support for building ``COLR`` v1 tables as per
+  the `colr-gradients-spec <https://github.com/googlefonts/colr-gradients-spec/blob/master/colr-gradients-spec.md>`__
+  draft proposal. **NOTE**: both the API and the XML dump of ``COLR`` v1 are
+  susceptible to change while the proposal is being discussed and formalized (#1822).
+
 4.7.0 (released 2020-04-03)
 ---------------------------
 
 - [cu2qu] Added ``fontTools.cu2qu`` package, imported from the original
   `cu2qu <https://github.com/googlefonts/cu2qu>`__ project. The ``cu2qu.pens`` module
   was moved to ``fontTools.pens.cu2quPen``. The optional cu2qu extension module
-  can be compiled by installing `Cython <https://cython.org/>` before installing
+  can be compiled by installing `Cython <https://cython.org/>`__ before installing
   fonttools from source (i.e. git repo or sdist tarball). The wheel package that
   is published on PyPI (i.e. the one ``pip`` downloads, unless ``--no-binary``
   option is used), will continue to be pure-Python for now (#1868).
diff --git a/PKG-INFO b/PKG-INFO
index 59a6af8..ba60e7c 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: fonttools
-Version: 4.7.0
+Version: 4.8.0
 Summary: Tools to manipulate font files
 Home-page: http://github.com/fonttools/fonttools
 Author: Just van Rossum
@@ -193,7 +193,7 @@
         
           The module exports a ElementTree-like API for reading/writing XML files, and
           allows to use as the backend either the built-in ``xml.etree`` module or
-          `lxml <https://http://lxml.de>`__. The latter is preferred whenever present,
+          `lxml <https://lxml.de>`__. The latter is preferred whenever present,
           as it is generally faster and more secure.
         
           *Extra:* ``lxml``
@@ -414,13 +414,29 @@
         Changelog
         ~~~~~~~~~
         
+        4.8.0 (released 2020-04-16)
+        ---------------------------
+        
+        - [feaLib] If Parser is initialized without a ``glyphNames`` parameter, it cannot
+          distinguish between a glyph name containing an hyphen, or a range of glyph names;
+          instead of raising an error, it now interprets them as literal glyph names, while
+          also outputting a logging warning to alert user about the ambiguity (#1768, #1870).
+        - [feaLib] When serializing AST to string, emit spaces around hyphens that denote
+          ranges. Also, fixed an issue with CID ranges when round-tripping AST->string->AST
+          (#1872).
+        - [Snippets/otf2ttf] In otf2ttf.py script update LSB in hmtx to match xMin (#1873).
+        - [colorLib] Added experimental support for building ``COLR`` v1 tables as per
+          the `colr-gradients-spec <https://github.com/googlefonts/colr-gradients-spec/blob/master/colr-gradients-spec.md>`__
+          draft proposal. **NOTE**: both the API and the XML dump of ``COLR`` v1 are
+          susceptible to change while the proposal is being discussed and formalized (#1822).
+        
         4.7.0 (released 2020-04-03)
         ---------------------------
         
         - [cu2qu] Added ``fontTools.cu2qu`` package, imported from the original
           `cu2qu <https://github.com/googlefonts/cu2qu>`__ project. The ``cu2qu.pens`` module
           was moved to ``fontTools.pens.cu2quPen``. The optional cu2qu extension module
-          can be compiled by installing `Cython <https://cython.org/>` before installing
+          can be compiled by installing `Cython <https://cython.org/>`__ before installing
           fonttools from source (i.e. git repo or sdist tarball). The wheel package that
           is published on PyPI (i.e. the one ``pip`` downloads, unless ``--no-binary``
           option is used), will continue to be pure-Python for now (#1868).
@@ -2031,13 +2047,13 @@
 Classifier: Topic :: Multimedia :: Graphics
 Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
 Requires-Python: >=3.6
-Provides-Extra: unicode
-Provides-Extra: ufo
-Provides-Extra: woff
-Provides-Extra: type1
 Provides-Extra: graphite
+Provides-Extra: interpolatable
 Provides-Extra: all
+Provides-Extra: unicode
+Provides-Extra: symfont
 Provides-Extra: lxml
 Provides-Extra: plot
-Provides-Extra: symfont
-Provides-Extra: interpolatable
+Provides-Extra: type1
+Provides-Extra: ufo
+Provides-Extra: woff
diff --git a/README.rst b/README.rst
index a6a3533..05b44b7 100644
--- a/README.rst
+++ b/README.rst
@@ -183,7 +183,7 @@
 
   The module exports a ElementTree-like API for reading/writing XML files, and
   allows to use as the backend either the built-in ``xml.etree`` module or
-  `lxml <https://http://lxml.de>`__. The latter is preferred whenever present,
+  `lxml <https://lxml.de>`__. The latter is preferred whenever present,
   as it is generally faster and more secure.
 
   *Extra:* ``lxml``
diff --git a/Snippets/otf2ttf.py b/Snippets/otf2ttf.py
index 7a6bfe8..b925b33 100755
--- a/Snippets/otf2ttf.py
+++ b/Snippets/otf2ttf.py
@@ -38,6 +38,13 @@
     return quadGlyphs
 
 
+def update_hmtx(ttFont, glyf):
+    hmtx = ttFont["hmtx"]
+    for glyphName, glyph in glyf.glyphs.items():
+        if hasattr(glyph, 'xMin'):
+            hmtx[glyphName] = (hmtx[glyphName][0], glyph.xMin)
+
+
 def otf_to_ttf(ttFont, post_format=POST_FORMAT, **kwargs):
     assert ttFont.sfntVersion == "OTTO"
     assert "CFF " in ttFont
@@ -50,6 +57,7 @@
     glyf.glyphs = glyphs_to_quadratic(ttFont.getGlyphSet(), **kwargs)
     del ttFont["CFF "]
     glyf.compile(ttFont)
+    update_hmtx(ttFont, glyf)
 
     ttFont["maxp"] = maxp = newTable("maxp")
     maxp.tableVersion = 0x00010000
diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py
index c517187..24cac8b 100644
--- a/Tests/colorLib/builder_test.py
+++ b/Tests/colorLib/builder_test.py
@@ -1,4 +1,5 @@
 from fontTools.ttLib import newTable
+from fontTools.ttLib.tables import otTables as ot
 from fontTools.colorLib import builder
 from fontTools.colorLib.errors import ColorLibError
 import pytest
@@ -185,3 +186,552 @@
         ),
     ):
         builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]])
+
+
+def test_buildColor():
+    c = builder.buildColor(0)
+    assert c.PaletteIndex == 0
+    assert c.Transparency.value == 0.0
+    assert c.Transparency.varIdx == 0
+
+    c = builder.buildColor(1, transparency=0.5)
+    assert c.PaletteIndex == 1
+    assert c.Transparency.value == 0.5
+    assert c.Transparency.varIdx == 0
+
+    c = builder.buildColor(3, transparency=builder.VariableFloat(0.5, varIdx=2))
+    assert c.PaletteIndex == 3
+    assert c.Transparency.value == 0.5
+    assert c.Transparency.varIdx == 2
+
+
+def test_buildSolidColorPaint():
+    p = builder.buildSolidColorPaint(0)
+    assert p.Format == 1
+    assert p.Color.PaletteIndex == 0
+    assert p.Color.Transparency.value == 0.0
+    assert p.Color.Transparency.varIdx == 0
+
+    p = builder.buildSolidColorPaint(1, transparency=0.5)
+    assert p.Format == 1
+    assert p.Color.PaletteIndex == 1
+    assert p.Color.Transparency.value == 0.5
+    assert p.Color.Transparency.varIdx == 0
+
+    p = builder.buildSolidColorPaint(
+        3, transparency=builder.VariableFloat(0.5, varIdx=2)
+    )
+    assert p.Format == 1
+    assert p.Color.PaletteIndex == 3
+    assert p.Color.Transparency.value == 0.5
+    assert p.Color.Transparency.varIdx == 2
+
+
+def test_buildColorStop():
+    s = builder.buildColorStop(0.1, 2)
+    assert s.StopOffset == builder.VariableFloat(0.1)
+    assert s.Color.PaletteIndex == 2
+    assert s.Color.Transparency == builder._DEFAULT_TRANSPARENCY
+
+    s = builder.buildColorStop(offset=0.2, paletteIndex=3, transparency=0.4)
+    assert s.StopOffset == builder.VariableFloat(0.2)
+    assert s.Color == builder.buildColor(3, transparency=0.4)
+
+    s = builder.buildColorStop(
+        offset=builder.VariableFloat(0.0, varIdx=1),
+        paletteIndex=0,
+        transparency=builder.VariableFloat(0.3, varIdx=2),
+    )
+    assert s.StopOffset == builder.VariableFloat(0.0, varIdx=1)
+    assert s.Color.PaletteIndex == 0
+    assert s.Color.Transparency == builder.VariableFloat(0.3, varIdx=2)
+
+
+def test_buildColorLine():
+    stops = [(0.0, 0), (0.5, 1), (1.0, 2)]
+
+    cline = builder.buildColorLine(stops)
+    assert cline.Extend == builder.ExtendMode.PAD
+    assert cline.StopCount == 3
+    assert [
+        (cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop
+    ] == stops
+
+    cline = builder.buildColorLine(stops, extend="pad")
+    assert cline.Extend == builder.ExtendMode.PAD
+
+    cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REPEAT)
+    assert cline.Extend == builder.ExtendMode.REPEAT
+
+    cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REFLECT)
+    assert cline.Extend == builder.ExtendMode.REFLECT
+
+    cline = builder.buildColorLine([builder.buildColorStop(*s) for s in stops])
+    assert [
+        (cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop
+    ] == stops
+
+    stops = [
+        {"offset": (0.0, 1), "paletteIndex": 0, "transparency": (0.5, 2)},
+        {"offset": (1.0, 3), "paletteIndex": 1, "transparency": (0.3, 4)},
+    ]
+    cline = builder.buildColorLine(stops)
+    assert [
+        {
+            "offset": cs.StopOffset,
+            "paletteIndex": cs.Color.PaletteIndex,
+            "transparency": cs.Color.Transparency,
+        }
+        for cs in cline.ColorStop
+    ] == stops
+
+
+def test_buildPoint():
+    pt = builder.buildPoint(0, 1)
+    assert pt.x == builder.VariableInt(0)
+    assert pt.y == builder.VariableInt(1)
+
+    pt = builder.buildPoint(
+        builder.VariableInt(2, varIdx=1), builder.VariableInt(3, varIdx=2)
+    )
+    assert pt.x == builder.VariableInt(2, varIdx=1)
+    assert pt.y == builder.VariableInt(3, varIdx=2)
+
+    # float coords are rounded
+    pt = builder.buildPoint(x=-2.5, y=3.5)
+    assert pt.x == builder.VariableInt(-2)
+    assert pt.y == builder.VariableInt(4)
+
+    # tuple args are cast to VariableInt namedtuple
+    pt = builder.buildPoint((1, 2), (3, 4))
+    assert pt.x == builder.VariableInt(1, varIdx=2)
+    assert pt.y == builder.VariableInt(3, varIdx=4)
+
+
+def test_buildAffine2x2():
+    matrix = builder.buildAffine2x2(1.5, 0, 0.5, 2.0)
+    assert matrix.xx == builder.VariableFloat(1.5)
+    assert matrix.xy == builder.VariableFloat(0.0)
+    assert matrix.yx == builder.VariableFloat(0.5)
+    assert matrix.yy == builder.VariableFloat(2.0)
+
+
+def test_buildLinearGradientPaint():
+    color_stops = [
+        builder.buildColorStop(0.0, 0),
+        builder.buildColorStop(0.5, 1),
+        builder.buildColorStop(1.0, 2, transparency=0.8),
+    ]
+    color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT)
+    p0 = builder.buildPoint(x=100, y=200)
+    p1 = builder.buildPoint(x=150, y=250)
+
+    gradient = builder.buildLinearGradientPaint(color_line, p0, p1)
+    assert gradient.Format == 2
+    assert gradient.ColorLine == color_line
+    assert gradient.p0 == p0
+    assert gradient.p1 == p1
+    assert gradient.p2 == gradient.p1
+    assert gradient.p2 is not gradient.p1
+
+    gradient = builder.buildLinearGradientPaint({"stops": color_stops}, p0, p1)
+    assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
+    assert gradient.ColorLine.ColorStop == color_stops
+
+    gradient = builder.buildLinearGradientPaint(color_line, p0, p1, p2=(150, 230))
+    assert gradient.p2 == builder.buildPoint(x=150, y=230)
+    assert gradient.p2 != gradient.p1
+
+
+def test_buildRadialGradientPaint():
+    color_stops = [
+        builder.buildColorStop(0.0, 0),
+        builder.buildColorStop(0.5, 1),
+        builder.buildColorStop(1.0, 2, transparency=0.8),
+    ]
+    color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT)
+    c0 = builder.buildPoint(x=100, y=200)
+    c1 = builder.buildPoint(x=150, y=250)
+    r0 = builder.VariableInt(10)
+    r1 = builder.VariableInt(5)
+
+    gradient = builder.buildRadialGradientPaint(color_line, c0, c1, r0, r1)
+    assert gradient.Format == 3
+    assert gradient.ColorLine == color_line
+    assert gradient.c0 == c0
+    assert gradient.c1 == c1
+    assert gradient.r0 == r0
+    assert gradient.r1 == r1
+    assert gradient.Affine is None
+
+    gradient = builder.buildRadialGradientPaint({"stops": color_stops}, c0, c1, r0, r1)
+    assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
+    assert gradient.ColorLine.ColorStop == color_stops
+
+    matrix = builder.buildAffine2x2(2.0, 0.0, 0.0, 2.0)
+    gradient = builder.buildRadialGradientPaint(
+        color_line, c0, c1, r0, r1, affine=matrix
+    )
+    assert gradient.Affine == matrix
+
+    gradient = builder.buildRadialGradientPaint(
+        color_line, c0, c1, r0, r1, affine=(2.0, 0.0, 0.0, 2.0)
+    )
+    assert gradient.Affine == matrix
+
+
+def test_buildLayerV1Record():
+    layer = builder.buildLayerV1Record("a", 2)
+    assert layer.LayerGlyph == "a"
+    assert layer.Paint.Format == 1
+    assert layer.Paint.Color.PaletteIndex == 2
+
+    layer = builder.buildLayerV1Record("a", builder.buildSolidColorPaint(3, 0.9))
+    assert layer.Paint.Format == 1
+    assert layer.Paint.Color.PaletteIndex == 3
+    assert layer.Paint.Color.Transparency.value == 0.9
+
+    layer = builder.buildLayerV1Record(
+        "a",
+        builder.buildLinearGradientPaint(
+            {"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250)
+        ),
+    )
+    assert layer.Paint.Format == 2
+    assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
+    assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 3
+    assert layer.Paint.ColorLine.ColorStop[1].StopOffset.value == 1.0
+    assert layer.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 4
+    assert layer.Paint.p0.x.value == 100
+    assert layer.Paint.p0.y.value == 200
+    assert layer.Paint.p1.x.value == 150
+    assert layer.Paint.p1.y.value == 250
+
+    layer = builder.buildLayerV1Record(
+        "a",
+        builder.buildRadialGradientPaint(
+            {
+                "stops": [
+                    (0.0, 5),
+                    {"offset": 0.5, "paletteIndex": 6, "transparency": 0.8},
+                    (1.0, 7),
+                ]
+            },
+            (50, 50),
+            (75, 75),
+            30,
+            10,
+        ),
+    )
+    assert layer.Paint.Format == 3
+    assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
+    assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 5
+    assert layer.Paint.ColorLine.ColorStop[1].StopOffset.value == 0.5
+    assert layer.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 6
+    assert layer.Paint.ColorLine.ColorStop[1].Color.Transparency.value == 0.8
+    assert layer.Paint.ColorLine.ColorStop[2].StopOffset.value == 1.0
+    assert layer.Paint.ColorLine.ColorStop[2].Color.PaletteIndex == 7
+    assert layer.Paint.c0.x.value == 50
+    assert layer.Paint.c0.y.value == 50
+    assert layer.Paint.c1.x.value == 75
+    assert layer.Paint.c1.y.value == 75
+    assert layer.Paint.r0.value == 30
+    assert layer.Paint.r1.value == 10
+
+
+def test_buildLayerV1Record_from_dict():
+    layer = builder.buildLayerV1Record("a", {"format": 1, "paletteIndex": 0})
+    assert layer.LayerGlyph == "a"
+    assert layer.Paint.Format == 1
+    assert layer.Paint.Color.PaletteIndex == 0
+
+    layer = builder.buildLayerV1Record(
+        "a",
+        {
+            "format": 2,
+            "colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
+            "p0": (0, 0),
+            "p1": (10, 10),
+        },
+    )
+    assert layer.Paint.Format == 2
+    assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
+
+    layer = builder.buildLayerV1Record(
+        "a",
+        {
+            "format": 3,
+            "colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
+            "c0": (0, 0),
+            "c1": (10, 10),
+            "r0": 4,
+            "r1": 0,
+        },
+    )
+    assert layer.Paint.Format == 3
+    assert layer.Paint.r0.value == 4
+
+
+def test_buildLayerV1Array():
+    layers = [
+        ("a", 1),
+        ("b", {"format": 1, "paletteIndex": 2, "transparency": 0.5}),
+        (
+            "c",
+            {
+                "format": 2,
+                "colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "repeat"},
+                "p0": (100, 200),
+                "p1": (150, 250),
+            },
+        ),
+        (
+            "d",
+            {
+                "format": 3,
+                "colorLine": {
+                    "stops": [
+                        {"offset": 0.0, "paletteIndex": 5},
+                        {"offset": 0.5, "paletteIndex": 6, "transparency": 0.8},
+                        {"offset": 1.0, "paletteIndex": 7},
+                    ]
+                },
+                "c0": (50, 50),
+                "c1": (75, 75),
+                "r0": 30,
+                "r1": 10,
+            },
+        ),
+        builder.buildLayerV1Record("e", builder.buildSolidColorPaint(8)),
+    ]
+    layersArray = builder.buildLayerV1Array(layers)
+
+    assert layersArray.LayerCount == len(layersArray.LayerV1Record)
+    assert all(isinstance(l, ot.LayerV1Record) for l in layersArray.LayerV1Record)
+
+
+def test_buildBaseGlyphV1Record():
+    baseGlyphRec = builder.buildBaseGlyphV1Record("a", [("b", 0), ("c", 1)])
+    assert baseGlyphRec.BaseGlyph == "a"
+    assert isinstance(baseGlyphRec.LayerV1Array, ot.LayerV1Array)
+
+    layerArray = builder.buildLayerV1Array([("b", 0), ("c", 1)])
+    baseGlyphRec = builder.buildBaseGlyphV1Record("a", layerArray)
+    assert baseGlyphRec.BaseGlyph == "a"
+    assert baseGlyphRec.LayerV1Array == layerArray
+
+
+def test_buildBaseGlyphV1Array():
+    colorGlyphs = {
+        "a": [("b", 0), ("c", 1)],
+        "d": [
+            ("e", {"format": 1, "paletteIndex": 2, "transparency": 0.8}),
+            (
+                "f",
+                {
+                    "format": 3,
+                    "colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "reflect"},
+                    "c0": (0, 0),
+                    "c1": (0, 0),
+                    "r0": 10,
+                    "r1": 0,
+                },
+            ),
+        ],
+        "g": builder.buildLayerV1Array([("h", 5)]),
+    }
+    glyphMap = {
+        ".notdef": 0,
+        "a": 4,
+        "b": 3,
+        "c": 2,
+        "d": 1,
+        "e": 5,
+        "f": 6,
+        "g": 7,
+        "h": 8,
+    }
+
+    baseGlyphArray = builder.buildBaseGlyphV1Array(colorGlyphs, glyphMap)
+    assert baseGlyphArray.BaseGlyphCount == len(colorGlyphs)
+    assert baseGlyphArray.BaseGlyphV1Record[0].BaseGlyph == "d"
+    assert baseGlyphArray.BaseGlyphV1Record[1].BaseGlyph == "a"
+    assert baseGlyphArray.BaseGlyphV1Record[2].BaseGlyph == "g"
+
+    baseGlyphArray = builder.buildBaseGlyphV1Array(colorGlyphs)
+    assert baseGlyphArray.BaseGlyphCount == len(colorGlyphs)
+    assert baseGlyphArray.BaseGlyphV1Record[0].BaseGlyph == "a"
+    assert baseGlyphArray.BaseGlyphV1Record[1].BaseGlyph == "d"
+    assert baseGlyphArray.BaseGlyphV1Record[2].BaseGlyph == "g"
+
+
+def test_splitSolidAndGradientGlyphs():
+    colorGlyphs = {
+        "a": [
+            ("b", 0),
+            ("c", 1),
+            ("d", {"format": 1, "paletteIndex": 2}),
+            ("e", builder.buildSolidColorPaint(paletteIndex=3)),
+        ]
+    }
+
+    colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
+
+    assert colorGlyphsV0 == {"a": [("b", 0), ("c", 1), ("d", 2), ("e", 3)]}
+    assert not colorGlyphsV1
+
+    colorGlyphs = {
+        "a": [("b", builder.buildSolidColorPaint(paletteIndex=0, transparency=1.0))]
+    }
+
+    colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
+
+    assert not colorGlyphsV0
+    assert colorGlyphsV1 == colorGlyphs
+
+    colorGlyphs = {
+        "a": [("b", 0)],
+        "c": [
+            ("d", 1),
+            (
+                "e",
+                {
+                    "format": 2,
+                    "colorLine": {"stops": [(0.0, 2), (1.0, 3)]},
+                    "p0": (0, 0),
+                    "p1": (10, 10),
+                },
+            ),
+        ],
+    }
+
+    colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
+
+    assert colorGlyphsV0 == {"a": [("b", 0)]}
+    assert "a" not in colorGlyphsV1
+    assert "c" in colorGlyphsV1
+    assert len(colorGlyphsV1["c"]) == 2
+
+    layer_d = colorGlyphsV1["c"][0]
+    assert layer_d[0] == "d"
+    assert isinstance(layer_d[1], ot.Paint)
+    assert layer_d[1].Format == 1
+
+    layer_e = colorGlyphsV1["c"][1]
+    assert layer_e[0] == "e"
+    assert isinstance(layer_e[1], ot.Paint)
+    assert layer_e[1].Format == 2
+
+
+class BuildCOLRTest(object):
+    def test_automatic_version_all_solid_color_glyphs(self):
+        colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]})
+        assert colr.version == 0
+        assert hasattr(colr, "ColorLayers")
+        assert colr.ColorLayers["a"][0].name == "b"
+        assert colr.ColorLayers["a"][1].name == "c"
+
+    def test_automatic_version_no_solid_color_glyphs(self):
+        colr = builder.buildCOLR(
+            {
+                "a": [
+                    (
+                        "b",
+                        {
+                            "format": 3,
+                            "colorLine": {
+                                "stops": [(0.0, 0), (1.0, 1)],
+                                "extend": "repeat",
+                            },
+                            "c0": (1, 0),
+                            "c1": (10, 0),
+                            "r0": 4,
+                            "r1": 2,
+                        },
+                    ),
+                    ("c", {"format": 1, "paletteIndex": 2, "transparency": 0.8}),
+                ],
+                "d": [
+                    (
+                        "e",
+                        {
+                            "format": 2,
+                            "colorLine": {
+                                "stops": [(0.0, 2), (1.0, 3)],
+                                "extend": "reflect",
+                            },
+                            "p0": (1, 2),
+                            "p1": (3, 4),
+                            "p2": (2, 2),
+                        },
+                    )
+                ],
+            }
+        )
+        assert colr.version == 1
+        assert not hasattr(colr, "ColorLayers")
+        assert hasattr(colr, "table")
+        assert isinstance(colr.table, ot.COLR)
+        assert colr.table.BaseGlyphRecordCount == 0
+        assert colr.table.BaseGlyphRecordArray is None
+        assert colr.table.LayerRecordCount == 0
+        assert colr.table.LayerRecordArray is None
+
+    def test_automatic_version_mixed_solid_and_gradient_glyphs(self):
+        colr = builder.buildCOLR(
+            {
+                "a": [("b", 0), ("c", 1)],
+                "d": [
+                    (
+                        "e",
+                        {
+                            "format": 2,
+                            "colorLine": {"stops": [(0.0, 2), (1.0, 3)]},
+                            "p0": (1, 2),
+                            "p1": (3, 4),
+                            "p2": (2, 2),
+                        },
+                    )
+                ],
+            }
+        )
+        assert colr.version == 1
+        assert not hasattr(colr, "ColorLayers")
+        assert hasattr(colr, "table")
+        assert isinstance(colr.table, ot.COLR)
+        assert colr.table.VarStore is None
+
+        assert colr.table.BaseGlyphRecordCount == 1
+        assert isinstance(colr.table.BaseGlyphRecordArray, ot.BaseGlyphRecordArray)
+        assert colr.table.LayerRecordCount == 2
+        assert isinstance(colr.table.LayerRecordArray, ot.LayerRecordArray)
+
+        assert isinstance(colr.table.BaseGlyphV1Array, ot.BaseGlyphV1Array)
+        assert colr.table.BaseGlyphV1Array.BaseGlyphCount == 1
+        assert isinstance(
+            colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0], ot.BaseGlyphV1Record
+        )
+        assert colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0].BaseGlyph == "d"
+        assert isinstance(
+            colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0].LayerV1Array,
+            ot.LayerV1Array,
+        )
+        assert (
+            colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0]
+            .LayerV1Array.LayerV1Record[0]
+            .LayerGlyph
+            == "e"
+        )
+
+    def test_explicit_version_0(self):
+        colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0)
+        assert colr.version == 0
+        assert hasattr(colr, "ColorLayers")
+
+    def test_explicit_version_1(self):
+        colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=1)
+        assert colr.version == 1
+        assert not hasattr(colr, "ColorLayers")
+        assert hasattr(colr, "table")
+        assert isinstance(colr.table, ot.COLR)
+        assert colr.table.VarStore is None
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 0400210..8120d25 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -43,6 +43,7 @@
         damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial
         by feature lookup sub table uni0327 uni0328 e.fina
     """.split()
+    glyphs.extend("cid{:05d}".format(cid) for cid in range(800, 1001 + 1))
     font = TTFont()
     font.setGlyphOrder(glyphs)
     return font
@@ -51,7 +52,7 @@
 class BuilderTest(unittest.TestCase):
     # Feature files in data/*.fea; output gets compared to data/*.ttx.
     TEST_FEATURE_FILES = """
-        Attach enum markClass language_required
+        Attach cid_range enum markClass language_required
         GlyphClassDef LigatureCaretByIndex LigatureCaretByPos
         lookup lookupflag feature_aalt ignore_pos
         GPOS_1 GPOS_1_zero GPOS_2 GPOS_2b GPOS_3 GPOS_4 GPOS_5 GPOS_6 GPOS_8
diff --git a/Tests/feaLib/data/ChainPosSubtable.fea b/Tests/feaLib/data/ChainPosSubtable.fea
index 4650622..e64f886 100644
--- a/Tests/feaLib/data/ChainPosSubtable.fea
+++ b/Tests/feaLib/data/ChainPosSubtable.fea
@@ -1,7 +1,7 @@
 feature test {
-    pos X [A-B]' -40 B' -40 A' -40 Y;
+    pos X [A - B]' -40 B' -40 A' -40 Y;
     subtable;
     pos X A' -111 Y;
     subtable;
-    pos X B' -40 A' -111 [A-C]' -40 Y;
+    pos X B' -40 A' -111 [A - C]' -40 Y;
 } test;
diff --git a/Tests/feaLib/data/GSUB_6.fea b/Tests/feaLib/data/GSUB_6.fea
index 82fdac2..2230670 100644
--- a/Tests/feaLib/data/GSUB_6.fea
+++ b/Tests/feaLib/data/GSUB_6.fea
@@ -1,10 +1,10 @@
 lookup ChainedSingleSubst {
     sub [one two] three A' by A.sc;
-    sub [B-D]' seven [eight nine] by [B.sc-D.sc];
+    sub [B - D]' seven [eight nine] by [B.sc - D.sc];
 } ChainedSingleSubst;
 
 lookup ChainedMultipleSubst {
-    sub [A-C a-c] [D d] E c_t' V [W w] [X-Z x-z] by c t;
+    sub [A - C a - c] [D d] E c_t' V [W w] [X - Z x - z] by c t;
 } ChainedMultipleSubst;
 
 lookup ChainedAlternateSubst {
diff --git a/Tests/feaLib/data/GSUB_8.fea b/Tests/feaLib/data/GSUB_8.fea
index 62c7bee..8f41749 100644
--- a/Tests/feaLib/data/GSUB_8.fea
+++ b/Tests/feaLib/data/GSUB_8.fea
@@ -2,7 +2,7 @@
 
 feature test {
     rsub [a A] [b B] [c C] q' [d D] [e E] [f F] by Q;
-    rsub [a A] [b B] [c C] [s-z]' [d D] [e E] [f F] by [S-Z];
+    rsub [a A] [b B] [c C] [s - z]' [d D] [e E] [f F] by [S - Z];
 
     # Having no context for a reverse chaining substitution rule
     # is a little degenerate (we define a chain without linking it
diff --git a/Tests/feaLib/data/GSUB_error.fea b/Tests/feaLib/data/GSUB_error.fea
new file mode 100644
index 0000000..ae175c5
--- /dev/null
+++ b/Tests/feaLib/data/GSUB_error.fea
@@ -0,0 +1,13 @@
+# Trigger a parser error in the function parse_substitute_ in order to improve the error message.
+# Note that this is not a valid substitution, this test is made to trigger an error.
+
+languagesystem latn dflt;
+
+@base = [a e];
+@accents = [acute grave];
+
+feature abvs {
+    lookup lookup1 {
+        sub @base @accents by @base;
+    } lookup1;
+} abvs;
diff --git a/Tests/feaLib/data/bug514.fea b/Tests/feaLib/data/bug514.fea
index 26da865..1ef5af6 100644
--- a/Tests/feaLib/data/bug514.fea
+++ b/Tests/feaLib/data/bug514.fea
@@ -5,7 +5,7 @@
 # makeotf produces {A:-40, B:-40, C:-40} and {A:-111, B:-40} which
 # is redundant. https://github.com/adobe-type-tools/afdko/issues/169
 feature test {
-    pos X [A-B]' -40 B' -40 A' -40 Y;
+    pos X [A - B]' -40 B' -40 A' -40 Y;
     pos X A' -111 Y;
-    pos X B' -40 A' -111 [A-C]' -40 Y;
+    pos X B' -40 A' -111 [A - C]' -40 Y;
 } test;
diff --git a/Tests/feaLib/data/cid_range.fea b/Tests/feaLib/data/cid_range.fea
new file mode 100644
index 0000000..7a17aed
--- /dev/null
+++ b/Tests/feaLib/data/cid_range.fea
@@ -0,0 +1,6 @@
+# A CID range can be valid even if it is invalid as a glyph name range.
+# For example, [cid00800 - cid01001] is invalid.
+
+feature zero {
+  sub [\800 - \1001] by zero;
+} zero;
diff --git a/Tests/feaLib/data/cid_range.ttx b/Tests/feaLib/data/cid_range.ttx
new file mode 100644
index 0000000..48b502b
--- /dev/null
+++ b/Tests/feaLib/data/cid_range.ttx
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+  <GSUB>
+    <Version value="0x00010000"/>
+    <ScriptList>
+      <!-- ScriptCount=1 -->
+      <ScriptRecord index="0">
+        <ScriptTag value="DFLT"/>
+        <Script>
+          <DefaultLangSys>
+            <ReqFeatureIndex value="65535"/>
+            <!-- FeatureCount=1 -->
+            <FeatureIndex index="0" value="0"/>
+          </DefaultLangSys>
+          <!-- LangSysCount=0 -->
+        </Script>
+      </ScriptRecord>
+    </ScriptList>
+    <FeatureList>
+      <!-- FeatureCount=1 -->
+      <FeatureRecord index="0">
+        <FeatureTag value="zero"/>
+        <Feature>
+          <!-- LookupCount=1 -->
+          <LookupListIndex index="0" value="0"/>
+        </Feature>
+      </FeatureRecord>
+    </FeatureList>
+    <LookupList>
+      <!-- LookupCount=1 -->
+      <Lookup index="0">
+        <LookupType value="1"/>
+        <LookupFlag value="0"/>
+        <!-- SubTableCount=1 -->
+        <SingleSubst index="0">
+          <Substitution in="cid00800" out="zero"/>
+          <Substitution in="cid00801" out="zero"/>
+          <Substitution in="cid00802" out="zero"/>
+          <Substitution in="cid00803" out="zero"/>
+          <Substitution in="cid00804" out="zero"/>
+          <Substitution in="cid00805" out="zero"/>
+          <Substitution in="cid00806" out="zero"/>
+          <Substitution in="cid00807" out="zero"/>
+          <Substitution in="cid00808" out="zero"/>
+          <Substitution in="cid00809" out="zero"/>
+          <Substitution in="cid00810" out="zero"/>
+          <Substitution in="cid00811" out="zero"/>
+          <Substitution in="cid00812" out="zero"/>
+          <Substitution in="cid00813" out="zero"/>
+          <Substitution in="cid00814" out="zero"/>
+          <Substitution in="cid00815" out="zero"/>
+          <Substitution in="cid00816" out="zero"/>
+          <Substitution in="cid00817" out="zero"/>
+          <Substitution in="cid00818" out="zero"/>
+          <Substitution in="cid00819" out="zero"/>
+          <Substitution in="cid00820" out="zero"/>
+          <Substitution in="cid00821" out="zero"/>
+          <Substitution in="cid00822" out="zero"/>
+          <Substitution in="cid00823" out="zero"/>
+          <Substitution in="cid00824" out="zero"/>
+          <Substitution in="cid00825" out="zero"/>
+          <Substitution in="cid00826" out="zero"/>
+          <Substitution in="cid00827" out="zero"/>
+          <Substitution in="cid00828" out="zero"/>
+          <Substitution in="cid00829" out="zero"/>
+          <Substitution in="cid00830" out="zero"/>
+          <Substitution in="cid00831" out="zero"/>
+          <Substitution in="cid00832" out="zero"/>
+          <Substitution in="cid00833" out="zero"/>
+          <Substitution in="cid00834" out="zero"/>
+          <Substitution in="cid00835" out="zero"/>
+          <Substitution in="cid00836" out="zero"/>
+          <Substitution in="cid00837" out="zero"/>
+          <Substitution in="cid00838" out="zero"/>
+          <Substitution in="cid00839" out="zero"/>
+          <Substitution in="cid00840" out="zero"/>
+          <Substitution in="cid00841" out="zero"/>
+          <Substitution in="cid00842" out="zero"/>
+          <Substitution in="cid00843" out="zero"/>
+          <Substitution in="cid00844" out="zero"/>
+          <Substitution in="cid00845" out="zero"/>
+          <Substitution in="cid00846" out="zero"/>
+          <Substitution in="cid00847" out="zero"/>
+          <Substitution in="cid00848" out="zero"/>
+          <Substitution in="cid00849" out="zero"/>
+          <Substitution in="cid00850" out="zero"/>
+          <Substitution in="cid00851" out="zero"/>
+          <Substitution in="cid00852" out="zero"/>
+          <Substitution in="cid00853" out="zero"/>
+          <Substitution in="cid00854" out="zero"/>
+          <Substitution in="cid00855" out="zero"/>
+          <Substitution in="cid00856" out="zero"/>
+          <Substitution in="cid00857" out="zero"/>
+          <Substitution in="cid00858" out="zero"/>
+          <Substitution in="cid00859" out="zero"/>
+          <Substitution in="cid00860" out="zero"/>
+          <Substitution in="cid00861" out="zero"/>
+          <Substitution in="cid00862" out="zero"/>
+          <Substitution in="cid00863" out="zero"/>
+          <Substitution in="cid00864" out="zero"/>
+          <Substitution in="cid00865" out="zero"/>
+          <Substitution in="cid00866" out="zero"/>
+          <Substitution in="cid00867" out="zero"/>
+          <Substitution in="cid00868" out="zero"/>
+          <Substitution in="cid00869" out="zero"/>
+          <Substitution in="cid00870" out="zero"/>
+          <Substitution in="cid00871" out="zero"/>
+          <Substitution in="cid00872" out="zero"/>
+          <Substitution in="cid00873" out="zero"/>
+          <Substitution in="cid00874" out="zero"/>
+          <Substitution in="cid00875" out="zero"/>
+          <Substitution in="cid00876" out="zero"/>
+          <Substitution in="cid00877" out="zero"/>
+          <Substitution in="cid00878" out="zero"/>
+          <Substitution in="cid00879" out="zero"/>
+          <Substitution in="cid00880" out="zero"/>
+          <Substitution in="cid00881" out="zero"/>
+          <Substitution in="cid00882" out="zero"/>
+          <Substitution in="cid00883" out="zero"/>
+          <Substitution in="cid00884" out="zero"/>
+          <Substitution in="cid00885" out="zero"/>
+          <Substitution in="cid00886" out="zero"/>
+          <Substitution in="cid00887" out="zero"/>
+          <Substitution in="cid00888" out="zero"/>
+          <Substitution in="cid00889" out="zero"/>
+          <Substitution in="cid00890" out="zero"/>
+          <Substitution in="cid00891" out="zero"/>
+          <Substitution in="cid00892" out="zero"/>
+          <Substitution in="cid00893" out="zero"/>
+          <Substitution in="cid00894" out="zero"/>
+          <Substitution in="cid00895" out="zero"/>
+          <Substitution in="cid00896" out="zero"/>
+          <Substitution in="cid00897" out="zero"/>
+          <Substitution in="cid00898" out="zero"/>
+          <Substitution in="cid00899" out="zero"/>
+          <Substitution in="cid00900" out="zero"/>
+          <Substitution in="cid00901" out="zero"/>
+          <Substitution in="cid00902" out="zero"/>
+          <Substitution in="cid00903" out="zero"/>
+          <Substitution in="cid00904" out="zero"/>
+          <Substitution in="cid00905" out="zero"/>
+          <Substitution in="cid00906" out="zero"/>
+          <Substitution in="cid00907" out="zero"/>
+          <Substitution in="cid00908" out="zero"/>
+          <Substitution in="cid00909" out="zero"/>
+          <Substitution in="cid00910" out="zero"/>
+          <Substitution in="cid00911" out="zero"/>
+          <Substitution in="cid00912" out="zero"/>
+          <Substitution in="cid00913" out="zero"/>
+          <Substitution in="cid00914" out="zero"/>
+          <Substitution in="cid00915" out="zero"/>
+          <Substitution in="cid00916" out="zero"/>
+          <Substitution in="cid00917" out="zero"/>
+          <Substitution in="cid00918" out="zero"/>
+          <Substitution in="cid00919" out="zero"/>
+          <Substitution in="cid00920" out="zero"/>
+          <Substitution in="cid00921" out="zero"/>
+          <Substitution in="cid00922" out="zero"/>
+          <Substitution in="cid00923" out="zero"/>
+          <Substitution in="cid00924" out="zero"/>
+          <Substitution in="cid00925" out="zero"/>
+          <Substitution in="cid00926" out="zero"/>
+          <Substitution in="cid00927" out="zero"/>
+          <Substitution in="cid00928" out="zero"/>
+          <Substitution in="cid00929" out="zero"/>
+          <Substitution in="cid00930" out="zero"/>
+          <Substitution in="cid00931" out="zero"/>
+          <Substitution in="cid00932" out="zero"/>
+          <Substitution in="cid00933" out="zero"/>
+          <Substitution in="cid00934" out="zero"/>
+          <Substitution in="cid00935" out="zero"/>
+          <Substitution in="cid00936" out="zero"/>
+          <Substitution in="cid00937" out="zero"/>
+          <Substitution in="cid00938" out="zero"/>
+          <Substitution in="cid00939" out="zero"/>
+          <Substitution in="cid00940" out="zero"/>
+          <Substitution in="cid00941" out="zero"/>
+          <Substitution in="cid00942" out="zero"/>
+          <Substitution in="cid00943" out="zero"/>
+          <Substitution in="cid00944" out="zero"/>
+          <Substitution in="cid00945" out="zero"/>
+          <Substitution in="cid00946" out="zero"/>
+          <Substitution in="cid00947" out="zero"/>
+          <Substitution in="cid00948" out="zero"/>
+          <Substitution in="cid00949" out="zero"/>
+          <Substitution in="cid00950" out="zero"/>
+          <Substitution in="cid00951" out="zero"/>
+          <Substitution in="cid00952" out="zero"/>
+          <Substitution in="cid00953" out="zero"/>
+          <Substitution in="cid00954" out="zero"/>
+          <Substitution in="cid00955" out="zero"/>
+          <Substitution in="cid00956" out="zero"/>
+          <Substitution in="cid00957" out="zero"/>
+          <Substitution in="cid00958" out="zero"/>
+          <Substitution in="cid00959" out="zero"/>
+          <Substitution in="cid00960" out="zero"/>
+          <Substitution in="cid00961" out="zero"/>
+          <Substitution in="cid00962" out="zero"/>
+          <Substitution in="cid00963" out="zero"/>
+          <Substitution in="cid00964" out="zero"/>
+          <Substitution in="cid00965" out="zero"/>
+          <Substitution in="cid00966" out="zero"/>
+          <Substitution in="cid00967" out="zero"/>
+          <Substitution in="cid00968" out="zero"/>
+          <Substitution in="cid00969" out="zero"/>
+          <Substitution in="cid00970" out="zero"/>
+          <Substitution in="cid00971" out="zero"/>
+          <Substitution in="cid00972" out="zero"/>
+          <Substitution in="cid00973" out="zero"/>
+          <Substitution in="cid00974" out="zero"/>
+          <Substitution in="cid00975" out="zero"/>
+          <Substitution in="cid00976" out="zero"/>
+          <Substitution in="cid00977" out="zero"/>
+          <Substitution in="cid00978" out="zero"/>
+          <Substitution in="cid00979" out="zero"/>
+          <Substitution in="cid00980" out="zero"/>
+          <Substitution in="cid00981" out="zero"/>
+          <Substitution in="cid00982" out="zero"/>
+          <Substitution in="cid00983" out="zero"/>
+          <Substitution in="cid00984" out="zero"/>
+          <Substitution in="cid00985" out="zero"/>
+          <Substitution in="cid00986" out="zero"/>
+          <Substitution in="cid00987" out="zero"/>
+          <Substitution in="cid00988" out="zero"/>
+          <Substitution in="cid00989" out="zero"/>
+          <Substitution in="cid00990" out="zero"/>
+          <Substitution in="cid00991" out="zero"/>
+          <Substitution in="cid00992" out="zero"/>
+          <Substitution in="cid00993" out="zero"/>
+          <Substitution in="cid00994" out="zero"/>
+          <Substitution in="cid00995" out="zero"/>
+          <Substitution in="cid00996" out="zero"/>
+          <Substitution in="cid00997" out="zero"/>
+          <Substitution in="cid00998" out="zero"/>
+          <Substitution in="cid00999" out="zero"/>
+          <Substitution in="cid01000" out="zero"/>
+          <Substitution in="cid01001" out="zero"/>
+        </SingleSubst>
+      </Lookup>
+    </LookupList>
+  </GSUB>
+
+</ttFont>
diff --git a/Tests/feaLib/data/language_required.fea b/Tests/feaLib/data/language_required.fea
index 4005a78..687c48a 100644
--- a/Tests/feaLib/data/language_required.fea
+++ b/Tests/feaLib/data/language_required.fea
@@ -18,5 +18,5 @@
 } liga;
 
 feature scmp {
-  sub [a-z] by [A.sc-Z.sc];
+  sub [a - z] by [A.sc - Z.sc];
 } scmp;
diff --git a/Tests/feaLib/data/spec4h1.fea b/Tests/feaLib/data/spec4h1.fea
index b43e13b..a3d2494 100644
--- a/Tests/feaLib/data/spec4h1.fea
+++ b/Tests/feaLib/data/spec4h1.fea
@@ -8,7 +8,7 @@
 languagesystem cyrl dflt;
 
 feature smcp {
-    sub [a-z] by [A.sc-Z.sc];
+    sub [a - z] by [A.sc - Z.sc];
 
     # Since all the rules in this feature are of the same type, they
     # will be grouped in a single lookup.  Since no script or language
diff --git a/Tests/feaLib/data/spec5f_ii_2.fea b/Tests/feaLib/data/spec5f_ii_2.fea
index b20a74c..916f797 100644
--- a/Tests/feaLib/data/spec5f_ii_2.fea
+++ b/Tests/feaLib/data/spec5f_ii_2.fea
@@ -3,7 +3,7 @@
 # http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
 
 feature test {
-    @LETTER = [a-z];
+    @LETTER = [a - z];
     ignore sub @LETTER f' i';
     sub f' i' by f_i.begin;
 } test;
diff --git a/Tests/feaLib/data/spec5f_ii_3.fea b/Tests/feaLib/data/spec5f_ii_3.fea
index 5fd1991..af06770 100644
--- a/Tests/feaLib/data/spec5f_ii_3.fea
+++ b/Tests/feaLib/data/spec5f_ii_3.fea
@@ -3,7 +3,7 @@
 # http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
 
 feature test {
-    @LETTER = [a-z];
+    @LETTER = [a - z];
     ignore sub @LETTER a' n' d', a' n' d' @LETTER;
     sub a' n' d' by a_n_d;
 } test;
diff --git a/Tests/feaLib/data/spec5f_ii_4.fea b/Tests/feaLib/data/spec5f_ii_4.fea
index 731a1f6..bc6fda4 100644
--- a/Tests/feaLib/data/spec5f_ii_4.fea
+++ b/Tests/feaLib/data/spec5f_ii_4.fea
@@ -2,13 +2,13 @@
 # "Specifying exceptions to the Chain Sub rule"
 # http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
 
-@LETTER = [A-Z a-z];
+@LETTER = [A - Z a - z];
 
 feature cswh {
 
     # --- Glyph classes used in this feature:
-    @BEGINNINGS = [A-N P-Z T_h m];
-    @BEGINNINGS_SWASH = [A.swash-N.swash P.swash-Z.swash T_h.swash m.begin];
+    @BEGINNINGS = [A - N P - Z T_h m];
+    @BEGINNINGS_SWASH = [A.swash - N.swash P.swash - Z.swash T_h.swash m.begin];
     @ENDINGS = [a e z];
     @ENDINGS_SWASH = [a.end e.end z.end];
 
diff --git a/Tests/feaLib/data/spec5fi3.fea b/Tests/feaLib/data/spec5fi3.fea
index e47a6f8..e44a732 100644
--- a/Tests/feaLib/data/spec5fi3.fea
+++ b/Tests/feaLib/data/spec5fi3.fea
@@ -5,5 +5,5 @@
 languagesystem latn dflt;
 
 feature test {
-    sub [A-Z] [A.sc-Z.sc]' by [a-z];
+    sub [A - Z] [A.sc - Z.sc]' by [a - z];
 } test;
diff --git a/Tests/feaLib/data/spec8a.fea b/Tests/feaLib/data/spec8a.fea
index b4d7dd2..4054821 100644
--- a/Tests/feaLib/data/spec8a.fea
+++ b/Tests/feaLib/data/spec8a.fea
@@ -10,7 +10,7 @@
 } aalt;
 
 feature smcp {
-    sub [a-c] by [A.sc-C.sc];
+    sub [a - c] by [A.sc - C.sc];
     sub f i by f_i;  # not considered for aalt
 } smcp;
 
diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py
index cb4e689..d05a824 100644
--- a/Tests/feaLib/parser_test.py
+++ b/Tests/feaLib/parser_test.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
+from fontTools.misc.loggingTools import CapturingLogHandler
 from fontTools.feaLib.error import FeatureLibError
 from fontTools.feaLib.parser import Parser, SymbolTable
-from fontTools.misc.py23 import *
+from io import StringIO
 import warnings
 import fontTools.feaLib.ast as ast
 import os
@@ -62,7 +63,7 @@
         glyphMap = {'a': 0, 'b': 1, 'c': 2}
         with warnings.catch_warnings(record=True) as w:
             warnings.simplefilter("always")
-            parser = Parser(UnicodeIO(), glyphMap=glyphMap)
+            parser = Parser(StringIO(), glyphMap=glyphMap)
 
             self.assertEqual(len(w), 1)
             self.assertEqual(w[-1].category, UserWarning)
@@ -71,11 +72,11 @@
 
             self.assertRaisesRegex(
                 TypeError, "mutually exclusive",
-                Parser, UnicodeIO(), ("a",), glyphMap={"a": 0})
+                Parser, StringIO(), ("a",), glyphMap={"a": 0})
 
             self.assertRaisesRegex(
                 TypeError, "unsupported keyword argument",
-                Parser, UnicodeIO(), foo="bar")
+                Parser, StringIO(), foo="bar")
 
     def test_comments(self):
         doc = self.parse(
@@ -346,6 +347,19 @@
         [gc] = self.parse("@range = [A-foo.sc - C-foo.sc];", gn).statements
         self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C-foo.sc"))
 
+    def test_glyphclass_ambiguous_dash_no_glyph_names(self):
+        # If Parser is initialized without a glyphNames parameter (or with empty one)
+        # it cannot distinguish between a glyph name containing an hyphen, or a
+        # range of glyph names; thus it will interpret them as literal glyph names
+        # while also outputting a logging warning to alert user about the ambiguity.
+        # https://github.com/fonttools/fonttools/issues/1768
+        glyphNames = ()
+        with CapturingLogHandler("fontTools.feaLib.parser", level="WARNING") as caplog:
+            [gc] = self.parse("@class = [A-foo.sc B-foo.sc C D];", glyphNames).statements
+        self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C", "D"))
+        self.assertEqual(len(caplog.records), 2)
+        caplog.assertRegex("Ambiguous glyph name that looks like a range:")
+
     def test_glyphclass_glyph_name_should_win_over_range(self):
         # The OpenType Feature File Specification v1.20 makes it clear
         # that if a dashed name could be interpreted either as a glyph name
@@ -1502,6 +1516,13 @@
             FeatureLibError,
             'Expected "by", "from" or explicit lookup references',
             self.parse, "feature liga {substitute f f i;} liga;")
+    
+    def test_substitute_invalid_statement(self):
+        self.assertRaisesRegex(
+            FeatureLibError,
+            "Invalid substitution statement",
+            Parser(self.getpath("GSUB_error.fea"), GLYPHNAMES).parse
+        )
 
     def test_subtable(self):
         doc = self.parse("feature test {subtable;} test;")
@@ -1720,7 +1741,7 @@
             self.assertEqual(doc.statements[0].statements, [])
 
     def parse(self, text, glyphNames=GLYPHNAMES, followIncludes=True):
-        featurefile = UnicodeIO(text)
+        featurefile = StringIO(text)
         p = Parser(featurefile, glyphNames, followIncludes=followIncludes)
         return p.parse()
 
diff --git a/Tests/ttLib/tables/C_O_L_R_test.py b/Tests/ttLib/tables/C_O_L_R_test.py
new file mode 100644
index 0000000..52346e1
--- /dev/null
+++ b/Tests/ttLib/tables/C_O_L_R_test.py
@@ -0,0 +1,313 @@
+from fontTools import ttLib
+from fontTools.misc.testTools import getXML, parseXML
+from fontTools.ttLib.tables.C_O_L_R_ import table_C_O_L_R_
+
+import pytest
+
+
+COLR_V0_DATA = (
+    b"\x00\x00"  # Version (0)
+    b"\x00\x01"  # BaseGlyphRecordCount (1)
+    b"\x00\x00\x00\x0e"  # Offset to BaseGlyphRecordArray
+    b"\x00\x00\x00\x14"  # Offset to LayerRecordArray
+    b"\x00\x03"  # LayerRecordCount (3)
+    b"\x00\x06"  # BaseGlyphRecord[0].BaseGlyph (6)
+    b"\x00\x00"  # BaseGlyphRecord[0].FirstLayerIndex (0)
+    b"\x00\x03"  # BaseGlyphRecord[0].NumLayers (3)
+    b"\x00\x07"  # LayerRecord[0].LayerGlyph (7)
+    b"\x00\x00"  # LayerRecord[0].PaletteIndex (0)
+    b"\x00\x08"  # LayerRecord[1].LayerGlyph (8)
+    b"\x00\x01"  # LayerRecord[1].PaletteIndex (1)
+    b"\x00\t"  # LayerRecord[2].LayerGlyph (9)
+    b"\x00\x02"  # LayerRecord[3].PaletteIndex (2)
+)
+
+
+COLR_V0_XML = [
+    '<version value="0"/>',
+    '<ColorGlyph name="glyph00006">',
+    '  <layer colorID="0" name="glyph00007"/>',
+    '  <layer colorID="1" name="glyph00008"/>',
+    '  <layer colorID="2" name="glyph00009"/>',
+    "</ColorGlyph>",
+]
+
+
+def dump(table, ttFont=None):
+    print("\n".join(getXML(table.toXML, ttFont)))
+
+
+@pytest.fixture
+def font():
+    font = ttLib.TTFont()
+    font.setGlyphOrder(["glyph%05d" % i for i in range(30)])
+    return font
+
+
+class COLR_V0_Test(object):
+    def test_decompile_and_compile(self, font):
+        colr = table_C_O_L_R_()
+        colr.decompile(COLR_V0_DATA, font)
+        assert colr.compile(font) == COLR_V0_DATA
+
+    def test_decompile_and_dump_xml(self, font):
+        colr = table_C_O_L_R_()
+        colr.decompile(COLR_V0_DATA, font)
+
+        dump(colr, font)
+        assert getXML(colr.toXML, font) == COLR_V0_XML
+
+    def test_load_from_xml_and_compile(self, font):
+        colr = table_C_O_L_R_()
+        for name, attrs, content in parseXML(COLR_V0_XML):
+            colr.fromXML(name, attrs, content, font)
+
+        assert colr.compile(font) == COLR_V0_DATA
+
+
+COLR_V1_DATA = (
+    b"\x00\x01"  # Version (1)
+    b"\x00\x01"  # BaseGlyphRecordCount (1)
+    b"\x00\x00\x00\x16"  # Offset to BaseGlyphRecordArray
+    b"\x00\x00\x00\x1c"  # Offset to LayerRecordArray
+    b"\x00\x03"  # LayerRecordCount (3)
+    b"\x00\x00\x00("  # Offset to BaseGlyphV1Array
+    b"\x00\x00\x00\x00"  # Offset to VarStore (NULL)
+    b"\x00\x06"  # BaseGlyphRecord[0].BaseGlyph (6)
+    b"\x00\x00"  # BaseGlyphRecord[0].FirstLayerIndex (0)
+    b"\x00\x03"  # BaseGlyphRecord[0].NumLayers (3)
+    b"\x00\x07"  # LayerRecord[0].LayerGlyph (7)
+    b"\x00\x00"  # LayerRecord[0].PaletteIndex (0)
+    b"\x00\x08"  # LayerRecord[1].LayerGlyph (8)
+    b"\x00\x01"  # LayerRecord[1].PaletteIndex (1)
+    b"\x00\t"  # LayerRecord[2].LayerGlyph (9)
+    b"\x00\x02"  # LayerRecord[3].PaletteIndex (2)
+    b"\x00\x00\x00\x01"  # BaseGlyphV1Array.BaseGlyphCount (1)
+    b"\x00\n"  # BaseGlyphV1Array.BaseGlyphV1Record[0].BaseGlyph (10)
+    b"\x00\x00\x00\n"  # Offset to LayerV1Array
+    b"\x00\x00\x00\x03"  # LayerV1Array.LayerCount (3)
+    b"\x00\x0b"  # LayerV1Array.LayerV1Record[0].LayerGlyph (11)
+    b"\x00\x00\x00\x16"  # Offset to Paint
+    b"\x00\x0c"  # LayerV1Array.LayerV1Record[1].LayerGlyph (12)
+    b"\x00\x00\x00 "  # Offset to Paint
+    b"\x00\r"  # LayerV1Array.LayerV1Record[2].LayerGlyph (13)
+    b"\x00\x00\x00x"  # Offset to Paint
+    b"\x00\x01"  # Paint.Format (1)
+    b"\x00\x02"  # Paint.Color.PaletteIndex (2)
+    b" \x00"  # Paint.Color.Transparency.value (0.5)
+    b"\x00\x00\x00\x00"  # Paint.Color.Transparency.varIdx (0)
+    b"\x00\x02"  # Paint.Format (2)
+    b"\x00\x00\x00*"  # Offset to ColorLine
+    b"\x00\x01"  # Paint.p0.x.value (1)
+    b"\x00\x00\x00\x00"  # Paint.p0.x.varIdx (0)
+    b"\x00\x02"  # Paint.p0.y.value (2)
+    b"\x00\x00\x00\x00"  # Paint.p0.y.varIdx (0)
+    b"\xff\xfd"  # Paint.p1.x.value (-3)
+    b"\x00\x00\x00\x00"  # Paint.p1.x.varIdx (0)
+    b"\xff\xfc"  # Paint.p1.y.value (-4)
+    b"\x00\x00\x00\x00"  # Paint.p1.y.varIdx (0)
+    b"\x00\x05"  # Paint.p2.x.value (5)
+    b"\x00\x00\x00\x00"  # Paint.p2.y.varIdx (0)
+    b"\x00\x06"  # Paint.p2.y.value (5)
+    b"\x00\x00\x00\x00"  # Paint.p2.y.varIdx (0)
+    b"\x00\x01"  # ColorLine.Extend (1 or "repeat")
+    b"\x00\x03"  # ColorLine.StopCount (3)
+    b"\x00\x00"  # ColorLine.ColorStop[0].StopOffset.value (0.0)
+    b"\x00\x00\x00\x00"  # ColorLine.ColorStop[0].StopOffset.varIdx (0)
+    b"\x00\x03"  # ColorLine.ColorStop[0].Color.PaletteIndex (3)
+    b"\x00\x00"  # ColorLine.ColorStop[0].Color.Transparency.value (0.0)
+    b"\x00\x00\x00\x00"  # ColorLine.ColorStop[0].Color.Transparency.varIdx (0)
+    b" \x00"  # ColorLine.ColorStop[1].StopOffset.value (0.5)
+    b"\x00\x00\x00\x00"  # ColorLine.ColorStop[1].StopOffset.varIdx (0)
+    b"\x00\x04"  # ColorLine.ColorStop[1].Color.PaletteIndex (4)
+    b"\x00\x00"  # ColorLine.ColorStop[1].Color.Transparency.value (0.0)
+    b"\x00\x00\x00\x00"  # ColorLine.ColorStop[1].Color.Transparency.varIdx (0)
+    b"@\x00"  # ColorLine.ColorStop[2].StopOffset.value (1.0)
+    b"\x00\x00\x00\x00"  # ColorLine.ColorStop[2].StopOffset.varIdx (0)
+    b"\x00\x05"  # ColorLine.ColorStop[2].Color.PaletteIndex (5)
+    b"\x00\x00"  # ColorLine.ColorStop[2].Color.Transparency.value (0.0)
+    b"\x00\x00\x00\x00"  # ColorLine.ColorStop[2].Color.Transparency.varIdx (0)
+    b"\x00\x03"  # Paint.Format (3)
+    b"\x00\x00\x00."  # Offset to ColorLine
+    b"\x00\x07"  # Paint.c0.x.value (7)
+    b"\x00\x00\x00\x00"
+    b"\x00\x08"  # Paint.c0.y.value (8)
+    b"\x00\x00\x00\x00"
+    b"\x00\t"  # Paint.c1.x.value (9)
+    b"\x00\x00\x00\x00"
+    b"\x00\n"  # Paint.c1.y.value (10)
+    b"\x00\x00\x00\x00"
+    b"\x00\x0b"  # Paint.r0.value (11)
+    b"\x00\x00\x00\x00"
+    b"\x00\x0c"  # Paint.r1.value (12)
+    b"\x00\x00\x00\x00"
+    b"\x00\x00\x00N"  # Offset to Affine2x2
+    b"\x00\x00"  # ColorLine.Extend (0 or "pad")
+    b"\x00\x02"  # ColorLine.StopCount (2)
+    b"\x00\x00"  # ColorLine.ColorStop[0].StopOffset.value (0.0)
+    b"\x00\x00\x00\x00"
+    b"\x00\x06"  # ColorLine.ColorStop[0].Color.PaletteIndex (6)
+    b"\x00\x00"
+    b"\x00\x00\x00\x00"
+    b"@\x00"  # ColorLine.ColorStop[1].StopOffset.value (1.0)
+    b"\x00\x00\x00\x00"
+    b"\x00\x07"  # ColorLine.ColorStop[1].Color.PaletteIndex (7)
+    b"\x19\x9a"  # ColorLine.ColorStop[1].Color.Transparency.value (0.4)
+    b"\x00\x00\x00\x00"
+    b"\xff\xf3\x00\x00"  # Affine2x2.xx.value (-13)
+    b"\x00\x00\x00\x00"
+    b"\x00\x0e\x00\x00"  # Affine2x2.xy.value (14)
+    b"\x00\x00\x00\x00"
+    b"\x00\x0f\x00\x00"  # Affine2x2.yx.value (15)
+    b"\x00\x00\x00\x00"
+    b"\xff\xef\x00\x00"  # Affine2x2.yy.value (-17)
+    b"\x00\x00\x00\x00"
+)
+
+
+COLR_V1_XML = [
+    '<Version value="1"/>',
+    "<!-- BaseGlyphRecordCount=1 -->",
+    "<BaseGlyphRecordArray>",
+    '  <BaseGlyphRecord index="0">',
+    '    <BaseGlyph value="glyph00006"/>',
+    '    <FirstLayerIndex value="0"/>',
+    '    <NumLayers value="3"/>',
+    "  </BaseGlyphRecord>",
+    "</BaseGlyphRecordArray>",
+    "<LayerRecordArray>",
+    '  <LayerRecord index="0">',
+    '    <LayerGlyph value="glyph00007"/>',
+    '    <PaletteIndex value="0"/>',
+    "  </LayerRecord>",
+    '  <LayerRecord index="1">',
+    '    <LayerGlyph value="glyph00008"/>',
+    '    <PaletteIndex value="1"/>',
+    "  </LayerRecord>",
+    '  <LayerRecord index="2">',
+    '    <LayerGlyph value="glyph00009"/>',
+    '    <PaletteIndex value="2"/>',
+    "  </LayerRecord>",
+    "</LayerRecordArray>",
+    "<!-- LayerRecordCount=3 -->",
+    "<BaseGlyphV1Array>",
+    "  <!-- BaseGlyphCount=1 -->",
+    '  <BaseGlyphV1Record index="0">',
+    '    <BaseGlyph value="glyph00010"/>',
+    "    <LayerV1Array>",
+    "      <!-- LayerCount=3 -->",
+    '      <LayerV1Record index="0">',
+    '        <LayerGlyph value="glyph00011"/>',
+    '        <Paint Format="1">',
+    "          <Color>",
+    '            <PaletteIndex value="2"/>',
+    '            <Transparency value="0.5"/>',
+    "          </Color>",
+    "        </Paint>",
+    "      </LayerV1Record>",
+    '      <LayerV1Record index="1">',
+    '        <LayerGlyph value="glyph00012"/>',
+    '        <Paint Format="2">',
+    "          <ColorLine>",
+    '            <Extend value="repeat"/>',
+    "            <!-- StopCount=3 -->",
+    '            <ColorStop index="0">',
+    '              <StopOffset value="0.0"/>',
+    "              <Color>",
+    '                <PaletteIndex value="3"/>',
+    '                <Transparency value="0.0"/>',
+    "              </Color>",
+    "            </ColorStop>",
+    '            <ColorStop index="1">',
+    '              <StopOffset value="0.5"/>',
+    "              <Color>",
+    '                <PaletteIndex value="4"/>',
+    '                <Transparency value="0.0"/>',
+    "              </Color>",
+    "            </ColorStop>",
+    '            <ColorStop index="2">',
+    '              <StopOffset value="1.0"/>',
+    "              <Color>",
+    '                <PaletteIndex value="5"/>',
+    '                <Transparency value="0.0"/>',
+    "              </Color>",
+    "            </ColorStop>",
+    "          </ColorLine>",
+    "          <p0>",
+    '            <x value="1"/>',
+    '            <y value="2"/>',
+    "          </p0>",
+    "          <p1>",
+    '            <x value="-3"/>',
+    '            <y value="-4"/>',
+    "          </p1>",
+    "          <p2>",
+    '            <x value="5"/>',
+    '            <y value="6"/>',
+    "          </p2>",
+    "        </Paint>",
+    "      </LayerV1Record>",
+    '      <LayerV1Record index="2">',
+    '        <LayerGlyph value="glyph00013"/>',
+    '        <Paint Format="3">',
+    "          <ColorLine>",
+    '            <Extend value="pad"/>',
+    "            <!-- StopCount=2 -->",
+    '            <ColorStop index="0">',
+    '              <StopOffset value="0.0"/>',
+    "              <Color>",
+    '                <PaletteIndex value="6"/>',
+    '                <Transparency value="0.0"/>',
+    "              </Color>",
+    "            </ColorStop>",
+    '            <ColorStop index="1">',
+    '              <StopOffset value="1.0"/>',
+    "              <Color>",
+    '                <PaletteIndex value="7"/>',
+    '                <Transparency value="0.4"/>',
+    "              </Color>",
+    "            </ColorStop>",
+    "          </ColorLine>",
+    "          <c0>",
+    '            <x value="7"/>',
+    '            <y value="8"/>',
+    "          </c0>",
+    "          <c1>",
+    '            <x value="9"/>',
+    '            <y value="10"/>',
+    "          </c1>",
+    '          <r0 value="11"/>',
+    '          <r1 value="12"/>',
+    "          <Affine>",
+    '            <xx value="-13.0"/>',
+    '            <xy value="14.0"/>',
+    '            <yx value="15.0"/>',
+    '            <yy value="-17.0"/>',
+    "          </Affine>",
+    "        </Paint>",
+    "      </LayerV1Record>",
+    "    </LayerV1Array>",
+    "  </BaseGlyphV1Record>",
+    "</BaseGlyphV1Array>",
+]
+
+
+class COLR_V1_Test(object):
+    def test_decompile_and_compile(self, font):
+        colr = table_C_O_L_R_()
+        colr.decompile(COLR_V1_DATA, font)
+        assert colr.compile(font) == COLR_V1_DATA
+
+    def test_decompile_and_dump_xml(self, font):
+        colr = table_C_O_L_R_()
+        colr.decompile(COLR_V1_DATA, font)
+
+        dump(colr, font)
+        assert getXML(colr.toXML, font) == COLR_V1_XML
+
+    def test_load_from_xml_and_compile(self, font):
+        colr = table_C_O_L_R_()
+        for name, attrs, content in parseXML(COLR_V1_XML):
+            colr.fromXML(name, attrs, content, font)
+
+        assert colr.compile(font) == COLR_V1_DATA
diff --git a/setup.cfg b/setup.cfg
index 000db1f..51f7fac 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 4.7.0
+current_version = 4.8.0
 commit = True
 tag = False
 tag_name = {new_version}
diff --git a/setup.py b/setup.py
index f9ad12a..20fee0b 100755
--- a/setup.py
+++ b/setup.py
@@ -437,7 +437,7 @@
 
 setup_params = dict(
 	name="fonttools",
-	version="4.7.0",
+	version="4.8.0",
 	description="Tools to manipulate font files",
 	author="Just van Rossum",
 	author_email="just@letterror.com",