Generic build fns

This commit is contained in:
rsheeter 2021-02-04 21:57:20 -08:00
parent 587b4868cc
commit cf4a4087be
5 changed files with 1048 additions and 780 deletions

View File

@ -25,7 +25,6 @@ from fontTools.misc.fixedTools import fixedToFloat
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.otBase import BaseTable
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otTables import (
ExtendMode,
@ -36,6 +35,11 @@ from fontTools.ttLib.tables.otTables import (
)
from .errors import ColorLibError
from .geometry import round_start_circle_stable_containment
from .table_builder import (
convertTupleClass,
BuildCallback,
TableBuilder,
)
# TODO move type aliases to colorLib.types?
@ -45,21 +49,63 @@ _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]]]
_Number = Union[int, float]
_ScalarInput = Union[_Number, VariableValue, Tuple[_Number, int]]
_ColorStopTuple = Tuple[_ScalarInput, int]
_ColorStopInput = Union[_ColorStopTuple, _Kwargs, ot.ColorStop]
_ColorStopsList = Sequence[_ColorStopInput]
_ExtendInput = Union[int, str, ExtendMode]
_CompositeInput = Union[int, str, CompositeMode]
_ColorLineInput = Union[_Kwargs, ot.ColorLine]
_PointTuple = Tuple[_ScalarInput, _ScalarInput]
_AffineTuple = Tuple[
_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput
]
_AffineInput = Union[_AffineTuple, ot.Affine2x3]
MAX_PAINT_COLR_LAYER_COUNT = 255
_DEFAULT_ALPHA = VariableFloat(1.0)
def _beforeBuildPaintRadialGradient(paint, source):
# normalize input types (which may or may not specify a varIdx)
x0 = convertTupleClass(VariableFloat, source.get("x0", 0.0))
y0 = convertTupleClass(VariableFloat, source.get("y0", 0.0))
r0 = convertTupleClass(VariableFloat, source.get("r0", 0.0))
x1 = convertTupleClass(VariableFloat, source.get("x1", 0.0))
y1 = convertTupleClass(VariableFloat, source.get("y1", 0.0))
r1 = convertTupleClass(VariableFloat, source.get("r1", 0.0))
# 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
source["x0"] = x0
source["y0"] = y0
source["r0"] = r0
source["x1"] = x1
source["y1"] = y1
source["r1"] = r1
return paint, source
def _defaultColorIndex():
colorIndex = ot.ColorIndex()
colorIndex.Alpha = _DEFAULT_ALPHA
return colorIndex
def _defaultColorLine():
colorLine = ot.ColorLine()
colorLine.Extend = ExtendMode.PAD
return colorLine
def _buildPaintCallbacks():
return {
(
BuildCallback.BEFORE_BUILD,
ot.Paint,
ot.PaintFormat.PaintRadialGradient,
): _beforeBuildPaintRadialGradient,
(BuildCallback.CREATE_DEFAULT, ot.ColorIndex): _defaultColorIndex,
(BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine,
}
def populateCOLRv0(
@ -112,7 +158,6 @@ def buildCOLR(
varStore: Optional[ot.VarStore] = None,
) -> 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
@ -124,7 +169,6 @@ def buildCOLR(
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.
Return:
A new COLR table.
"""
@ -295,8 +339,6 @@ def buildCPAL(
# COLR v1 tables
# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
_DEFAULT_ALPHA = VariableFloat(1.0)
def _is_colrv0_layer(layer: Any) -> bool:
# Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which
@ -328,116 +370,6 @@ def _split_color_glyphs_by_version(
return colorGlyphsV0, colorGlyphsV1
def _to_variable_value(
value: _ScalarInput,
cls: Type[VariableValue] = VariableFloat,
minValue: Optional[_Number] = None,
maxValue: Optional[_Number] = None,
) -> VariableValue:
if not isinstance(value, cls):
try:
it = iter(value)
except TypeError: # not iterable
value = cls(value)
else:
value = cls._make(it)
if minValue is not None and value.value < minValue:
raise OverflowError(f"{cls.__name__}: {value.value} < {minValue}")
if maxValue is not None and 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,
)
def buildColorIndex(
paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA
) -> ot.ColorIndex:
self = ot.ColorIndex()
self.PaletteIndex = int(paletteIndex)
self.Alpha = _to_variable_f2dot14_float(alpha)
return self
def buildColorStop(
offset: _ScalarInput,
paletteIndex: int,
alpha: _ScalarInput = _DEFAULT_ALPHA,
) -> ot.ColorStop:
self = ot.ColorStop()
self.StopOffset = _to_variable_f2dot14_float(offset)
self.Color = buildColorIndex(paletteIndex, alpha)
return self
def _to_enum_value(v: Union[str, int, T], enumClass: Type[T]) -> T:
if isinstance(v, enumClass):
return v
elif isinstance(v, str):
try:
return getattr(enumClass, v.upper())
except AttributeError:
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)
def buildColorLine(
stops: _ColorStopsList, extend: _ExtendInput = ExtendMode.PAD
) -> ot.ColorLine:
self = ot.ColorLine()
self.Extend = _to_extend_mode(extend)
self.StopCount = len(stops)
self.ColorStop = [
stop
if isinstance(stop, ot.ColorStop)
else buildColorStop(**stop)
if isinstance(stop, collections.abc.Mapping)
else buildColorStop(*stop)
for stop in stops
]
return self
def _to_color_line(obj):
if isinstance(obj, ot.ColorLine):
return obj
elif isinstance(obj, collections.abc.Mapping):
return buildColorLine(**obj)
raise TypeError(obj)
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):
@ -463,6 +395,17 @@ class LayerV1ListBuilder:
self.tuples = {}
self.keepAlive = []
# We need to intercept construction of PaintColrLayers
callbacks = _buildPaintCallbacks()
callbacks[
(
BuildCallback.BEFORE_BUILD,
ot.Paint,
ot.PaintFormat.PaintColrLayers,
)
] = self._beforeBuildPaintColrLayers
self.tableBuilder = TableBuilder(callbacks)
def _paint_tuple(self, paint: ot.Paint):
# start simple, who even cares about cyclic graphs or interesting field types
def _tuple_safe(value):
@ -488,186 +431,37 @@ class LayerV1ListBuilder:
def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
return tuple(self._paint_tuple(p) for p in paints)
def buildPaintSolid(
self, paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA
) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintSolid)
ot_paint.Color = buildColorIndex(paletteIndex, alpha)
return ot_paint
# 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)
def buildPaintLinearGradient(
self,
colorLine: _ColorLineInput,
p0: _PointTuple,
p1: _PointTuple,
p2: Optional[_PointTuple] = None,
) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintLinearGradient)
ot_paint.ColorLine = _to_color_line(colorLine)
# 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"]
if p2 is None:
p2 = copy.copy(p1)
for i, (x, y) in enumerate((p0, p1, p2)):
setattr(ot_paint, f"x{i}", _to_variable_int16(x))
setattr(ot_paint, f"y{i}", _to_variable_int16(y))
return ot_paint
def buildPaintRadialGradient(
self,
colorLine: _ColorLineInput,
c0: _PointTuple,
c1: _PointTuple,
r0: _ScalarInput,
r1: _ScalarInput,
) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintRadialGradient)
ot_paint.ColorLine = _to_color_line(colorLine)
# normalize input types (which may or may not specify a varIdx)
x0, y0 = _to_variable_value(c0[0]), _to_variable_value(c0[1])
r0 = _to_variable_value(r0)
x1, y1 = _to_variable_value(c1[0]), _to_variable_value(c1[1])
r1 = _to_variable_value(r1)
# 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)
for i, (x, y, r) in enumerate(((x0, y0, r0), (x1, y1, r1))):
# rounding happens here as floats are converted to integers
setattr(ot_paint, f"x{i}", _to_variable_int16(x))
setattr(ot_paint, f"y{i}", _to_variable_int16(y))
setattr(ot_paint, f"r{i}", _to_variable_uint16(r))
return ot_paint
def buildPaintSweepGradient(
self,
colorLine: _ColorLineInput,
centerX: _ScalarInput,
centerY: _ScalarInput,
startAngle: _ScalarInput,
endAngle: _ScalarInput,
) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintSweepGradient)
ot_paint.ColorLine = _to_color_line(colorLine)
ot_paint.centerX = _to_variable_int16(centerX)
ot_paint.centerY = _to_variable_int16(centerY)
ot_paint.startAngle = _to_variable_f16dot16_float(startAngle)
ot_paint.endAngle = _to_variable_f16dot16_float(endAngle)
return ot_paint
def buildPaintGlyph(self, glyph: str, paint: _PaintInput) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintGlyph)
ot_paint.Glyph = glyph
ot_paint.Paint = self.buildPaint(paint)
return ot_paint
def buildPaintColrGlyph(self, glyph: str) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintColrGlyph)
ot_paint.Glyph = glyph
return ot_paint
def buildPaintTransform(
self, transform: _AffineInput, paint: _PaintInput
) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintTransform)
if not isinstance(transform, ot.Affine2x3):
transform = buildAffine2x3(transform)
ot_paint.Transform = transform
ot_paint.Paint = self.buildPaint(paint)
return ot_paint
def buildPaintTranslate(
self, paint: _PaintInput, dx: _ScalarInput, dy: _ScalarInput
):
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintTranslate)
ot_paint.Paint = self.buildPaint(paint)
ot_paint.dx = _to_variable_f16dot16_float(dx)
ot_paint.dy = _to_variable_f16dot16_float(dy)
return ot_paint
def buildPaintRotate(
self,
paint: _PaintInput,
angle: _ScalarInput,
centerX: _ScalarInput,
centerY: _ScalarInput,
) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintRotate)
ot_paint.Paint = self.buildPaint(paint)
ot_paint.angle = _to_variable_f16dot16_float(angle)
ot_paint.centerX = _to_variable_f16dot16_float(centerX)
ot_paint.centerY = _to_variable_f16dot16_float(centerY)
return ot_paint
def buildPaintSkew(
self,
paint: _PaintInput,
xSkewAngle: _ScalarInput,
ySkewAngle: _ScalarInput,
centerX: _ScalarInput,
centerY: _ScalarInput,
) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintSkew)
ot_paint.Paint = self.buildPaint(paint)
ot_paint.xSkewAngle = _to_variable_f16dot16_float(xSkewAngle)
ot_paint.ySkewAngle = _to_variable_f16dot16_float(ySkewAngle)
ot_paint.centerX = _to_variable_f16dot16_float(centerX)
ot_paint.centerY = _to_variable_f16dot16_float(centerY)
return ot_paint
def buildPaintComposite(
self,
mode: _CompositeInput,
source: _PaintInput,
backdrop: _PaintInput,
):
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintComposite)
ot_paint.SourcePaint = self.buildPaint(source)
ot_paint.CompositeMode = _to_composite_mode(mode)
ot_paint.BackdropPaint = self.buildPaint(backdrop)
return ot_paint
def buildColrLayers(self, paints: List[_PaintInput]) -> ot.Paint:
ot_paint = ot.Paint()
ot_paint.Format = int(ot.PaintFormat.PaintColrLayers)
self.slices.append(ot_paint)
paints = [
self.buildPaint(p)
for p in _build_n_ary_tree(paints, n=MAX_PAINT_COLR_LAYER_COUNT)
]
# Convert maps seqs or whatever into typed objects
layers = [self.buildPaint(l) for l in layers]
# Look for reuse, with preference to longer sequences
# This may make the layer list smaller
found_reuse = True
while found_reuse:
found_reuse = False
ranges = sorted(
_reuse_ranges(len(paints)),
_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(paints[lbound:ubound]), -1
self._as_tuple(layers[lbound:ubound]), -1
)
if reuse_lbound == -1:
continue
@ -675,47 +469,43 @@ class LayerV1ListBuilder:
new_slice.Format = int(ot.PaintFormat.PaintColrLayers)
new_slice.NumLayers = ubound - lbound
new_slice.FirstLayerIndex = reuse_lbound
paints = paints[:lbound] + [new_slice] + paints[ubound:]
layers = layers[:lbound] + [new_slice] + layers[ubound:]
found_reuse = True
break
ot_paint.NumLayers = len(paints)
ot_paint.FirstLayerIndex = len(self.layers)
self.layers.extend(paints)
# The layer list is now final; if it's too big we need to tree it
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)
# Register our parts for reuse
for lbound, ubound in _reuse_ranges(len(paints)):
self.reusePool[self._as_tuple(paints[lbound:ubound])] = (
lbound + ot_paint.FirstLayerIndex
# TODO what if we made ourselves a lovely little tree
for lbound, ubound in _reuse_ranges(len(layers)):
self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
lbound + paint.FirstLayerIndex
)
return ot_paint
# we've fully built dest; empty source prevents generalized build from kicking in
return paint, {}
def buildPaint(self, paint: _PaintInput) -> ot.Paint:
if isinstance(paint, ot.Paint):
return paint
elif isinstance(paint, int):
paletteIndex = paint
return self.buildPaintSolid(paletteIndex)
elif isinstance(paint, tuple):
layerGlyph, paint = paint
return self.buildPaintGlyph(layerGlyph, paint)
elif isinstance(paint, list):
# implicit PaintColrLayers for a list of > 1
if len(paint) == 0:
raise ValueError("An empty list is hard to paint")
elif len(paint) == 1:
return self.buildPaint(paint[0])
else:
return self.buildColrLayers(paint)
elif isinstance(paint, collections.abc.Mapping):
kwargs = dict(paint)
fmt = kwargs.pop("format")
try:
return LayerV1ListBuilder._buildFunctions[fmt](self, **kwargs)
except KeyError:
raise NotImplementedError(fmt)
raise TypeError(f"Not sure what to do with {type(paint).__name__}: {paint!r}")
return self.tableBuilder.build(ot.Paint, paint)
def build(self) -> ot.LayerV1List:
layers = ot.LayerV1List()
@ -724,31 +514,6 @@ class LayerV1ListBuilder:
return layers
LayerV1ListBuilder._buildFunctions = {
pf.value: getattr(LayerV1ListBuilder, "build" + pf.name)
for pf in ot.PaintFormat
if pf != ot.PaintFormat.PaintColrLayers
}
def buildAffine2x3(transform: _AffineTuple) -> ot.Affine2x3:
if len(transform) != 6:
raise ValueError(f"Expected 6-tuple of floats, found: {transform!r}")
self = ot.Affine2x3()
# 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]))
return self
def buildBaseGlyphV1Record(
baseGlyph: str, layerBuilder: LayerV1ListBuilder, paint: _PaintInput
) -> ot.BaseGlyphV1List:

View File

@ -0,0 +1,194 @@
"""
colorLib.table_builder: Generic helper for filling in BaseTable derivatives from tuples and maps and such.
"""
import collections
import enum
from fontTools.ttLib.tables.otBase import (
BaseTable,
FormatSwitchingBaseTable,
UInt8FormatSwitchingBaseTable,
)
from fontTools.ttLib.tables.otConverters import (
ComputedInt,
GlyphID,
Struct,
Short,
UInt8,
UShort,
VarInt16,
VarUInt16,
)
def _to_glyph_id(value):
assert isinstance(value, str), "Expected a glyph name"
return value
_CONVERTER_OVERRIDES = {
Short: int,
UShort: int,
GlyphID: _to_glyph_id,
}
class BuildCallback(enum.Enum):
"""Keyed on (BEFORE_BUILD, class[, Format if available]).
Receives (dest, source).
Should return (dest, source), which can be new objects.
"""
BEFORE_BUILD = enum.auto()
"""Keyed on (AFTER_BUILD, class[, Format if available]).
Receives (dest).
Should return dest, which can be a new object.
"""
AFTER_BUILD = enum.auto()
"""Keyed on (CREATE_DEFAULT, class).
Receives no arguments.
Should return a new instance of class.
"""
CREATE_DEFAULT = enum.auto()
def _assignable(convertersByName):
return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)}
def convertTupleClass(tupleClass, value):
if isinstance(value, tupleClass):
return value
if isinstance(value, tuple):
return tupleClass(*value)
return tupleClass(value)
def _isNonStrSequence(value):
return isinstance(value, collections.abc.Sequence) and not isinstance(value, str)
def _set_format(dest, source):
if _isNonStrSequence(source):
assert len(source) > 0, f"{type(dest)} needs at least format from {source}"
dest.Format = source[0]
source = source[1:]
elif isinstance(source, collections.abc.Mapping):
assert "Format" in source, f"{type(dest)} needs at least Format from {source}"
dest.Format = source["Format"]
else:
raise ValueError(f"Not sure how to populate {type(dest)} from {source}")
assert isinstance(
dest.Format, collections.abc.Hashable
), f"{type(dest)} Format is not hashable: {dest.Format}"
assert (
dest.Format in dest.convertersByName
), f"{dest.Format} invalid Format of {cls}"
return source
class TableBuilder:
"""
Helps to populate things derived from BaseTable from maps, tuples, etc.
A table of lifecycle callbacks may be provided to add logic beyond what is possible
based on otData info for the target class. See BuildCallbacks.
"""
def __init__(self, callbackTable=None):
if callbackTable is None:
callbackTable = {}
self._callbackTable = callbackTable
def _convert(self, dest, field, converter, value):
converter = _CONVERTER_OVERRIDES.get(type(converter), converter)
enumClass = getattr(converter, "enumClass", None)
tupleClass = getattr(converter, "tupleClass", None)
if tupleClass:
value = convertTupleClass(tupleClass, value)
elif enumClass:
if isinstance(value, enumClass):
pass
elif isinstance(value, str):
try:
value = getattr(enumClass, value.upper())
except AttributeError:
raise ValueError(f"{value} is not a valid {enumClass}")
else:
value = enumClass(value)
elif isinstance(converter, Struct):
if converter.repeat:
if _isNonStrSequence(value):
value = [self.build(converter.tableClass, v) for v in value]
else:
value = [self.build(converter.tableClass, value)]
setattr(dest, converter.repeat, len(value))
else:
value = self.build(converter.tableClass, value)
elif callable(converter):
value = converter(value)
setattr(dest, field, value)
def build(self, cls, source):
assert issubclass(cls, BaseTable)
if isinstance(source, cls):
return source
callbackKey = (cls,)
dest = self._callbackTable.get(
(BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls()
)()
assert isinstance(dest, cls)
convByName = _assignable(cls.convertersByName)
skippedFields = set()
# For format switchers we need to resolve converters based on format
if issubclass(cls, FormatSwitchingBaseTable):
source = _set_format(dest, source)
convByName = _assignable(convByName[dest.Format])
skippedFields.add("Format")
callbackKey = (cls, dest.Format)
# Convert sequence => mapping so before thunk only has to handle one format
if _isNonStrSequence(source):
# Sequence (typically list or tuple) assumed to match fields in declaration order
assert len(source) <= len(
convByName
), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values"
source = dict(zip(convByName.keys(), source))
dest, source = self._callbackTable.get(
(BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s)
)(dest, source)
if isinstance(source, collections.abc.Mapping):
for field, value in source.items():
if field in skippedFields:
continue
converter = convByName.get(field, None)
if not converter:
raise ValueError(
f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}"
)
self._convert(dest, field, converter, value)
else:
# let's try as a 1-tuple
dest = self.build(cls, (source,))
dest = self._callbackTable.get(
(BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d
)(dest)
return dest

View File

@ -1588,6 +1588,15 @@ otData = [
('LOffset', 'Paint', 'LayerCount', 0, 'Array of offsets to Paint tables, from the start of the LayerV1List table.'),
]),
# 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
('Affine2x3', [
('VarFixed', 'xx', None, None, 'x-part of x basis vector'),
('VarFixed', 'yx', None, None, 'y-part of x basis vector'),

File diff suppressed because it is too large Load Diff

View File

@ -5,115 +5,141 @@ import pytest
TEST_COLOR_GLYPHS = {
"glyph00010": [
{
"format": int(ot.PaintFormat.PaintGlyph),
"glyph": "glyph00011",
"paint": {
"format": int(ot.PaintFormat.PaintSolid),
"paletteIndex": 2,
"alpha": 0.5,
},
},
{
"format": int(ot.PaintFormat.PaintGlyph),
"glyph": "glyph00012",
"paint": {
"format": int(ot.PaintFormat.PaintLinearGradient),
"colorLine": {
"stops": [
{"offset": 0.0, "paletteIndex": 3, "alpha": 1.0},
{"offset": 0.5, "paletteIndex": 4, "alpha": 1.0},
{"offset": 1.0, "paletteIndex": 5, "alpha": 1.0},
],
"extend": "repeat",
"glyph00010": (
ot.PaintFormat.PaintColrLayers,
[
{
"Format": int(ot.PaintFormat.PaintGlyph),
"Glyph": "glyph00011",
"Paint": {
"Format": int(ot.PaintFormat.PaintSolid),
"Color": {
"PaletteIndex": 2,
"Alpha": 0.5,
},
},
"p0": (1, 2),
"p1": (-3, -4),
"p2": (5, 6),
},
},
{
"format": int(ot.PaintFormat.PaintGlyph),
"glyph": "glyph00013",
"paint": {
"format": int(ot.PaintFormat.PaintTransform),
"transform": (-13.0, 14.0, 15.0, -17.0, 18.0, 19.0),
"paint": {
"format": int(ot.PaintFormat.PaintRadialGradient),
"colorLine": {
"stops": [
{"offset": 0.0, "paletteIndex": 6, "alpha": 1.0},
{
"Format": int(ot.PaintFormat.PaintGlyph),
"Glyph": "glyph00012",
"Paint": {
"Format": int(ot.PaintFormat.PaintLinearGradient),
"ColorLine": {
"ColorStop": [
{
"offset": 1.0,
"paletteIndex": 7,
"alpha": 0.4,
"StopOffset": 0.0,
"Color": {"PaletteIndex": 3, "Alpha": 1.0},
},
{
"StopOffset": 0.5,
"Color": {"PaletteIndex": 4, "Alpha": 1.0},
},
{
"StopOffset": 1.0,
"Color": {"PaletteIndex": 5, "Alpha": 1.0},
},
],
"extend": "pad",
"Extend": "repeat",
},
"c0": (7, 8),
"r0": 9,
"c1": (10, 11),
"r1": 12,
"x0": 1,
"y0": 2,
"x1": -3,
"y1": -4,
"x2": 5,
"y2": 6,
},
},
},
{
"format": int(ot.PaintFormat.PaintTranslate),
"dx": 257.0,
"dy": 258.0,
"paint": {
"format": int(ot.PaintFormat.PaintRotate),
"angle": 45.0,
"centerX": 255.0,
"centerY": 256.0,
"paint": {
"format": int(ot.PaintFormat.PaintSkew),
"xSkewAngle": -11.0,
"ySkewAngle": 5.0,
"centerX": 253.0,
"centerY": 254.0,
"paint": {
"format": int(ot.PaintFormat.PaintGlyph),
"glyph": "glyph00011",
"paint": {
"format": int(ot.PaintFormat.PaintSolid),
"paletteIndex": 2,
"alpha": 0.5,
{
"Format": int(ot.PaintFormat.PaintGlyph),
"Glyph": "glyph00013",
"Paint": {
"Format": int(ot.PaintFormat.PaintTransform),
"Transform": (-13.0, 14.0, 15.0, -17.0, 18.0, 19.0),
"Paint": {
"Format": int(ot.PaintFormat.PaintRadialGradient),
"ColorLine": {
"ColorStop": [
{
"StopOffset": 0.0,
"Color": {"PaletteIndex": 6, "Alpha": 1.0},
},
{
"StopOffset": 1.0,
"Color": {
"PaletteIndex": 7,
"Alpha": 0.4,
},
},
],
"Extend": "pad",
},
"x0": 7,
"y0": 8,
"r0": 9,
"x1": 10,
"y1": 11,
"r1": 12,
},
},
},
{
"Format": int(ot.PaintFormat.PaintTranslate),
"dx": 257.0,
"dy": 258.0,
"Paint": {
"Format": int(ot.PaintFormat.PaintRotate),
"angle": 45.0,
"centerX": 255.0,
"centerY": 256.0,
"Paint": {
"Format": int(ot.PaintFormat.PaintSkew),
"xSkewAngle": -11.0,
"ySkewAngle": 5.0,
"centerX": 253.0,
"centerY": 254.0,
"Paint": {
"Format": int(ot.PaintFormat.PaintGlyph),
"Glyph": "glyph00011",
"Paint": {
"Format": int(ot.PaintFormat.PaintSolid),
"Color": {
"PaletteIndex": 2,
"Alpha": 0.5,
},
},
},
},
},
},
},
],
],
),
"glyph00014": {
"format": int(ot.PaintFormat.PaintComposite),
"mode": "src_over",
"source": {
"format": int(ot.PaintFormat.PaintColrGlyph),
"glyph": "glyph00010",
"Format": int(ot.PaintFormat.PaintComposite),
"CompositeMode": "src_over",
"SourcePaint": {
"Format": int(ot.PaintFormat.PaintColrGlyph),
"Glyph": "glyph00010",
},
"backdrop": {
"format": int(ot.PaintFormat.PaintTransform),
"transform": (1.0, 0.0, 0.0, 1.0, 300.0, 0.0),
"paint": {
"format": int(ot.PaintFormat.PaintColrGlyph),
"glyph": "glyph00010",
"BackdropPaint": {
"Format": int(ot.PaintFormat.PaintTransform),
"Transform": (1.0, 0.0, 0.0, 1.0, 300.0, 0.0),
"Paint": {
"Format": int(ot.PaintFormat.PaintColrGlyph),
"Glyph": "glyph00010",
},
},
},
"glyph00015": {
"format": int(ot.PaintFormat.PaintGlyph),
"glyph": "glyph00011",
"paint": {
"format": int(ot.PaintFormat.PaintSweepGradient),
"colorLine": {
"stops": [
{"offset": 0.0, "paletteIndex": 3, "alpha": 1.0},
{"offset": 1.0, "paletteIndex": 5, "alpha": 1.0},
"Format": int(ot.PaintFormat.PaintGlyph),
"Glyph": "glyph00011",
"Paint": {
"Format": int(ot.PaintFormat.PaintSweepGradient),
"ColorLine": {
"ColorStop": [
{"StopOffset": 0.0, "Color": {"PaletteIndex": 3, "Alpha": 1.0}},
{"StopOffset": 1.0, "Color": {"PaletteIndex": 5, "Alpha": 1.0}},
],
"extend": "pad",
"Extend": "pad",
},
"centerX": 259,
"centerY": 300,
@ -121,35 +147,52 @@ TEST_COLOR_GLYPHS = {
"endAngle": 135.0,
},
},
"glyph00016": [
{
"format": int(ot.PaintFormat.PaintGlyph),
"glyph": "glyph00011",
"paint": {
"format": int(ot.PaintFormat.PaintSolid),
"paletteIndex": 2,
"alpha": 0.5,
},
},
{
"format": int(ot.PaintFormat.PaintGlyph),
"glyph": "glyph00012",
"paint": {
"format": int(ot.PaintFormat.PaintLinearGradient),
"colorLine": {
"stops": [
{"offset": 0.0, "paletteIndex": 3, "alpha": 1.0},
{"offset": 0.5, "paletteIndex": 4, "alpha": 1.0},
{"offset": 1.0, "paletteIndex": 5, "alpha": 1.0},
],
"extend": "repeat",
"glyph00016": (
ot.PaintFormat.PaintColrLayers,
[
{
"Format": int(ot.PaintFormat.PaintGlyph),
"Glyph": "glyph00011",
"Paint": {
"Format": int(ot.PaintFormat.PaintSolid),
"Color": {
"PaletteIndex": 2,
"Alpha": 0.5,
},
},
"p0": (1, 2),
"p1": (-3, -4),
"p2": (5, 6),
},
},
],
{
"Format": int(ot.PaintFormat.PaintGlyph),
"Glyph": "glyph00012",
"Paint": {
"Format": int(ot.PaintFormat.PaintLinearGradient),
"ColorLine": {
"ColorStop": [
{
"StopOffset": 0.0,
"Color": {"PaletteIndex": 3, "Alpha": 1.0},
},
{
"StopOffset": 0.5,
"Color": {"PaletteIndex": 4, "Alpha": 1.0},
},
{
"StopOffset": 1.0,
"Color": {"PaletteIndex": 5, "Alpha": 1.0},
},
],
"Extend": "repeat",
},
"x0": 1,
"y0": 2,
"x1": -3,
"y1": -4,
"x2": 5,
"y2": 6,
},
},
],
),
}