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
|
2020-10-09 18:16:51 +01:00
|
|
|
from typing import (
|
|
|
|
Any,
|
|
|
|
Dict,
|
|
|
|
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
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
_ColorGlyphsDict = Dict[str, Union[_PaintInputList, ot.LayerV1List]]
|
2020-03-10 11:38:29 +00:00
|
|
|
_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
|
2020-03-06 18:37:30 +00:00
|
|
|
_Number = Union[int, float]
|
2020-03-11 13:27:59 +00:00
|
|
|
_ScalarInput = Union[_Number, VariableValue, Tuple[_Number, int]]
|
2020-03-12 16:03:11 +00:00
|
|
|
_ColorStopTuple = Tuple[_ScalarInput, int]
|
|
|
|
_ColorStopInput = Union[_ColorStopTuple, _Kwargs, ot.ColorStop]
|
|
|
|
_ColorStopsList = Sequence[_ColorStopInput]
|
2020-03-11 19:07:14 +00:00
|
|
|
_ExtendInput = Union[int, str, ExtendMode]
|
2020-10-09 18:16:51 +01:00
|
|
|
_CompositeInput = Union[int, str, CompositeMode]
|
2020-03-11 13:27:59 +00:00
|
|
|
_ColorLineInput = Union[_Kwargs, ot.ColorLine]
|
|
|
|
_PointTuple = Tuple[_ScalarInput, _ScalarInput]
|
2020-10-09 18:16:51 +01:00
|
|
|
_AffineTuple = Tuple[
|
|
|
|
_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput
|
|
|
|
]
|
|
|
|
_AffineInput = Union[_AffineTuple, ot.Affine2x3]
|
2020-03-06 18:37:30 +00:00
|
|
|
|
2020-10-23 16:58:09 +01:00
|
|
|
MAX_LAYER_V1_COUNT = 255
|
|
|
|
|
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,
|
|
|
|
color palette index) tuples for COLRv0; or list of Paints (dicts)
|
|
|
|
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-07-01 18:16:07 +01:00
|
|
|
colr.BaseGlyphV1List = buildBaseGlyphV1List(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:
|
|
|
|
self._fromOTTable(colr)
|
|
|
|
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-07-02 10:27:47 +01:00
|
|
|
_DEFAULT_ALPHA = VariableFloat(1.0)
|
2020-03-11 14:22:58 +00:00
|
|
|
|
|
|
|
|
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-10-09 18:16:51 +01:00
|
|
|
if all(isinstance(l, tuple) and isinstance(l[1], int) for l in layers):
|
|
|
|
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-10-23 16:58:09 +01:00
|
|
|
def _to_variable_value(
|
|
|
|
value: _ScalarInput,
|
|
|
|
minValue: _Number,
|
|
|
|
maxValue: _Number,
|
|
|
|
cls: Type[VariableValue],
|
|
|
|
) -> VariableValue:
|
|
|
|
if not isinstance(value, cls):
|
|
|
|
try:
|
|
|
|
it = iter(value)
|
|
|
|
except TypeError: # not iterable
|
|
|
|
value = cls(value)
|
|
|
|
else:
|
|
|
|
value = cls._make(it)
|
|
|
|
if value.value < minValue:
|
|
|
|
raise OverflowError(f"{cls.__name__}: {value.value} < {minValue}")
|
|
|
|
if value.value > maxValue:
|
|
|
|
raise OverflowError(f"{cls.__name__}: {value.value} < {maxValue}")
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
_to_variable_f16dot16_float = partial(
|
|
|
|
_to_variable_value,
|
|
|
|
cls=VariableFloat,
|
|
|
|
minValue=-(2 ** 15),
|
|
|
|
maxValue=fixedToFloat(2 ** 31 - 1, 16),
|
|
|
|
)
|
|
|
|
_to_variable_f2dot14_float = partial(
|
|
|
|
_to_variable_value,
|
|
|
|
cls=VariableFloat,
|
|
|
|
minValue=-2.0,
|
|
|
|
maxValue=fixedToFloat(2 ** 15 - 1, 14),
|
|
|
|
)
|
|
|
|
_to_variable_int16 = partial(
|
|
|
|
_to_variable_value,
|
|
|
|
cls=VariableInt,
|
|
|
|
minValue=-(2 ** 15),
|
|
|
|
maxValue=2 ** 15 - 1,
|
|
|
|
)
|
|
|
|
_to_variable_uint16 = partial(
|
|
|
|
_to_variable_value,
|
|
|
|
cls=VariableInt,
|
|
|
|
minValue=0,
|
|
|
|
maxValue=2 ** 16,
|
|
|
|
)
|
2020-03-06 18:37:30 +00:00
|
|
|
|
|
|
|
|
2020-07-02 10:27:47 +01:00
|
|
|
def buildColorIndex(
|
|
|
|
paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA
|
|
|
|
) -> ot.ColorIndex:
|
|
|
|
self = ot.ColorIndex()
|
2020-03-06 18:37:30 +00:00
|
|
|
self.PaletteIndex = int(paletteIndex)
|
2020-10-23 16:58:09 +01:00
|
|
|
self.Alpha = _to_variable_f2dot14_float(alpha)
|
2020-03-06 18:37:30 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-10-09 18:30:50 +01:00
|
|
|
def buildPaintSolid(
|
2020-07-02 10:27:47 +01:00
|
|
|
paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA
|
2020-03-06 18:37:30 +00:00
|
|
|
) -> ot.Paint:
|
|
|
|
self = ot.Paint()
|
2020-10-26 15:47:22 +00:00
|
|
|
self.Format = int(ot.Paint.Format.PaintSolid)
|
2020-07-02 10:27:47 +01:00
|
|
|
self.Color = buildColorIndex(paletteIndex, alpha)
|
2020-03-06 18:37:30 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-03-12 16:03:11 +00:00
|
|
|
def buildColorStop(
|
|
|
|
offset: _ScalarInput,
|
|
|
|
paletteIndex: int,
|
2020-07-02 10:27:47 +01:00
|
|
|
alpha: _ScalarInput = _DEFAULT_ALPHA,
|
2020-03-12 16:03:11 +00:00
|
|
|
) -> ot.ColorStop:
|
2020-03-06 18:37:30 +00:00
|
|
|
self = ot.ColorStop()
|
2020-10-23 16:58:09 +01:00
|
|
|
self.StopOffset = _to_variable_f2dot14_float(offset)
|
2020-07-02 10:27:47 +01:00
|
|
|
self.Color = buildColorIndex(paletteIndex, alpha)
|
2020-03-06 18:37:30 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-10-09 18:16:51 +01:00
|
|
|
def _to_enum_value(v: Union[str, int, T], enumClass: Type[T]) -> T:
|
|
|
|
if isinstance(v, enumClass):
|
2020-03-11 13:27:59 +00:00
|
|
|
return v
|
|
|
|
elif isinstance(v, str):
|
|
|
|
try:
|
2020-10-09 18:16:51 +01:00
|
|
|
return getattr(enumClass, v.upper())
|
2020-03-11 13:27:59 +00:00
|
|
|
except AttributeError:
|
2020-10-09 18:16:51 +01:00
|
|
|
raise ValueError(f"{v!r} is not a valid {enumClass.__name__}")
|
|
|
|
return enumClass(v)
|
|
|
|
|
|
|
|
|
|
|
|
def _to_extend_mode(v: _ExtendInput) -> ExtendMode:
|
|
|
|
return _to_enum_value(v, ExtendMode)
|
|
|
|
|
|
|
|
|
|
|
|
def _to_composite_mode(v: _CompositeInput) -> CompositeMode:
|
|
|
|
return _to_enum_value(v, CompositeMode)
|
2020-03-11 13:27:59 +00:00
|
|
|
|
|
|
|
|
2020-03-06 18:37:30 +00:00
|
|
|
def buildColorLine(
|
2020-03-11 19:07:14 +00:00
|
|
|
stops: _ColorStopsList, extend: _ExtendInput = ExtendMode.PAD
|
2020-03-06 18:37:30 +00:00
|
|
|
) -> ot.ColorLine:
|
|
|
|
self = ot.ColorLine()
|
2020-03-11 13:27:59 +00:00
|
|
|
self.Extend = _to_extend_mode(extend)
|
|
|
|
self.StopCount = len(stops)
|
2020-03-06 18:37:30 +00:00
|
|
|
self.ColorStop = [
|
|
|
|
stop
|
|
|
|
if isinstance(stop, ot.ColorStop)
|
2020-03-12 16:03:11 +00:00
|
|
|
else buildColorStop(**stop)
|
|
|
|
if isinstance(stop, collections.abc.Mapping)
|
|
|
|
else buildColorStop(*stop)
|
2020-03-11 13:27:59 +00:00
|
|
|
for stop in stops
|
2020-03-06 18:37:30 +00:00
|
|
|
]
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-03-11 13:27:59 +00:00
|
|
|
def _to_color_line(obj):
|
|
|
|
if isinstance(obj, ot.ColorLine):
|
|
|
|
return obj
|
|
|
|
elif isinstance(obj, collections.abc.Mapping):
|
|
|
|
return buildColorLine(**obj)
|
|
|
|
raise TypeError(obj)
|
|
|
|
|
|
|
|
|
2020-10-09 18:30:50 +01:00
|
|
|
def buildPaintLinearGradient(
|
2020-03-11 13:27:59 +00:00
|
|
|
colorLine: _ColorLineInput,
|
2020-07-02 12:11:40 +01:00
|
|
|
p0: _PointTuple,
|
|
|
|
p1: _PointTuple,
|
|
|
|
p2: Optional[_PointTuple] = None,
|
2020-03-06 18:37:30 +00:00
|
|
|
) -> ot.Paint:
|
|
|
|
self = ot.Paint()
|
2020-10-26 15:47:22 +00:00
|
|
|
self.Format = int(ot.Paint.Format.PaintLinearGradient)
|
2020-03-11 13:27:59 +00:00
|
|
|
self.ColorLine = _to_color_line(colorLine)
|
2020-03-06 18:37:30 +00:00
|
|
|
|
|
|
|
if p2 is None:
|
2020-03-09 19:18:59 +00:00
|
|
|
p2 = copy.copy(p1)
|
2020-07-02 12:11:40 +01:00
|
|
|
for i, (x, y) in enumerate((p0, p1, p2)):
|
2020-10-23 16:58:09 +01:00
|
|
|
setattr(self, f"x{i}", _to_variable_int16(x))
|
|
|
|
setattr(self, f"y{i}", _to_variable_int16(y))
|
2020-03-06 18:37:30 +00:00
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-10-30 19:27:54 +00:00
|
|
|
def buildAffine2x3(transform: _AffineTuple) -> ot.Affine2x3:
|
|
|
|
if len(transform) != 6:
|
2020-11-03 11:06:23 -08:00
|
|
|
raise ValueError(f"Expected 6-tuple of floats, found: {transform!r}")
|
2020-10-09 18:16:51 +01:00
|
|
|
self = ot.Affine2x3()
|
2020-10-30 19:27:54 +00:00
|
|
|
# COLRv1 Affine2x3 uses the same column-major order to serialize a 2D
|
|
|
|
# Affine Transformation as the one used by fontTools.misc.transform.
|
|
|
|
# However, for historical reasons, the labels 'xy' and 'yx' are swapped.
|
|
|
|
# Their fundamental meaning is the same though.
|
|
|
|
# COLRv1 Affine2x3 follows the names found in FreeType and Cairo.
|
|
|
|
# In all case, the second element in the 6-tuple correspond to the
|
|
|
|
# y-part of the x basis vector, and the third to the x-part of the y
|
|
|
|
# basis vector.
|
|
|
|
# See https://github.com/googlefonts/colr-gradients-spec/pull/85
|
|
|
|
for i, attr in enumerate(("xx", "yx", "xy", "yy", "dx", "dy")):
|
|
|
|
setattr(self, attr, _to_variable_f16dot16_float(transform[i]))
|
2020-03-06 18:37:30 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-10-09 18:30:50 +01:00
|
|
|
def buildPaintRadialGradient(
|
2020-03-11 13:27:59 +00:00
|
|
|
colorLine: _ColorLineInput,
|
2020-07-02 12:11:40 +01:00
|
|
|
c0: _PointTuple,
|
|
|
|
c1: _PointTuple,
|
2020-03-11 13:27:59 +00:00
|
|
|
r0: _ScalarInput,
|
|
|
|
r1: _ScalarInput,
|
2020-03-06 18:37:30 +00:00
|
|
|
) -> ot.Paint:
|
|
|
|
|
|
|
|
self = ot.Paint()
|
2020-10-26 15:47:22 +00:00
|
|
|
self.Format = int(ot.Paint.Format.PaintRadialGradient)
|
2020-03-11 13:27:59 +00:00
|
|
|
self.ColorLine = _to_color_line(colorLine)
|
2020-03-06 18:37:30 +00:00
|
|
|
|
2020-07-02 12:11:40 +01:00
|
|
|
for i, (x, y), r in [(0, c0, r0), (1, c1, r1)]:
|
2020-10-23 16:58:09 +01:00
|
|
|
setattr(self, f"x{i}", _to_variable_int16(x))
|
|
|
|
setattr(self, f"y{i}", _to_variable_int16(y))
|
|
|
|
setattr(self, f"r{i}", _to_variable_uint16(r))
|
2020-03-06 18:37:30 +00:00
|
|
|
|
2020-10-09 18:16:51 +01:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
def buildPaintGlyph(glyph: str, paint: _PaintInput) -> ot.Paint:
|
|
|
|
self = ot.Paint()
|
2020-10-26 15:47:22 +00:00
|
|
|
self.Format = int(ot.Paint.Format.PaintGlyph)
|
2020-10-09 18:16:51 +01:00
|
|
|
self.Glyph = glyph
|
|
|
|
self.Paint = buildPaint(paint)
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-10-27 19:15:50 +00:00
|
|
|
def buildPaintColrSlice(
|
2020-10-27 12:47:10 +00:00
|
|
|
glyph: str, firstLayerIndex: int = 0, lastLayerIndex: int = 255
|
|
|
|
) -> ot.Paint:
|
2020-10-09 18:16:51 +01:00
|
|
|
self = ot.Paint()
|
2020-10-27 19:15:50 +00:00
|
|
|
self.Format = int(ot.Paint.Format.PaintColrSlice)
|
2020-10-09 18:16:51 +01:00
|
|
|
self.Glyph = glyph
|
2020-10-27 12:47:10 +00:00
|
|
|
if firstLayerIndex > lastLayerIndex:
|
|
|
|
raise ValueError(
|
|
|
|
f"Expected first <= last index, found: {firstLayerIndex} > {lastLayerIndex}"
|
|
|
|
)
|
|
|
|
for prefix in ("first", "last"):
|
|
|
|
indexName = f"{prefix}LayerIndex"
|
|
|
|
index = locals()[indexName]
|
|
|
|
if index < 0 or index > 255:
|
|
|
|
raise OverflowError(f"{indexName} ({index}) out of range [0..255]")
|
|
|
|
self.FirstLayerIndex = firstLayerIndex
|
|
|
|
self.LastLayerIndex = lastLayerIndex
|
2020-10-09 18:16:51 +01:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
def buildPaintTransform(transform: _AffineInput, paint: _PaintInput) -> ot.Paint:
|
|
|
|
self = ot.Paint()
|
2020-10-26 15:47:22 +00:00
|
|
|
self.Format = int(ot.Paint.Format.PaintTransform)
|
2020-10-09 18:16:51 +01:00
|
|
|
if not isinstance(transform, ot.Affine2x3):
|
2020-10-30 19:27:54 +00:00
|
|
|
transform = buildAffine2x3(transform)
|
2020-07-02 12:21:12 +01:00
|
|
|
self.Transform = transform
|
2020-10-09 18:16:51 +01:00
|
|
|
self.Paint = buildPaint(paint)
|
|
|
|
return self
|
|
|
|
|
2020-03-06 18:37:30 +00:00
|
|
|
|
2020-10-09 18:16:51 +01:00
|
|
|
def buildPaintComposite(
|
|
|
|
mode: _CompositeInput, source: _PaintInput, backdrop: _PaintInput
|
|
|
|
):
|
|
|
|
self = ot.Paint()
|
2020-10-26 15:47:22 +00:00
|
|
|
self.Format = int(ot.Paint.Format.PaintComposite)
|
2020-10-09 18:16:51 +01:00
|
|
|
self.SourcePaint = buildPaint(source)
|
|
|
|
self.CompositeMode = _to_composite_mode(mode)
|
|
|
|
self.BackdropPaint = buildPaint(backdrop)
|
2020-03-06 18:37:30 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-10-09 18:16:51 +01:00
|
|
|
_PAINT_BUILDERS = {
|
2020-10-09 18:30:50 +01:00
|
|
|
1: buildPaintSolid,
|
|
|
|
2: buildPaintLinearGradient,
|
|
|
|
3: buildPaintRadialGradient,
|
2020-10-09 18:16:51 +01:00
|
|
|
4: buildPaintGlyph,
|
2020-10-27 19:15:50 +00:00
|
|
|
5: buildPaintColrSlice,
|
2020-10-09 18:16:51 +01:00
|
|
|
6: buildPaintTransform,
|
|
|
|
7: buildPaintComposite,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def buildPaint(paint: _PaintInput) -> ot.Paint:
|
2020-03-11 13:27:59 +00:00
|
|
|
if isinstance(paint, ot.Paint):
|
|
|
|
return paint
|
|
|
|
elif isinstance(paint, int):
|
|
|
|
paletteIndex = paint
|
2020-10-09 18:30:50 +01:00
|
|
|
return buildPaintSolid(paletteIndex)
|
2020-10-09 18:16:51 +01:00
|
|
|
elif isinstance(paint, tuple):
|
|
|
|
layerGlyph, paint = paint
|
|
|
|
return buildPaintGlyph(layerGlyph, paint)
|
2020-03-11 13:27:59 +00:00
|
|
|
elif isinstance(paint, collections.abc.Mapping):
|
2020-10-09 18:16:51 +01:00
|
|
|
kwargs = dict(paint)
|
|
|
|
fmt = kwargs.pop("format")
|
|
|
|
try:
|
|
|
|
return _PAINT_BUILDERS[fmt](**kwargs)
|
|
|
|
except KeyError:
|
|
|
|
raise NotImplementedError(fmt)
|
|
|
|
raise TypeError(
|
|
|
|
f"expected int, Mapping or ot.Paint, found {type(paint).__name__}: {paint!r}"
|
|
|
|
)
|
2020-03-10 15:33:52 +00:00
|
|
|
|
|
|
|
|
2020-10-09 18:16:51 +01:00
|
|
|
def buildLayerV1List(layers: _PaintInputList) -> ot.LayerV1List:
|
2020-07-01 18:22:03 +01:00
|
|
|
self = ot.LayerV1List()
|
2020-10-23 16:58:09 +01:00
|
|
|
layerCount = len(layers)
|
|
|
|
if layerCount > MAX_LAYER_V1_COUNT:
|
|
|
|
raise OverflowError(
|
|
|
|
"LayerV1List.LayerCount: {layerCount} > {MAX_LAYER_V1_COUNT}"
|
|
|
|
)
|
|
|
|
self.LayerCount = layerCount
|
2020-10-09 18:16:51 +01:00
|
|
|
self.Paint = [buildPaint(layer) for layer in layers]
|
2020-03-06 18:37:30 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
|
2020-03-10 15:33:52 +00:00
|
|
|
def buildBaseGlyphV1Record(
|
2020-10-09 18:16:51 +01:00
|
|
|
baseGlyph: str, layers: Union[_PaintInputList, ot.LayerV1List]
|
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-07-01 18:22:03 +01:00
|
|
|
if not isinstance(layers, ot.LayerV1List):
|
|
|
|
layers = buildLayerV1List(layers)
|
|
|
|
self.LayerV1List = layers
|
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-07-01 18:16:07 +01:00
|
|
|
def buildBaseGlyphV1List(
|
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-07-01 18:16:07 +01:00
|
|
|
) -> 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 = {}
|
|
|
|
records = []
|
|
|
|
for baseGlyph, layers in colorGlyphItems:
|
|
|
|
try:
|
|
|
|
records.append(buildBaseGlyphV1Record(baseGlyph, layers))
|
|
|
|
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-07-01 18:16:07 +01:00
|
|
|
self = ot.BaseGlyphV1List()
|
2020-03-06 18:37:30 +00:00
|
|
|
self.BaseGlyphCount = len(records)
|
|
|
|
self.BaseGlyphV1Record = records
|
|
|
|
return self
|