Upgrade fonttools to 4.19.0 am: f8d82f7a26 am: 104832b19d am: 5bd3cf43f5 am: 4d11c5532a

Original change: https://android-review.googlesource.com/c/platform/external/fonttools/+/1559685

MUST ONLY BE SUBMITTED BY AUTOMERGER

Change-Id: Ifd89f1a0c501b1791a25fc605d531011daf1dda0
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 09dce76..cc7d69f 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -4,6 +4,6 @@
 
 log = logging.getLogger(__name__)
 
-version = __version__ = "4.18.2"
+version = __version__ = "4.19.0"
 
 __all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py
index 724136a..998ab60 100644
--- a/Lib/fontTools/colorLib/builder.py
+++ b/Lib/fontTools/colorLib/builder.py
@@ -6,6 +6,7 @@
 import copy
 import enum
 from functools import partial
+from math import ceil, log
 from typing import (
     Any,
     Dict,
@@ -34,6 +35,7 @@
     VariableInt,
 )
 from .errors import ColorLibError
+from .geometry import round_start_circle_stable_containment
 
 
 # TODO move type aliases to colorLib.types?
@@ -328,9 +330,9 @@
 
 def _to_variable_value(
     value: _ScalarInput,
-    minValue: _Number,
-    maxValue: _Number,
-    cls: Type[VariableValue],
+    cls: Type[VariableValue] = VariableFloat,
+    minValue: Optional[_Number] = None,
+    maxValue: Optional[_Number] = None,
 ) -> VariableValue:
     if not isinstance(value, cls):
         try:
@@ -339,9 +341,9 @@
             value = cls(value)
         else:
             value = cls._make(it)
-    if value.value < minValue:
+    if minValue is not None and value.value < minValue:
         raise OverflowError(f"{cls.__name__}: {value.value} < {minValue}")
-    if value.value > maxValue:
+    if maxValue is not None and value.value > maxValue:
         raise OverflowError(f"{cls.__name__}: {value.value} < {maxValue}")
     return value
 
@@ -526,7 +528,21 @@
         ot_paint.Format = int(ot.Paint.Format.PaintRadialGradient)
         ot_paint.ColorLine = _to_color_line(colorLine)
 
-        for i, (x, y), r in [(0, c0, r0), (1, c1, r1)]:
+        # normalize input types (which may or may not specify a varIdx)
+        x0, y0 = _to_variable_value(c0[0]), _to_variable_value(c0[1])
+        r0 = _to_variable_value(r0)
+        x1, y1 = _to_variable_value(c1[0]), _to_variable_value(c1[1])
+        r1 = _to_variable_value(r1)
+
+        # avoid abrupt change after rounding when c0 is near c1's perimeter
+        c = round_start_circle_stable_containment(
+            (x0.value, y0.value), r0.value, (x1.value, y1.value), r1.value
+        )
+        x0, y0 = x0._replace(value=c.centre[0]), y0._replace(value=c.centre[1])
+        r0 = r0._replace(value=c.radius)
+
+        for i, (x, y, r) in enumerate(((x0, y0, r0), (x1, y1, r1))):
+            # rounding happens here as floats are converted to integers
             setattr(ot_paint, f"x{i}", _to_variable_int16(x))
             setattr(ot_paint, f"y{i}", _to_variable_int16(y))
             setattr(ot_paint, f"r{i}", _to_variable_uint16(r))
@@ -617,7 +633,10 @@
         ot_paint.Format = int(ot.Paint.Format.PaintColrLayers)
         self.slices.append(ot_paint)
 
-        paints = [self.buildPaint(p) for p in paints]
+        paints = [
+            self.buildPaint(p)
+            for p in _build_n_ary_tree(paints, n=MAX_PAINT_COLR_LAYER_COUNT)
+        ]
 
         # Look for reuse, with preference to longer sequences
         found_reuse = True
@@ -761,3 +780,45 @@
     glyphs.BaseGlyphCount = len(baseGlyphs)
     glyphs.BaseGlyphV1Record = baseGlyphs
     return (layers, glyphs)
