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