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