+
+
+def _build_n_ary_tree(leaves, n):
+    """Build N-ary tree from sequence of leaf nodes.
+
+    Return a list of lists where each non-leaf node is a list containing
+    max n nodes.
+    """
+    if not leaves:
+        return []
+
+    assert n > 1
+
+    depth = ceil(log(len(leaves), n))
+
+    if depth <= 1:
+        return list(leaves)
+
+    # Fully populate complete subtrees of root until we have enough leaves left
+    root = []
+    unassigned = None
+    full_step = n ** (depth - 1)
+    for i in range(0, len(leaves), full_step):
+        subtree = leaves[i : i + full_step]
+        if len(subtree) < full_step:
+            unassigned = subtree
+            break
+        while len(subtree) > n:
+            subtree = [subtree[k : k + n] for k in range(0, len(subtree), n)]
+        root.append(subtree)
+
+    if unassigned:
+        # Recurse to fill the last subtree, which is the only partially populated one
+        subtree = _build_n_ary_tree(unassigned, n)
+        if len(subtree) <= n - len(root):
+            # replace last subtree with its children if they can still fit
+            root.extend(subtree)
+        else:
+            root.append(subtree)
+        assert len(root) <= n
+
+    return root
diff --git a/Lib/fontTools/colorLib/geometry.py b/Lib/fontTools/colorLib/geometry.py
new file mode 100644
index 0000000..ec64753
--- /dev/null
+++ b/Lib/fontTools/colorLib/geometry.py
@@ -0,0 +1,145 @@
+"""Helpers for manipulating 2D points and vectors in COLR table."""
+
+from math import copysign, cos, hypot, pi
+from fontTools.misc.fixedTools import otRound
+
+
+def _vector_between(origin, target):
+    return (target[0] - origin[0], target[1] - origin[1])
+
+
+def _round_point(pt):
+    return (otRound(pt[0]), otRound(pt[1]))
+
+
+def _unit_vector(vec):
+    length = hypot(*vec)
+    if length == 0:
+        return None
+    return (vec[0] / length, vec[1] / length)
+
+
+# This is the same tolerance used by Skia's SkTwoPointConicalGradient.cpp to detect
+# when a radial gradient's focal point lies on the end circle.
+_NEARLY_ZERO = 1 / (1 << 12)  # 0.000244140625
+
+
+# The unit vector's X and Y components are respectively
+#   U = (cos(α), sin(α))
+# where α is the angle between the unit vector and the positive x axis.
+_UNIT_VECTOR_THRESHOLD = cos(3 / 8 * pi)  # == sin(1/8 * pi) == 0.38268343236508984
+
+
+def _rounding_offset(direction):
+    # Return 2-tuple of -/+ 1.0 or 0.0 approximately based on the direction vector.
+    # We divide the unit circle in 8 equal slices oriented towards the cardinal
+    # (N, E, S, W) and intermediate (NE, SE, SW, NW) directions. To each slice we
+    # map one of the possible cases: -1, 0, +1 for either X and Y coordinate.
+    # E.g. Return (+1.0, -1.0) if unit vector is oriented towards SE, or
+    # (-1.0, 0.0) if it's pointing West, etc.
+    uv = _unit_vector(direction)
+    if not uv:
+        return (0, 0)
+
+    result = []
+    for uv_component in uv:
+        if -_UNIT_VECTOR_THRESHOLD <= uv_component < _UNIT_VECTOR_THRESHOLD:
+            # unit vector component near 0: direction almost orthogonal to the
+            # direction of the current axis, thus keep coordinate unchanged
+            result.append(0)
+        else:
+            # nudge coord by +/- 1.0 in direction of unit vector
+            result.append(copysign(1.0, uv_component))
+    return tuple(result)
+
+
+class Circle:
+    def __init__(self, centre, radius):
+        self.centre = centre
+        self.radius = radius
+
+    def __repr__(self):
+        return f"Circle(centre={self.centre}, radius={self.radius})"
+
+    def round(self):
+        return Circle(_round_point(self.centre), otRound(self.radius))
+
+    def inside(self, outer_circle):
+        dist = self.radius + hypot(*_vector_between(self.centre, outer_circle.centre))
+        return (
+            abs(outer_circle.radius - dist) <= _NEARLY_ZERO
+            or outer_circle.radius > dist
+        )
+
+    def concentric(self, other):
+        return self.centre == other.centre
+
+    def move(self, dx, dy):
+        self.centre = (self.centre[0] + dx, self.centre[1] + dy)
+
+
+def round_start_circle_stable_containment(c0, r0, c1, r1):
+    """Round start circle so that it stays inside/outside end circle after rounding.
+
+    The rounding of circle coordinates to integers may cause an abrupt change
+    if the start circle c0 is so close to the end circle c1's perimiter that
+    it ends up falling outside (or inside) as a result of the rounding.
+    To keep the gradient unchanged, we nudge it in the right direction.
+
+    See:
+    https://github.com/googlefonts/colr-gradients-spec/issues/204
+    https://github.com/googlefonts/picosvg/issues/158
+    """
+    start, end = Circle(c0, r0), Circle(c1, r1)
+
+    inside_before_round = start.inside(end)
+
+    round_start = start.round()
+    round_end = end.round()
+    inside_after_round = round_start.inside(round_end)
+
+    if inside_before_round == inside_after_round:
+        return round_start
+    elif inside_after_round:
+        # start was outside before rounding: we need to push start away from end
+        direction = _vector_between(round_end.centre, round_start.centre)
+        radius_delta = +1.0
+    else:
+        # start was inside before rounding: we need to push start towards end
+        direction = _vector_between(round_start.centre, round_end.centre)
+        radius_delta = -1.0
+    dx, dy = _rounding_offset(direction)
+
+    # At most 2 iterations ought to be enough to converge. Before the loop, we
+    # know the start circle didn't keep containment after normal rounding; thus
+    # we continue adjusting by -/+ 1.0 until containment is restored.
+    # Normal rounding can at most move each coordinates -/+0.5; in the worst case
+    # both the start and end circle's centres and radii will be rounded in opposite
+    # directions, e.g. when they move along a 45 degree diagonal:
+    #   c0 = (1.5, 1.5) ===> (2.0, 2.0)
+    #   r0 = 0.5 ===> 1.0
+    #   c1 = (0.499, 0.499) ===> (0.0, 0.0)
+    #   r1 = 2.499 ===> 2.0
+    # In this example, the relative distance between the circles, calculated
+    # as r1 - (r0 + distance(c0, c1)) is initially 0.57437 (c0 is inside c1), and
+    # -1.82842 after rounding (c0 is now outside c1). Nudging c0 by -1.0 on both
+    # x and y axes moves it towards c1 by hypot(-1.0, -1.0) = 1.41421. Two of these
+    # moves cover twice that distance, which is enough to restore containment.
+    max_attempts = 2
+    for _ in range(max_attempts):
+        if round_start.concentric(round_end):
+            # can't move c0 towards c1 (they are the same), so we change the radius
+            round_start.radius += radius_delta
+            assert round_start.radius >= 0
+        else:
+            round_start.move(dx, dy)
+        if inside_before_round == round_start.inside(round_end):
+            break
+    else:  # likely a bug
+        raise AssertionError(
+            f"Rounding circle {start} "
+            f"{'inside' if inside_before_round else 'outside'} "
+            f"{end} failed after {max_attempts} attempts!"
+        )
+
+    return round_start
diff --git a/Lib/fontTools/encodings/codecs.py b/Lib/fontTools/encodings/codecs.py
index ac2b990..c2288a7 100644
--- a/Lib/fontTools/encodings/codecs.py
+++ b/Lib/fontTools/encodings/codecs.py
@@ -16,43 +16,29 @@
 		self.info = codecs.CodecInfo(name=self.name, encode=self.encode, decode=self.decode)
 		codecs.register_error(name, self.error)
 
