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