• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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