-	def encode(self, input, errors='strict'):
-		assert errors == 'strict'
-		#return codecs.encode(input, self.base_encoding, self.name), len(input)
-
-		# The above line could totally be all we needed, relying on the error
-		# handling to replace the unencodable Unicode characters with our extended
-		# byte sequences.
-		#
-		# However, there seems to be a design bug in Python (probably intentional):
-		# the error handler for encoding is supposed to return a **Unicode** character,
-		# that then needs to be encodable itself...  Ugh.
-		#
-		# So we implement what codecs.encode() should have been doing: which is expect
-		# error handler to return bytes() to be added to the output.
-		#
-		# This seems to have been fixed in Python 3.3.  We should try using that and
-		# use fallback only if that failed.
-		# https://docs.python.org/3.3/library/codecs.html#codecs.register_error
-
+	def _map(self, mapper, output_type, exc_type, input, errors):
+		base_error_handler = codecs.lookup_error(errors)
 		length = len(input)
-		out = b''
+		out = output_type()
 		while input:
+			# first try to use self.error as the error handler
 			try:
-				part = codecs.encode(input, self.base_encoding)
+				part = mapper(input, self.base_encoding, errors=self.name)
 				out += part
-				input = '' # All converted
-			except UnicodeEncodeError as e:
-				# Convert the correct part
-				out += codecs.encode(input[:e.start], self.base_encoding)
-				replacement, pos = self.error(e)
+				break  # All converted
+			except exc_type as e:
+				# else convert the correct part, handle error as requested and continue
+				out += mapper(input[:e.start], self.base_encoding, self.name)
+				replacement, pos = base_error_handler(e)
 				out += replacement
 				input = input[pos:]
 		return out, length
 
+	def encode(self, input, errors='strict'):
+		return self._map(codecs.encode, bytes, UnicodeEncodeError, input, errors)
+
 	def decode(self, input, errors='strict'):
-		assert errors == 'strict'
-		return codecs.decode(input, self.base_encoding, self.name), len(input)
+		return self._map(codecs.decode, str, UnicodeDecodeError, input, errors)
 
 	def error(self, e):
 		if isinstance(e, UnicodeDecodeError):
diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py
index 7e14445..029aa3f 100644
--- a/Lib/fontTools/otlLib/builder.py
+++ b/Lib/fontTools/otlLib/builder.py
@@ -2574,7 +2574,8 @@
             return
         self.classes_.add(glyphs)
         for glyph in glyphs:
-            assert glyph not in self.glyphs_
+            if glyph in self.glyphs_:
+                raise OpenTypeLibError(f"Glyph {glyph} is already present in class.", None)
             self.glyphs_[glyph] = glyphs
 
     def classes(self):
diff --git a/Lib/fontTools/pens/basePen.py b/Lib/fontTools/pens/basePen.py
index 1593024..c8c4c55 100644
--- a/Lib/fontTools/pens/basePen.py
+++ b/Lib/fontTools/pens/basePen.py
@@ -147,6 +147,10 @@
 	pass
 
 
+class MissingComponentError(KeyError):
+	"""Indicates a component pointing to a non-existent glyph in the glyphset."""
+
+
 class DecomposingPen(LoggingPen):
 
 	""" Implements a 'addComponent' method that decomposes components
@@ -155,10 +159,12 @@
 
 	You must override moveTo, lineTo, curveTo and qCurveTo. You may
 	additionally override closePath, endPath and addComponent.
+
+	By default a warning message is logged when a base glyph is missing;
+	set the class variable ``skipMissingComponents`` to False if you want
+	to raise a :class:`MissingComponentError` exception.
 	"""
 
-	# By default a warning message is logged when a base glyph is missing;
-	# set this to False if you want to raise a 'KeyError' exception
 	skipMissingComponents = True
 
 	def __init__(self, glyphSet):
@@ -176,7 +182,7 @@
 			glyph = self.glyphSet[glyphName]
 		except KeyError:
 			if not self.skipMissingComponents:
-				raise
+				raise MissingComponentError(glyphName)
 			self.log.warning(
 				"glyph '%s' is missing from glyphSet; skipped" % glyphName)
 		else:
diff --git a/Lib/fontTools/pens/hashPointPen.py b/Lib/fontTools/pens/hashPointPen.py
index f3276f7..9aef5d8 100644
--- a/Lib/fontTools/pens/hashPointPen.py
+++ b/Lib/fontTools/pens/hashPointPen.py
@@ -1,6 +1,7 @@
 # Modified from https://github.com/adobe-type-tools/psautohint/blob/08b346865710ed3c172f1eb581d6ef243b203f99/python/psautohint/ufoFont.py#L800-L838
 import hashlib
 
+from fontTools.pens.basePen import MissingComponentError
 from fontTools.pens.pointPen import AbstractPointPen
 
 
@@ -69,5 +70,8 @@
     ):
         tr = "".join([f"{t:+}" for t in transformation])
         self.data.append("[")
