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

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)