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