-        self.glyphset[baseGlyphName].drawPoints(self)
+        try:
+            self.glyphset[baseGlyphName].drawPoints(self)
+        except KeyError:
+            raise MissingComponentError(baseGlyphName)
         self.data.append(f"({tr})]")
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index d78aa8a..82605d5 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -2207,7 +2207,17 @@
 @_add_method(ttLib.getTableClass('cmap'))
 def subset_glyphs(self, s):
 	s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only
+
+	tables_format12_bmp = []
+	table_plat0_enc3 = {}  # Unicode platform, Unicode BMP only, keyed by language
+	table_plat3_enc1 = {}  # Windows platform, Unicode BMP, keyed by language
+
 	for t in self.tables:
+		if t.platformID == 0 and t.platEncID == 3:
+			table_plat0_enc3[t.language] = t
+		if t.platformID == 3 and t.platEncID == 1:
+			table_plat3_enc1[t.language] = t
+
 		if t.format == 14:
 			# TODO(behdad) We drop all the default-UVS mappings
 			# for glyphs_requested.  So it's the caller's responsibility to make
@@ -2219,16 +2229,38 @@
 		elif t.isUnicode():
 			t.cmap = {u:g for u,g in t.cmap.items()
 				      if g in s.glyphs_requested or u in s.unicodes_requested}
+			# Collect format 12 tables that hold only basic multilingual plane
+			# codepoints.
+			if t.format == 12 and t.cmap and max(t.cmap.keys()) < 0x10000:
+				tables_format12_bmp.append(t)
 		else:
 			t.cmap = {u:g for u,g in t.cmap.items()
 				      if g in s.glyphs_requested}
+
+	# Fomat 12 tables are redundant if they contain just the same BMP codepoints
+	# their little BMP-only encoding siblings contain.
+	for t in tables_format12_bmp:
+		if (
+			t.platformID == 0  # Unicode platform
+			and t.platEncID == 4  # Unicode full repertoire
+			and t.language in table_plat0_enc3  # Have a BMP-only sibling?
+			and table_plat0_enc3[t.language].cmap == t.cmap
+		):
+			t.cmap.clear()
+		elif (
+			t.platformID == 3  # Windows platform
+			and t.platEncID == 10  # Unicode full repertoire
+			and t.language in table_plat3_enc1  # Have a BMP-only sibling?
+			and table_plat3_enc1[t.language].cmap == t.cmap
+		):
+			t.cmap.clear()
+
 	self.tables = [t for t in self.tables
 			 if (t.cmap if t.format != 14 else t.uvsDict)]
 	self.numSubTables = len(self.tables)
 	# TODO(behdad) Convert formats when needed.
 	# In particular, if we have a format=12 without non-BMP
-	# characters, either drop format=12 one or convert it
-	# to format=4 if there's not one.
+	# characters, convert it to format=4 if there's not one.
 	return True # Required table
 
 @_add_method(ttLib.getTableClass('DSIG'))
diff --git a/Lib/fontTools/varLib/featureVars.py b/Lib/fontTools/varLib/featureVars.py
index 76e8cc4..45f3d83 100644
--- a/Lib/fontTools/varLib/featureVars.py
+++ b/Lib/fontTools/varLib/featureVars.py
@@ -10,7 +10,7 @@
 from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable
 from collections import OrderedDict
 
-from .errors import VarLibValidationError
+from .errors import VarLibError, VarLibValidationError
 
 
 def addFeatureVariations(font, conditionalSubstitutions, featureTag='rvrn'):
@@ -298,6 +298,11 @@
         varFeatureIndex = gsub.FeatureList.FeatureRecord.index(varFeature)
 
         for scriptRecord in gsub.ScriptList.ScriptRecord:
+            if scriptRecord.Script.DefaultLangSys is None:
+                raise VarLibError(
+                    "Feature variations require that the script "
+                    f"'{scriptRecord.ScriptTag}' defines a default language system."
+                )
             langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord]
             for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems:
                 langSys.FeatureIndex.append(varFeatureIndex)
diff --git a/METADATA b/METADATA
index 389a623..5606bbf 100644
--- a/METADATA
+++ b/METADATA
@@ -10,16 +10,16 @@
   }
   url {
     type: ARCHIVE
-    value: "https://github.com/fonttools/fonttools/archive/4.18.2.zip"
+    value: "https://github.com/fonttools/fonttools/archive/4.19.0.zip"
   }
-  version: "4.18.2"
+  version: "4.19.0"
   # would be NOTICE except for:
   #   Doc/README.md
   # or RESTRICTED except for the SIL OFL 1.1 license
   license_type: BY_EXCEPTION_ONLY
   last_upgrade_date {
-    year: 2020
-    month: 12
-    day: 16
+    year: 2021
+    month: 1
+    day: 25
   }
 }
