Merge pull request #1822 from fonttools/otdata-colr
Define COLR using otData; add builders for COLRv1
This commit is contained in:
commit
17bff73866
@ -1,31 +1,148 @@
|
|||||||
|
import collections
|
||||||
|
import copy
|
||||||
import enum
|
import enum
|
||||||
from typing import Dict, Iterable, List, Optional, Tuple, Union
|
from functools import partial
|
||||||
from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord, table_C_O_L_R_
|
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
|
||||||
from fontTools.ttLib.tables.C_P_A_L_ import Color, table_C_P_A_L_
|
from fontTools.ttLib.tables import C_O_L_R_
|
||||||
from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e
|
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,
|
||||||
|
VariableValue,
|
||||||
|
VariableFloat,
|
||||||
|
VariableInt,
|
||||||
|
)
|
||||||
from .errors import ColorLibError
|
from .errors import ColorLibError
|
||||||
|
|
||||||
|
|
||||||
def buildCOLR(colorLayers: Dict[str, List[Tuple[str, int]]]) -> table_C_O_L_R_:
|
# TODO move type aliases to colorLib.types?
|
||||||
|
_Kwargs = Mapping[str, Any]
|
||||||
|
_PaintInput = Union[int, _Kwargs, ot.Paint]
|
||||||
|
_LayerTuple = Tuple[str, _PaintInput]
|
||||||
|
_LayersList = Sequence[_LayerTuple]
|
||||||
|
_ColorGlyphsDict = Dict[str, _LayersList]
|
||||||
|
_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]
|
||||||
|
_ColorLineInput = Union[_Kwargs, ot.ColorLine]
|
||||||
|
_PointTuple = Tuple[_ScalarInput, _ScalarInput]
|
||||||
|
_PointInput = Union[_PointTuple, ot.Point]
|
||||||
|
_AffineTuple = Tuple[_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput]
|
||||||
|
_AffineInput = Union[_AffineTuple, ot.Affine2x2]
|
||||||
|
|
||||||
|
|
||||||
|
def populateCOLRv0(
|
||||||
|
table: ot.COLR,
|
||||||
|
colorGlyphsV0: _ColorGlyphsV0Dict,
|
||||||
|
glyphMap: Optional[Mapping[str, int]] = None,
|
||||||
|
):
|
||||||
|
"""Build v0 color layers and add to existing COLR table.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table: a raw otTables.COLR() object (not ttLib's table_C_O_L_R_).
|
||||||
|
colorGlyphsV0: map of base glyph names to lists of (layer glyph names,
|
||||||
|
color palette index) tuples.
|
||||||
|
glyphMap: a map from glyph names to glyph indices, as returned from
|
||||||
|
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
|
||||||
|
"""
|
||||||
|
if glyphMap is not None:
|
||||||
|
colorGlyphItems = sorted(
|
||||||
|
colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
colorGlyphItems = colorGlyphsV0.items()
|
||||||
|
baseGlyphRecords = []
|
||||||
|
layerRecords = []
|
||||||
|
for baseGlyph, layers in colorGlyphItems:
|
||||||
|
baseRec = ot.BaseGlyphRecord()
|
||||||
|
baseRec.BaseGlyph = baseGlyph
|
||||||
|
baseRec.FirstLayerIndex = len(layerRecords)
|
||||||
|
baseRec.NumLayers = len(layers)
|
||||||
|
baseGlyphRecords.append(baseRec)
|
||||||
|
|
||||||
|
for layerGlyph, paletteIndex in layers:
|
||||||
|
layerRec = ot.LayerRecord()
|
||||||
|
layerRec.LayerGlyph = layerGlyph
|
||||||
|
layerRec.PaletteIndex = paletteIndex
|
||||||
|
layerRecords.append(layerRec)
|
||||||
|
|
||||||
|
table.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(
|
||||||
|
colorGlyphs: _ColorGlyphsDict,
|
||||||
|
version: Optional[int] = None,
|
||||||
|
glyphMap: Optional[Mapping[str, int]] = None,
|
||||||
|
varStore: Optional[ot.VarStore] = None,
|
||||||
|
) -> C_O_L_R_.table_C_O_L_R_:
|
||||||
"""Build COLR table from color layers mapping.
|
"""Build COLR table from color layers mapping.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
colorLayers: : map of base glyph names to lists of (layer glyph names,
|
colorGlyphs: map of base glyph names to lists of (layer glyph names,
|
||||||
palette indices) tuples.
|
Paint) tuples. For COLRv0, a paint is simply the color palette index
|
||||||
|
(int); for COLRv1, paint can be either solid colors (with variable
|
||||||
|
opacity), linear gradients or radial gradients.
|
||||||
|
version: the version of COLR table. If None, the version is determined
|
||||||
|
by the presence of gradients or variation data (varStore), which
|
||||||
|
require version 1; otherwise, if there are only simple colors, version
|
||||||
|
0 is used.
|
||||||
|
glyphMap: a map from glyph names to glyph indices, as returned from
|
||||||
|
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
|
||||||
|
varStore: Optional ItemVarationStore for deltas associated with v1 layer.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A new COLRv0 table.
|
A new COLR table.
|
||||||
"""
|
"""
|
||||||
colorLayerLists = {}
|
self = C_O_L_R_.table_C_O_L_R_()
|
||||||
for baseGlyphName, layers in colorLayers.items():
|
|
||||||
colorLayerLists[baseGlyphName] = [
|
|
||||||
LayerRecord(layerGlyphName, colorID) for layerGlyphName, colorID in layers
|
|
||||||
]
|
|
||||||
|
|
||||||
colr = table_C_O_L_R_()
|
if varStore is not None and version == 0:
|
||||||
colr.version = 0
|
raise ValueError("Can't add VarStore to COLRv0")
|
||||||
colr.ColorLayers = colorLayerLists
|
|
||||||
return colr
|
if version in (None, 0) and not varStore:
|
||||||
|
# split color glyphs into v0 and v1 and encode separately
|
||||||
|
colorGlyphsV0, colorGlyphsV1 = _splitSolidAndGradientGlyphs(colorGlyphs)
|
||||||
|
if version == 0 and colorGlyphsV1:
|
||||||
|
# TODO Derive "average" solid color from gradients?
|
||||||
|
raise ValueError("Can't encode gradients in COLRv0")
|
||||||
|
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:
|
||||||
|
colr.BaseGlyphV1Array = buildBaseGlyphV1Array(colorGlyphsV1, glyphMap)
|
||||||
|
|
||||||
|
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:
|
||||||
|
colr.VarStore = varStore
|
||||||
|
self.table = colr
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class ColorPaletteType(enum.IntFlag):
|
class ColorPaletteType(enum.IntFlag):
|
||||||
@ -45,12 +162,12 @@ _OptionalLocalizedString = Union[None, str, Dict[str, str]]
|
|||||||
|
|
||||||
|
|
||||||
def buildPaletteLabels(
|
def buildPaletteLabels(
|
||||||
labels: List[_OptionalLocalizedString], nameTable: table__n_a_m_e
|
labels: Iterable[_OptionalLocalizedString], nameTable: _n_a_m_e.table__n_a_m_e
|
||||||
) -> List[Optional[int]]:
|
) -> List[Optional[int]]:
|
||||||
return [
|
return [
|
||||||
nameTable.addMultilingualName(l, mac=False)
|
nameTable.addMultilingualName(l, mac=False)
|
||||||
if isinstance(l, dict)
|
if isinstance(l, dict)
|
||||||
else table_C_P_A_L_.NO_NAME_ID
|
else C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
|
||||||
if l is None
|
if l is None
|
||||||
else nameTable.addMultilingualName({"en": l}, mac=False)
|
else nameTable.addMultilingualName({"en": l}, mac=False)
|
||||||
for l in labels
|
for l in labels
|
||||||
@ -58,12 +175,12 @@ def buildPaletteLabels(
|
|||||||
|
|
||||||
|
|
||||||
def buildCPAL(
|
def buildCPAL(
|
||||||
palettes: List[List[Tuple[float, float, float, float]]],
|
palettes: Sequence[Sequence[Tuple[float, float, float, float]]],
|
||||||
paletteTypes: Optional[List[ColorPaletteType]] = None,
|
paletteTypes: Optional[Sequence[ColorPaletteType]] = None,
|
||||||
paletteLabels: Optional[List[_OptionalLocalizedString]] = None,
|
paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
|
||||||
paletteEntryLabels: Optional[List[_OptionalLocalizedString]] = None,
|
paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
|
||||||
nameTable: Optional[table__n_a_m_e] = None,
|
nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None,
|
||||||
) -> table_C_P_A_L_:
|
) -> C_P_A_L_.table_C_P_A_L_:
|
||||||
"""Build CPAL table from list of color palettes.
|
"""Build CPAL table from list of color palettes.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -89,7 +206,7 @@ def buildCPAL(
|
|||||||
"nameTable is required if palette or palette entries have labels"
|
"nameTable is required if palette or palette entries have labels"
|
||||||
)
|
)
|
||||||
|
|
||||||
cpal = table_C_P_A_L_()
|
cpal = C_P_A_L_.table_C_P_A_L_()
|
||||||
cpal.numPaletteEntries = len(palettes[0])
|
cpal.numPaletteEntries = len(palettes[0])
|
||||||
|
|
||||||
cpal.palettes = []
|
cpal.palettes = []
|
||||||
@ -106,7 +223,9 @@ def buildCPAL(
|
|||||||
)
|
)
|
||||||
# input colors are RGBA, CPAL encodes them as BGRA
|
# input colors are RGBA, CPAL encodes them as BGRA
|
||||||
red, green, blue, alpha = color
|
red, green, blue, alpha = color
|
||||||
colors.append(Color(*(round(v * 255) for v in (blue, green, red, alpha))))
|
colors.append(
|
||||||
|
C_P_A_L_.Color(*(round(v * 255) for v in (blue, green, red, alpha)))
|
||||||
|
)
|
||||||
cpal.palettes.append(colors)
|
cpal.palettes.append(colors)
|
||||||
|
|
||||||
if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
|
if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
|
||||||
@ -119,7 +238,9 @@ def buildCPAL(
|
|||||||
)
|
)
|
||||||
cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes]
|
cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes]
|
||||||
else:
|
else:
|
||||||
cpal.paletteTypes = [table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(palettes)
|
cpal.paletteTypes = [C_P_A_L_.table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(
|
||||||
|
palettes
|
||||||
|
)
|
||||||
|
|
||||||
if paletteLabels is not None:
|
if paletteLabels is not None:
|
||||||
if len(paletteLabels) != len(palettes):
|
if len(paletteLabels) != len(palettes):
|
||||||
@ -128,7 +249,7 @@ def buildCPAL(
|
|||||||
)
|
)
|
||||||
cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
|
cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
|
||||||
else:
|
else:
|
||||||
cpal.paletteLabels = [table_C_P_A_L_.NO_NAME_ID] * len(palettes)
|
cpal.paletteLabels = [C_P_A_L_.table_C_P_A_L_.NO_NAME_ID] * len(palettes)
|
||||||
|
|
||||||
if paletteEntryLabels is not None:
|
if paletteEntryLabels is not None:
|
||||||
if len(paletteEntryLabels) != cpal.numPaletteEntries:
|
if len(paletteEntryLabels) != cpal.numPaletteEntries:
|
||||||
@ -139,9 +260,275 @@ def buildCPAL(
|
|||||||
cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
|
cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
|
||||||
else:
|
else:
|
||||||
cpal.paletteEntryLabels = [
|
cpal.paletteEntryLabels = [
|
||||||
table_C_P_A_L_.NO_NAME_ID
|
C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
|
||||||
] * cpal.numPaletteEntries
|
] * cpal.numPaletteEntries
|
||||||
else:
|
else:
|
||||||
cpal.version = 0
|
cpal.version = 0
|
||||||
|
|
||||||
return cpal
|
return cpal
|
||||||
|
|
||||||
|
|
||||||
|
# COLR v1 tables
|
||||||
|
# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
|
||||||
|
|
||||||
|
_DEFAULT_TRANSPARENCY = VariableFloat(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _splitSolidAndGradientGlyphs(
|
||||||
|
colorGlyphs: _ColorGlyphsDict,
|
||||||
|
) -> Tuple[Dict[str, List[Tuple[str, int]]], Dict[str, List[Tuple[str, ot.Paint]]]]:
|
||||||
|
colorGlyphsV0 = {}
|
||||||
|
colorGlyphsV1 = {}
|
||||||
|
for baseGlyph, layers in colorGlyphs.items():
|
||||||
|
newLayers = []
|
||||||
|
allSolidColors = True
|
||||||
|
for layerGlyph, paint in layers:
|
||||||
|
paint = _to_ot_paint(paint)
|
||||||
|
if (
|
||||||
|
paint.Format != 1
|
||||||
|
or paint.Color.Transparency.value != _DEFAULT_TRANSPARENCY.value
|
||||||
|
):
|
||||||
|
allSolidColors = False
|
||||||
|
newLayers.append((layerGlyph, paint))
|
||||||
|
if allSolidColors:
|
||||||
|
colorGlyphsV0[baseGlyph] = [
|
||||||
|
(layerGlyph, paint.Color.PaletteIndex)
|
||||||
|
for layerGlyph, paint in newLayers
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
colorGlyphsV1[baseGlyph] = newLayers
|
||||||
|
|
||||||
|
# sanity check
|
||||||
|
assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
|
||||||
|
|
||||||
|
return colorGlyphsV0, colorGlyphsV1
|
||||||
|
|
||||||
|
|
||||||
|
def _to_variable_value(value: _ScalarInput, cls=VariableValue) -> VariableValue:
|
||||||
|
if isinstance(value, cls):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
it = iter(value)
|
||||||
|
except TypeError: # not iterable
|
||||||
|
return cls(value)
|
||||||
|
else:
|
||||||
|
return cls._make(it)
|
||||||
|
|
||||||
|
|
||||||
|
_to_variable_float = partial(_to_variable_value, cls=VariableFloat)
|
||||||
|
_to_variable_int = partial(_to_variable_value, cls=VariableInt)
|
||||||
|
|
||||||
|
|
||||||
|
def buildColor(
|
||||||
|
paletteIndex: int, transparency: _ScalarInput = _DEFAULT_TRANSPARENCY
|
||||||
|
) -> ot.Color:
|
||||||
|
self = ot.Color()
|
||||||
|
self.PaletteIndex = int(paletteIndex)
|
||||||
|
self.Transparency = _to_variable_float(transparency)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def buildSolidColorPaint(
|
||||||
|
paletteIndex: int, transparency: _ScalarInput = _DEFAULT_TRANSPARENCY
|
||||||
|
) -> ot.Paint:
|
||||||
|
self = ot.Paint()
|
||||||
|
self.Format = 1
|
||||||
|
self.Color = buildColor(paletteIndex, transparency)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def buildColorStop(
|
||||||
|
offset: _ScalarInput,
|
||||||
|
paletteIndex: int,
|
||||||
|
transparency: _ScalarInput = _DEFAULT_TRANSPARENCY,
|
||||||
|
) -> ot.ColorStop:
|
||||||
|
self = ot.ColorStop()
|
||||||
|
self.StopOffset = _to_variable_float(offset)
|
||||||
|
self.Color = buildColor(paletteIndex, transparency)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def _to_extend_mode(v: _ExtendInput) -> ExtendMode:
|
||||||
|
if isinstance(v, ExtendMode):
|
||||||
|
return v
|
||||||
|
elif isinstance(v, str):
|
||||||
|
try:
|
||||||
|
return getattr(ExtendMode, v.upper())
|
||||||
|
except AttributeError:
|
||||||
|
raise ValueError(f"{v!r} is not a valid ExtendMode")
|
||||||
|
return ExtendMode(v)
|
||||||
|
|
||||||
|
|
||||||
|
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 buildPoint(x: _ScalarInput, y: _ScalarInput) -> ot.Point:
|
||||||
|
self = ot.Point()
|
||||||
|
# positions are encoded as Int16 so round to int
|
||||||
|
self.x = _to_variable_int(x)
|
||||||
|
self.y = _to_variable_int(y)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def _to_variable_point(pt: _PointInput) -> ot.Point:
|
||||||
|
if isinstance(pt, ot.Point):
|
||||||
|
return pt
|
||||||
|
return buildPoint(*pt)
|
||||||
|
|
||||||
|
|
||||||
|
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 buildLinearGradientPaint(
|
||||||
|
colorLine: _ColorLineInput,
|
||||||
|
p0: _PointInput,
|
||||||
|
p1: _PointInput,
|
||||||
|
p2: Optional[_PointInput] = None,
|
||||||
|
) -> ot.Paint:
|
||||||
|
self = ot.Paint()
|
||||||
|
self.Format = 2
|
||||||
|
self.ColorLine = _to_color_line(colorLine)
|
||||||
|
|
||||||
|
if p2 is None:
|
||||||
|
p2 = copy.copy(p1)
|
||||||
|
for i, pt in enumerate((p0, p1, p2)):
|
||||||
|
setattr(self, f"p{i}", _to_variable_point(pt))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def buildAffine2x2(
|
||||||
|
xx: _ScalarInput, xy: _ScalarInput, yx: _ScalarInput, yy: _ScalarInput
|
||||||
|
) -> ot.Affine2x2:
|
||||||
|
self = ot.Affine2x2()
|
||||||
|
locs = locals()
|
||||||
|
for attr in ("xx", "xy", "yx", "yy"):
|
||||||
|
value = locs[attr]
|
||||||
|
setattr(self, attr, _to_variable_float(value))
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def buildRadialGradientPaint(
|
||||||
|
colorLine: _ColorLineInput,
|
||||||
|
c0: _PointInput,
|
||||||
|
c1: _PointInput,
|
||||||
|
r0: _ScalarInput,
|
||||||
|
r1: _ScalarInput,
|
||||||
|
affine: Optional[_AffineInput] = None,
|
||||||
|
) -> ot.Paint:
|
||||||
|
|
||||||
|
self = ot.Paint()
|
||||||
|
self.Format = 3
|
||||||
|
self.ColorLine = _to_color_line(colorLine)
|
||||||
|
|
||||||
|
for i, pt in [(0, c0), (1, c1)]:
|
||||||
|
setattr(self, f"c{i}", _to_variable_point(pt))
|
||||||
|
|
||||||
|
for i, r in [(0, r0), (1, r1)]:
|
||||||
|
# distances are encoded as UShort so we round to int
|
||||||
|
setattr(self, f"r{i}", _to_variable_int(r))
|
||||||
|
|
||||||
|
if affine is not None and not isinstance(affine, ot.Affine2x2):
|
||||||
|
affine = buildAffine2x2(*affine)
|
||||||
|
self.Affine = affine
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def _to_ot_paint(paint: _PaintInput) -> ot.Paint:
|
||||||
|
if isinstance(paint, ot.Paint):
|
||||||
|
return paint
|
||||||
|
elif isinstance(paint, int):
|
||||||
|
paletteIndex = paint
|
||||||
|
return buildSolidColorPaint(paletteIndex)
|
||||||
|
elif isinstance(paint, collections.abc.Mapping):
|
||||||
|
return buildPaint(**paint)
|
||||||
|
raise TypeError(f"expected int, Mapping or ot.Paint, found {type(paint.__name__)}")
|
||||||
|
|
||||||
|
|
||||||
|
def buildLayerV1Record(layerGlyph: str, paint: _PaintInput) -> ot.LayerV1Record:
|
||||||
|
self = ot.LayerV1Record()
|
||||||
|
self.LayerGlyph = layerGlyph
|
||||||
|
self.Paint = _to_ot_paint(paint)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def buildLayerV1Array(
|
||||||
|
layers: Sequence[Union[_LayerTuple, ot.LayerV1Record]]
|
||||||
|
) -> ot.LayerV1Array:
|
||||||
|
self = ot.LayerV1Array()
|
||||||
|
self.LayerCount = len(layers)
|
||||||
|
records = []
|
||||||
|
for layer in layers:
|
||||||
|
if isinstance(layer, ot.LayerV1Record):
|
||||||
|
record = layer
|
||||||
|
else:
|
||||||
|
layerGlyph, paint = layer
|
||||||
|
record = buildLayerV1Record(layerGlyph, paint)
|
||||||
|
records.append(record)
|
||||||
|
self.LayerV1Record = records
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def buildBaseGlyphV1Record(
|
||||||
|
baseGlyph: str, layers: Union[_LayersList, ot.LayerV1Array]
|
||||||
|
) -> ot.BaseGlyphV1Array:
|
||||||
|
self = ot.BaseGlyphV1Record()
|
||||||
|
self.BaseGlyph = baseGlyph
|
||||||
|
if not isinstance(layers, ot.LayerV1Array):
|
||||||
|
layers = buildLayerV1Array(layers)
|
||||||
|
self.LayerV1Array = layers
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def buildBaseGlyphV1Array(
|
||||||
|
colorGlyphs: Union[_ColorGlyphsDict, Dict[str, ot.LayerV1Array]],
|
||||||
|
glyphMap: Optional[Mapping[str, int]] = None,
|
||||||
|
) -> ot.BaseGlyphV1Array:
|
||||||
|
if glyphMap is not None:
|
||||||
|
colorGlyphItems = sorted(
|
||||||
|
colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
colorGlyphItems = colorGlyphs.items()
|
||||||
|
records = [
|
||||||
|
buildBaseGlyphV1Record(baseGlyph, layers)
|
||||||
|
for baseGlyph, layers in colorGlyphItems
|
||||||
|
]
|
||||||
|
self = ot.BaseGlyphV1Array()
|
||||||
|
self.BaseGlyphCount = len(records)
|
||||||
|
self.BaseGlyphV1Record = records
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
_PAINT_BUILDERS = {
|
||||||
|
1: buildSolidColorPaint,
|
||||||
|
2: buildLinearGradientPaint,
|
||||||
|
3: buildRadialGradientPaint,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def buildPaint(format: int, **kwargs) -> ot.Paint:
|
||||||
|
try:
|
||||||
|
return _PAINT_BUILDERS[format](**kwargs)
|
||||||
|
except KeyError:
|
||||||
|
raise NotImplementedError(format)
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
from fontTools.misc.py23 import *
|
from fontTools.misc.py23 import *
|
||||||
from fontTools.misc.textTools import safeEval
|
from fontTools.misc.textTools import safeEval
|
||||||
from . import DefaultTable
|
from . import DefaultTable
|
||||||
import struct
|
|
||||||
|
|
||||||
|
|
||||||
class table_C_O_L_R_(DefaultTable.DefaultTable):
|
class table_C_O_L_R_(DefaultTable.DefaultTable):
|
||||||
@ -15,91 +14,91 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
|
|||||||
ttFont['COLR'][<glyphName>] = <value> will set the color layers for any glyph.
|
ttFont['COLR'][<glyphName>] = <value> will set the color layers for any glyph.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decompile(self, data, ttFont):
|
def _fromOTTable(self, table):
|
||||||
self.getGlyphName = ttFont.getGlyphName # for use in get/set item functions, for access by GID
|
self.version = 0
|
||||||
self.version, numBaseGlyphRecords, offsetBaseGlyphRecord, offsetLayerRecord, numLayerRecords = struct.unpack(">HHLLH", data[:14])
|
|
||||||
assert (self.version == 0), "Version of COLR table is higher than I know how to handle"
|
|
||||||
glyphOrder = ttFont.getGlyphOrder()
|
|
||||||
gids = []
|
|
||||||
layerLists = []
|
|
||||||
glyphPos = offsetBaseGlyphRecord
|
|
||||||
for i in range(numBaseGlyphRecords):
|
|
||||||
gid, firstLayerIndex, numLayers = struct.unpack(">HHH", data[glyphPos:glyphPos+6])
|
|
||||||
glyphPos += 6
|
|
||||||
gids.append(gid)
|
|
||||||
assert (firstLayerIndex + numLayers <= numLayerRecords)
|
|
||||||
layerPos = offsetLayerRecord + firstLayerIndex * 4
|
|
||||||
layers = []
|
|
||||||
for j in range(numLayers):
|
|
||||||
layerGid, colorID = struct.unpack(">HH", data[layerPos:layerPos+4])
|
|
||||||
try:
|
|
||||||
layerName = glyphOrder[layerGid]
|
|
||||||
except IndexError:
|
|
||||||
layerName = self.getGlyphName(layerGid)
|
|
||||||
layerPos += 4
|
|
||||||
layers.append(LayerRecord(layerName, colorID))
|
|
||||||
layerLists.append(layers)
|
|
||||||
|
|
||||||
self.ColorLayers = colorLayerLists = {}
|
self.ColorLayers = colorLayerLists = {}
|
||||||
try:
|
layerRecords = table.LayerRecordArray.LayerRecord
|
||||||
names = [glyphOrder[gid] for gid in gids]
|
numLayerRecords = len(layerRecords)
|
||||||
except IndexError:
|
for baseRec in table.BaseGlyphRecordArray.BaseGlyphRecord:
|
||||||
getGlyphName = self.getGlyphName
|
baseGlyph = baseRec.BaseGlyph
|
||||||
names = map(getGlyphName, gids)
|
firstLayerIndex = baseRec.FirstLayerIndex
|
||||||
|
numLayers = baseRec.NumLayers
|
||||||
|
assert (firstLayerIndex + numLayers <= numLayerRecords)
|
||||||
|
layers = []
|
||||||
|
for i in range(firstLayerIndex, firstLayerIndex+numLayers):
|
||||||
|
layerRec = layerRecords[i]
|
||||||
|
layers.append(
|
||||||
|
LayerRecord(layerRec.LayerGlyph, layerRec.PaletteIndex)
|
||||||
|
)
|
||||||
|
colorLayerLists[baseGlyph] = layers
|
||||||
|
|
||||||
for name, layerList in zip(names, layerLists):
|
def _toOTTable(self, ttFont):
|
||||||
colorLayerLists[name] = layerList
|
from . import otTables
|
||||||
|
from fontTools.colorLib.builder import populateCOLRv0
|
||||||
|
|
||||||
|
tableClass = getattr(otTables, self.tableTag)
|
||||||
|
table = tableClass()
|
||||||
|
table.Version = self.version
|
||||||
|
|
||||||
|
populateCOLRv0(
|
||||||
|
table,
|
||||||
|
{
|
||||||
|
baseGlyph: [(layer.name, layer.colorID) for layer in layers]
|
||||||
|
for baseGlyph, layers in self.ColorLayers.items()
|
||||||
|
},
|
||||||
|
glyphMap=ttFont.getReverseGlyphMap(rebuild=True),
|
||||||
|
)
|
||||||
|
return table
|
||||||
|
|
||||||
|
def decompile(self, data, ttFont):
|
||||||
|
from .otBase import OTTableReader
|
||||||
|
from . import otTables
|
||||||
|
|
||||||
|
# We use otData to decompile, but we adapt the decompiled otTables to the
|
||||||
|
# existing COLR v0 API for backward compatibility.
|
||||||
|
reader = OTTableReader(data, tableTag=self.tableTag)
|
||||||
|
tableClass = getattr(otTables, self.tableTag)
|
||||||
|
table = tableClass()
|
||||||
|
table.decompile(reader, ttFont)
|
||||||
|
|
||||||
|
if table.Version == 0:
|
||||||
|
self._fromOTTable(table)
|
||||||
|
else:
|
||||||
|
# for new versions, keep the raw otTables around
|
||||||
|
self.table = table
|
||||||
|
|
||||||
def compile(self, ttFont):
|
def compile(self, ttFont):
|
||||||
ordered = []
|
from .otBase import OTTableWriter
|
||||||
ttFont.getReverseGlyphMap(rebuild=True)
|
|
||||||
glyphNames = self.ColorLayers.keys()
|
|
||||||
for glyphName in glyphNames:
|
|
||||||
try:
|
|
||||||
gid = ttFont.getGlyphID(glyphName)
|
|
||||||
except:
|
|
||||||
assert 0, "COLR table contains a glyph name not in ttFont.getGlyphNames(): " + str(glyphName)
|
|
||||||
ordered.append([gid, glyphName, self.ColorLayers[glyphName]])
|
|
||||||
ordered.sort()
|
|
||||||
|
|
||||||
glyphMap = []
|
if hasattr(self, "table"):
|
||||||
layerMap = []
|
table = self.table
|
||||||
for (gid, glyphName, layers) in ordered:
|
else:
|
||||||
glyphMap.append(struct.pack(">HHH", gid, len(layerMap), len(layers)))
|
table = self._toOTTable(ttFont)
|
||||||
for layer in layers:
|
|
||||||
layerMap.append(struct.pack(">HH", ttFont.getGlyphID(layer.name), layer.colorID))
|
|
||||||
|
|
||||||
dataList = [struct.pack(">HHLLH", self.version, len(glyphMap), 14, 14+6*len(glyphMap), len(layerMap))]
|
writer = OTTableWriter(tableTag=self.tableTag)
|
||||||
dataList.extend(glyphMap)
|
table.compile(writer, ttFont)
|
||||||
dataList.extend(layerMap)
|
return writer.getAllData()
|
||||||
data = bytesjoin(dataList)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def toXML(self, writer, ttFont):
|
def toXML(self, writer, ttFont):
|
||||||
writer.simpletag("version", value=self.version)
|
if hasattr(self, "table"):
|
||||||
writer.newline()
|
self.table.toXML2(writer, ttFont)
|
||||||
ordered = []
|
else:
|
||||||
glyphNames = self.ColorLayers.keys()
|
writer.simpletag("version", value=self.version)
|
||||||
for glyphName in glyphNames:
|
|
||||||
try:
|
|
||||||
gid = ttFont.getGlyphID(glyphName)
|
|
||||||
except:
|
|
||||||
assert 0, "COLR table contains a glyph name not in ttFont.getGlyphNames(): " + str(glyphName)
|
|
||||||
ordered.append([gid, glyphName, self.ColorLayers[glyphName]])
|
|
||||||
ordered.sort()
|
|
||||||
for entry in ordered:
|
|
||||||
writer.begintag("ColorGlyph", name=entry[1])
|
|
||||||
writer.newline()
|
|
||||||
for layer in entry[2]:
|
|
||||||
layer.toXML(writer, ttFont)
|
|
||||||
writer.endtag("ColorGlyph")
|
|
||||||
writer.newline()
|
writer.newline()
|
||||||
|
for baseGlyph in sorted(self.ColorLayers.keys(), key=ttFont.getGlyphID):
|
||||||
|
writer.begintag("ColorGlyph", name=baseGlyph)
|
||||||
|
writer.newline()
|
||||||
|
for layer in self.ColorLayers[baseGlyph]:
|
||||||
|
layer.toXML(writer, ttFont)
|
||||||
|
writer.endtag("ColorGlyph")
|
||||||
|
writer.newline()
|
||||||
|
|
||||||
def fromXML(self, name, attrs, content, ttFont):
|
def fromXML(self, name, attrs, content, ttFont):
|
||||||
if not hasattr(self, "ColorLayers"):
|
if name == "version": # old COLR v0 API
|
||||||
self.ColorLayers = {}
|
setattr(self, name, safeEval(attrs["value"]))
|
||||||
self.getGlyphName = ttFont.getGlyphName # for use in get/set item functions, for access by GID
|
elif name == "ColorGlyph":
|
||||||
if name == "ColorGlyph":
|
if not hasattr(self, "ColorLayers"):
|
||||||
|
self.ColorLayers = {}
|
||||||
glyphName = attrs["name"]
|
glyphName = attrs["name"]
|
||||||
for element in content:
|
for element in content:
|
||||||
if isinstance(element, basestring):
|
if isinstance(element, basestring):
|
||||||
@ -111,32 +110,31 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
|
|||||||
layer = LayerRecord()
|
layer = LayerRecord()
|
||||||
layer.fromXML(element[0], element[1], element[2], ttFont)
|
layer.fromXML(element[0], element[1], element[2], ttFont)
|
||||||
layers.append (layer)
|
layers.append (layer)
|
||||||
self[glyphName] = layers
|
self.ColorLayers[glyphName] = layers
|
||||||
elif "value" in attrs:
|
else: # new COLR v1 API
|
||||||
setattr(self, name, safeEval(attrs["value"]))
|
from . import otTables
|
||||||
|
|
||||||
def __getitem__(self, glyphSelector):
|
if not hasattr(self, "table"):
|
||||||
if isinstance(glyphSelector, int):
|
tableClass = getattr(otTables, self.tableTag)
|
||||||
# its a gid, convert to glyph name
|
self.table = tableClass()
|
||||||
glyphSelector = self.getGlyphName(glyphSelector)
|
self.table.fromXML(name, attrs, content, ttFont)
|
||||||
|
self.table.populateDefaults()
|
||||||
|
|
||||||
if glyphSelector not in self.ColorLayers:
|
def __getitem__(self, glyphName):
|
||||||
return None
|
if not isinstance(glyphName, str):
|
||||||
|
raise TypeError(f"expected str, found {type(glyphName).__name__}")
|
||||||
|
return self.ColorLayers[glyphName]
|
||||||
|
|
||||||
return self.ColorLayers[glyphSelector]
|
def __setitem__(self, glyphName, value):
|
||||||
|
if not isinstance(glyphName, str):
|
||||||
|
raise TypeError(f"expected str, found {type(glyphName).__name__}")
|
||||||
|
if value is not None:
|
||||||
|
self.ColorLayers[glyphName] = value
|
||||||
|
elif glyphName in self.ColorLayers:
|
||||||
|
del self.ColorLayers[glyphName]
|
||||||
|
|
||||||
def __setitem__(self, glyphSelector, value):
|
def __delitem__(self, glyphName):
|
||||||
if isinstance(glyphSelector, int):
|
del self.ColorLayers[glyphName]
|
||||||
# its a gid, convert to glyph name
|
|
||||||
glyphSelector = self.getGlyphName(glyphSelector)
|
|
||||||
|
|
||||||
if value:
|
|
||||||
self.ColorLayers[glyphSelector] = value
|
|
||||||
elif glyphSelector in self.ColorLayers:
|
|
||||||
del self.ColorLayers[glyphSelector]
|
|
||||||
|
|
||||||
def __delitem__(self, glyphSelector):
|
|
||||||
del self.ColorLayers[glyphSelector]
|
|
||||||
|
|
||||||
class LayerRecord(object):
|
class LayerRecord(object):
|
||||||
|
|
||||||
@ -151,8 +149,6 @@ class LayerRecord(object):
|
|||||||
def fromXML(self, eltname, attrs, content, ttFont):
|
def fromXML(self, eltname, attrs, content, ttFont):
|
||||||
for (name, value) in attrs.items():
|
for (name, value) in attrs.items():
|
||||||
if name == "name":
|
if name == "name":
|
||||||
if isinstance(value, int):
|
|
||||||
value = ttFont.getGlyphName(value)
|
|
||||||
setattr(self, name, value)
|
setattr(self, name, value)
|
||||||
else:
|
else:
|
||||||
setattr(self, name, safeEval(value))
|
setattr(self, name, safeEval(value))
|
||||||
|
@ -652,6 +652,15 @@ class BaseTable(object):
|
|||||||
else:
|
else:
|
||||||
table = self.__dict__.copy()
|
table = self.__dict__.copy()
|
||||||
|
|
||||||
|
# some count references may have been initialized in a custom preWrite; we set
|
||||||
|
# these in the writer's state beforehand (instead of sequentially) so they will
|
||||||
|
# be propagated to all nested subtables even if the count appears in the current
|
||||||
|
# table only *after* the offset to the subtable that it is counting.
|
||||||
|
for conv in self.getConverters():
|
||||||
|
if conv.isCount and conv.isPropagated:
|
||||||
|
value = table.get(conv.name)
|
||||||
|
if isinstance(value, CountReference):
|
||||||
|
writer[conv.name] = value
|
||||||
|
|
||||||
if hasattr(self, 'sortCoverageLast'):
|
if hasattr(self, 'sortCoverageLast'):
|
||||||
writer.sortCoverageLast = 1
|
writer.sortCoverageLast = 1
|
||||||
|
@ -13,7 +13,9 @@ from .otBase import (CountReference, FormatSwitchingBaseTable,
|
|||||||
OTTableReader, OTTableWriter, ValueRecordFactory)
|
OTTableReader, OTTableWriter, ValueRecordFactory)
|
||||||
from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
|
from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
|
||||||
ContextualMorphAction, LigatureMorphAction,
|
ContextualMorphAction, LigatureMorphAction,
|
||||||
InsertionMorphAction, MorxSubtable)
|
InsertionMorphAction, MorxSubtable, VariableFloat,
|
||||||
|
VariableInt, ExtendMode as _ExtendMode)
|
||||||
|
from itertools import zip_longest
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
@ -134,7 +136,22 @@ class BaseConverter(object):
|
|||||||
self.tableClass = tableClass
|
self.tableClass = tableClass
|
||||||
self.isCount = name.endswith("Count") or name in ['DesignAxisRecordSize', 'ValueRecordSize']
|
self.isCount = name.endswith("Count") or name in ['DesignAxisRecordSize', 'ValueRecordSize']
|
||||||
self.isLookupType = name.endswith("LookupType") or name == "MorphType"
|
self.isLookupType = name.endswith("LookupType") or name == "MorphType"
|
||||||
self.isPropagated = name in ["ClassCount", "Class2Count", "FeatureTag", "SettingsCount", "VarRegionCount", "MappingCount", "RegionAxisCount", 'DesignAxisCount', 'DesignAxisRecordSize', 'AxisValueCount', 'ValueRecordSize', 'AxisCount']
|
self.isPropagated = name in [
|
||||||
|
"ClassCount",
|
||||||
|
"Class2Count",
|
||||||
|
"FeatureTag",
|
||||||
|
"SettingsCount",
|
||||||
|
"VarRegionCount",
|
||||||
|
"MappingCount",
|
||||||
|
"RegionAxisCount",
|
||||||
|
"DesignAxisCount",
|
||||||
|
"DesignAxisRecordSize",
|
||||||
|
"AxisValueCount",
|
||||||
|
"ValueRecordSize",
|
||||||
|
"AxisCount",
|
||||||
|
"BaseGlyphRecordCount",
|
||||||
|
"LayerRecordCount",
|
||||||
|
]
|
||||||
|
|
||||||
def readArray(self, reader, font, tableDict, count):
|
def readArray(self, reader, font, tableDict, count):
|
||||||
"""Read an array of values from the reader."""
|
"""Read an array of values from the reader."""
|
||||||
@ -185,15 +202,22 @@ class BaseConverter(object):
|
|||||||
|
|
||||||
|
|
||||||
class SimpleValue(BaseConverter):
|
class SimpleValue(BaseConverter):
|
||||||
|
@staticmethod
|
||||||
|
def toString(value):
|
||||||
|
return value
|
||||||
|
@staticmethod
|
||||||
|
def fromString(value):
|
||||||
|
return value
|
||||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||||
xmlWriter.simpletag(name, attrs + [("value", value)])
|
xmlWriter.simpletag(name, attrs + [("value", self.toString(value))])
|
||||||
xmlWriter.newline()
|
xmlWriter.newline()
|
||||||
def xmlRead(self, attrs, content, font):
|
def xmlRead(self, attrs, content, font):
|
||||||
return attrs["value"]
|
return self.fromString(attrs["value"])
|
||||||
|
|
||||||
class IntValue(SimpleValue):
|
class IntValue(SimpleValue):
|
||||||
def xmlRead(self, attrs, content, font):
|
@staticmethod
|
||||||
return int(attrs["value"], 0)
|
def fromString(value):
|
||||||
|
return int(value, 0)
|
||||||
|
|
||||||
class Long(IntValue):
|
class Long(IntValue):
|
||||||
staticSize = 4
|
staticSize = 4
|
||||||
@ -210,9 +234,9 @@ class ULong(IntValue):
|
|||||||
writer.writeULong(value)
|
writer.writeULong(value)
|
||||||
|
|
||||||
class Flags32(ULong):
|
class Flags32(ULong):
|
||||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
@staticmethod
|
||||||
xmlWriter.simpletag(name, attrs + [("value", "0x%08X" % value)])
|
def toString(value):
|
||||||
xmlWriter.newline()
|
return "0x%08X" % value
|
||||||
|
|
||||||
class Short(IntValue):
|
class Short(IntValue):
|
||||||
staticSize = 2
|
staticSize = 2
|
||||||
@ -303,8 +327,9 @@ class NameID(UShort):
|
|||||||
|
|
||||||
|
|
||||||
class FloatValue(SimpleValue):
|
class FloatValue(SimpleValue):
|
||||||
def xmlRead(self, attrs, content, font):
|
@staticmethod
|
||||||
return float(attrs["value"])
|
def fromString(value):
|
||||||
|
return float(value)
|
||||||
|
|
||||||
class DeciPoints(FloatValue):
|
class DeciPoints(FloatValue):
|
||||||
staticSize = 2
|
staticSize = 2
|
||||||
@ -320,11 +345,12 @@ class Fixed(FloatValue):
|
|||||||
return fi2fl(reader.readLong(), 16)
|
return fi2fl(reader.readLong(), 16)
|
||||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||||
writer.writeLong(fl2fi(value, 16))
|
writer.writeLong(fl2fi(value, 16))
|
||||||
def xmlRead(self, attrs, content, font):
|
@staticmethod
|
||||||
return str2fl(attrs["value"], 16)
|
def fromString(value):
|
||||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
return str2fl(value, 16)
|
||||||
xmlWriter.simpletag(name, attrs + [("value", fl2str(value, 16))])
|
@staticmethod
|
||||||
xmlWriter.newline()
|
def toString(value):
|
||||||
|
return fl2str(value, 16)
|
||||||
|
|
||||||
class F2Dot14(FloatValue):
|
class F2Dot14(FloatValue):
|
||||||
staticSize = 2
|
staticSize = 2
|
||||||
@ -332,13 +358,14 @@ class F2Dot14(FloatValue):
|
|||||||
return fi2fl(reader.readShort(), 14)
|
return fi2fl(reader.readShort(), 14)
|
||||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||||
writer.writeShort(fl2fi(value, 14))
|
writer.writeShort(fl2fi(value, 14))
|
||||||
def xmlRead(self, attrs, content, font):
|
@staticmethod
|
||||||
return str2fl(attrs["value"], 14)
|
def fromString(value):
|
||||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
return str2fl(value, 14)
|
||||||
xmlWriter.simpletag(name, attrs + [("value", fl2str(value, 14))])
|
@staticmethod
|
||||||
xmlWriter.newline()
|
def toString(value):
|
||||||
|
return fl2str(value, 14)
|
||||||
|
|
||||||
class Version(BaseConverter):
|
class Version(SimpleValue):
|
||||||
staticSize = 4
|
staticSize = 4
|
||||||
def read(self, reader, font, tableDict):
|
def read(self, reader, font, tableDict):
|
||||||
value = reader.readLong()
|
value = reader.readLong()
|
||||||
@ -348,16 +375,12 @@ class Version(BaseConverter):
|
|||||||
value = fi2ve(value)
|
value = fi2ve(value)
|
||||||
assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
|
assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
|
||||||
writer.writeLong(value)
|
writer.writeLong(value)
|
||||||
def xmlRead(self, attrs, content, font):
|
@staticmethod
|
||||||
value = attrs["value"]
|
def fromString(value):
|
||||||
value = ve2fi(value)
|
return ve2fi(value)
|
||||||
return value
|
@staticmethod
|
||||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
def toString(value):
|
||||||
value = fi2ve(value)
|
return "0x%08x" % value
|
||||||
value = "0x%08x" % value
|
|
||||||
xmlWriter.simpletag(name, attrs + [("value", value)])
|
|
||||||
xmlWriter.newline()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fromFloat(v):
|
def fromFloat(v):
|
||||||
return fl2fi(v, 16)
|
return fl2fi(v, 16)
|
||||||
@ -1583,6 +1606,111 @@ class LookupFlag(UShort):
|
|||||||
xmlWriter.comment(" ".join(flags))
|
xmlWriter.comment(" ".join(flags))
|
||||||
xmlWriter.newline()
|
xmlWriter.newline()
|
||||||
|
|
||||||
|
def _issubclass_namedtuple(x):
|
||||||
|
return (
|
||||||
|
issubclass(x, tuple)
|
||||||
|
and getattr(x, "_fields", None) is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _NamedTupleConverter(BaseConverter):
|
||||||
|
# subclasses must override this
|
||||||
|
tupleClass = NotImplemented
|
||||||
|
# List[SimpleValue]
|
||||||
|
converterClasses = NotImplemented
|
||||||
|
|
||||||
|
def __init__(self, name, repeat, aux, tableClass=None):
|
||||||
|
# we expect all converters to be subclasses of SimpleValue
|
||||||
|
assert all(issubclass(klass, SimpleValue) for klass in self.converterClasses)
|
||||||
|
assert _issubclass_namedtuple(self.tupleClass), repr(self.tupleClass)
|
||||||
|
assert len(self.tupleClass._fields) == len(self.converterClasses)
|
||||||
|
assert tableClass is None # tableClass is unused by SimplValues
|
||||||
|
BaseConverter.__init__(self, name, repeat, aux)
|
||||||
|
self.converters = [
|
||||||
|
klass(name=name, repeat=None, aux=None)
|
||||||
|
for name, klass in zip(self.tupleClass._fields, self.converterClasses)
|
||||||
|
]
|
||||||
|
self.convertersByName = {conv.name: conv for conv in self.converters}
|
||||||
|
# returned by getRecordSize method
|
||||||
|
self.staticSize = sum(c.staticSize for c in self.converters)
|
||||||
|
|
||||||
|
def read(self, reader, font, tableDict):
|
||||||
|
kwargs = {
|
||||||
|
conv.name: conv.read(reader, font, tableDict)
|
||||||
|
for conv in self.converters
|
||||||
|
}
|
||||||
|
return self.tupleClass(**kwargs)
|
||||||
|
|
||||||
|
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||||
|
for conv in self.converters:
|
||||||
|
v = getattr(value, conv.name)
|
||||||
|
# repeatIndex is unused for SimpleValues
|
||||||
|
conv.write(writer, font, tableDict, v, repeatIndex=None)
|
||||||
|
|
||||||
|
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||||
|
assert value is not None
|
||||||
|
defaults = value.__new__.__defaults__ or ()
|
||||||
|
assert len(self.converters) >= len(defaults)
|
||||||
|
values = {}
|
||||||
|
required = object()
|
||||||
|
for conv, default in zip_longest(
|
||||||
|
reversed(self.converters),
|
||||||
|
reversed(defaults),
|
||||||
|
fillvalue=required,
|
||||||
|
):
|
||||||
|
v = getattr(value, conv.name)
|
||||||
|
if default is required or v != default:
|
||||||
|
values[conv.name] = conv.toString(v)
|
||||||
|
if attrs is None:
|
||||||
|
attrs = []
|
||||||
|
attrs.extend(
|
||||||
|
(conv.name, values[conv.name])
|
||||||
|
for conv in self.converters
|
||||||
|
if conv.name in values
|
||||||
|
)
|
||||||
|
xmlWriter.simpletag(name, attrs)
|
||||||
|
xmlWriter.newline()
|
||||||
|
|
||||||
|
def xmlRead(self, attrs, content, font):
|
||||||
|
converters = self.convertersByName
|
||||||
|
kwargs = {
|
||||||
|
k: converters[k].fromString(v)
|
||||||
|
for k, v in attrs.items()
|
||||||
|
}
|
||||||
|
return self.tupleClass(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class VariableScalar(_NamedTupleConverter):
|
||||||
|
tupleClass = VariableFloat
|
||||||
|
converterClasses = [Fixed, ULong]
|
||||||
|
|
||||||
|
|
||||||
|
class VariableNormalizedScalar(_NamedTupleConverter):
|
||||||
|
tupleClass = VariableFloat
|
||||||
|
converterClasses = [F2Dot14, ULong]
|
||||||
|
|
||||||
|
|
||||||
|
class VariablePosition(_NamedTupleConverter):
|
||||||
|
tupleClass = VariableInt
|
||||||
|
converterClasses = [Short, ULong]
|
||||||
|
|
||||||
|
|
||||||
|
class VariableDistance(_NamedTupleConverter):
|
||||||
|
tupleClass = VariableInt
|
||||||
|
converterClasses = [UShort, ULong]
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendMode(UShort):
|
||||||
|
def read(self, reader, font, tableDict):
|
||||||
|
return _ExtendMode(super().read(reader, font, tableDict))
|
||||||
|
@staticmethod
|
||||||
|
def fromString(value):
|
||||||
|
return getattr(_ExtendMode, value.upper())
|
||||||
|
@staticmethod
|
||||||
|
def toString(value):
|
||||||
|
return _ExtendMode(value).name.lower()
|
||||||
|
|
||||||
|
|
||||||
converterMapping = {
|
converterMapping = {
|
||||||
# type class
|
# type class
|
||||||
"int8": Int8,
|
"int8": Int8,
|
||||||
@ -1609,6 +1737,7 @@ converterMapping = {
|
|||||||
"VarIdxMapValue": VarIdxMapValue,
|
"VarIdxMapValue": VarIdxMapValue,
|
||||||
"VarDataValue": VarDataValue,
|
"VarDataValue": VarDataValue,
|
||||||
"LookupFlag": LookupFlag,
|
"LookupFlag": LookupFlag,
|
||||||
|
"ExtendMode": ExtendMode,
|
||||||
|
|
||||||
# AAT
|
# AAT
|
||||||
"CIDGlyphMap": CIDGlyphMap,
|
"CIDGlyphMap": CIDGlyphMap,
|
||||||
@ -1624,4 +1753,10 @@ converterMapping = {
|
|||||||
"STXHeader": lambda C: partial(STXHeader, tableClass=C),
|
"STXHeader": lambda C: partial(STXHeader, tableClass=C),
|
||||||
"OffsetTo": lambda C: partial(Table, tableClass=C),
|
"OffsetTo": lambda C: partial(Table, tableClass=C),
|
||||||
"LOffsetTo": lambda C: partial(LTable, tableClass=C),
|
"LOffsetTo": lambda C: partial(LTable, tableClass=C),
|
||||||
|
|
||||||
|
# Variable types
|
||||||
|
"VariableScalar": VariableScalar,
|
||||||
|
"VariableNormalizedScalar": VariableNormalizedScalar,
|
||||||
|
"VariablePosition": VariablePosition,
|
||||||
|
"VariableDistance": VariableDistance,
|
||||||
}
|
}
|
||||||
|
@ -1539,4 +1539,107 @@ otData = [
|
|||||||
('int16', 'CVTValueArray', 'NumCVTEntries', 0, 'CVT value'),
|
('int16', 'CVTValueArray', 'NumCVTEntries', 0, 'CVT value'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
#
|
||||||
|
# COLR
|
||||||
|
#
|
||||||
|
|
||||||
|
('COLR', [
|
||||||
|
('uint16', 'Version', None, None, 'Table version number (starts at 0).'),
|
||||||
|
('uint16', 'BaseGlyphRecordCount', None, None, 'Number of Base Glyph Records.'),
|
||||||
|
('LOffset', 'BaseGlyphRecordArray', None, None, 'Offset (from beginning of COLR table) to Base Glyph records.'),
|
||||||
|
('LOffset', 'LayerRecordArray', None, None, 'Offset (from beginning of COLR table) to Layer Records.'),
|
||||||
|
('uint16', 'LayerRecordCount', None, None, 'Number of Layer Records.'),
|
||||||
|
('LOffset', 'BaseGlyphV1Array', None, 'Version >= 1', 'Offset (from beginning of COLR table) to array of Version-1 Base Glyph records.'),
|
||||||
|
('LOffset', 'VarStore', None, 'Version >= 1', 'Offset to variation store (may be NULL)'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('BaseGlyphRecordArray', [
|
||||||
|
('BaseGlyphRecord', 'BaseGlyphRecord', 'BaseGlyphRecordCount', 0, 'Base Glyph records.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('BaseGlyphRecord', [
|
||||||
|
('GlyphID', 'BaseGlyph', None, None, 'Glyph ID of reference glyph. This glyph is for reference only and is not rendered for color.'),
|
||||||
|
('uint16', 'FirstLayerIndex', None, None, 'Index (from beginning of the Layer Records) to the layer record. There will be numLayers consecutive entries for this base glyph.'),
|
||||||
|
('uint16', 'NumLayers', None, None, 'Number of color layers associated with this glyph.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('LayerRecordArray', [
|
||||||
|
('LayerRecord', 'LayerRecord', 'LayerRecordCount', 0, 'Layer records.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('LayerRecord', [
|
||||||
|
('GlyphID', 'LayerGlyph', None, None, 'Glyph ID of layer glyph (must be in z-order from bottom to top).'),
|
||||||
|
('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('BaseGlyphV1Array', [
|
||||||
|
('uint32', 'BaseGlyphCount', None, None, 'Number of Version-1 Base Glyph records'),
|
||||||
|
('struct', 'BaseGlyphV1Record', 'BaseGlyphCount', 0, 'Array of Version-1 Base Glyph records'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('BaseGlyphV1Record', [
|
||||||
|
('GlyphID', 'BaseGlyph', None, None, 'Glyph ID of reference glyph.'),
|
||||||
|
('LOffset', 'LayerV1Array', None, None, 'Offset (from beginning of BaseGlyphV1Array) to LayerV1Array.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('LayerV1Array', [
|
||||||
|
('uint32', 'LayerCount', None, None, 'Number of Version-1 Layer records'),
|
||||||
|
('struct', 'LayerV1Record', 'LayerCount', 0, 'Array of Version-1 Layer records'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('LayerV1Record', [
|
||||||
|
('GlyphID', 'LayerGlyph', None, None, 'Glyph ID of layer glyph (must be in z-order from bottom to top).'),
|
||||||
|
('LOffset', 'Paint', None, None, 'Offset (from beginning of LayerV1Array) to Paint subtable.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('Affine2x2', [
|
||||||
|
('VariableScalar', 'xx', None, None, ''),
|
||||||
|
('VariableScalar', 'xy', None, None, ''),
|
||||||
|
('VariableScalar', 'yx', None, None, ''),
|
||||||
|
('VariableScalar', 'yy', None, None, ''),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('Point', [
|
||||||
|
('VariablePosition', 'x', None, None, ''),
|
||||||
|
('VariablePosition', 'y', None, None, ''),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('Color', [
|
||||||
|
('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
|
||||||
|
('VariableNormalizedScalar', 'Transparency', None, None, 'Values outsided [0.,1.] reserved'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('ColorStop', [
|
||||||
|
('VariableNormalizedScalar', 'StopOffset', None, None, ''),
|
||||||
|
('Color', 'Color', None, None, ''),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('ColorLine', [
|
||||||
|
('ExtendMode', 'Extend', None, None, 'Enum {PAD = 0, REPEAT = 1, REFLECT = 2}'),
|
||||||
|
('uint16', 'StopCount', None, None, 'Number of Color stops.'),
|
||||||
|
('ColorStop', 'ColorStop', 'StopCount', 0, 'Array of Color stops.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('PaintFormat1', [
|
||||||
|
('uint16', 'PaintFormat', None, None, 'Format identifier-format = 1'),
|
||||||
|
('Color', 'Color', None, None, 'A solid color paint.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('PaintFormat2', [
|
||||||
|
('uint16', 'PaintFormat', None, None, 'Format identifier-format = 2'),
|
||||||
|
('LOffset', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
|
||||||
|
('Point', 'p0', None, None, ''),
|
||||||
|
('Point', 'p1', None, None, ''),
|
||||||
|
('Point', 'p2', None, None, 'Normal; equal to p1 in simple cases.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
('PaintFormat3', [
|
||||||
|
('uint16', 'PaintFormat', None, None, 'Format identifier-format = 3'),
|
||||||
|
('LOffset', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
|
||||||
|
('Point', 'c0', None, None, ''),
|
||||||
|
('Point', 'c1', None, None, ''),
|
||||||
|
('VariableDistance', 'r0', None, None, ''),
|
||||||
|
('VariableDistance', 'r1', None, None, ''),
|
||||||
|
('LOffsetTo(Affine2x2)', 'Affine', None, None, 'Offset (from beginning of Paint table) to Affine2x2 subtable.'),
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
|
@ -5,7 +5,11 @@ OpenType subtables.
|
|||||||
Most are constructed upon import from data in otData.py, all are populated with
|
Most are constructed upon import from data in otData.py, all are populated with
|
||||||
converter objects from otConverters.py.
|
converter objects from otConverters.py.
|
||||||
"""
|
"""
|
||||||
|
from enum import IntEnum
|
||||||
|
import itertools
|
||||||
|
from collections import namedtuple
|
||||||
from fontTools.misc.py23 import *
|
from fontTools.misc.py23 import *
|
||||||
|
from fontTools.misc.fixedTools import otRound
|
||||||
from fontTools.misc.textTools import pad, safeEval
|
from fontTools.misc.textTools import pad, safeEval
|
||||||
from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference
|
from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference
|
||||||
import logging
|
import logging
|
||||||
@ -1152,6 +1156,100 @@ class LigatureSubst(FormatSwitchingBaseTable):
|
|||||||
ligs.append(lig)
|
ligs.append(lig)
|
||||||
|
|
||||||
|
|
||||||
|
class COLR(BaseTable):
|
||||||
|
|
||||||
|
def decompile(self, reader, font):
|
||||||
|
# COLRv0 is exceptional in that LayerRecordCount appears *after* the
|
||||||
|
# LayerRecordArray it counts, but the parser logic expects Count fields
|
||||||
|
# to always precede the arrays. Here we work around this by parsing the
|
||||||
|
# LayerRecordCount before the rest of the table, and storing it in
|
||||||
|
# the reader's local state.
|
||||||
|
subReader = reader.getSubReader(offset=0)
|
||||||
|
for conv in self.getConverters():
|
||||||
|
if conv.name != "LayerRecordCount":
|
||||||
|
subReader.advance(conv.staticSize)
|
||||||
|
continue
|
||||||
|
conv = self.getConverterByName("LayerRecordCount")
|
||||||
|
reader[conv.name] = conv.read(subReader, font, tableDict={})
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise AssertionError("LayerRecordCount converter not found")
|
||||||
|
return BaseTable.decompile(self, reader, font)
|
||||||
|
|
||||||
|
def preWrite(self, font):
|
||||||
|
# The writer similarly assumes Count values precede the things counted,
|
||||||
|
# thus here we pre-initialize a CountReference; the actual count value
|
||||||
|
# will be set to the lenght of the array by the time this is assembled.
|
||||||
|
self.LayerRecordCount = None
|
||||||
|
return {
|
||||||
|
**self.__dict__,
|
||||||
|
"LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BaseGlyphRecordArray(BaseTable):
|
||||||
|
|
||||||
|
def preWrite(self, font):
|
||||||
|
self.BaseGlyphRecord = sorted(
|
||||||
|
self.BaseGlyphRecord,
|
||||||
|
key=lambda rec: font.getGlyphID(rec.BaseGlyph)
|
||||||
|
)
|
||||||
|
return self.__dict__.copy()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseGlyphV1Array(BaseTable):
|
||||||
|
|
||||||
|
def preWrite(self, font):
|
||||||
|
self.BaseGlyphV1Record = sorted(
|
||||||
|
self.BaseGlyphV1Record,
|
||||||
|
key=lambda rec: font.getGlyphID(rec.BaseGlyph)
|
||||||
|
)
|
||||||
|
return self.__dict__.copy()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class VariableValue(namedtuple("VariableValue", ["value", "varIdx"])):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
_value_mapper = None
|
||||||
|
|
||||||
|
def __new__(cls, value, varIdx=0):
|
||||||
|
return super().__new__(
|
||||||
|
cls,
|
||||||
|
cls._value_mapper(value) if cls._value_mapper else value,
|
||||||
|
varIdx
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _make(cls, iterable):
|
||||||
|
if cls._value_mapper:
|
||||||
|
it = iter(iterable)
|
||||||
|
try:
|
||||||
|
value = next(it)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
value = cls._value_mapper(value)
|
||||||
|
iterable = itertools.chain((value,), it)
|
||||||
|
return super()._make(iterable)
|
||||||
|
|
||||||
|
|
||||||
|
class VariableFloat(VariableValue):
|
||||||
|
__slots__ = ()
|
||||||
|
_value_mapper = float
|
||||||
|
|
||||||
|
|
||||||
|
class VariableInt(VariableValue):
|
||||||
|
__slots__ = ()
|
||||||
|
_value_mapper = otRound
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendMode(IntEnum):
|
||||||
|
PAD = 0
|
||||||
|
REPEAT = 1
|
||||||
|
REFLECT = 2
|
||||||
|
|
||||||
|
|
||||||
# For each subtable format there is a class. However, we don't really distinguish
|
# For each subtable format there is a class. However, we don't really distinguish
|
||||||
# between "field name" and "format name": often these are the same. Yet there's
|
# between "field name" and "format name": often these are the same. Yet there's
|
||||||
# a whole bunch of fields with different names. The following dict is a mapping
|
# a whole bunch of fields with different names. The following dict is a mapping
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from fontTools.ttLib import newTable
|
from fontTools.ttLib import newTable
|
||||||
|
from fontTools.ttLib.tables import otTables as ot
|
||||||
from fontTools.colorLib import builder
|
from fontTools.colorLib import builder
|
||||||
from fontTools.colorLib.errors import ColorLibError
|
from fontTools.colorLib.errors import ColorLibError
|
||||||
import pytest
|
import pytest
|
||||||
@ -185,3 +186,552 @@ def test_buildCPAL_invalid_color():
|
|||||||
),
|
),
|
||||||
):
|
):
|
||||||
builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]])
|
builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]])
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildColor():
|
||||||
|
c = builder.buildColor(0)
|
||||||
|
assert c.PaletteIndex == 0
|
||||||
|
assert c.Transparency.value == 0.0
|
||||||
|
assert c.Transparency.varIdx == 0
|
||||||
|
|
||||||
|
c = builder.buildColor(1, transparency=0.5)
|
||||||
|
assert c.PaletteIndex == 1
|
||||||
|
assert c.Transparency.value == 0.5
|
||||||
|
assert c.Transparency.varIdx == 0
|
||||||
|
|
||||||
|
c = builder.buildColor(3, transparency=builder.VariableFloat(0.5, varIdx=2))
|
||||||
|
assert c.PaletteIndex == 3
|
||||||
|
assert c.Transparency.value == 0.5
|
||||||
|
assert c.Transparency.varIdx == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildSolidColorPaint():
|
||||||
|
p = builder.buildSolidColorPaint(0)
|
||||||
|
assert p.Format == 1
|
||||||
|
assert p.Color.PaletteIndex == 0
|
||||||
|
assert p.Color.Transparency.value == 0.0
|
||||||
|
assert p.Color.Transparency.varIdx == 0
|
||||||
|
|
||||||
|
p = builder.buildSolidColorPaint(1, transparency=0.5)
|
||||||
|
assert p.Format == 1
|
||||||
|
assert p.Color.PaletteIndex == 1
|
||||||
|
assert p.Color.Transparency.value == 0.5
|
||||||
|
assert p.Color.Transparency.varIdx == 0
|
||||||
|
|
||||||
|
p = builder.buildSolidColorPaint(
|
||||||
|
3, transparency=builder.VariableFloat(0.5, varIdx=2)
|
||||||
|
)
|
||||||
|
assert p.Format == 1
|
||||||
|
assert p.Color.PaletteIndex == 3
|
||||||
|
assert p.Color.Transparency.value == 0.5
|
||||||
|
assert p.Color.Transparency.varIdx == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildColorStop():
|
||||||
|
s = builder.buildColorStop(0.1, 2)
|
||||||
|
assert s.StopOffset == builder.VariableFloat(0.1)
|
||||||
|
assert s.Color.PaletteIndex == 2
|
||||||
|
assert s.Color.Transparency == builder._DEFAULT_TRANSPARENCY
|
||||||
|
|
||||||
|
s = builder.buildColorStop(offset=0.2, paletteIndex=3, transparency=0.4)
|
||||||
|
assert s.StopOffset == builder.VariableFloat(0.2)
|
||||||
|
assert s.Color == builder.buildColor(3, transparency=0.4)
|
||||||
|
|
||||||
|
s = builder.buildColorStop(
|
||||||
|
offset=builder.VariableFloat(0.0, varIdx=1),
|
||||||
|
paletteIndex=0,
|
||||||
|
transparency=builder.VariableFloat(0.3, varIdx=2),
|
||||||
|
)
|
||||||
|
assert s.StopOffset == builder.VariableFloat(0.0, varIdx=1)
|
||||||
|
assert s.Color.PaletteIndex == 0
|
||||||
|
assert s.Color.Transparency == builder.VariableFloat(0.3, varIdx=2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildColorLine():
|
||||||
|
stops = [(0.0, 0), (0.5, 1), (1.0, 2)]
|
||||||
|
|
||||||
|
cline = builder.buildColorLine(stops)
|
||||||
|
assert cline.Extend == builder.ExtendMode.PAD
|
||||||
|
assert cline.StopCount == 3
|
||||||
|
assert [
|
||||||
|
(cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop
|
||||||
|
] == stops
|
||||||
|
|
||||||
|
cline = builder.buildColorLine(stops, extend="pad")
|
||||||
|
assert cline.Extend == builder.ExtendMode.PAD
|
||||||
|
|
||||||
|
cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REPEAT)
|
||||||
|
assert cline.Extend == builder.ExtendMode.REPEAT
|
||||||
|
|
||||||
|
cline = builder.buildColorLine(stops, extend=builder.ExtendMode.REFLECT)
|
||||||
|
assert cline.Extend == builder.ExtendMode.REFLECT
|
||||||
|
|
||||||
|
cline = builder.buildColorLine([builder.buildColorStop(*s) for s in stops])
|
||||||
|
assert [
|
||||||
|
(cs.StopOffset.value, cs.Color.PaletteIndex) for cs in cline.ColorStop
|
||||||
|
] == stops
|
||||||
|
|
||||||
|
stops = [
|
||||||
|
{"offset": (0.0, 1), "paletteIndex": 0, "transparency": (0.5, 2)},
|
||||||
|
{"offset": (1.0, 3), "paletteIndex": 1, "transparency": (0.3, 4)},
|
||||||
|
]
|
||||||
|
cline = builder.buildColorLine(stops)
|
||||||
|
assert [
|
||||||
|
{
|
||||||
|
"offset": cs.StopOffset,
|
||||||
|
"paletteIndex": cs.Color.PaletteIndex,
|
||||||
|
"transparency": cs.Color.Transparency,
|
||||||
|
}
|
||||||
|
for cs in cline.ColorStop
|
||||||
|
] == stops
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildPoint():
|
||||||
|
pt = builder.buildPoint(0, 1)
|
||||||
|
assert pt.x == builder.VariableInt(0)
|
||||||
|
assert pt.y == builder.VariableInt(1)
|
||||||
|
|
||||||
|
pt = builder.buildPoint(
|
||||||
|
builder.VariableInt(2, varIdx=1), builder.VariableInt(3, varIdx=2)
|
||||||
|
)
|
||||||
|
assert pt.x == builder.VariableInt(2, varIdx=1)
|
||||||
|
assert pt.y == builder.VariableInt(3, varIdx=2)
|
||||||
|
|
||||||
|
# float coords are rounded
|
||||||
|
pt = builder.buildPoint(x=-2.5, y=3.5)
|
||||||
|
assert pt.x == builder.VariableInt(-2)
|
||||||
|
assert pt.y == builder.VariableInt(4)
|
||||||
|
|
||||||
|
# tuple args are cast to VariableInt namedtuple
|
||||||
|
pt = builder.buildPoint((1, 2), (3, 4))
|
||||||
|
assert pt.x == builder.VariableInt(1, varIdx=2)
|
||||||
|
assert pt.y == builder.VariableInt(3, varIdx=4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildAffine2x2():
|
||||||
|
matrix = builder.buildAffine2x2(1.5, 0, 0.5, 2.0)
|
||||||
|
assert matrix.xx == builder.VariableFloat(1.5)
|
||||||
|
assert matrix.xy == builder.VariableFloat(0.0)
|
||||||
|
assert matrix.yx == builder.VariableFloat(0.5)
|
||||||
|
assert matrix.yy == builder.VariableFloat(2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildLinearGradientPaint():
|
||||||
|
color_stops = [
|
||||||
|
builder.buildColorStop(0.0, 0),
|
||||||
|
builder.buildColorStop(0.5, 1),
|
||||||
|
builder.buildColorStop(1.0, 2, transparency=0.8),
|
||||||
|
]
|
||||||
|
color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT)
|
||||||
|
p0 = builder.buildPoint(x=100, y=200)
|
||||||
|
p1 = builder.buildPoint(x=150, y=250)
|
||||||
|
|
||||||
|
gradient = builder.buildLinearGradientPaint(color_line, p0, p1)
|
||||||
|
assert gradient.Format == 2
|
||||||
|
assert gradient.ColorLine == color_line
|
||||||
|
assert gradient.p0 == p0
|
||||||
|
assert gradient.p1 == p1
|
||||||
|
assert gradient.p2 == gradient.p1
|
||||||
|
assert gradient.p2 is not gradient.p1
|
||||||
|
|
||||||
|
gradient = builder.buildLinearGradientPaint({"stops": color_stops}, p0, p1)
|
||||||
|
assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
|
||||||
|
assert gradient.ColorLine.ColorStop == color_stops
|
||||||
|
|
||||||
|
gradient = builder.buildLinearGradientPaint(color_line, p0, p1, p2=(150, 230))
|
||||||
|
assert gradient.p2 == builder.buildPoint(x=150, y=230)
|
||||||
|
assert gradient.p2 != gradient.p1
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildRadialGradientPaint():
|
||||||
|
color_stops = [
|
||||||
|
builder.buildColorStop(0.0, 0),
|
||||||
|
builder.buildColorStop(0.5, 1),
|
||||||
|
builder.buildColorStop(1.0, 2, transparency=0.8),
|
||||||
|
]
|
||||||
|
color_line = builder.buildColorLine(color_stops, extend=builder.ExtendMode.REPEAT)
|
||||||
|
c0 = builder.buildPoint(x=100, y=200)
|
||||||
|
c1 = builder.buildPoint(x=150, y=250)
|
||||||
|
r0 = builder.VariableInt(10)
|
||||||
|
r1 = builder.VariableInt(5)
|
||||||
|
|
||||||
|
gradient = builder.buildRadialGradientPaint(color_line, c0, c1, r0, r1)
|
||||||
|
assert gradient.Format == 3
|
||||||
|
assert gradient.ColorLine == color_line
|
||||||
|
assert gradient.c0 == c0
|
||||||
|
assert gradient.c1 == c1
|
||||||
|
assert gradient.r0 == r0
|
||||||
|
assert gradient.r1 == r1
|
||||||
|
assert gradient.Affine is None
|
||||||
|
|
||||||
|
gradient = builder.buildRadialGradientPaint({"stops": color_stops}, c0, c1, r0, r1)
|
||||||
|
assert gradient.ColorLine.Extend == builder.ExtendMode.PAD
|
||||||
|
assert gradient.ColorLine.ColorStop == color_stops
|
||||||
|
|
||||||
|
matrix = builder.buildAffine2x2(2.0, 0.0, 0.0, 2.0)
|
||||||
|
gradient = builder.buildRadialGradientPaint(
|
||||||
|
color_line, c0, c1, r0, r1, affine=matrix
|
||||||
|
)
|
||||||
|
assert gradient.Affine == matrix
|
||||||
|
|
||||||
|
gradient = builder.buildRadialGradientPaint(
|
||||||
|
color_line, c0, c1, r0, r1, affine=(2.0, 0.0, 0.0, 2.0)
|
||||||
|
)
|
||||||
|
assert gradient.Affine == matrix
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildLayerV1Record():
|
||||||
|
layer = builder.buildLayerV1Record("a", 2)
|
||||||
|
assert layer.LayerGlyph == "a"
|
||||||
|
assert layer.Paint.Format == 1
|
||||||
|
assert layer.Paint.Color.PaletteIndex == 2
|
||||||
|
|
||||||
|
layer = builder.buildLayerV1Record("a", builder.buildSolidColorPaint(3, 0.9))
|
||||||
|
assert layer.Paint.Format == 1
|
||||||
|
assert layer.Paint.Color.PaletteIndex == 3
|
||||||
|
assert layer.Paint.Color.Transparency.value == 0.9
|
||||||
|
|
||||||
|
layer = builder.buildLayerV1Record(
|
||||||
|
"a",
|
||||||
|
builder.buildLinearGradientPaint(
|
||||||
|
{"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert layer.Paint.Format == 2
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 3
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[1].StopOffset.value == 1.0
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 4
|
||||||
|
assert layer.Paint.p0.x.value == 100
|
||||||
|
assert layer.Paint.p0.y.value == 200
|
||||||
|
assert layer.Paint.p1.x.value == 150
|
||||||
|
assert layer.Paint.p1.y.value == 250
|
||||||
|
|
||||||
|
layer = builder.buildLayerV1Record(
|
||||||
|
"a",
|
||||||
|
builder.buildRadialGradientPaint(
|
||||||
|
{
|
||||||
|
"stops": [
|
||||||
|
(0.0, 5),
|
||||||
|
{"offset": 0.5, "paletteIndex": 6, "transparency": 0.8},
|
||||||
|
(1.0, 7),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
(50, 50),
|
||||||
|
(75, 75),
|
||||||
|
30,
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert layer.Paint.Format == 3
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 5
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[1].StopOffset.value == 0.5
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 6
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[1].Color.Transparency.value == 0.8
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[2].StopOffset.value == 1.0
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[2].Color.PaletteIndex == 7
|
||||||
|
assert layer.Paint.c0.x.value == 50
|
||||||
|
assert layer.Paint.c0.y.value == 50
|
||||||
|
assert layer.Paint.c1.x.value == 75
|
||||||
|
assert layer.Paint.c1.y.value == 75
|
||||||
|
assert layer.Paint.r0.value == 30
|
||||||
|
assert layer.Paint.r1.value == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildLayerV1Record_from_dict():
|
||||||
|
layer = builder.buildLayerV1Record("a", {"format": 1, "paletteIndex": 0})
|
||||||
|
assert layer.LayerGlyph == "a"
|
||||||
|
assert layer.Paint.Format == 1
|
||||||
|
assert layer.Paint.Color.PaletteIndex == 0
|
||||||
|
|
||||||
|
layer = builder.buildLayerV1Record(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
"format": 2,
|
||||||
|
"colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
|
||||||
|
"p0": (0, 0),
|
||||||
|
"p1": (10, 10),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert layer.Paint.Format == 2
|
||||||
|
assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0
|
||||||
|
|
||||||
|
layer = builder.buildLayerV1Record(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
"format": 3,
|
||||||
|
"colorLine": {"stops": [(0.0, 0), (1.0, 1)]},
|
||||||
|
"c0": (0, 0),
|
||||||
|
"c1": (10, 10),
|
||||||
|
"r0": 4,
|
||||||
|
"r1": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert layer.Paint.Format == 3
|
||||||
|
assert layer.Paint.r0.value == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildLayerV1Array():
|
||||||
|
layers = [
|
||||||
|
("a", 1),
|
||||||
|
("b", {"format": 1, "paletteIndex": 2, "transparency": 0.5}),
|
||||||
|
(
|
||||||
|
"c",
|
||||||
|
{
|
||||||
|
"format": 2,
|
||||||
|
"colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "repeat"},
|
||||||
|
"p0": (100, 200),
|
||||||
|
"p1": (150, 250),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"d",
|
||||||
|
{
|
||||||
|
"format": 3,
|
||||||
|
"colorLine": {
|
||||||
|
"stops": [
|
||||||
|
{"offset": 0.0, "paletteIndex": 5},
|
||||||
|
{"offset": 0.5, "paletteIndex": 6, "transparency": 0.8},
|
||||||
|
{"offset": 1.0, "paletteIndex": 7},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"c0": (50, 50),
|
||||||
|
"c1": (75, 75),
|
||||||
|
"r0": 30,
|
||||||
|
"r1": 10,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
builder.buildLayerV1Record("e", builder.buildSolidColorPaint(8)),
|
||||||
|
]
|
||||||
|
layersArray = builder.buildLayerV1Array(layers)
|
||||||
|
|
||||||
|
assert layersArray.LayerCount == len(layersArray.LayerV1Record)
|
||||||
|
assert all(isinstance(l, ot.LayerV1Record) for l in layersArray.LayerV1Record)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildBaseGlyphV1Record():
|
||||||
|
baseGlyphRec = builder.buildBaseGlyphV1Record("a", [("b", 0), ("c", 1)])
|
||||||
|
assert baseGlyphRec.BaseGlyph == "a"
|
||||||
|
assert isinstance(baseGlyphRec.LayerV1Array, ot.LayerV1Array)
|
||||||
|
|
||||||
|
layerArray = builder.buildLayerV1Array([("b", 0), ("c", 1)])
|
||||||
|
baseGlyphRec = builder.buildBaseGlyphV1Record("a", layerArray)
|
||||||
|
assert baseGlyphRec.BaseGlyph == "a"
|
||||||
|
assert baseGlyphRec.LayerV1Array == layerArray
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildBaseGlyphV1Array():
|
||||||
|
colorGlyphs = {
|
||||||
|
"a": [("b", 0), ("c", 1)],
|
||||||
|
"d": [
|
||||||
|
("e", {"format": 1, "paletteIndex": 2, "transparency": 0.8}),
|
||||||
|
(
|
||||||
|
"f",
|
||||||
|
{
|
||||||
|
"format": 3,
|
||||||
|
"colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "reflect"},
|
||||||
|
"c0": (0, 0),
|
||||||
|
"c1": (0, 0),
|
||||||
|
"r0": 10,
|
||||||
|
"r1": 0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"g": builder.buildLayerV1Array([("h", 5)]),
|
||||||
|
}
|
||||||
|
glyphMap = {
|
||||||
|
".notdef": 0,
|
||||||
|
"a": 4,
|
||||||
|
"b": 3,
|
||||||
|
"c": 2,
|
||||||
|
"d": 1,
|
||||||
|
"e": 5,
|
||||||
|
"f": 6,
|
||||||
|
"g": 7,
|
||||||
|
"h": 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
baseGlyphArray = builder.buildBaseGlyphV1Array(colorGlyphs, glyphMap)
|
||||||
|
assert baseGlyphArray.BaseGlyphCount == len(colorGlyphs)
|
||||||
|
assert baseGlyphArray.BaseGlyphV1Record[0].BaseGlyph == "d"
|
||||||
|
assert baseGlyphArray.BaseGlyphV1Record[1].BaseGlyph == "a"
|
||||||
|
assert baseGlyphArray.BaseGlyphV1Record[2].BaseGlyph == "g"
|
||||||
|
|
||||||
|
baseGlyphArray = builder.buildBaseGlyphV1Array(colorGlyphs)
|
||||||
|
assert baseGlyphArray.BaseGlyphCount == len(colorGlyphs)
|
||||||
|
assert baseGlyphArray.BaseGlyphV1Record[0].BaseGlyph == "a"
|
||||||
|
assert baseGlyphArray.BaseGlyphV1Record[1].BaseGlyph == "d"
|
||||||
|
assert baseGlyphArray.BaseGlyphV1Record[2].BaseGlyph == "g"
|
||||||
|
|
||||||
|
|
||||||
|
def test_splitSolidAndGradientGlyphs():
|
||||||
|
colorGlyphs = {
|
||||||
|
"a": [
|
||||||
|
("b", 0),
|
||||||
|
("c", 1),
|
||||||
|
("d", {"format": 1, "paletteIndex": 2}),
|
||||||
|
("e", builder.buildSolidColorPaint(paletteIndex=3)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
|
||||||
|
|
||||||
|
assert colorGlyphsV0 == {"a": [("b", 0), ("c", 1), ("d", 2), ("e", 3)]}
|
||||||
|
assert not colorGlyphsV1
|
||||||
|
|
||||||
|
colorGlyphs = {
|
||||||
|
"a": [("b", builder.buildSolidColorPaint(paletteIndex=0, transparency=1.0))]
|
||||||
|
}
|
||||||
|
|
||||||
|
colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
|
||||||
|
|
||||||
|
assert not colorGlyphsV0
|
||||||
|
assert colorGlyphsV1 == colorGlyphs
|
||||||
|
|
||||||
|
colorGlyphs = {
|
||||||
|
"a": [("b", 0)],
|
||||||
|
"c": [
|
||||||
|
("d", 1),
|
||||||
|
(
|
||||||
|
"e",
|
||||||
|
{
|
||||||
|
"format": 2,
|
||||||
|
"colorLine": {"stops": [(0.0, 2), (1.0, 3)]},
|
||||||
|
"p0": (0, 0),
|
||||||
|
"p1": (10, 10),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
colorGlyphsV0, colorGlyphsV1 = builder._splitSolidAndGradientGlyphs(colorGlyphs)
|
||||||
|
|
||||||
|
assert colorGlyphsV0 == {"a": [("b", 0)]}
|
||||||
|
assert "a" not in colorGlyphsV1
|
||||||
|
assert "c" in colorGlyphsV1
|
||||||
|
assert len(colorGlyphsV1["c"]) == 2
|
||||||
|
|
||||||
|
layer_d = colorGlyphsV1["c"][0]
|
||||||
|
assert layer_d[0] == "d"
|
||||||
|
assert isinstance(layer_d[1], ot.Paint)
|
||||||
|
assert layer_d[1].Format == 1
|
||||||
|
|
||||||
|
layer_e = colorGlyphsV1["c"][1]
|
||||||
|
assert layer_e[0] == "e"
|
||||||
|
assert isinstance(layer_e[1], ot.Paint)
|
||||||
|
assert layer_e[1].Format == 2
|
||||||
|
|
||||||
|
|
||||||
|
class BuildCOLRTest(object):
|
||||||
|
def test_automatic_version_all_solid_color_glyphs(self):
|
||||||
|
colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]})
|
||||||
|
assert colr.version == 0
|
||||||
|
assert hasattr(colr, "ColorLayers")
|
||||||
|
assert colr.ColorLayers["a"][0].name == "b"
|
||||||
|
assert colr.ColorLayers["a"][1].name == "c"
|
||||||
|
|
||||||
|
def test_automatic_version_no_solid_color_glyphs(self):
|
||||||
|
colr = builder.buildCOLR(
|
||||||
|
{
|
||||||
|
"a": [
|
||||||
|
(
|
||||||
|
"b",
|
||||||
|
{
|
||||||
|
"format": 3,
|
||||||
|
"colorLine": {
|
||||||
|
"stops": [(0.0, 0), (1.0, 1)],
|
||||||
|
"extend": "repeat",
|
||||||
|
},
|
||||||
|
"c0": (1, 0),
|
||||||
|
"c1": (10, 0),
|
||||||
|
"r0": 4,
|
||||||
|
"r1": 2,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("c", {"format": 1, "paletteIndex": 2, "transparency": 0.8}),
|
||||||
|
],
|
||||||
|
"d": [
|
||||||
|
(
|
||||||
|
"e",
|
||||||
|
{
|
||||||
|
"format": 2,
|
||||||
|
"colorLine": {
|
||||||
|
"stops": [(0.0, 2), (1.0, 3)],
|
||||||
|
"extend": "reflect",
|
||||||
|
},
|
||||||
|
"p0": (1, 2),
|
||||||
|
"p1": (3, 4),
|
||||||
|
"p2": (2, 2),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert colr.version == 1
|
||||||
|
assert not hasattr(colr, "ColorLayers")
|
||||||
|
assert hasattr(colr, "table")
|
||||||
|
assert isinstance(colr.table, ot.COLR)
|
||||||
|
assert colr.table.BaseGlyphRecordCount == 0
|
||||||
|
assert colr.table.BaseGlyphRecordArray is None
|
||||||
|
assert colr.table.LayerRecordCount == 0
|
||||||
|
assert colr.table.LayerRecordArray is None
|
||||||
|
|
||||||
|
def test_automatic_version_mixed_solid_and_gradient_glyphs(self):
|
||||||
|
colr = builder.buildCOLR(
|
||||||
|
{
|
||||||
|
"a": [("b", 0), ("c", 1)],
|
||||||
|
"d": [
|
||||||
|
(
|
||||||
|
"e",
|
||||||
|
{
|
||||||
|
"format": 2,
|
||||||
|
"colorLine": {"stops": [(0.0, 2), (1.0, 3)]},
|
||||||
|
"p0": (1, 2),
|
||||||
|
"p1": (3, 4),
|
||||||
|
"p2": (2, 2),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert colr.version == 1
|
||||||
|
assert not hasattr(colr, "ColorLayers")
|
||||||
|
assert hasattr(colr, "table")
|
||||||
|
assert isinstance(colr.table, ot.COLR)
|
||||||
|
assert colr.table.VarStore is None
|
||||||
|
|
||||||
|
assert colr.table.BaseGlyphRecordCount == 1
|
||||||
|
assert isinstance(colr.table.BaseGlyphRecordArray, ot.BaseGlyphRecordArray)
|
||||||
|
assert colr.table.LayerRecordCount == 2
|
||||||
|
assert isinstance(colr.table.LayerRecordArray, ot.LayerRecordArray)
|
||||||
|
|
||||||
|
assert isinstance(colr.table.BaseGlyphV1Array, ot.BaseGlyphV1Array)
|
||||||
|
assert colr.table.BaseGlyphV1Array.BaseGlyphCount == 1
|
||||||
|
assert isinstance(
|
||||||
|
colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0], ot.BaseGlyphV1Record
|
||||||
|
)
|
||||||
|
assert colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0].BaseGlyph == "d"
|
||||||
|
assert isinstance(
|
||||||
|
colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0].LayerV1Array,
|
||||||
|
ot.LayerV1Array,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
colr.table.BaseGlyphV1Array.BaseGlyphV1Record[0]
|
||||||
|
.LayerV1Array.LayerV1Record[0]
|
||||||
|
.LayerGlyph
|
||||||
|
== "e"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_explicit_version_0(self):
|
||||||
|
colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0)
|
||||||
|
assert colr.version == 0
|
||||||
|
assert hasattr(colr, "ColorLayers")
|
||||||
|
|
||||||
|
def test_explicit_version_1(self):
|
||||||
|
colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=1)
|
||||||
|
assert colr.version == 1
|
||||||
|
assert not hasattr(colr, "ColorLayers")
|
||||||
|
assert hasattr(colr, "table")
|
||||||
|
assert isinstance(colr.table, ot.COLR)
|
||||||
|
assert colr.table.VarStore is None
|
||||||
|
313
Tests/ttLib/tables/C_O_L_R_test.py
Normal file
313
Tests/ttLib/tables/C_O_L_R_test.py
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
from fontTools import ttLib
|
||||||
|
from fontTools.misc.testTools import getXML, parseXML
|
||||||
|
from fontTools.ttLib.tables.C_O_L_R_ import table_C_O_L_R_
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
COLR_V0_DATA = (
|
||||||
|
b"\x00\x00" # Version (0)
|
||||||
|
b"\x00\x01" # BaseGlyphRecordCount (1)
|
||||||
|
b"\x00\x00\x00\x0e" # Offset to BaseGlyphRecordArray
|
||||||
|
b"\x00\x00\x00\x14" # Offset to LayerRecordArray
|
||||||
|
b"\x00\x03" # LayerRecordCount (3)
|
||||||
|
b"\x00\x06" # BaseGlyphRecord[0].BaseGlyph (6)
|
||||||
|
b"\x00\x00" # BaseGlyphRecord[0].FirstLayerIndex (0)
|
||||||
|
b"\x00\x03" # BaseGlyphRecord[0].NumLayers (3)
|
||||||
|
b"\x00\x07" # LayerRecord[0].LayerGlyph (7)
|
||||||
|
b"\x00\x00" # LayerRecord[0].PaletteIndex (0)
|
||||||
|
b"\x00\x08" # LayerRecord[1].LayerGlyph (8)
|
||||||
|
b"\x00\x01" # LayerRecord[1].PaletteIndex (1)
|
||||||
|
b"\x00\t" # LayerRecord[2].LayerGlyph (9)
|
||||||
|
b"\x00\x02" # LayerRecord[3].PaletteIndex (2)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
COLR_V0_XML = [
|
||||||
|
'<version value="0"/>',
|
||||||
|
'<ColorGlyph name="glyph00006">',
|
||||||
|
' <layer colorID="0" name="glyph00007"/>',
|
||||||
|
' <layer colorID="1" name="glyph00008"/>',
|
||||||
|
' <layer colorID="2" name="glyph00009"/>',
|
||||||
|
"</ColorGlyph>",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def dump(table, ttFont=None):
|
||||||
|
print("\n".join(getXML(table.toXML, ttFont)))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def font():
|
||||||
|
font = ttLib.TTFont()
|
||||||
|
font.setGlyphOrder(["glyph%05d" % i for i in range(30)])
|
||||||
|
return font
|
||||||
|
|
||||||
|
|
||||||
|
class COLR_V0_Test(object):
|
||||||
|
def test_decompile_and_compile(self, font):
|
||||||
|
colr = table_C_O_L_R_()
|
||||||
|
colr.decompile(COLR_V0_DATA, font)
|
||||||
|
assert colr.compile(font) == COLR_V0_DATA
|
||||||
|
|
||||||
|
def test_decompile_and_dump_xml(self, font):
|
||||||
|
colr = table_C_O_L_R_()
|
||||||
|
colr.decompile(COLR_V0_DATA, font)
|
||||||
|
|
||||||
|
dump(colr, font)
|
||||||
|
assert getXML(colr.toXML, font) == COLR_V0_XML
|
||||||
|
|
||||||
|
def test_load_from_xml_and_compile(self, font):
|
||||||
|
colr = table_C_O_L_R_()
|
||||||
|
for name, attrs, content in parseXML(COLR_V0_XML):
|
||||||
|
colr.fromXML(name, attrs, content, font)
|
||||||
|
|
||||||
|
assert colr.compile(font) == COLR_V0_DATA
|
||||||
|
|
||||||
|
|
||||||
|
COLR_V1_DATA = (
|
||||||
|
b"\x00\x01" # Version (1)
|
||||||
|
b"\x00\x01" # BaseGlyphRecordCount (1)
|
||||||
|
b"\x00\x00\x00\x16" # Offset to BaseGlyphRecordArray
|
||||||
|
b"\x00\x00\x00\x1c" # Offset to LayerRecordArray
|
||||||
|
b"\x00\x03" # LayerRecordCount (3)
|
||||||
|
b"\x00\x00\x00(" # Offset to BaseGlyphV1Array
|
||||||
|
b"\x00\x00\x00\x00" # Offset to VarStore (NULL)
|
||||||
|
b"\x00\x06" # BaseGlyphRecord[0].BaseGlyph (6)
|
||||||
|
b"\x00\x00" # BaseGlyphRecord[0].FirstLayerIndex (0)
|
||||||
|
b"\x00\x03" # BaseGlyphRecord[0].NumLayers (3)
|
||||||
|
b"\x00\x07" # LayerRecord[0].LayerGlyph (7)
|
||||||
|
b"\x00\x00" # LayerRecord[0].PaletteIndex (0)
|
||||||
|
b"\x00\x08" # LayerRecord[1].LayerGlyph (8)
|
||||||
|
b"\x00\x01" # LayerRecord[1].PaletteIndex (1)
|
||||||
|
b"\x00\t" # LayerRecord[2].LayerGlyph (9)
|
||||||
|
b"\x00\x02" # LayerRecord[3].PaletteIndex (2)
|
||||||
|
b"\x00\x00\x00\x01" # BaseGlyphV1Array.BaseGlyphCount (1)
|
||||||
|
b"\x00\n" # BaseGlyphV1Array.BaseGlyphV1Record[0].BaseGlyph (10)
|
||||||
|
b"\x00\x00\x00\n" # Offset to LayerV1Array
|
||||||
|
b"\x00\x00\x00\x03" # LayerV1Array.LayerCount (3)
|
||||||
|
b"\x00\x0b" # LayerV1Array.LayerV1Record[0].LayerGlyph (11)
|
||||||
|
b"\x00\x00\x00\x16" # Offset to Paint
|
||||||
|
b"\x00\x0c" # LayerV1Array.LayerV1Record[1].LayerGlyph (12)
|
||||||
|
b"\x00\x00\x00 " # Offset to Paint
|
||||||
|
b"\x00\r" # LayerV1Array.LayerV1Record[2].LayerGlyph (13)
|
||||||
|
b"\x00\x00\x00x" # Offset to Paint
|
||||||
|
b"\x00\x01" # Paint.Format (1)
|
||||||
|
b"\x00\x02" # Paint.Color.PaletteIndex (2)
|
||||||
|
b" \x00" # Paint.Color.Transparency.value (0.5)
|
||||||
|
b"\x00\x00\x00\x00" # Paint.Color.Transparency.varIdx (0)
|
||||||
|
b"\x00\x02" # Paint.Format (2)
|
||||||
|
b"\x00\x00\x00*" # Offset to ColorLine
|
||||||
|
b"\x00\x01" # Paint.p0.x.value (1)
|
||||||
|
b"\x00\x00\x00\x00" # Paint.p0.x.varIdx (0)
|
||||||
|
b"\x00\x02" # Paint.p0.y.value (2)
|
||||||
|
b"\x00\x00\x00\x00" # Paint.p0.y.varIdx (0)
|
||||||
|
b"\xff\xfd" # Paint.p1.x.value (-3)
|
||||||
|
b"\x00\x00\x00\x00" # Paint.p1.x.varIdx (0)
|
||||||
|
b"\xff\xfc" # Paint.p1.y.value (-4)
|
||||||
|
b"\x00\x00\x00\x00" # Paint.p1.y.varIdx (0)
|
||||||
|
b"\x00\x05" # Paint.p2.x.value (5)
|
||||||
|
b"\x00\x00\x00\x00" # Paint.p2.y.varIdx (0)
|
||||||
|
b"\x00\x06" # Paint.p2.y.value (5)
|
||||||
|
b"\x00\x00\x00\x00" # Paint.p2.y.varIdx (0)
|
||||||
|
b"\x00\x01" # ColorLine.Extend (1 or "repeat")
|
||||||
|
b"\x00\x03" # ColorLine.StopCount (3)
|
||||||
|
b"\x00\x00" # ColorLine.ColorStop[0].StopOffset.value (0.0)
|
||||||
|
b"\x00\x00\x00\x00" # ColorLine.ColorStop[0].StopOffset.varIdx (0)
|
||||||
|
b"\x00\x03" # ColorLine.ColorStop[0].Color.PaletteIndex (3)
|
||||||
|
b"\x00\x00" # ColorLine.ColorStop[0].Color.Transparency.value (0.0)
|
||||||
|
b"\x00\x00\x00\x00" # ColorLine.ColorStop[0].Color.Transparency.varIdx (0)
|
||||||
|
b" \x00" # ColorLine.ColorStop[1].StopOffset.value (0.5)
|
||||||
|
b"\x00\x00\x00\x00" # ColorLine.ColorStop[1].StopOffset.varIdx (0)
|
||||||
|
b"\x00\x04" # ColorLine.ColorStop[1].Color.PaletteIndex (4)
|
||||||
|
b"\x00\x00" # ColorLine.ColorStop[1].Color.Transparency.value (0.0)
|
||||||
|
b"\x00\x00\x00\x00" # ColorLine.ColorStop[1].Color.Transparency.varIdx (0)
|
||||||
|
b"@\x00" # ColorLine.ColorStop[2].StopOffset.value (1.0)
|
||||||
|
b"\x00\x00\x00\x00" # ColorLine.ColorStop[2].StopOffset.varIdx (0)
|
||||||
|
b"\x00\x05" # ColorLine.ColorStop[2].Color.PaletteIndex (5)
|
||||||
|
b"\x00\x00" # ColorLine.ColorStop[2].Color.Transparency.value (0.0)
|
||||||
|
b"\x00\x00\x00\x00" # ColorLine.ColorStop[2].Color.Transparency.varIdx (0)
|
||||||
|
b"\x00\x03" # Paint.Format (3)
|
||||||
|
b"\x00\x00\x00." # Offset to ColorLine
|
||||||
|
b"\x00\x07" # Paint.c0.x.value (7)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\x08" # Paint.c0.y.value (8)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\t" # Paint.c1.x.value (9)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\n" # Paint.c1.y.value (10)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\x0b" # Paint.r0.value (11)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\x0c" # Paint.r1.value (12)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\x00\x00N" # Offset to Affine2x2
|
||||||
|
b"\x00\x00" # ColorLine.Extend (0 or "pad")
|
||||||
|
b"\x00\x02" # ColorLine.StopCount (2)
|
||||||
|
b"\x00\x00" # ColorLine.ColorStop[0].StopOffset.value (0.0)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\x06" # ColorLine.ColorStop[0].Color.PaletteIndex (6)
|
||||||
|
b"\x00\x00"
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"@\x00" # ColorLine.ColorStop[1].StopOffset.value (1.0)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\x07" # ColorLine.ColorStop[1].Color.PaletteIndex (7)
|
||||||
|
b"\x19\x9a" # ColorLine.ColorStop[1].Color.Transparency.value (0.4)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\xff\xf3\x00\x00" # Affine2x2.xx.value (-13)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\x0e\x00\x00" # Affine2x2.xy.value (14)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\x00\x0f\x00\x00" # Affine2x2.yx.value (15)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
b"\xff\xef\x00\x00" # Affine2x2.yy.value (-17)
|
||||||
|
b"\x00\x00\x00\x00"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
COLR_V1_XML = [
|
||||||
|
'<Version value="1"/>',
|
||||||
|
"<!-- BaseGlyphRecordCount=1 -->",
|
||||||
|
"<BaseGlyphRecordArray>",
|
||||||
|
' <BaseGlyphRecord index="0">',
|
||||||
|
' <BaseGlyph value="glyph00006"/>',
|
||||||
|
' <FirstLayerIndex value="0"/>',
|
||||||
|
' <NumLayers value="3"/>',
|
||||||
|
" </BaseGlyphRecord>",
|
||||||
|
"</BaseGlyphRecordArray>",
|
||||||
|
"<LayerRecordArray>",
|
||||||
|
' <LayerRecord index="0">',
|
||||||
|
' <LayerGlyph value="glyph00007"/>',
|
||||||
|
' <PaletteIndex value="0"/>',
|
||||||
|
" </LayerRecord>",
|
||||||
|
' <LayerRecord index="1">',
|
||||||
|
' <LayerGlyph value="glyph00008"/>',
|
||||||
|
' <PaletteIndex value="1"/>',
|
||||||
|
" </LayerRecord>",
|
||||||
|
' <LayerRecord index="2">',
|
||||||
|
' <LayerGlyph value="glyph00009"/>',
|
||||||
|
' <PaletteIndex value="2"/>',
|
||||||
|
" </LayerRecord>",
|
||||||
|
"</LayerRecordArray>",
|
||||||
|
"<!-- LayerRecordCount=3 -->",
|
||||||
|
"<BaseGlyphV1Array>",
|
||||||
|
" <!-- BaseGlyphCount=1 -->",
|
||||||
|
' <BaseGlyphV1Record index="0">',
|
||||||
|
' <BaseGlyph value="glyph00010"/>',
|
||||||
|
" <LayerV1Array>",
|
||||||
|
" <!-- LayerCount=3 -->",
|
||||||
|
' <LayerV1Record index="0">',
|
||||||
|
' <LayerGlyph value="glyph00011"/>',
|
||||||
|
' <Paint Format="1">',
|
||||||
|
" <Color>",
|
||||||
|
' <PaletteIndex value="2"/>',
|
||||||
|
' <Transparency value="0.5"/>',
|
||||||
|
" </Color>",
|
||||||
|
" </Paint>",
|
||||||
|
" </LayerV1Record>",
|
||||||
|
' <LayerV1Record index="1">',
|
||||||
|
' <LayerGlyph value="glyph00012"/>',
|
||||||
|
' <Paint Format="2">',
|
||||||
|
" <ColorLine>",
|
||||||
|
' <Extend value="repeat"/>',
|
||||||
|
" <!-- StopCount=3 -->",
|
||||||
|
' <ColorStop index="0">',
|
||||||
|
' <StopOffset value="0.0"/>',
|
||||||
|
" <Color>",
|
||||||
|
' <PaletteIndex value="3"/>',
|
||||||
|
' <Transparency value="0.0"/>',
|
||||||
|
" </Color>",
|
||||||
|
" </ColorStop>",
|
||||||
|
' <ColorStop index="1">',
|
||||||
|
' <StopOffset value="0.5"/>',
|
||||||
|
" <Color>",
|
||||||
|
' <PaletteIndex value="4"/>',
|
||||||
|
' <Transparency value="0.0"/>',
|
||||||
|
" </Color>",
|
||||||
|
" </ColorStop>",
|
||||||
|
' <ColorStop index="2">',
|
||||||
|
' <StopOffset value="1.0"/>',
|
||||||
|
" <Color>",
|
||||||
|
' <PaletteIndex value="5"/>',
|
||||||
|
' <Transparency value="0.0"/>',
|
||||||
|
" </Color>",
|
||||||
|
" </ColorStop>",
|
||||||
|
" </ColorLine>",
|
||||||
|
" <p0>",
|
||||||
|
' <x value="1"/>',
|
||||||
|
' <y value="2"/>',
|
||||||
|
" </p0>",
|
||||||
|
" <p1>",
|
||||||
|
' <x value="-3"/>',
|
||||||
|
' <y value="-4"/>',
|
||||||
|
" </p1>",
|
||||||
|
" <p2>",
|
||||||
|
' <x value="5"/>',
|
||||||
|
' <y value="6"/>',
|
||||||
|
" </p2>",
|
||||||
|
" </Paint>",
|
||||||
|
" </LayerV1Record>",
|
||||||
|
' <LayerV1Record index="2">',
|
||||||
|
' <LayerGlyph value="glyph00013"/>',
|
||||||
|
' <Paint Format="3">',
|
||||||
|
" <ColorLine>",
|
||||||
|
' <Extend value="pad"/>',
|
||||||
|
" <!-- StopCount=2 -->",
|
||||||
|
' <ColorStop index="0">',
|
||||||
|
' <StopOffset value="0.0"/>',
|
||||||
|
" <Color>",
|
||||||
|
' <PaletteIndex value="6"/>',
|
||||||
|
' <Transparency value="0.0"/>',
|
||||||
|
" </Color>",
|
||||||
|
" </ColorStop>",
|
||||||
|
' <ColorStop index="1">',
|
||||||
|
' <StopOffset value="1.0"/>',
|
||||||
|
" <Color>",
|
||||||
|
' <PaletteIndex value="7"/>',
|
||||||
|
' <Transparency value="0.4"/>',
|
||||||
|
" </Color>",
|
||||||
|
" </ColorStop>",
|
||||||
|
" </ColorLine>",
|
||||||
|
" <c0>",
|
||||||
|
' <x value="7"/>',
|
||||||
|
' <y value="8"/>',
|
||||||
|
" </c0>",
|
||||||
|
" <c1>",
|
||||||
|
' <x value="9"/>',
|
||||||
|
' <y value="10"/>',
|
||||||
|
" </c1>",
|
||||||
|
' <r0 value="11"/>',
|
||||||
|
' <r1 value="12"/>',
|
||||||
|
" <Affine>",
|
||||||
|
' <xx value="-13.0"/>',
|
||||||
|
' <xy value="14.0"/>',
|
||||||
|
' <yx value="15.0"/>',
|
||||||
|
' <yy value="-17.0"/>',
|
||||||
|
" </Affine>",
|
||||||
|
" </Paint>",
|
||||||
|
" </LayerV1Record>",
|
||||||
|
" </LayerV1Array>",
|
||||||
|
" </BaseGlyphV1Record>",
|
||||||
|
"</BaseGlyphV1Array>",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class COLR_V1_Test(object):
|
||||||
|
def test_decompile_and_compile(self, font):
|
||||||
|
colr = table_C_O_L_R_()
|
||||||
|
colr.decompile(COLR_V1_DATA, font)
|
||||||
|
assert colr.compile(font) == COLR_V1_DATA
|
||||||
|
|
||||||
|
def test_decompile_and_dump_xml(self, font):
|
||||||
|
colr = table_C_O_L_R_()
|
||||||
|
colr.decompile(COLR_V1_DATA, font)
|
||||||
|
|
||||||
|
dump(colr, font)
|
||||||
|
assert getXML(colr.toXML, font) == COLR_V1_XML
|
||||||
|
|
||||||
|
def test_load_from_xml_and_compile(self, font):
|
||||||
|
colr = table_C_O_L_R_()
|
||||||
|
for name, attrs, content in parseXML(COLR_V1_XML):
|
||||||
|
colr.fromXML(name, attrs, content, font)
|
||||||
|
|
||||||
|
assert colr.compile(font) == COLR_V1_DATA
|
Loading…
x
Reference in New Issue
Block a user