• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2colorLib.builder: Build COLR/CPAL tables from scratch
3
4"""
5import collections
6import copy
7import enum
8from functools import partial
9from math import ceil, log
10from typing import (
11    Any,
12    Dict,
13    Generator,
14    Iterable,
15    List,
16    Mapping,
17    Optional,
18    Sequence,
19    Tuple,
20    Type,
21    TypeVar,
22    Union,
23)
24from fontTools.misc.arrayTools import intRect
25from fontTools.misc.fixedTools import fixedToFloat
26from fontTools.misc.treeTools import build_n_ary_tree
27from fontTools.ttLib.tables import C_O_L_R_
28from fontTools.ttLib.tables import C_P_A_L_
29from fontTools.ttLib.tables import _n_a_m_e
30from fontTools.ttLib.tables import otTables as ot
31from fontTools.ttLib.tables.otTables import ExtendMode, CompositeMode
32from .errors import ColorLibError
33from .geometry import round_start_circle_stable_containment
34from .table_builder import BuildCallback, TableBuilder
35
36
37# TODO move type aliases to colorLib.types?
38T = TypeVar("T")
39_Kwargs = Mapping[str, Any]
40_PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]]
41_PaintInputList = Sequence[_PaintInput]
42_ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]]
43_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
44_ClipBoxInput = Union[
45    Tuple[int, int, int, int, int],  # format 1, variable
46    Tuple[int, int, int, int],  # format 0, non-variable
47    ot.ClipBox,
48]
49
50
51MAX_PAINT_COLR_LAYER_COUNT = 255
52_DEFAULT_ALPHA = 1.0
53_MAX_REUSE_LEN = 32
54
55
56def _beforeBuildPaintRadialGradient(paint, source):
57    x0 = source["x0"]
58    y0 = source["y0"]
59    r0 = source["r0"]
60    x1 = source["x1"]
61    y1 = source["y1"]
62    r1 = source["r1"]
63
64    # TODO apparently no builder_test confirms this works (?)
65
66    # avoid abrupt change after rounding when c0 is near c1's perimeter
67    c = round_start_circle_stable_containment((x0, y0), r0, (x1, y1), r1)
68    x0, y0 = c.centre
69    r0 = c.radius
70
71    # update source to ensure paint is built with corrected values
72    source["x0"] = x0
73    source["y0"] = y0
74    source["r0"] = r0
75    source["x1"] = x1
76    source["y1"] = y1
77    source["r1"] = r1
78
79    return paint, source
80
81
82def _defaultColorStop():
83    colorStop = ot.ColorStop()
84    colorStop.Alpha = _DEFAULT_ALPHA
85    return colorStop
86
87
88def _defaultVarColorStop():
89    colorStop = ot.VarColorStop()
90    colorStop.Alpha = _DEFAULT_ALPHA
91    return colorStop
92
93
94def _defaultColorLine():
95    colorLine = ot.ColorLine()
96    colorLine.Extend = ExtendMode.PAD
97    return colorLine
98
99
100def _defaultVarColorLine():
101    colorLine = ot.VarColorLine()
102    colorLine.Extend = ExtendMode.PAD
103    return colorLine
104
105
106def _defaultPaintSolid():
107    paint = ot.Paint()
108    paint.Alpha = _DEFAULT_ALPHA
109    return paint
110
111
112def _buildPaintCallbacks():
113    return {
114        (
115            BuildCallback.BEFORE_BUILD,
116            ot.Paint,
117            ot.PaintFormat.PaintRadialGradient,
118        ): _beforeBuildPaintRadialGradient,
119        (
120            BuildCallback.BEFORE_BUILD,
121            ot.Paint,
122            ot.PaintFormat.PaintVarRadialGradient,
123        ): _beforeBuildPaintRadialGradient,
124        (BuildCallback.CREATE_DEFAULT, ot.ColorStop): _defaultColorStop,
125        (BuildCallback.CREATE_DEFAULT, ot.VarColorStop): _defaultVarColorStop,
126        (BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine,
127        (BuildCallback.CREATE_DEFAULT, ot.VarColorLine): _defaultVarColorLine,
128        (
129            BuildCallback.CREATE_DEFAULT,
130            ot.Paint,
131            ot.PaintFormat.PaintSolid,
132        ): _defaultPaintSolid,
133        (
134            BuildCallback.CREATE_DEFAULT,
135            ot.Paint,
136            ot.PaintFormat.PaintVarSolid,
137        ): _defaultPaintSolid,
138    }
139
140
141def populateCOLRv0(
142    table: ot.COLR,
143    colorGlyphsV0: _ColorGlyphsV0Dict,
144    glyphMap: Optional[Mapping[str, int]] = None,
145):
146    """Build v0 color layers and add to existing COLR table.
147
148    Args:
149        table: a raw ``otTables.COLR()`` object (not ttLib's ``table_C_O_L_R_``).
150        colorGlyphsV0: map of base glyph names to lists of (layer glyph names,
151            color palette index) tuples. Can be empty.
152        glyphMap: a map from glyph names to glyph indices, as returned from
153            ``TTFont.getReverseGlyphMap()``, to optionally sort base records by GID.
154    """
155    if glyphMap is not None:
156        colorGlyphItems = sorted(
157            colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
158        )
159    else:
160        colorGlyphItems = colorGlyphsV0.items()
161    baseGlyphRecords = []
162    layerRecords = []
163    for baseGlyph, layers in colorGlyphItems:
164        baseRec = ot.BaseGlyphRecord()
165        baseRec.BaseGlyph = baseGlyph
166        baseRec.FirstLayerIndex = len(layerRecords)
167        baseRec.NumLayers = len(layers)
168        baseGlyphRecords.append(baseRec)
169
170        for layerGlyph, paletteIndex in layers:
171            layerRec = ot.LayerRecord()
172            layerRec.LayerGlyph = layerGlyph
173            layerRec.PaletteIndex = paletteIndex
174            layerRecords.append(layerRec)
175
176    table.BaseGlyphRecordArray = table.LayerRecordArray = None
177    if baseGlyphRecords:
178        table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray()
179        table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords
180    if layerRecords:
181        table.LayerRecordArray = ot.LayerRecordArray()
182        table.LayerRecordArray.LayerRecord = layerRecords
183    table.BaseGlyphRecordCount = len(baseGlyphRecords)
184    table.LayerRecordCount = len(layerRecords)
185
186
187def buildCOLR(
188    colorGlyphs: _ColorGlyphsDict,
189    version: Optional[int] = None,
190    *,
191    glyphMap: Optional[Mapping[str, int]] = None,
192    varStore: Optional[ot.VarStore] = None,
193    varIndexMap: Optional[ot.DeltaSetIndexMap] = None,
194    clipBoxes: Optional[Dict[str, _ClipBoxInput]] = None,
195    allowLayerReuse: bool = True,
196) -> C_O_L_R_.table_C_O_L_R_:
197    """Build COLR table from color layers mapping.
198
199    Args:
200
201        colorGlyphs: map of base glyph name to, either list of (layer glyph name,
202            color palette index) tuples for COLRv0; or a single ``Paint`` (dict) or
203            list of ``Paint`` for COLRv1.
204        version: the version of COLR table. If None, the version is determined
205            by the presence of COLRv1 paints or variation data (varStore), which
206            require version 1; otherwise, if all base glyphs use only simple color
207            layers, version 0 is used.
208        glyphMap: a map from glyph names to glyph indices, as returned from
209            TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
210        varStore: Optional ItemVarationStore for deltas associated with v1 layer.
211        varIndexMap: Optional DeltaSetIndexMap for deltas associated with v1 layer.
212        clipBoxes: Optional map of base glyph name to clip box 4- or 5-tuples:
213            (xMin, yMin, xMax, yMax) or (xMin, yMin, xMax, yMax, varIndexBase).
214
215    Returns:
216        A new COLR table.
217    """
218    self = C_O_L_R_.table_C_O_L_R_()
219
220    if varStore is not None and version == 0:
221        raise ValueError("Can't add VarStore to COLRv0")
222
223    if version in (None, 0) and not varStore:
224        # split color glyphs into v0 and v1 and encode separately
225        colorGlyphsV0, colorGlyphsV1 = _split_color_glyphs_by_version(colorGlyphs)
226        if version == 0 and colorGlyphsV1:
227            raise ValueError("Can't encode COLRv1 glyphs in COLRv0")
228    else:
229        # unless explicitly requested for v1 or have variations, in which case
230        # we encode all color glyph as v1
231        colorGlyphsV0, colorGlyphsV1 = {}, colorGlyphs
232
233    colr = ot.COLR()
234
235    populateCOLRv0(colr, colorGlyphsV0, glyphMap)
236
237    colr.LayerList, colr.BaseGlyphList = buildColrV1(
238        colorGlyphsV1,
239        glyphMap,
240        allowLayerReuse=allowLayerReuse,
241    )
242
243    if version is None:
244        version = 1 if (varStore or colorGlyphsV1) else 0
245    elif version not in (0, 1):
246        raise NotImplementedError(version)
247    self.version = colr.Version = version
248
249    if version == 0:
250        self.ColorLayers = self._decompileColorLayersV0(colr)
251    else:
252        colr.ClipList = buildClipList(clipBoxes) if clipBoxes else None
253        colr.VarIndexMap = varIndexMap
254        colr.VarStore = varStore
255        self.table = colr
256
257    return self
258
259
260def buildClipList(clipBoxes: Dict[str, _ClipBoxInput]) -> ot.ClipList:
261    clipList = ot.ClipList()
262    clipList.Format = 1
263    clipList.clips = {name: buildClipBox(box) for name, box in clipBoxes.items()}
264    return clipList
265
266
267def buildClipBox(clipBox: _ClipBoxInput) -> ot.ClipBox:
268    if isinstance(clipBox, ot.ClipBox):
269        return clipBox
270    n = len(clipBox)
271    clip = ot.ClipBox()
272    if n not in (4, 5):
273        raise ValueError(f"Invalid ClipBox: expected 4 or 5 values, found {n}")
274    clip.xMin, clip.yMin, clip.xMax, clip.yMax = intRect(clipBox[:4])
275    clip.Format = int(n == 5) + 1
276    if n == 5:
277        clip.VarIndexBase = int(clipBox[4])
278    return clip
279
280
281class ColorPaletteType(enum.IntFlag):
282    USABLE_WITH_LIGHT_BACKGROUND = 0x0001
283    USABLE_WITH_DARK_BACKGROUND = 0x0002
284
285    @classmethod
286    def _missing_(cls, value):
287        # enforce reserved bits
288        if isinstance(value, int) and (value < 0 or value & 0xFFFC != 0):
289            raise ValueError(f"{value} is not a valid {cls.__name__}")
290        return super()._missing_(value)
291
292
293# None, 'abc' or {'en': 'abc', 'de': 'xyz'}
294_OptionalLocalizedString = Union[None, str, Dict[str, str]]
295
296
297def buildPaletteLabels(
298    labels: Iterable[_OptionalLocalizedString], nameTable: _n_a_m_e.table__n_a_m_e
299) -> List[Optional[int]]:
300    return [
301        nameTable.addMultilingualName(l, mac=False)
302        if isinstance(l, dict)
303        else C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
304        if l is None
305        else nameTable.addMultilingualName({"en": l}, mac=False)
306        for l in labels
307    ]
308
309
310def buildCPAL(
311    palettes: Sequence[Sequence[Tuple[float, float, float, float]]],
312    paletteTypes: Optional[Sequence[ColorPaletteType]] = None,
313    paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
314    paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
315    nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None,
316) -> C_P_A_L_.table_C_P_A_L_:
317    """Build CPAL table from list of color palettes.
318
319    Args:
320        palettes: list of lists of colors encoded as tuples of (R, G, B, A) floats
321            in the range [0..1].
322        paletteTypes: optional list of ColorPaletteType, one for each palette.
323        paletteLabels: optional list of palette labels. Each lable can be either:
324            None (no label), a string (for for default English labels), or a
325            localized string (as a dict keyed with BCP47 language codes).
326        paletteEntryLabels: optional list of palette entry labels, one for each
327            palette entry (see paletteLabels).
328        nameTable: optional name table where to store palette and palette entry
329            labels. Required if either paletteLabels or paletteEntryLabels is set.
330
331    Return:
332        A new CPAL v0 or v1 table, if custom palette types or labels are specified.
333    """
334    if len({len(p) for p in palettes}) != 1:
335        raise ColorLibError("color palettes have different lengths")
336
337    if (paletteLabels or paletteEntryLabels) and not nameTable:
338        raise TypeError(
339            "nameTable is required if palette or palette entries have labels"
340        )
341
342    cpal = C_P_A_L_.table_C_P_A_L_()
343    cpal.numPaletteEntries = len(palettes[0])
344
345    cpal.palettes = []
346    for i, palette in enumerate(palettes):
347        colors = []
348        for j, color in enumerate(palette):
349            if not isinstance(color, tuple) or len(color) != 4:
350                raise ColorLibError(
351                    f"In palette[{i}][{j}]: expected (R, G, B, A) tuple, got {color!r}"
352                )
353            if any(v > 1 or v < 0 for v in color):
354                raise ColorLibError(
355                    f"palette[{i}][{j}] has invalid out-of-range [0..1] color: {color!r}"
356                )
357            # input colors are RGBA, CPAL encodes them as BGRA
358            red, green, blue, alpha = color
359            colors.append(
360                C_P_A_L_.Color(*(round(v * 255) for v in (blue, green, red, alpha)))
361            )
362        cpal.palettes.append(colors)
363
364    if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
365        cpal.version = 1
366
367        if paletteTypes is not None:
368            if len(paletteTypes) != len(palettes):
369                raise ColorLibError(
370                    f"Expected {len(palettes)} paletteTypes, got {len(paletteTypes)}"
371                )
372            cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes]
373        else:
374            cpal.paletteTypes = [C_P_A_L_.table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(
375                palettes
376            )
377
378        if paletteLabels is not None:
379            if len(paletteLabels) != len(palettes):
380                raise ColorLibError(
381                    f"Expected {len(palettes)} paletteLabels, got {len(paletteLabels)}"
382                )
383            cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
384        else:
385            cpal.paletteLabels = [C_P_A_L_.table_C_P_A_L_.NO_NAME_ID] * len(palettes)
386
387        if paletteEntryLabels is not None:
388            if len(paletteEntryLabels) != cpal.numPaletteEntries:
389                raise ColorLibError(
390                    f"Expected {cpal.numPaletteEntries} paletteEntryLabels, "
391                    f"got {len(paletteEntryLabels)}"
392                )
393            cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
394        else:
395            cpal.paletteEntryLabels = [
396                C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
397            ] * cpal.numPaletteEntries
398    else:
399        cpal.version = 0
400
401    return cpal
402
403
404# COLR v1 tables
405# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
406
407
408def _is_colrv0_layer(layer: Any) -> bool:
409    # Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which
410    # the first element is a str (the layerGlyph) and the second element is an int
411    # (CPAL paletteIndex).
412    # https://github.com/googlefonts/ufo2ft/issues/426
413    try:
414        layerGlyph, paletteIndex = layer
415    except (TypeError, ValueError):
416        return False
417    else:
418        return isinstance(layerGlyph, str) and isinstance(paletteIndex, int)
419
420
421def _split_color_glyphs_by_version(
422    colorGlyphs: _ColorGlyphsDict,
423) -> Tuple[_ColorGlyphsV0Dict, _ColorGlyphsDict]:
424    colorGlyphsV0 = {}
425    colorGlyphsV1 = {}
426    for baseGlyph, layers in colorGlyphs.items():
427        if all(_is_colrv0_layer(l) for l in layers):
428            colorGlyphsV0[baseGlyph] = layers
429        else:
430            colorGlyphsV1[baseGlyph] = layers
431
432    # sanity check
433    assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
434
435    return colorGlyphsV0, colorGlyphsV1
436
437
438def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
439    # TODO feels like something itertools might have already
440    for lbound in range(num_layers):
441        # Reuse of very large #s of layers is relatively unlikely
442        # +2: we want sequences of at least 2
443        # otData handles single-record duplication
444        for ubound in range(
445            lbound + 2, min(num_layers + 1, lbound + 2 + _MAX_REUSE_LEN)
446        ):
447            yield (lbound, ubound)
448
449
450class LayerReuseCache:
451    reusePool: Mapping[Tuple[Any, ...], int]
452    tuples: Mapping[int, Tuple[Any, ...]]
453    keepAlive: List[ot.Paint]  # we need id to remain valid
454
455    def __init__(self):
456        self.reusePool = {}
457        self.tuples = {}
458        self.keepAlive = []
459
460    def _paint_tuple(self, paint: ot.Paint):
461        # start simple, who even cares about cyclic graphs or interesting field types
462        def _tuple_safe(value):
463            if isinstance(value, enum.Enum):
464                return value
465            elif hasattr(value, "__dict__"):
466                return tuple(
467                    (k, _tuple_safe(v)) for k, v in sorted(value.__dict__.items())
468                )
469            elif isinstance(value, collections.abc.MutableSequence):
470                return tuple(_tuple_safe(e) for e in value)
471            return value
472
473        # Cache the tuples for individual Paint instead of the whole sequence
474        # because the seq could be a transient slice
475        result = self.tuples.get(id(paint), None)
476        if result is None:
477            result = _tuple_safe(paint)
478            self.tuples[id(paint)] = result
479            self.keepAlive.append(paint)
480        return result
481
482    def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
483        return tuple(self._paint_tuple(p) for p in paints)
484
485    def try_reuse(self, layers: List[ot.Paint]) -> List[ot.Paint]:
486        found_reuse = True
487        while found_reuse:
488            found_reuse = False
489
490            ranges = sorted(
491                _reuse_ranges(len(layers)),
492                key=lambda t: (t[1] - t[0], t[1], t[0]),
493                reverse=True,
494            )
495            for lbound, ubound in ranges:
496                reuse_lbound = self.reusePool.get(
497                    self._as_tuple(layers[lbound:ubound]), -1
498                )
499                if reuse_lbound == -1:
500                    continue
501                new_slice = ot.Paint()
502                new_slice.Format = int(ot.PaintFormat.PaintColrLayers)
503                new_slice.NumLayers = ubound - lbound
504                new_slice.FirstLayerIndex = reuse_lbound
505                layers = layers[:lbound] + [new_slice] + layers[ubound:]
506                found_reuse = True
507                break
508        return layers
509
510    def add(self, layers: List[ot.Paint], first_layer_index: int):
511        for lbound, ubound in _reuse_ranges(len(layers)):
512            self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
513                lbound + first_layer_index
514            )
515
516
517class LayerListBuilder:
518    layers: List[ot.Paint]
519    cache: LayerReuseCache
520    allowLayerReuse: bool
521
522    def __init__(self, *, allowLayerReuse=True):
523        self.layers = []
524        if allowLayerReuse:
525            self.cache = LayerReuseCache()
526        else:
527            self.cache = None
528
529        # We need to intercept construction of PaintColrLayers
530        callbacks = _buildPaintCallbacks()
531        callbacks[
532            (
533                BuildCallback.BEFORE_BUILD,
534                ot.Paint,
535                ot.PaintFormat.PaintColrLayers,
536            )
537        ] = self._beforeBuildPaintColrLayers
538        self.tableBuilder = TableBuilder(callbacks)
539
540    # COLR layers is unusual in that it modifies shared state
541    # so we need a callback into an object
542    def _beforeBuildPaintColrLayers(self, dest, source):
543        # Sketchy gymnastics: a sequence input will have dropped it's layers
544        # into NumLayers; get it back
545        if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
546            layers = source["NumLayers"]
547        else:
548            layers = source["Layers"]
549
550        # Convert maps seqs or whatever into typed objects
551        layers = [self.buildPaint(l) for l in layers]
552
553        # No reason to have a colr layers with just one entry
554        if len(layers) == 1:
555            return layers[0], {}
556
557        if self.cache is not None:
558            # Look for reuse, with preference to longer sequences
559            # This may make the layer list smaller
560            layers = self.cache.try_reuse(layers)
561
562        # The layer list is now final; if it's too big we need to tree it
563        is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
564        layers = build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
565
566        # We now have a tree of sequences with Paint leaves.
567        # Convert the sequences into PaintColrLayers.
568        def listToColrLayers(layer):
569            if isinstance(layer, collections.abc.Sequence):
570                return self.buildPaint(
571                    {
572                        "Format": ot.PaintFormat.PaintColrLayers,
573                        "Layers": [listToColrLayers(l) for l in layer],
574                    }
575                )
576            return layer
577
578        layers = [listToColrLayers(l) for l in layers]
579
580        # No reason to have a colr layers with just one entry
581        if len(layers) == 1:
582            return layers[0], {}
583
584        paint = ot.Paint()
585        paint.Format = int(ot.PaintFormat.PaintColrLayers)
586        paint.NumLayers = len(layers)
587        paint.FirstLayerIndex = len(self.layers)
588        self.layers.extend(layers)
589
590        # Register our parts for reuse provided we aren't a tree
591        # If we are a tree the leaves registered for reuse and that will suffice
592        if self.cache is not None and not is_tree:
593            self.cache.add(layers, paint.FirstLayerIndex)
594
595        # we've fully built dest; empty source prevents generalized build from kicking in
596        return paint, {}
597
598    def buildPaint(self, paint: _PaintInput) -> ot.Paint:
599        return self.tableBuilder.build(ot.Paint, paint)
600
601    def build(self) -> Optional[ot.LayerList]:
602        if not self.layers:
603            return None
604        layers = ot.LayerList()
605        layers.LayerCount = len(self.layers)
606        layers.Paint = self.layers
607        return layers
608
609
610def buildBaseGlyphPaintRecord(
611    baseGlyph: str, layerBuilder: LayerListBuilder, paint: _PaintInput
612) -> ot.BaseGlyphList:
613    self = ot.BaseGlyphPaintRecord()
614    self.BaseGlyph = baseGlyph
615    self.Paint = layerBuilder.buildPaint(paint)
616    return self
617
618
619def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
620    lines = []
621    for baseGlyph, error in sorted(errors.items()):
622        lines.append(f"    {baseGlyph} => {type(error).__name__}: {error}")
623    return "\n".join(lines)
624
625
626def buildColrV1(
627    colorGlyphs: _ColorGlyphsDict,
628    glyphMap: Optional[Mapping[str, int]] = None,
629    *,
630    allowLayerReuse: bool = True,
631) -> Tuple[Optional[ot.LayerList], ot.BaseGlyphList]:
632    if glyphMap is not None:
633        colorGlyphItems = sorted(
634            colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
635        )
636    else:
637        colorGlyphItems = colorGlyphs.items()
638
639    errors = {}
640    baseGlyphs = []
641    layerBuilder = LayerListBuilder(allowLayerReuse=allowLayerReuse)
642    for baseGlyph, paint in colorGlyphItems:
643        try:
644            baseGlyphs.append(buildBaseGlyphPaintRecord(baseGlyph, layerBuilder, paint))
645
646        except (ColorLibError, OverflowError, ValueError, TypeError) as e:
647            errors[baseGlyph] = e
648
649    if errors:
650        failed_glyphs = _format_glyph_errors(errors)
651        exc = ColorLibError(f"Failed to build BaseGlyphList:\n{failed_glyphs}")
652        exc.errors = errors
653        raise exc from next(iter(errors.values()))
654
655    layers = layerBuilder.build()
656    glyphs = ot.BaseGlyphList()
657    glyphs.BaseGlyphCount = len(baseGlyphs)
658    glyphs.BaseGlyphPaintRecord = baseGlyphs
659    return (layers, glyphs)
660