2020-05-12 11:19:17 +01:00
|
|
|
"""
|
|
|
|
colorLib.builder: Build COLR/CPAL tables from scratch
|
|
|
|
|
|
|
|
"""
|
2020-03-11 13:27:59 +00:00
|
|
|
import collections
|
2020-03-09 19:18:59 +00:00
|
|
|
import copy
|
2020-02-17 18:25:45 +00:00
|
|
|
import enum
|
2020-03-06 18:37:30 +00:00
|
|
|
from functools import partial
|
2021-01-18 17:25:59 +00:00
|
|
|
from math import ceil, log
|
2020-10-09 18:16:51 +01:00
|
|
|
from typing import (
|
|
|
|
Any,
|
|
|
|
Dict,
|
2020-11-03 23:33:01 -08:00
|
|
|
Generator,
|
2020-10-09 18:16:51 +01:00
|
|
|
Iterable,
|
|
|
|
List,
|
|
|
|
Mapping,
|
|
|
|
Optional,
|
|
|
|
Sequence,
|
|
|
|
Tuple,
|
|
|
|
Type,
|
|
|
|
TypeVar,
|
|
|
|
Union,
|
|
|
|
)
|
2020-10-23 16:58:09 +01:00
|
|
|
from fontTools.misc.fixedTools import fixedToFloat
|
2020-03-06 18:37:30 +00:00
|
|
|
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,
|
2020-10-09 18:16:51 +01:00
|
|
|
CompositeMode,
|
2020-03-06 18:37:30 +00:00
|
|
|
VariableValue,
|
|
|
|
VariableFloat,
|
|
|
|
VariableInt,
|
|
|
|
)
|
2020-02-10 17:31:14 +00:00
|
|
|
from .errors import ColorLibError
|
2021-01-15 17:37:11 +00:00
|
|
|
from .geometry import round_start_circle_stable_containment
|
2021-02-04 21:57:20 -08:00
|
|
|
from .table_builder import (
|
|
|
|
convertTupleClass,
|
|
|
|
BuildCallback,
|
|
|
|
TableBuilder,
|
|
|
|
)
|
2020-02-10 17:31:14 +00:00
|
|
|
|
|
|
|
|
2020-03-06 18:37:30 +00:00
|
|
|
# TODO move type aliases to colorLib.types?
|
2020-10-09 18:16:51 +01:00
|
|
|
T = TypeVar("T")
|
2020-03-11 13:27:59 +00:00
|
|
|
_Kwargs = Mapping[str, Any]
|
2020-10-09 18:16:51 +01:00
|
|
|
_PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]]
|
|
|
|
_PaintInputList = Sequence[_PaintInput]
|
2020-11-03 19:11:24 +00:00
|
|
|
_ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]]
|
2020-03-10 11:38:29 +00:00
|
|
|
_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
|
2021-02-04 21:57:20 -08:00
|
|
|
|
2020-03-06 18:37:30 +00:00
|
|
|
|
2020-11-03 19:11:24 +00:00
|
|
|
MAX_PAINT_COLR_LAYER_COUNT = 255
|
2021-02-04 21:57:20 -08:00
|
|
|
_DEFAULT_ALPHA = VariableFloat(1.0)
|
2021-02-11 20:36:38 -08:00
|
|
|
_MAX_REUSE_LEN = 32
|
2021-02-04 21:57:20 -08:00
|
|
|
|
|
|
|
|
2021-02-10 22:33:48 -08:00
|
|
|
def _beforeBuildPaintVarRadialGradient(paint, source, srcMapFn=lambda v: v):
|
2021-02-04 21:57:20 -08:00
|
|
|
# normalize input types (which may or may not specify a varIdx)
|
2021-02-11 20:22:02 -08:00
|
|
|
x0 = convertTupleClass(VariableFloat, source["x0"])
|
2021-02-11 20:36:38 -08:00
|
|
|
y0 = convertTupleClass(VariableFloat, source["y0"])
|
|
|
|
r0 = convertTupleClass(VariableFloat, source["r0"])
|
|
|
|
x1 = convertTupleClass(VariableFloat, source["x1"])
|
|
|
|
y1 = convertTupleClass(VariableFloat, source["y1"])
|
|
|
|
r1 = convertTupleClass(VariableFloat, source["r1"])
|
2021-02-04 21:57:20 -08:00
|
|
|
|
|
|
|
# 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.value, y0.value), r0.value, (x1.value, y1.value), r1.value
|
|
|
|
)
|
|
|
|
x0, y0 = x0._replace(value=c.centre[0]), y0._replace(value=c.centre[1])
|
|
|
|
r0 = r0._replace(value=c.radius)
|
|
|
|
|
|
|
|
# update source to ensure paint is built with corrected values
|
2021-02-10 22:33:48 -08:00
|
|
|
source["x0"] = srcMapFn(x0)
|
|
|
|
source["y0"] = srcMapFn(y0)
|
|
|
|
source["r0"] = srcMapFn(r0)
|
|
|
|
source["x1"] = srcMapFn(x1)
|
|
|
|
source["y1"] = srcMapFn(y1)
|
|
|
|
source["r1"] = srcMapFn(r1)
|
2021-02-04 21:57:20 -08:00
|
|
|
|
|
|
|
return paint, source
|
|
|
|
|
|
|
|
|
2021-02-10 22:33:48 -08:00
|
|
|
def _beforeBuildPaintRadialGradient(paint, source):
|
|
|
|
return _beforeBuildPaintVarRadialGradient(paint, source, lambda v: v.value)
|
|
|
|
|
|
|
|
|
2021-02-04 21:57:20 -08:00
|
|
|
def _defaultColorIndex():
|
|
|
|
colorIndex = ot.ColorIndex()
|
2021-02-10 22:33:48 -08:00
|
|
|
colorIndex.Alpha = _DEFAULT_ALPHA.value
|
|
|
|
return colorIndex
|
|
|
|
|
|
|
|
|
|
|
|
def _defaultVarColorIndex():
|
|
|
|
colorIndex = ot.VarColorIndex()
|
2021-02-04 21:57:20 -08:00
|
|
|
colorIndex.Alpha = _DEFAULT_ALPHA
|
|
|
|
return colorIndex
|
|
|
|
|
|
|
|
|
|
|
|
def _defaultColorLine():
|
|
|
|
colorLine = ot.ColorLine()
|
|
|
|
colorLine.Extend = ExtendMode.PAD
|
|
|
|
return colorLine
|
|
|
|
|
|
|
|
|
2021-02-10 22:33:48 -08:00
|
|
|
def _defaultVarColorLine():
|
|
|
|
colorLine = ot.VarColorLine()
|
|
|
|
colorLine.Extend = ExtendMode.PAD
|
|
|
|
return colorLine
|
|
|
|
|
|
|
|
|
2021-02-04 21:57:20 -08:00
|
|
|
def _buildPaintCallbacks():
|
|
|
|
return {
|
|
|
|
(
|
|
|
|
BuildCallback.BEFORE_BUILD,
|
|
|
|
ot.Paint,
|
|
|
|
ot.PaintFormat.PaintRadialGradient,
|
|
|
|
): _beforeBuildPaintRadialGradient,
|
2021-02-10 22:33:48 -08:00
|
|
|
(
|
|
|
|
BuildCallback.BEFORE_BUILD,
|
|
|
|
ot.Paint,
|
|
|
|
ot.PaintFormat.PaintVarRadialGradient,
|
|
|
|
): _beforeBuildPaintVarRadialGradient,
|
2021-02-04 21:57:20 -08:00
|
|
|
(BuildCallback.CREATE_DEFAULT, ot.ColorIndex): _defaultColorIndex,
|
2021-02-10 22:33:48 -08:00
|
|
|
(BuildCallback.CREATE_DEFAULT, ot.VarColorIndex): _defaultVarColorIndex,
|
2021-02-04 21:57:20 -08:00
|
|
|
(BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine,
|
2021-02-10 22:33:48 -08:00
|
|
|
(BuildCallback.CREATE_DEFAULT, ot.VarColorLine): _defaultVarColorLine,
|
2021-02-04 21:57:20 -08:00
|
|
|
}
|
2020-10-23 16:58:09 +01:00
|
|
|
|
2020-03-06 18:37:30 +00:00
|
|
|
|
|
|
|
def populateCOLRv0(
|
|
|
|
table: ot.COLR,
|
|
|
|
colorGlyphsV0: _ColorGlyphsV0Dict,
|
2020-03-10 15:33:52 +00:00
|
|
|
glyphMap: Optional[Mapping[str, int]] = None,
|
2020-03-06 18:37:30 +00:00
|
|
|
):
|
|
|
|
"""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.
|
|
|
|
glyphMap: a map from glyph names to glyph indices, as returned from
|
|
|
|
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
|
|
|
|
"""
|
2020-03-11 13:27:59 +00:00
|
|
|
if glyphMap is not None:
|
|
|
|
colorGlyphItems = sorted(
|
|
|
|
colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
colorGlyphItems = colorGlyphsV0.items()
|
2020-03-06 18:37:30 +00:00
|
|
|
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.BaseGlyphRecordCount = len(baseGlyphRecords)
|
|
|
|
table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray()
|
|
|
|
table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords
|
|
|
|
table.LayerRecordArray = ot.LayerRecordArray()
|
|
|
|
table.LayerRecordArray.LayerRecord = layerRecords
|
|
|
|
table.LayerRecordCount = len(layerRecords)
|
|
|
|
|
|
|
|
|
|
|
|
def buildCOLR(
|
2020-03-09 15:01:50 +00:00
|
|
|
colorGlyphs: _ColorGlyphsDict,
|
2020-03-06 18:37:30 +00:00
|
|
|
version: Optional[int] = None,
|
2020-03-10 15:33:52 +00:00
|
|
|
glyphMap: Optional[Mapping[str, int]] = None,
|
2020-03-06 18:37:30 +00:00
|
|
|
varStore: Optional[ot.VarStore] = None,
|
|
|
|
) -> C_O_L_R_.table_C_O_L_R_:
|
2020-02-10 17:31:14 +00:00
|
|
|
"""Build COLR table from color layers mapping.
|
|
|
|
Args:
|
2020-10-09 18:16:51 +01:00
|
|
|
colorGlyphs: map of base glyph name to, either list of (layer glyph name,
|
2020-11-03 23:33:01 -08:00
|
|
|
color palette index) tuples for COLRv0; or a single Paint (dict) or
|
|
|
|
list of Paint for COLRv1.
|
2020-03-06 18:37:30 +00:00
|
|
|
version: the version of COLR table. If None, the version is determined
|
2020-10-09 18:16:51 +01:00
|
|
|
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.
|
2020-03-06 18:37:30 +00:00
|
|
|
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.
|
2020-02-10 17:31:14 +00:00
|
|
|
Return:
|
2020-03-06 18:37:30 +00:00
|
|
|
A new COLR table.
|
2020-02-10 17:31:14 +00:00
|
|
|
"""
|
2020-03-06 18:37:30 +00:00
|
|
|
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
|
2020-10-09 18:16:51 +01:00
|
|
|
colorGlyphsV0, colorGlyphsV1 = _split_color_glyphs_by_version(colorGlyphs)
|
2020-03-06 18:37:30 +00:00
|
|
|
if version == 0 and colorGlyphsV1:
|
2020-10-09 18:16:51 +01:00
|
|
|
raise ValueError("Can't encode COLRv1 glyphs in COLRv0")
|
2020-03-06 18:37:30 +00:00
|
|
|
else:
|
|
|
|
# unless explicitly requested for v1 or have variations, in which case
|
|
|
|
# we encode all color glyph as v1
|
|
|
|
colorGlyphsV0, colorGlyphsV1 = None, colorGlyphs
|
|
|
|
|
|
|
|
colr = ot.COLR()
|
|
|
|
|
|
|
|
if colorGlyphsV0:
|
|
|
|
populateCOLRv0(colr, colorGlyphsV0, glyphMap)
|
|
|
|
else:
|
|
|
|
colr.BaseGlyphRecordCount = colr.LayerRecordCount = 0
|
|
|
|
colr.BaseGlyphRecordArray = colr.LayerRecordArray = None
|
|
|
|
|
|
|
|
if colorGlyphsV1:
|
2020-11-03 23:33:01 -08:00
|
|
|
colr.LayerV1List, colr.BaseGlyphV1List = buildColrV1(colorGlyphsV1, glyphMap)
|
|
|
|
|
2020-03-06 18:37:30 +00:00
|
|
|
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:
|
2021-02-05 16:31:42 +00:00
|
|
|
self.ColorLayers = self._decompileColorLayersV0(colr)
|
2020-03-06 18:37:30 +00:00
|
|
|
else:
|
2020-03-11 16:46:33 +00:00
|
|
|
colr.VarStore = varStore
|
2020-03-06 18:37:30 +00:00
|
|
|
self.table = colr
|
|
|
|
|
|
|
|
return self
|
2020-02-10 17:31:14 +00:00
|
|
|
|
|
|
|
|
2020-02-17 18:25:45 +00:00
|
|
|
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(
|
2020-03-10 11:38:29 +00:00
|
|
|
labels: Iterable[_OptionalLocalizedString], nameTable: _n_a_m_e.table__n_a_m_e
|
2020-02-17 18:25:45 +00:00
|
|
|
) -> List[Optional[int]]:
|
|
|
|
return [
|
|
|
|
nameTable.addMultilingualName(l, mac=False)
|
|
|
|
if isinstance(l, dict)
|
2020-03-06 18:37:30 +00:00
|
|
|
else C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
|
2020-02-17 18:25:45 +00:00
|
|
|
if l is None
|
|
|
|
else nameTable.addMultilingualName({"en": l}, mac=False)
|
|
|
|
for l in labels
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-02-17 12:11:32 +00:00
|
|
|
def buildCPAL(
|
2020-03-10 11:38:29 +00:00
|
|
|
palettes: Sequence[Sequence[Tuple[float, float, float, float]]],
|
|
|
|
paletteTypes: Optional[Sequence[ColorPaletteType]] = None,
|
|
|
|
paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
|
|
|
|
paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
|
2020-03-06 18:37:30 +00:00
|
|
|
nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None,
|
|
|
|
) -> C_P_A_L_.table_C_P_A_L_:
|
2020-02-10 17:31:14 +00:00
|
|
|
"""Build CPAL table from list of color palettes.
|
|
|
|
|
|
|
|
Args:
|
2020-02-17 18:25:45 +00:00
|
|
|
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.
|
2020-02-10 17:31:14 +00:00
|
|
|
|
|
|
|
Return:
|
2020-02-17 18:25:45 +00:00
|
|
|
A new CPAL v0 or v1 table, if custom palette types or labels are specified.
|
2020-02-10 17:31:14 +00:00
|
|
|
"""
|
|
|
|
if len({len(p) for p in palettes}) != 1:
|
|
|
|
raise ColorLibError("color palettes have different lengths")
|
2020-02-17 18:25:45 +00:00
|
|
|
|
|
|
|
if (paletteLabels or paletteEntryLabels) and not nameTable:
|
|
|
|
raise TypeError(
|
|
|
|
"nameTable is required if palette or palette entries have labels"
|
|
|
|
)
|
|
|
|
|
2020-03-06 18:37:30 +00:00
|
|
|
cpal = C_P_A_L_.table_C_P_A_L_()
|
2020-02-10 17:31:14 +00:00
|
|
|
cpal.numPaletteEntries = len(palettes[0])
|
2020-02-17 18:25:45 +00:00
|
|
|
|
|
|
|
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
|
2020-03-06 18:37:30 +00:00
|
|
|
colors.append(
|
|
|
|
C_P_A_L_.Color(*(round(v * 255) for v in (blue, green, red, alpha)))
|
|
|
|
)
|
2020-02-17 18:25:45 +00:00
|
|
|
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:
|
2020-03-06 18:37:30 +00:00
|
|
|
cpal.paletteTypes = [C_P_A_L_.table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(
|
|
|
|
palettes
|
|
|
|
)
|
2020-02-17 18:25:45 +00:00
|
|
|
|
|
|
|
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:
|
2020-03-06 18:37:30 +00:00
|
|
|
cpal.paletteLabels = [C_P_A_L_.table_C_P_A_L_.NO_NAME_ID] * len(palettes)
|
2020-02-17 18:25:45 +00:00
|
|
|
|
|
|
|
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 = [
|
2020-03-06 18:37:30 +00:00
|
|
|
C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
|
2020-02-17 18:25:45 +00:00
|
|
|
] * cpal.numPaletteEntries
|
|
|
|
else:
|
|
|
|
cpal.version = 0
|
|
|
|
|
2020-02-10 17:31:14 +00:00
|
|
|
return cpal
|
2020-03-06 18:37:30 +00:00
|
|
|
|
|
|
|
|
2020-03-11 14:22:58 +00:00
|
|
|
# COLR v1 tables
|
|
|
|
# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
|
|
|
|
|
|
|
|
|
2020-11-16 16:38:22 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2020-10-09 18:16:51 +01:00
|
|
|
def _split_color_glyphs_by_version(
|
2020-03-06 18:37:30 +00:00
|
|
|
colorGlyphs: _ColorGlyphsDict,
|
2020-10-09 18:16:51 +01:00
|
|
|
) -> Tuple[_ColorGlyphsV0Dict, _ColorGlyphsDict]:
|
2020-03-06 18:37:30 +00:00
|
|
|
colorGlyphsV0 = {}
|
|
|
|
colorGlyphsV1 = {}
|
|
|
|
for baseGlyph, layers in colorGlyphs.items():
|
2020-11-16 16:38:22 +00:00
|
|
|
if all(_is_colrv0_layer(l) for l in layers):
|
2020-10-09 18:16:51 +01:00
|
|
|
colorGlyphsV0[baseGlyph] = layers
|
2020-03-06 18:37:30 +00:00
|
|
|
else:
|
2020-10-09 18:16:51 +01:00
|
|
|
colorGlyphsV1[baseGlyph] = layers
|
2020-03-06 18:37:30 +00:00
|
|
|
|
|
|
|
# sanity check
|
|
|
|
assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
|
|
|
|
|
|
|
|
return colorGlyphsV0, colorGlyphsV1
|
|
|
|
|
|
|
|
|
2020-11-03 23:33:01 -08:00
|
|
|
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
|
2021-02-11 20:36:38 -08:00
|
|
|
for ubound in range(
|
|
|
|
lbound + 2, min(num_layers + 1, lbound + 2 + _MAX_REUSE_LEN)
|
|
|
|
):
|
2020-11-03 23:33:01 -08:00
|
|
|
yield (lbound, ubound)
|
|
|
|
|
|
|
|
|
2020-11-10 21:51:33 -08:00
|
|
|
class LayerV1ListBuilder:
|
2020-11-04 18:47:16 -08:00
|
|
|
slices: List[ot.Paint]
|
|
|
|
layers: List[ot.Paint]
|
|
|
|
reusePool: Mapping[Tuple[Any, ...], int]
|
2020-12-04 11:42:14 -08:00
|
|
|
tuples: Mapping[int, Tuple[Any, ...]]
|
2020-12-07 12:59:12 -08:00
|
|
|
keepAlive: List[ot.Paint] # we need id to remain valid
|
2020-11-03 23:33:01 -08:00
|
|
|
|
|
|
|
def __init__(self):
|
2020-11-04 18:47:16 -08:00
|
|
|
self.slices = []
|
|
|
|
self.layers = []
|
|
|
|
self.reusePool = {}
|
2020-12-04 11:42:14 -08:00
|
|
|
self.tuples = {}
|
2020-12-07 12:59:12 -08:00
|
|
|
self.keepAlive = []
|
2020-12-04 11:42:14 -08:00
|
|
|
|
2021-02-04 21:57:20 -08:00
|
|
|
# We need to intercept construction of PaintColrLayers
|
|
|
|
callbacks = _buildPaintCallbacks()
|
|
|
|
callbacks[
|
|
|
|
(
|
|
|
|
BuildCallback.BEFORE_BUILD,
|
|
|
|
ot.Paint,
|
|
|
|
ot.PaintFormat.PaintColrLayers,
|
|
|
|
)
|
|
|
|
] = self._beforeBuildPaintColrLayers
|
|
|
|
self.tableBuilder = TableBuilder(callbacks)
|
|
|
|
|
2020-12-04 11:42:14 -08:00
|
|
|
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__"):
|
2020-12-06 17:56:49 +00:00
|
|
|
return tuple(
|
|
|
|
(k, _tuple_safe(v)) for k, v in sorted(value.__dict__.items())
|
|
|
|
)
|
2020-12-04 11:42:14 -08:00
|
|
|
elif isinstance(value, collections.abc.MutableSequence):
|
|
|
|
return tuple(_tuple_safe(e) for e in value)
|
|
|
|
return value
|
|
|
|
|
2020-12-08 10:01:37 +00:00
|
|
|
# Cache the tuples for individual Paint instead of the whole sequence
|
|
|
|
# because the seq could be a transient slice
|
2020-12-04 11:42:14 -08:00
|
|
|
result = self.tuples.get(id(paint), None)
|
|
|
|
if result is None:
|
|
|
|
result = _tuple_safe(paint)
|
|
|
|
self.tuples[id(paint)] = result
|
2020-12-07 12:59:12 -08:00
|
|
|
self.keepAlive.append(paint)
|
2020-12-04 11:42:14 -08:00
|
|
|
return result
|
|
|
|
|
|
|
|
def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
|
|
|
|
return tuple(self._paint_tuple(p) for p in paints)
|
2020-11-03 23:33:01 -08:00
|
|
|
|
2021-02-04 21:57:20 -08:00
|
|
|
# COLR layers is unusual in that it modifies shared state
|
|
|
|
# so we need a callback into an object
|
|
|
|
def _beforeBuildPaintColrLayers(self, dest, source):
|
|
|
|
paint = ot.Paint()
|
|
|
|
paint.Format = int(ot.PaintFormat.PaintColrLayers)
|
|
|
|
self.slices.append(paint)
|
|
|
|
|
|
|
|
# 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]
|
2020-11-03 23:33:01 -08:00
|
|
|
|
2021-02-11 20:36:38 -08:00
|
|
|
# No reason to have a colr layers with just one entry
|
|
|
|
if len(layers) == 1:
|
|
|
|
return layers[0], {}
|
|
|
|
|
2020-11-03 23:33:01 -08:00
|
|
|
# Look for reuse, with preference to longer sequences
|
2021-02-04 21:57:20 -08:00
|
|
|
# This may make the layer list smaller
|
2020-11-03 23:33:01 -08:00
|
|
|
found_reuse = True
|
|
|
|
while found_reuse:
|
|
|
|
found_reuse = False
|
|
|
|
|
2020-11-10 21:51:33 -08:00
|
|
|
ranges = sorted(
|
2021-02-04 21:57:20 -08:00
|
|
|
_reuse_ranges(len(layers)),
|
2020-11-03 23:33:01 -08:00
|
|
|
key=lambda t: (t[1] - t[0], t[1], t[0]),
|
2020-11-10 21:51:33 -08:00
|
|
|
reverse=True,
|
|
|
|
)
|
2020-11-03 23:33:01 -08:00
|
|
|
for lbound, ubound in ranges:
|
2020-12-04 11:42:14 -08:00
|
|
|
reuse_lbound = self.reusePool.get(
|
2021-02-04 21:57:20 -08:00
|
|
|
self._as_tuple(layers[lbound:ubound]), -1
|
2020-12-04 11:42:14 -08:00
|
|
|
)
|
2020-11-03 23:33:01 -08:00
|
|
|
if reuse_lbound == -1:
|
|
|
|
continue
|
|
|
|
new_slice = ot.Paint()
|
2021-02-05 12:11:43 +00:00
|
|
|
new_slice.Format = int(ot.PaintFormat.PaintColrLayers)
|
2020-11-03 23:33:01 -08:00
|
|
|
new_slice.NumLayers = ubound - lbound
|
|
|
|
new_slice.FirstLayerIndex = reuse_lbound
|
2021-02-04 21:57:20 -08:00
|
|
|
layers = layers[:lbound] + [new_slice] + layers[ubound:]
|
2020-11-06 14:59:52 -08:00
|
|
|
found_reuse = True
|
|
|
|
break
|
2020-11-03 23:33:01 -08:00
|
|
|
|
2021-02-04 21:57:20 -08:00
|
|
|
# The layer list is now final; if it's too big we need to tree it
|
2021-02-11 20:36:38 -08:00
|
|
|
is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
|
2021-02-04 21:57:20 -08:00
|
|
|
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]
|
|
|
|
|
|
|
|
paint.NumLayers = len(layers)
|
|
|
|
paint.FirstLayerIndex = len(self.layers)
|
|
|
|
self.layers.extend(layers)
|
2020-11-03 23:33:01 -08:00
|
|
|
|
2021-02-11 20:36:38 -08:00
|
|
|
# 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 not is_tree:
|
|
|
|
for lbound, ubound in _reuse_ranges(len(layers)):
|
|
|
|
self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
|
|
|
|
lbound + paint.FirstLayerIndex
|
|
|
|
)
|
2020-11-03 23:33:01 -08:00
|
|
|
|
2021-02-04 21:57:20 -08:00
|
|
|
# we've fully built dest; empty source prevents generalized build from kicking in
|
|
|
|
return paint, {}
|
2020-11-03 23:33:01 -08:00
|
|
|
|
2020-11-10 21:51:33 -08:00
|
|
|
def buildPaint(self, paint: _PaintInput) -> ot.Paint:
|
2021-02-04 21:57:20 -08:00
|
|
|
return self.tableBuilder.build(ot.Paint, paint)
|
2020-11-03 23:33:01 -08:00
|
|
|
|
2020-11-10 21:51:33 -08:00
|
|
|
def build(self) -> ot.LayerV1List:
|
|
|
|
layers = ot.LayerV1List()
|
|
|
|
layers.LayerCount = len(self.layers)
|
|
|
|
layers.Paint = self.layers
|
|
|
|
return layers
|
2020-11-03 23:33:01 -08:00
|
|
|
|
2020-03-06 18:37:30 +00:00
|
|
|
|
2020-03-10 15:33:52 +00:00
|
|
|
def buildBaseGlyphV1Record(
|
2020-11-10 21:51:33 -08:00
|
|
|
baseGlyph: str, layerBuilder: LayerV1ListBuilder, paint: _PaintInput
|
2020-07-01 18:16:07 +01:00
|
|
|
) -> ot.BaseGlyphV1List:
|
2020-03-10 15:33:52 +00:00
|
|
|
self = ot.BaseGlyphV1Record()
|
|
|
|
self.BaseGlyph = baseGlyph
|
2020-11-10 21:51:33 -08:00
|
|
|
self.Paint = layerBuilder.buildPaint(paint)
|
2020-03-10 15:33:52 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-10-23 16:58:09 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2020-11-03 23:33:01 -08:00
|
|
|
def buildColrV1(
|
2020-10-09 18:16:51 +01:00
|
|
|
colorGlyphs: _ColorGlyphsDict,
|
2020-03-10 15:33:52 +00:00
|
|
|
glyphMap: Optional[Mapping[str, int]] = None,
|
2020-11-03 23:33:01 -08:00
|
|
|
) -> Tuple[ot.LayerV1List, ot.BaseGlyphV1List]:
|
2020-03-11 13:27:59 +00:00
|
|
|
if glyphMap is not None:
|
|
|
|
colorGlyphItems = sorted(
|
|
|
|
colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
colorGlyphItems = colorGlyphs.items()
|
2020-10-23 16:58:09 +01:00
|
|
|
|
|
|
|
errors = {}
|
2020-11-03 23:33:01 -08:00
|
|
|
baseGlyphs = []
|
2020-11-10 21:51:33 -08:00
|
|
|
layerBuilder = LayerV1ListBuilder()
|
2020-11-03 23:33:01 -08:00
|
|
|
for baseGlyph, paint in colorGlyphItems:
|
2020-10-23 16:58:09 +01:00
|
|
|
try:
|
2020-11-10 21:51:33 -08:00
|
|
|
baseGlyphs.append(buildBaseGlyphV1Record(baseGlyph, layerBuilder, paint))
|
2020-11-03 23:33:01 -08:00
|
|
|
|
2020-10-23 16:58:09 +01:00
|
|
|
except (ColorLibError, OverflowError, ValueError, TypeError) as e:
|
|
|
|
errors[baseGlyph] = e
|
|
|
|
|
|
|
|
if errors:
|
|
|
|
failed_glyphs = _format_glyph_errors(errors)
|
|
|
|
exc = ColorLibError(f"Failed to build BaseGlyphV1List:\n{failed_glyphs}")
|
|
|
|
exc.errors = errors
|
|
|
|
raise exc from next(iter(errors.values()))
|
|
|
|
|
2020-11-10 21:51:33 -08:00
|
|
|
layers = layerBuilder.build()
|
2020-11-03 23:33:01 -08:00
|
|
|
glyphs = ot.BaseGlyphV1List()
|
|
|
|
glyphs.BaseGlyphCount = len(baseGlyphs)
|
|
|
|
glyphs.BaseGlyphV1Record = baseGlyphs
|
|
|
|
return (layers, glyphs)
|
2021-01-18 17:25:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _build_n_ary_tree(leaves, n):
|
|
|
|
"""Build N-ary tree from sequence of leaf nodes.
|
|
|
|
|
|
|
|
Return a list of lists where each non-leaf node is a list containing
|
|
|
|
max n nodes.
|
|
|
|
"""
|
|
|
|
if not leaves:
|
|
|
|
return []
|
|
|
|
|
|
|
|
assert n > 1
|
|
|
|
|
|
|
|
depth = ceil(log(len(leaves), n))
|
|
|
|
|
|
|
|
if depth <= 1:
|
|
|
|
return list(leaves)
|
|
|
|
|
|
|
|
# Fully populate complete subtrees of root until we have enough leaves left
|
|
|
|
root = []
|
|
|
|
unassigned = None
|
|
|
|
full_step = n ** (depth - 1)
|
|
|
|
for i in range(0, len(leaves), full_step):
|
|
|
|
subtree = leaves[i : i + full_step]
|
|
|
|
if len(subtree) < full_step:
|
|
|
|
unassigned = subtree
|
|
|
|
break
|
|
|
|
while len(subtree) > n:
|
|
|
|
subtree = [subtree[k : k + n] for k in range(0, len(subtree), n)]
|
|
|
|
root.append(subtree)
|
|
|
|
|
|
|
|
if unassigned:
|
|
|
|
# Recurse to fill the last subtree, which is the only partially populated one
|
|
|
|
subtree = _build_n_ary_tree(unassigned, n)
|
|
|
|
if len(subtree) <= n - len(root):
|
|
|
|
# replace last subtree with its children if they can still fit
|
|
|
|
root.extend(subtree)
|
|
|
|
else:
|
|
|
|
root.append(subtree)
|
|
|
|
assert len(root) <= n
|
|
|
|
|
|
|
|
return root
|