123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659 |
- """
- colorLib.builder: Build COLR/CPAL tables from scratch
- """
- import collections
- import copy
- import enum
- from functools import partial
- from math import ceil, log
- from typing import (
- Any,
- Dict,
- Generator,
- Iterable,
- List,
- Mapping,
- Optional,
- Sequence,
- Tuple,
- Type,
- TypeVar,
- Union,
- )
- from fontTools.misc.arrayTools import intRect
- from fontTools.misc.fixedTools import fixedToFloat
- from fontTools.misc.treeTools import build_n_ary_tree
- from fontTools.ttLib.tables import C_O_L_R_
- from fontTools.ttLib.tables import C_P_A_L_
- from fontTools.ttLib.tables import _n_a_m_e
- from fontTools.ttLib.tables import otTables as ot
- from fontTools.ttLib.tables.otTables import ExtendMode, CompositeMode
- from .errors import ColorLibError
- from .geometry import round_start_circle_stable_containment
- from .table_builder import BuildCallback, TableBuilder
- # TODO move type aliases to colorLib.types?
- T = TypeVar("T")
- _Kwargs = Mapping[str, Any]
- _PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]]
- _PaintInputList = Sequence[_PaintInput]
- _ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]]
- _ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
- _ClipBoxInput = Union[
- Tuple[int, int, int, int, int], # format 1, variable
- Tuple[int, int, int, int], # format 0, non-variable
- ot.ClipBox,
- ]
- MAX_PAINT_COLR_LAYER_COUNT = 255
- _DEFAULT_ALPHA = 1.0
- _MAX_REUSE_LEN = 32
- def _beforeBuildPaintRadialGradient(paint, source):
- x0 = source["x0"]
- y0 = source["y0"]
- r0 = source["r0"]
- x1 = source["x1"]
- y1 = source["y1"]
- r1 = source["r1"]
- # TODO apparently no builder_test confirms this works (?)
- # avoid abrupt change after rounding when c0 is near c1's perimeter
- c = round_start_circle_stable_containment((x0, y0), r0, (x1, y1), r1)
- x0, y0 = c.centre
- r0 = c.radius
- # update source to ensure paint is built with corrected values
- source["x0"] = x0
- source["y0"] = y0
- source["r0"] = r0
- source["x1"] = x1
- source["y1"] = y1
- source["r1"] = r1
- return paint, source
- def _defaultColorStop():
- colorStop = ot.ColorStop()
- colorStop.Alpha = _DEFAULT_ALPHA
- return colorStop
- def _defaultVarColorStop():
- colorStop = ot.VarColorStop()
- colorStop.Alpha = _DEFAULT_ALPHA
- return colorStop
- def _defaultColorLine():
- colorLine = ot.ColorLine()
- colorLine.Extend = ExtendMode.PAD
- return colorLine
- def _defaultVarColorLine():
- colorLine = ot.VarColorLine()
- colorLine.Extend = ExtendMode.PAD
- return colorLine
- def _defaultPaintSolid():
- paint = ot.Paint()
- paint.Alpha = _DEFAULT_ALPHA
- return paint
- def _buildPaintCallbacks():
- return {
- (
- BuildCallback.BEFORE_BUILD,
- ot.Paint,
- ot.PaintFormat.PaintRadialGradient,
- ): _beforeBuildPaintRadialGradient,
- (
- BuildCallback.BEFORE_BUILD,
- ot.Paint,
- ot.PaintFormat.PaintVarRadialGradient,
- ): _beforeBuildPaintRadialGradient,
- (BuildCallback.CREATE_DEFAULT, ot.ColorStop): _defaultColorStop,
- (BuildCallback.CREATE_DEFAULT, ot.VarColorStop): _defaultVarColorStop,
- (BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine,
- (BuildCallback.CREATE_DEFAULT, ot.VarColorLine): _defaultVarColorLine,
- (
- BuildCallback.CREATE_DEFAULT,
- ot.Paint,
- ot.PaintFormat.PaintSolid,
- ): _defaultPaintSolid,
- (
- BuildCallback.CREATE_DEFAULT,
- ot.Paint,
- ot.PaintFormat.PaintVarSolid,
- ): _defaultPaintSolid,
- }
- def populateCOLRv0(
- table: ot.COLR,
- colorGlyphsV0: _ColorGlyphsV0Dict,
- glyphMap: Optional[Mapping[str, int]] = None,
- ):
- """Build v0 color layers and add to existing COLR table.
- Args:
- table: a raw ``otTables.COLR()`` object (not ttLib's ``table_C_O_L_R_``).
- colorGlyphsV0: map of base glyph names to lists of (layer glyph names,
- color palette index) tuples. Can be empty.
- glyphMap: a map from glyph names to glyph indices, as returned from
- ``TTFont.getReverseGlyphMap()``, to optionally sort base records by GID.
- """
- if glyphMap is not None:
- colorGlyphItems = sorted(
- colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
- )
- else:
- colorGlyphItems = colorGlyphsV0.items()
- baseGlyphRecords = []
- layerRecords = []
- for baseGlyph, layers in colorGlyphItems:
- baseRec = ot.BaseGlyphRecord()
- baseRec.BaseGlyph = baseGlyph
- baseRec.FirstLayerIndex = len(layerRecords)
- baseRec.NumLayers = len(layers)
- baseGlyphRecords.append(baseRec)
- for layerGlyph, paletteIndex in layers:
- layerRec = ot.LayerRecord()
- layerRec.LayerGlyph = layerGlyph
- layerRec.PaletteIndex = paletteIndex
- layerRecords.append(layerRec)
- table.BaseGlyphRecordArray = table.LayerRecordArray = None
- if baseGlyphRecords:
- table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray()
- table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords
- if layerRecords:
- table.LayerRecordArray = ot.LayerRecordArray()
- table.LayerRecordArray.LayerRecord = layerRecords
- table.BaseGlyphRecordCount = len(baseGlyphRecords)
- table.LayerRecordCount = len(layerRecords)
- def buildCOLR(
- colorGlyphs: _ColorGlyphsDict,
- version: Optional[int] = None,
- *,
- glyphMap: Optional[Mapping[str, int]] = None,
- varStore: Optional[ot.VarStore] = None,
- varIndexMap: Optional[ot.DeltaSetIndexMap] = None,
- clipBoxes: Optional[Dict[str, _ClipBoxInput]] = None,
- allowLayerReuse: bool = True,
- ) -> C_O_L_R_.table_C_O_L_R_:
- """Build COLR table from color layers mapping.
- Args:
- colorGlyphs: map of base glyph name to, either list of (layer glyph name,
- color palette index) tuples for COLRv0; or a single ``Paint`` (dict) or
- list of ``Paint`` for COLRv1.
- version: the version of COLR table. If None, the version is determined
- by the presence of COLRv1 paints or variation data (varStore), which
- require version 1; otherwise, if all base glyphs use only simple color
- layers, version 0 is used.
- glyphMap: a map from glyph names to glyph indices, as returned from
- TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
- varStore: Optional ItemVarationStore for deltas associated with v1 layer.
- varIndexMap: Optional DeltaSetIndexMap for deltas associated with v1 layer.
- clipBoxes: Optional map of base glyph name to clip box 4- or 5-tuples:
- (xMin, yMin, xMax, yMax) or (xMin, yMin, xMax, yMax, varIndexBase).
- Returns:
- A new COLR table.
- """
- self = C_O_L_R_.table_C_O_L_R_()
- if varStore is not None and version == 0:
- raise ValueError("Can't add VarStore to COLRv0")
- if version in (None, 0) and not varStore:
- # split color glyphs into v0 and v1 and encode separately
- colorGlyphsV0, colorGlyphsV1 = _split_color_glyphs_by_version(colorGlyphs)
- if version == 0 and colorGlyphsV1:
- raise ValueError("Can't encode COLRv1 glyphs in COLRv0")
- else:
- # unless explicitly requested for v1 or have variations, in which case
- # we encode all color glyph as v1
- colorGlyphsV0, colorGlyphsV1 = {}, colorGlyphs
- colr = ot.COLR()
- populateCOLRv0(colr, colorGlyphsV0, glyphMap)
- colr.LayerList, colr.BaseGlyphList = buildColrV1(
- colorGlyphsV1,
- glyphMap,
- allowLayerReuse=allowLayerReuse,
- )
- if version is None:
- version = 1 if (varStore or colorGlyphsV1) else 0
- elif version not in (0, 1):
- raise NotImplementedError(version)
- self.version = colr.Version = version
- if version == 0:
- self.ColorLayers = self._decompileColorLayersV0(colr)
- else:
- colr.ClipList = buildClipList(clipBoxes) if clipBoxes else None
- colr.VarIndexMap = varIndexMap
- colr.VarStore = varStore
- self.table = colr
- return self
- def buildClipList(clipBoxes: Dict[str, _ClipBoxInput]) -> ot.ClipList:
- clipList = ot.ClipList()
- clipList.Format = 1
- clipList.clips = {name: buildClipBox(box) for name, box in clipBoxes.items()}
- return clipList
- def buildClipBox(clipBox: _ClipBoxInput) -> ot.ClipBox:
- if isinstance(clipBox, ot.ClipBox):
- return clipBox
- n = len(clipBox)
- clip = ot.ClipBox()
- if n not in (4, 5):
- raise ValueError(f"Invalid ClipBox: expected 4 or 5 values, found {n}")
- clip.xMin, clip.yMin, clip.xMax, clip.yMax = intRect(clipBox[:4])
- clip.Format = int(n == 5) + 1
- if n == 5:
- clip.VarIndexBase = int(clipBox[4])
- return clip
- class ColorPaletteType(enum.IntFlag):
- USABLE_WITH_LIGHT_BACKGROUND = 0x0001
- USABLE_WITH_DARK_BACKGROUND = 0x0002
- @classmethod
- def _missing_(cls, value):
- # enforce reserved bits
- if isinstance(value, int) and (value < 0 or value & 0xFFFC != 0):
- raise ValueError(f"{value} is not a valid {cls.__name__}")
- return super()._missing_(value)
- # None, 'abc' or {'en': 'abc', 'de': 'xyz'}
- _OptionalLocalizedString = Union[None, str, Dict[str, str]]
- def buildPaletteLabels(
- labels: Iterable[_OptionalLocalizedString], nameTable: _n_a_m_e.table__n_a_m_e
- ) -> List[Optional[int]]:
- return [
- nameTable.addMultilingualName(l, mac=False)
- if isinstance(l, dict)
- else C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
- if l is None
- else nameTable.addMultilingualName({"en": l}, mac=False)
- for l in labels
- ]
- def buildCPAL(
- palettes: Sequence[Sequence[Tuple[float, float, float, float]]],
- paletteTypes: Optional[Sequence[ColorPaletteType]] = None,
- paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
- paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
- nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None,
- ) -> C_P_A_L_.table_C_P_A_L_:
- """Build CPAL table from list of color palettes.
- Args:
- palettes: list of lists of colors encoded as tuples of (R, G, B, A) floats
- in the range [0..1].
- paletteTypes: optional list of ColorPaletteType, one for each palette.
- paletteLabels: optional list of palette labels. Each lable can be either:
- None (no label), a string (for for default English labels), or a
- localized string (as a dict keyed with BCP47 language codes).
- paletteEntryLabels: optional list of palette entry labels, one for each
- palette entry (see paletteLabels).
- nameTable: optional name table where to store palette and palette entry
- labels. Required if either paletteLabels or paletteEntryLabels is set.
- Return:
- A new CPAL v0 or v1 table, if custom palette types or labels are specified.
- """
- if len({len(p) for p in palettes}) != 1:
- raise ColorLibError("color palettes have different lengths")
- if (paletteLabels or paletteEntryLabels) and not nameTable:
- raise TypeError(
- "nameTable is required if palette or palette entries have labels"
- )
- cpal = C_P_A_L_.table_C_P_A_L_()
- cpal.numPaletteEntries = len(palettes[0])
- cpal.palettes = []
- for i, palette in enumerate(palettes):
- colors = []
- for j, color in enumerate(palette):
- if not isinstance(color, tuple) or len(color) != 4:
- raise ColorLibError(
- f"In palette[{i}][{j}]: expected (R, G, B, A) tuple, got {color!r}"
- )
- if any(v > 1 or v < 0 for v in color):
- raise ColorLibError(
- f"palette[{i}][{j}] has invalid out-of-range [0..1] color: {color!r}"
- )
- # input colors are RGBA, CPAL encodes them as BGRA
- red, green, blue, alpha = color
- colors.append(
- C_P_A_L_.Color(*(round(v * 255) for v in (blue, green, red, alpha)))
- )
- cpal.palettes.append(colors)
- if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
- cpal.version = 1
- if paletteTypes is not None:
- if len(paletteTypes) != len(palettes):
- raise ColorLibError(
- f"Expected {len(palettes)} paletteTypes, got {len(paletteTypes)}"
- )
- cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes]
- else:
- cpal.paletteTypes = [C_P_A_L_.table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(
- palettes
- )
- if paletteLabels is not None:
- if len(paletteLabels) != len(palettes):
- raise ColorLibError(
- f"Expected {len(palettes)} paletteLabels, got {len(paletteLabels)}"
- )
- cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
- else:
- cpal.paletteLabels = [C_P_A_L_.table_C_P_A_L_.NO_NAME_ID] * len(palettes)
- if paletteEntryLabels is not None:
- if len(paletteEntryLabels) != cpal.numPaletteEntries:
- raise ColorLibError(
- f"Expected {cpal.numPaletteEntries} paletteEntryLabels, "
- f"got {len(paletteEntryLabels)}"
- )
- cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
- else:
- cpal.paletteEntryLabels = [
- C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
- ] * cpal.numPaletteEntries
- else:
- cpal.version = 0
- return cpal
- # COLR v1 tables
- # See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
- def _is_colrv0_layer(layer: Any) -> bool:
- # Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which
- # the first element is a str (the layerGlyph) and the second element is an int
- # (CPAL paletteIndex).
- # https://github.com/googlefonts/ufo2ft/issues/426
- try:
- layerGlyph, paletteIndex = layer
- except (TypeError, ValueError):
- return False
- else:
- return isinstance(layerGlyph, str) and isinstance(paletteIndex, int)
- def _split_color_glyphs_by_version(
- colorGlyphs: _ColorGlyphsDict,
- ) -> Tuple[_ColorGlyphsV0Dict, _ColorGlyphsDict]:
- colorGlyphsV0 = {}
- colorGlyphsV1 = {}
- for baseGlyph, layers in colorGlyphs.items():
- if all(_is_colrv0_layer(l) for l in layers):
- colorGlyphsV0[baseGlyph] = layers
- else:
- colorGlyphsV1[baseGlyph] = layers
- # sanity check
- assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
- return colorGlyphsV0, colorGlyphsV1
- def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
- # TODO feels like something itertools might have already
- for lbound in range(num_layers):
- # Reuse of very large #s of layers is relatively unlikely
- # +2: we want sequences of at least 2
- # otData handles single-record duplication
- for ubound in range(
- lbound + 2, min(num_layers + 1, lbound + 2 + _MAX_REUSE_LEN)
- ):
- yield (lbound, ubound)
- class LayerReuseCache:
- reusePool: Mapping[Tuple[Any, ...], int]
- tuples: Mapping[int, Tuple[Any, ...]]
- keepAlive: List[ot.Paint] # we need id to remain valid
- def __init__(self):
- self.reusePool = {}
- self.tuples = {}
- self.keepAlive = []
- def _paint_tuple(self, paint: ot.Paint):
- # start simple, who even cares about cyclic graphs or interesting field types
- def _tuple_safe(value):
- if isinstance(value, enum.Enum):
- return value
- elif hasattr(value, "__dict__"):
- return tuple(
- (k, _tuple_safe(v)) for k, v in sorted(value.__dict__.items())
- )
- elif isinstance(value, collections.abc.MutableSequence):
- return tuple(_tuple_safe(e) for e in value)
- return value
- # Cache the tuples for individual Paint instead of the whole sequence
- # because the seq could be a transient slice
- result = self.tuples.get(id(paint), None)
- if result is None:
- result = _tuple_safe(paint)
- self.tuples[id(paint)] = result
- self.keepAlive.append(paint)
- return result
- def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
- return tuple(self._paint_tuple(p) for p in paints)
- def try_reuse(self, layers: List[ot.Paint]) -> List[ot.Paint]:
- found_reuse = True
- while found_reuse:
- found_reuse = False
- ranges = sorted(
- _reuse_ranges(len(layers)),
- key=lambda t: (t[1] - t[0], t[1], t[0]),
- reverse=True,
- )
- for lbound, ubound in ranges:
- reuse_lbound = self.reusePool.get(
- self._as_tuple(layers[lbound:ubound]), -1
- )
- if reuse_lbound == -1:
- continue
- new_slice = ot.Paint()
- new_slice.Format = int(ot.PaintFormat.PaintColrLayers)
- new_slice.NumLayers = ubound - lbound
- new_slice.FirstLayerIndex = reuse_lbound
- layers = layers[:lbound] + [new_slice] + layers[ubound:]
- found_reuse = True
- break
- return layers
- def add(self, layers: List[ot.Paint], first_layer_index: int):
- for lbound, ubound in _reuse_ranges(len(layers)):
- self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
- lbound + first_layer_index
- )
- class LayerListBuilder:
- layers: List[ot.Paint]
- cache: LayerReuseCache
- allowLayerReuse: bool
- def __init__(self, *, allowLayerReuse=True):
- self.layers = []
- if allowLayerReuse:
- self.cache = LayerReuseCache()
- else:
- self.cache = None
- # We need to intercept construction of PaintColrLayers
- callbacks = _buildPaintCallbacks()
- callbacks[
- (
- BuildCallback.BEFORE_BUILD,
- ot.Paint,
- ot.PaintFormat.PaintColrLayers,
- )
- ] = self._beforeBuildPaintColrLayers
- self.tableBuilder = TableBuilder(callbacks)
- # COLR layers is unusual in that it modifies shared state
- # so we need a callback into an object
- def _beforeBuildPaintColrLayers(self, dest, source):
- # Sketchy gymnastics: a sequence input will have dropped it's layers
- # into NumLayers; get it back
- if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
- layers = source["NumLayers"]
- else:
- layers = source["Layers"]
- # Convert maps seqs or whatever into typed objects
- layers = [self.buildPaint(l) for l in layers]
- # No reason to have a colr layers with just one entry
- if len(layers) == 1:
- return layers[0], {}
- if self.cache is not None:
- # Look for reuse, with preference to longer sequences
- # This may make the layer list smaller
- layers = self.cache.try_reuse(layers)
- # The layer list is now final; if it's too big we need to tree it
- is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
- layers = build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
- # We now have a tree of sequences with Paint leaves.
- # Convert the sequences into PaintColrLayers.
- def listToColrLayers(layer):
- if isinstance(layer, collections.abc.Sequence):
- return self.buildPaint(
- {
- "Format": ot.PaintFormat.PaintColrLayers,
- "Layers": [listToColrLayers(l) for l in layer],
- }
- )
- return layer
- layers = [listToColrLayers(l) for l in layers]
- # No reason to have a colr layers with just one entry
- if len(layers) == 1:
- return layers[0], {}
- paint = ot.Paint()
- paint.Format = int(ot.PaintFormat.PaintColrLayers)
- paint.NumLayers = len(layers)
- paint.FirstLayerIndex = len(self.layers)
- self.layers.extend(layers)
- # Register our parts for reuse provided we aren't a tree
- # If we are a tree the leaves registered for reuse and that will suffice
- if self.cache is not None and not is_tree:
- self.cache.add(layers, paint.FirstLayerIndex)
- # we've fully built dest; empty source prevents generalized build from kicking in
- return paint, {}
- def buildPaint(self, paint: _PaintInput) -> ot.Paint:
- return self.tableBuilder.build(ot.Paint, paint)
- def build(self) -> Optional[ot.LayerList]:
- if not self.layers:
- return None
- layers = ot.LayerList()
- layers.LayerCount = len(self.layers)
- layers.Paint = self.layers
- return layers
- def buildBaseGlyphPaintRecord(
- baseGlyph: str, layerBuilder: LayerListBuilder, paint: _PaintInput
- ) -> ot.BaseGlyphList:
- self = ot.BaseGlyphPaintRecord()
- self.BaseGlyph = baseGlyph
- self.Paint = layerBuilder.buildPaint(paint)
- return self
- def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
- lines = []
- for baseGlyph, error in sorted(errors.items()):
- lines.append(f" {baseGlyph} => {type(error).__name__}: {error}")
- return "\n".join(lines)
- def buildColrV1(
- colorGlyphs: _ColorGlyphsDict,
- glyphMap: Optional[Mapping[str, int]] = None,
- *,
- allowLayerReuse: bool = True,
- ) -> Tuple[Optional[ot.LayerList], ot.BaseGlyphList]:
- if glyphMap is not None:
- colorGlyphItems = sorted(
- colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
- )
- else:
- colorGlyphItems = colorGlyphs.items()
- errors = {}
- baseGlyphs = []
- layerBuilder = LayerListBuilder(allowLayerReuse=allowLayerReuse)
- for baseGlyph, paint in colorGlyphItems:
- try:
- baseGlyphs.append(buildBaseGlyphPaintRecord(baseGlyph, layerBuilder, paint))
- except (ColorLibError, OverflowError, ValueError, TypeError) as e:
- errors[baseGlyph] = e
- if errors:
- failed_glyphs = _format_glyph_errors(errors)
- exc = ColorLibError(f"Failed to build BaseGlyphList:\n{failed_glyphs}")
- exc.errors = errors
- raise exc from next(iter(errors.values()))
- layers = layerBuilder.build()
- glyphs = ot.BaseGlyphList()
- glyphs.BaseGlyphCount = len(baseGlyphs)
- glyphs.BaseGlyphPaintRecord = baseGlyphs
- return (layers, glyphs)
|