diff --git a/NEWS.rst b/NEWS.rst
index 55542d0..e22a0f6 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,19 @@
+4.19.0 (released 2021-01-25)
+----------------------------
+
+- [codecs] Handle ``errors`` parameter different from 'strict' for the custom
+  extended mac encodings (#2137, #2132).
+- [featureVars] Raise better error message when a script is missing the required
+  default language system (#2154).
+- [COLRv1] Avoid abrupt change caused by rounding ``PaintRadialGradient.c0`` when
+  the start circle almost touches the end circle's perimeter (#2148).
+- [COLRv1] Support building unlimited lists of paints as 255-ary trees of
+  ``PaintColrLayers`` tables (#2153).
+- [subset] Prune redundant format-12 cmap subtables when all non-BMP characters
+  are dropped (#2146).
+- [basePen] Raise ``MissingComponentError`` instead of bare ``KeyError`` when a
+  referenced component is missing (#2145).
+
 4.18.2 (released 2020-12-16)
 ----------------------------
 
diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py
index d1e94df..43ec96a 100644
--- a/Tests/colorLib/builder_test.py
+++ b/Tests/colorLib/builder_test.py
@@ -1,7 +1,8 @@
 from fontTools.ttLib import newTable
 from fontTools.ttLib.tables import otTables as ot
 from fontTools.colorLib import builder
-from fontTools.colorLib.builder import LayerV1ListBuilder
+from fontTools.colorLib.geometry import round_start_circle_stable_containment, Circle
+from fontTools.colorLib.builder import LayerV1ListBuilder, _build_n_ary_tree
 from fontTools.colorLib.errors import ColorLibError
 import pytest
 from typing import List
@@ -673,6 +674,43 @@
     assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g"
 
 
+def test_buildColrV1_more_than_255_paints():
+    num_paints = 364
+    colorGlyphs = {
+        "a": [
+            {
+                "format": 5,  # PaintGlyph
+                "paint": 0,
+                "glyph": name,
+            }
+            for name in (f"glyph{i}" for i in range(num_paints))
+        ],
+    }
+    layers, baseGlyphs = builder.buildColrV1(colorGlyphs)
+    paints = layers.Paint
+
+    assert len(paints) == num_paints + 1
+
+    assert all(paints[i].Format == ot.Paint.Format.PaintGlyph for i in range(255))
+
+    assert paints[255].Format == ot.Paint.Format.PaintColrLayers
+    assert paints[255].FirstLayerIndex == 0
+    assert paints[255].NumLayers == 255
+
+    assert all(
+        paints[i].Format == ot.Paint.Format.PaintGlyph
+        for i in range(256, num_paints + 1)
+    )
+
+    assert baseGlyphs.BaseGlyphCount == len(colorGlyphs)
+    assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a"
+    assert (
+        baseGlyphs.BaseGlyphV1Record[0].Paint.Format == ot.Paint.Format.PaintColrLayers
+    )
+    assert baseGlyphs.BaseGlyphV1Record[0].Paint.FirstLayerIndex == 255
+    assert baseGlyphs.BaseGlyphV1Record[0].Paint.NumLayers == num_paints + 1 - 255
+
+
 def test_split_color_glyphs_by_version():
     layerBuilder = LayerV1ListBuilder()
     colorGlyphs = {
@@ -1055,3 +1093,130 @@
         assert hasattr(colr, "table")
         assert isinstance(colr.table, ot.COLR)
         assert colr.table.VarStore is None
+
+
+class TrickyRadialGradientTest:
+    @staticmethod
+    def circle_inside_circle(c0, r0, c1, r1, rounded=False):
+        if rounded:
+            return Circle(c0, r0).round().inside(Circle(c1, r1).round())
+        else:
+            return Circle(c0, r0).inside(Circle(c1, r1))
+
+    def round_start_circle(self, c0, r0, c1, r1, inside=True):
+        assert self.circle_inside_circle(c0, r0, c1, r1) is inside
+        assert self.circle_inside_circle(c0, r0, c1, r1, rounded=True) is not inside
+        r = round_start_circle_stable_containment(c0, r0, c1, r1)
+        assert (
+            self.circle_inside_circle(r.centre, r.radius, c1, r1, rounded=True)
+            is inside
+        )
+        return r.centre, r.radius
+
+    def test_noto_emoji_mosquito_u1f99f(self):
+        # https://github.com/googlefonts/picosvg/issues/158
+        c0 = (385.23508, 70.56727999999998)
+        r0 = 0
+        c1 = (642.99108, 104.70327999999995)
+        r1 = 260.0072
+        assert self.round_start_circle(c0, r0, c1, r1, inside=True) == ((386, 71), 0)
+
+    @pytest.mark.parametrize(
+        "c0, r0, c1, r1, inside, expected",
+        [
+            # inside before round, outside after round
+            ((1.4, 0), 0, (2.6, 0), 1.3, True, ((2, 0), 0)),
+            ((1, 0), 0.6, (2.8, 0), 2.45, True, ((2, 0), 1)),
+            ((6.49, 6.49), 0, (0.49, 0.49), 8.49, True, ((5, 5), 0)),
+            # outside before round, inside after round
+            ((0, 0), 0, (2, 0), 1.5, False, ((-1, 0), 0)),
+            ((0, -0.5), 0, (0, -2.5), 1.5, False, ((0, 1), 0)),
+            # the following ones require two nudges to round correctly
+            ((0.5, 0), 0, (9.4, 0), 8.8, False, ((-1, 0), 0)),
+            ((1.5, 1.5), 0, (0.49, 0.49), 1.49, True, ((0, 0), 0)),
+            # limit case when circle almost exactly overlap
+            ((0.5000001, 0), 0.5000001, (0.499999, 0), 0.4999999, True, ((0, 0), 0)),
+            # concentrical circles, r0 > r1
+            ((0, 0), 1.49, (0, 0), 1, False, ((0, 0), 2)),
+        ],
+    )
+    def test_nudge_start_circle_position(self, c0, r0, c1, r1, inside, expected):
+        assert self.round_start_circle(c0, r0, c1, r1, inside) == expected
+
+
+@pytest.mark.parametrize(
+    "lst, n, expected",
+    [
+        ([0], 2, [0]),
+        ([0, 1], 2, [0, 1]),
+        ([0, 1, 2], 2, [[0, 1], 2]),
+        ([0, 1, 2], 3, [0, 1, 2]),
+        ([0, 1, 2, 3], 2, [[0, 1], [2, 3]]),
+        ([0, 1, 2, 3], 3, [[0, 1, 2], 3]),
+        ([0, 1, 2, 3, 4], 3, [[0, 1, 2], 3, 4]),
+        ([0, 1, 2, 3, 4, 5], 3, [[0, 1, 2], [3, 4, 5]]),
+        (list(range(7)), 3, [[0, 1, 2], [3, 4, 5], 6]),
+        (list(range(8)), 3, [[0, 1, 2], [3, 4, 5], [6, 7]]),
+        (list(range(9)), 3, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]),
+        (list(range(10)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9]),
+        (list(range(11)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9, 10]),
+        (list(range(12)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11]]),
+        (list(range(13)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], 12]),
+        (
+            list(range(14)),
+            3,
+            [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], 12, 13]],
+        ),
+        (
+            list(range(15)),
+            3,
+            [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], [12, 13, 14]],
+        ),
+        (
+            list(range(16)),
+            3,
+            [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], [12, 13, 14], 15]],
+        ),
+        (
+            list(range(23)),
+            3,
+            [
+                [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
+                [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
+                [[18, 19, 20], 21, 22],
+            ],
+        ),
+        (
+            list(range(27)),
+            3,
+            [
+                [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
+                [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
+                [[18, 19, 20], [21, 22, 23], [24, 25, 26]],
+            ],
+        ),
+        (
+            list(range(28)),
+            3,
+            [
+                [
+                    [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
+                    [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
+                    [[18, 19, 20], [21, 22, 23], [24, 25, 26]],
+                ],
+                27,
+            ],
+        ),
+        (list(range(257)), 256, [list(range(256)), 256]),
+        (list(range(258)), 256, [list(range(256)), 256, 257]),
+        (list(range(512)), 256, [list(range(256)), list(range(256, 512))]),
+        (list(range(512 + 1)), 256, [list(range(256)), list(range(256, 512)), 512]),
+        (
+            list(range(256 ** 2)),
+            256,
+            [list(range(k * 256, k * 256 + 256)) for k in range(256)],
+        ),
+    ],
+)
+def test_build_n_ary_tree(lst, n, expected):
+    assert _build_n_ary_tree(lst, n) == expected
diff --git a/Tests/otlLib/builder_test.py b/Tests/otlLib/builder_test.py
index bdfc645..3ea5a74 100644
--- a/Tests/otlLib/builder_test.py
+++ b/Tests/otlLib/builder_test.py
@@ -2,7 +2,7 @@
 import struct
 from fontTools.misc.fixedTools import floatToFixed
 from fontTools.misc.testTools import getXML
-from fontTools.otlLib import builder
+from fontTools.otlLib import builder, error
 from fontTools import ttLib
 from fontTools.ttLib.tables import otTables
 import pytest
@@ -1101,6 +1101,12 @@
         assert not b.canAdd({"d", "e", "f"})
         assert not b.canAdd({"f"})
 
+    def test_add_exception(self):
+        b = builder.ClassDefBuilder(useClass0=True)
+        b.add({"a", "b", "c"})
+        with pytest.raises(error.OpenTypeLibError):
+            b.add({"a", "d"})
+
 
 buildStatTable_test_data = [
     ([
diff --git a/Tests/subset/data/CmapSubsetTest.subset.ttx b/Tests/subset/data/CmapSubsetTest.subset.ttx
new file mode 100644
index 0000000..10b94a3
--- /dev/null
+++ b/Tests/subset/data/CmapSubsetTest.subset.ttx
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.18">
+
+  <cmap>
+    <tableVersion version="0"/>
+    <cmap_format_4 platformID="0" platEncID="3" language="0">
+      <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+    </cmap_format_4>
+    <cmap_format_4 platformID="3" platEncID="1" language="0">
+      <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+    </cmap_format_4>
+  </cmap>
+
+</ttFont>
diff --git a/Tests/subset/data/CmapSubsetTest.ttx b/Tests/subset/data/CmapSubsetTest.ttx
new file mode 100644
index 0000000..ffbfae7
--- /dev/null
+++ b/Tests/subset/data/CmapSubsetTest.ttx
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.18">
+
+  <GlyphOrder>
+    <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+    <GlyphID id="0" name=".notdef"/>
+    <GlyphID id="1" name="a"/>
+    <GlyphID id="2" name="basket"/>
+  </GlyphOrder>
+
+  <head>
+    <!-- Most of this table will be recalculated by the compiler -->
+    <tableVersion value="1.0"/>
+    <fontRevision value="0.0"/>
+    <checkSumAdjustment value="0xc643119c"/>
+    <magicNumber value="0x5f0f3cf5"/>
+    <flags value="00000000 00000011"/>
+    <unitsPerEm value="1000"/>
+    <created value="Tue Jan 12 16:39:39 2021"/>
+    <modified value="Tue Jan 12 16:39:39 2021"/>
+    <xMin value="50"/>
+    <yMin value="-200"/>
+    <xMax value="450"/>
+    <yMax value="800"/>
+    <macStyle value="00000000 00000000"/>
+    <lowestRecPPEM value="6"/>
+    <fontDirectionHint value="2"/>
+    <indexToLocFormat value="0"/>
+    <glyphDataFormat value="0"/>
+  </head>
+
+  <hhea>
+    <tableVersion value="0x00010000"/>
+    <ascent value="1000"/>
+    <descent value="-200"/>
+    <lineGap value="0"/>
+    <advanceWidthMax value="942"/>
+    <minLeftSideBearing value="50"/>
+    <minRightSideBearing value="50"/>
+    <xMaxExtent value="450"/>
+    <caretSlopeRise value="1"/>
+    <caretSlopeRun value="0"/>
+    <caretOffset value="0"/>
+    <reserved0 value="0"/>
+    <reserved1 value="0"/>
+    <reserved2 value="0"/>
+    <reserved3 value="0"/>
+    <metricDataFormat value="0"/>
+    <numberOfHMetrics value="3"/>
+  </hhea>
+
+  <maxp>
+    <!-- Most of this table will be recalculated by the compiler -->
+    <tableVersion value="0x10000"/>
+    <numGlyphs value="3"/>
+    <maxPoints value="8"/>
+    <maxContours value="2"/>
+    <maxCompositePoints value="0"/>
+    <maxCompositeContours value="0"/>
+    <maxZones value="1"/>
+    <maxTwilightPoints value="0"/>
+    <maxStorage value="0"/>
+    <maxFunctionDefs value="0"/>
+    <maxInstructionDefs value="0"/>
+    <maxStackElements value="0"/>
+    <maxSizeOfInstructions value="0"/>
+    <maxComponentElements value="0"/>
+    <maxComponentDepth value="0"/>
+  </maxp>
+
+  <OS_2>
+    <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+         will be recalculated by the compiler -->
+    <version value="4"/>
+    <xAvgCharWidth value="660"/>
+    <usWeightClass value="400"/>
+    <usWidthClass value="5"/>
+    <fsType value="00000000 00000100"/>
+    <ySubscriptXSize value="650"/>
+    <ySubscriptYSize value="600"/>
+    <ySubscriptXOffset value="0"/>
+    <ySubscriptYOffset value="75"/>
+    <ySuperscriptXSize value="650"/>
+    <ySuperscriptYSize value="600"/>
+    <ySuperscriptXOffset value="0"/>
+    <ySuperscriptYOffset value="350"/>
+    <yStrikeoutSize value="50"/>
+    <yStrikeoutPosition value="300"/>
+    <sFamilyClass value="0"/>
+    <panose>
+      <bFamilyType value="0"/>
+      <bSerifStyle value="0"/>
+      <bWeight value="0"/>
+      <bProportion value="0"/>
+      <bContrast value="0"/>
+      <bStrokeVariation value="0"/>
+      <bArmStyle value="0"/>
+      <bLetterForm value="0"/>
+      <bMidline value="0"/>
+      <bXHeight value="0"/>
+    </panose>
+    <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+    <ulUnicodeRange2 value="00000010 00000000 00000000 00000000"/>
+    <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+    <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+    <achVendID value="NONE"/>
+    <fsSelection value="00000000 01000000"/>
+    <usFirstCharIndex value="97"/>
+    <usLastCharIndex value="65535"/>
+    <sTypoAscender value="800"/>
+    <sTypoDescender value="-200"/>
+    <sTypoLineGap value="200"/>
+    <usWinAscent value="1000"/>
+    <usWinDescent value="200"/>
+    <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+    <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+    <sxHeight value="500"/>
+    <sCapHeight value="700"/>
+    <usDefaultChar value="0"/>
+    <usBreakChar value="32"/>
+    <usMaxContext value="0"/>
+  </OS_2>
+
+  <hmtx>
+    <mtx name=".notdef" width="500" lsb="50"/>
+    <mtx name="a" width="538" lsb="0"/>
+    <mtx name="basket" width="942" lsb="0"/>
+  </hmtx>
+
+  <cmap>
+    <tableVersion version="0"/>
+    <cmap_format_4 platformID="0" platEncID="3" language="0">
+      <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+    </cmap_format_4>
+    <cmap_format_12 platformID="0" platEncID="4" format="12" reserved="0" length="40" language="0" nGroups="2">
+      <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+      <map code="0x1f9fa" name="basket"/><!-- BASKET -->
+    </cmap_format_12>
+    <cmap_format_4 platformID="3" platEncID="1" language="0">
+      <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+    </cmap_format_4>
+    <cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="40" language="0" nGroups="2">
+      <map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
+      <map code="0x1f9fa" name="basket"/><!-- BASKET -->
+    </cmap_format_12>
+  </cmap>
+
+  <loca>
+    <!-- The 'loca' table will be calculated by the compiler -->
+  </loca>
+
+  <glyf>
+
+    <!-- The xMin, yMin, xMax and yMax values
+         will be recalculated by the compiler. -->
+
+    <TTGlyph name=".notdef" xMin="50" yMin="-200" xMax="450" yMax="800">
+      <contour>
+        <pt x="50" y="-200" on="1"/>
+        <pt x="50" y="800" on="1"/>
+        <pt x="450" y="800" on="1"/>
+        <pt x="450" y="-200" on="1"/>
+      </contour>
+      <contour>
+        <pt x="100" y="-150" on="1"/>
+        <pt x="400" y="-150" on="1"/>
+        <pt x="400" y="750" on="1"/>
+        <pt x="100" y="750" on="1"/>
+      </contour>
+      <instructions/>
+    </TTGlyph>
+
+    <TTGlyph name="a"/><!-- contains no outline data -->
+
+    <TTGlyph name="basket"/><!-- contains no outline data -->
+
+  </glyf>
+
+  <name>
+    <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+      New Font
+    </namerecord>
+    <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+      Regular
+    </namerecord>
+    <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+      0.000;NONE;NewFont-Regular
+    </namerecord>
+    <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+      New Font Regular
+    </namerecord>
+    <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+      Version 0.000
+    </namerecord>
+    <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+      NewFont-Regular
+    </namerecord>
+  </name>
+
+  <post>
+    <formatType value="2.0"/>
+    <italicAngle value="0.0"/>
+    <underlinePosition value="-75"/>
+    <underlineThickness value="50"/>
+    <isFixedPitch value="0"/>
+    <minMemType42 value="0"/>
+    <maxMemType42 value="0"/>
+    <minMemType1 value="0"/>
+    <maxMemType1 value="0"/>
+    <psNames>
+      <!-- This file uses unique glyph names based on the information
+           found in the 'post' table. Since these names might not be unique,
+           we have to invent artificial names in case of clashes. In order to
+           be able to retain the original information, we need a name to
+           ps name mapping for those cases where they differ. That's what
+           you see below.
+            -->
+    </psNames>
+    <extraNames>
+      <!-- following are the name that are not taken from the standard Mac glyph order -->
+      <psName name="basket"/>
+    </extraNames>
+  </post>
+
+</ttFont>
diff --git a/Tests/subset/data/TestContextSubstFormat3.ttx b/Tests/subset/data/TestContextSubstFormat3.ttx
index 899b037..3e9bfcd 100644
--- a/Tests/subset/data/TestContextSubstFormat3.ttx
+++ b/Tests/subset/data/TestContextSubstFormat3.ttx
@@ -17,7 +17,7 @@
     <!-- Most of this table will be recalculated by the compiler -->
     <tableVersion value="1.0"/>
     <fontRevision value="1.0"/>
-    <checkSumAdjustment value="0xa69ed898"/>
+    <checkSumAdjustment value="0xa6bcdc24"/>
     <magicNumber value="0x5f0f3cf5"/>
     <flags value="00000000 00001111"/>
     <unitsPerEm value="1000"/>
@@ -142,15 +142,9 @@
     <cmap_format_4 platformID="0" platEncID="3" language="0">
       <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
     </cmap_format_4>
-    <cmap_format_12 platformID="0" platEncID="4" format="12" reserved="0" length="28" language="0" nGroups="1">
-      <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
-    </cmap_format_12>
     <cmap_format_4 platformID="3" platEncID="1" language="0">
       <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
     </cmap_format_4>
-    <cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="28" language="0" nGroups="1">
-      <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
-    </cmap_format_12>
   </cmap>
 
   <loca>
diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py
index 0d2f9fe..d37634f 100644
--- a/Tests/subset/subset_test.py
+++ b/Tests/subset/subset_test.py
@@ -753,6 +753,13 @@
         # check all glyphs are kept via GSUB closure, no changes expected
         self.expect_ttx(subsetfont, ttx)
 
+    def test_cmap_prune_format12(self):
+        _, fontpath = self.compile_font(self.getpath("CmapSubsetTest.ttx"), ".ttf")
+        subsetpath = self.temp_path(".ttf")
+        subset.main([fontpath, "--glyphs=a", "--output-file=%s" % subsetpath])
+        subsetfont = TTFont(subsetpath)
+        self.expect_ttx(subsetfont, self.getpath("CmapSubsetTest.subset.ttx"), ["cmap"])
+
 
 @pytest.fixture
 def featureVarsTestFont():
diff --git a/Tests/ttLib/tables/_n_a_m_e_test.py b/Tests/ttLib/tables/_n_a_m_e_test.py
index bc4aab2..11aeeba 100644
--- a/Tests/ttLib/tables/_n_a_m_e_test.py
+++ b/Tests/ttLib/tables/_n_a_m_e_test.py
@@ -432,6 +432,18 @@
 		name = makeName(b'\xfe', 123, 1, 1, 0) # Mac Japanese
 		self.assertEqual(name.toUnicode(), unichr(0x2122))
 
+	def test_extended_mac_encodings_errors(self):
+		s = "汉仪彩云体简"
+		name = makeName(s.encode("x_mac_simp_chinese_ttx"), 123, 1, 25, 0)
+		# first check we round-trip with 'strict'
+		self.assertEqual(name.toUnicode(errors="strict"), s)
+
+		# append an incomplete invalid sequence and check that we handle
+		# errors with the requested error handler
+		name.string += b"\xba"
+		self.assertEqual(name.toUnicode(errors="backslashreplace"), s + "\\xba")
+		self.assertEqual(name.toUnicode(errors="replace"), s + "�")
+
 	def test_extended_unknown(self):
 		name = makeName(b'\xfe', 123, 10, 11, 12)
 		self.assertEqual(name.getEncoding(), "ascii")
diff --git a/Tests/ttLib/woff2_test.py b/Tests/ttLib/woff2_test.py
index 661fd44..5923b7f 100644
--- a/Tests/ttLib/woff2_test.py
+++ b/Tests/ttLib/woff2_test.py
@@ -203,7 +203,7 @@
 	# drop DSIG but keep a copy
 	DSIG_copy = copy.deepcopy(font['DSIG'])
 	del font['DSIG']
-	# ovverride TTFont attributes
+	# override TTFont attributes
 	origFlavor = font.flavor
 	origRecalcBBoxes = font.recalcBBoxes
 	origRecalcTimestamp = font.recalcTimestamp
diff --git a/setup.cfg b/setup.cfg
index 285f8ef..8f8c530 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 4.18.2
+current_version = 4.19.0
 commit = True
 tag = False
 tag_name = {new_version}
diff --git a/setup.py b/setup.py
index 5f24baf..5e3226c 100755
--- a/setup.py
+++ b/setup.py
@@ -441,7 +441,7 @@
 
 setup_params = dict(
 	name="fonttools",
-	version="4.18.2",
+	version="4.19.0",
 	description="Tools to manipulate font files",
 	author="Just van Rossum",
 	author_email="just@letterror.com",