Coverage for /usr/lib/python3/dist-packages/fontTools/colorLib/builder.py: 21%
324 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2colorLib.builder: Build COLR/CPAL tables from scratch
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
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]
51MAX_PAINT_COLR_LAYER_COUNT = 255
52_DEFAULT_ALPHA = 1.0
53_MAX_REUSE_LEN = 32
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"]
64 # TODO apparently no builder_test confirms this works (?)
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
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
79 return paint, source
82def _defaultColorStop():
83 colorStop = ot.ColorStop()
84 colorStop.Alpha = _DEFAULT_ALPHA
85 return colorStop
88def _defaultVarColorStop():
89 colorStop = ot.VarColorStop()
90 colorStop.Alpha = _DEFAULT_ALPHA
91 return colorStop
94def _defaultColorLine():
95 colorLine = ot.ColorLine()
96 colorLine.Extend = ExtendMode.PAD
97 return colorLine
100def _defaultVarColorLine():
101 colorLine = ot.VarColorLine()
102 colorLine.Extend = ExtendMode.PAD
103 return colorLine
106def _defaultPaintSolid():
107 paint = ot.Paint()
108 paint.Alpha = _DEFAULT_ALPHA
109 return paint
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 }
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.
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)
170 for layerGlyph, paletteIndex in layers:
171 layerRec = ot.LayerRecord()
172 layerRec.LayerGlyph = layerGlyph
173 layerRec.PaletteIndex = paletteIndex
174 layerRecords.append(layerRec)
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)
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.
199 Args:
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).
215 Returns:
216 A new COLR table.
217 """
218 self = C_O_L_R_.table_C_O_L_R_()
220 if varStore is not None and version == 0:
221 raise ValueError("Can't add VarStore to COLRv0")
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
233 colr = ot.COLR()
235 populateCOLRv0(colr, colorGlyphsV0, glyphMap)
237 colr.LayerList, colr.BaseGlyphList = buildColrV1(
238 colorGlyphsV1,
239 glyphMap,
240 allowLayerReuse=allowLayerReuse,
241 )
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
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
257 return self
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
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
281class ColorPaletteType(enum.IntFlag):
282 USABLE_WITH_LIGHT_BACKGROUND = 0x0001
283 USABLE_WITH_DARK_BACKGROUND = 0x0002
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)
293# None, 'abc' or {'en': 'abc', 'de': 'xyz'}
294_OptionalLocalizedString = Union[None, str, Dict[str, str]]
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 ]
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.
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.
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")
337 if (paletteLabels or paletteEntryLabels) and not nameTable:
338 raise TypeError(
339 "nameTable is required if palette or palette entries have labels"
340 )
342 cpal = C_P_A_L_.table_C_P_A_L_()
343 cpal.numPaletteEntries = len(palettes[0])
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)
364 if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
365 cpal.version = 1
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 )
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)
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
401 return cpal
404# COLR v1 tables
405# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
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)
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
432 # sanity check
433 assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
435 return colorGlyphsV0, colorGlyphsV1
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)
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
455 def __init__(self):
456 self.reusePool = {}
457 self.tuples = {}
458 self.keepAlive = []
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
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
482 def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
483 return tuple(self._paint_tuple(p) for p in paints)
485 def try_reuse(self, layers: List[ot.Paint]) -> List[ot.Paint]:
486 found_reuse = True
487 while found_reuse:
488 found_reuse = False
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
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 )
517class LayerListBuilder:
518 layers: List[ot.Paint]
519 cache: LayerReuseCache
520 allowLayerReuse: bool
522 def __init__(self, *, allowLayerReuse=True):
523 self.layers = []
524 if allowLayerReuse:
525 self.cache = LayerReuseCache()
526 else:
527 self.cache = None
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)
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"]
550 # Convert maps seqs or whatever into typed objects
551 layers = [self.buildPaint(l) for l in layers]
553 # No reason to have a colr layers with just one entry
554 if len(layers) == 1:
555 return layers[0], {}
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)
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)
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
578 layers = [listToColrLayers(l) for l in layers]
580 # No reason to have a colr layers with just one entry
581 if len(layers) == 1:
582 return layers[0], {}
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)
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)
595 # we've fully built dest; empty source prevents generalized build from kicking in
596 return paint, {}
598 def buildPaint(self, paint: _PaintInput) -> ot.Paint:
599 return self.tableBuilder.build(ot.Paint, paint)
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
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
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)
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()
639 errors = {}
640 baseGlyphs = []
641 layerBuilder = LayerListBuilder(allowLayerReuse=allowLayerReuse)
642 for baseGlyph, paint in colorGlyphItems:
643 try:
644 baseGlyphs.append(buildBaseGlyphPaintRecord(baseGlyph, layerBuilder, paint))
646 except (ColorLibError, OverflowError, ValueError, TypeError) as e:
647 errors[baseGlyph] = e
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()))
655 layers = layerBuilder.build()
656 glyphs = ot.BaseGlyphList()
657 glyphs.BaseGlyphCount = len(baseGlyphs)
658 glyphs.BaseGlyphPaintRecord = baseGlyphs
659 return (layers, glyphs)