builder.py 22 KB

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