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
|
||||
from typing import Dict, Iterable, List, Optional, Tuple, Union
|
||||
from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord, table_C_O_L_R_
|
||||
from fontTools.ttLib.tables.C_P_A_L_ import Color, table_C_P_A_L_
|
||||
from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e
|
||||
from functools import partial
|
||||
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
|
||||
from fontTools.ttLib.tables import C_O_L_R_
|
||||
from fontTools.ttLib.tables import C_P_A_L_
|
||||
from fontTools.ttLib.tables import _n_a_m_e
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.ttLib.tables.otTables import (
|
||||
ExtendMode,
|
||||
VariableValue,
|
||||
VariableFloat,
|
||||
VariableInt,
|
||||
)
|
||||
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.
|
||||
|
||||
Args:
|
||||
colorLayers: : map of base glyph names to lists of (layer glyph names,
|
||||
palette indices) tuples.
|
||||
colorGlyphs: map of base glyph names to lists of (layer glyph names,
|
||||
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:
|
||||
A new COLRv0 table.
|
||||
A new COLR table.
|
||||
"""
|
||||
colorLayerLists = {}
|
||||
for baseGlyphName, layers in colorLayers.items():
|
||||
colorLayerLists[baseGlyphName] = [
|
||||
LayerRecord(layerGlyphName, colorID) for layerGlyphName, colorID in layers
|
||||
]
|
||||
self = C_O_L_R_.table_C_O_L_R_()
|
||||
|
||||
colr = table_C_O_L_R_()
|
||||
colr.version = 0
|
||||
colr.ColorLayers = colorLayerLists
|
||||
return colr
|
||||
if varStore is not None and version == 0:
|
||||
raise ValueError("Can't add VarStore to COLRv0")
|
||||
|
||||
if version in (None, 0) and not varStore:
|
||||
# split color glyphs into v0 and v1 and encode separately
|
||||
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):
|
||||
@ -45,12 +162,12 @@ _OptionalLocalizedString = Union[None, str, Dict[str, str]]
|
||||
|
||||
|
||||
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]]:
|
||||
return [
|
||||
nameTable.addMultilingualName(l, mac=False)
|
||||
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
|
||||
else nameTable.addMultilingualName({"en": l}, mac=False)
|
||||
for l in labels
|
||||
@ -58,12 +175,12 @@ def buildPaletteLabels(
|
||||
|
||||
|
||||
def buildCPAL(
|
||||
palettes: List[List[Tuple[float, float, float, float]]],
|
||||
paletteTypes: Optional[List[ColorPaletteType]] = None,
|
||||
paletteLabels: Optional[List[_OptionalLocalizedString]] = None,
|
||||
paletteEntryLabels: Optional[List[_OptionalLocalizedString]] = None,
|
||||
nameTable: Optional[table__n_a_m_e] = None,
|
||||
) -> table_C_P_A_L_:
|
||||
palettes: Sequence[Sequence[Tuple[float, float, float, float]]],
|
||||
paletteTypes: Optional[Sequence[ColorPaletteType]] = None,
|
||||
paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
|
||||
paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
|
||||
nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None,
|
||||
) -> C_P_A_L_.table_C_P_A_L_:
|
||||
"""Build CPAL table from list of color palettes.
|
||||
|
||||
Args:
|
||||
@ -89,7 +206,7 @@ def buildCPAL(
|
||||
"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.palettes = []
|
||||
@ -106,7 +223,9 @@ def buildCPAL(
|
||||
)
|
||||
# input colors are RGBA, CPAL encodes them as BGRA
|
||||
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)
|
||||
|
||||
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]
|
||||
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 len(paletteLabels) != len(palettes):
|
||||
@ -128,7 +249,7 @@ def buildCPAL(
|
||||
)
|
||||
cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
|
||||
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 len(paletteEntryLabels) != cpal.numPaletteEntries:
|
||||
@ -139,9 +260,275 @@ def buildCPAL(
|
||||
cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
|
||||
else:
|
||||
cpal.paletteEntryLabels = [
|
||||
table_C_P_A_L_.NO_NAME_ID
|
||||
C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
|
||||
] * cpal.numPaletteEntries
|
||||
else:
|
||||
cpal.version = 0
|
||||
|
||||
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.textTools import safeEval
|
||||
from . import DefaultTable
|
||||
import struct
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def decompile(self, data, ttFont):
|
||||
self.getGlyphName = ttFont.getGlyphName # for use in get/set item functions, for access by GID
|
||||
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)
|
||||
|
||||
def _fromOTTable(self, table):
|
||||
self.version = 0
|
||||
self.ColorLayers = colorLayerLists = {}
|
||||
try:
|
||||
names = [glyphOrder[gid] for gid in gids]
|
||||
except IndexError:
|
||||
getGlyphName = self.getGlyphName
|
||||
names = map(getGlyphName, gids)
|
||||
layerRecords = table.LayerRecordArray.LayerRecord
|
||||
numLayerRecords = len(layerRecords)
|
||||
for baseRec in table.BaseGlyphRecordArray.BaseGlyphRecord:
|
||||
baseGlyph = baseRec.BaseGlyph
|
||||
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):
|
||||
colorLayerLists[name] = layerList
|
||||
def _toOTTable(self, ttFont):
|
||||
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):
|
||||
ordered = []
|
||||
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()
|
||||
from .otBase import OTTableWriter
|
||||
|
||||
glyphMap = []
|
||||
layerMap = []
|
||||
for (gid, glyphName, layers) in ordered:
|
||||
glyphMap.append(struct.pack(">HHH", gid, len(layerMap), len(layers)))
|
||||
for layer in layers:
|
||||
layerMap.append(struct.pack(">HH", ttFont.getGlyphID(layer.name), layer.colorID))
|
||||
if hasattr(self, "table"):
|
||||
table = self.table
|
||||
else:
|
||||
table = self._toOTTable(ttFont)
|
||||
|
||||
dataList = [struct.pack(">HHLLH", self.version, len(glyphMap), 14, 14+6*len(glyphMap), len(layerMap))]
|
||||
dataList.extend(glyphMap)
|
||||
dataList.extend(layerMap)
|
||||
data = bytesjoin(dataList)
|
||||
return data
|
||||
writer = OTTableWriter(tableTag=self.tableTag)
|
||||
table.compile(writer, ttFont)
|
||||
return writer.getAllData()
|
||||
|
||||
def toXML(self, writer, ttFont):
|
||||
writer.simpletag("version", value=self.version)
|
||||
writer.newline()
|
||||
ordered = []
|
||||
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()
|
||||
for entry in ordered:
|
||||
writer.begintag("ColorGlyph", name=entry[1])
|
||||
writer.newline()
|
||||
for layer in entry[2]:
|
||||
layer.toXML(writer, ttFont)
|
||||
writer.endtag("ColorGlyph")
|
||||
if hasattr(self, "table"):
|
||||
self.table.toXML2(writer, ttFont)
|
||||
else:
|
||||
writer.simpletag("version", value=self.version)
|
||||
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):
|
||||
if not hasattr(self, "ColorLayers"):
|
||||
self.ColorLayers = {}
|
||||
self.getGlyphName = ttFont.getGlyphName # for use in get/set item functions, for access by GID
|
||||
if name == "ColorGlyph":
|
||||
if name == "version": # old COLR v0 API
|
||||
setattr(self, name, safeEval(attrs["value"]))
|
||||
elif name == "ColorGlyph":
|
||||
if not hasattr(self, "ColorLayers"):
|
||||
self.ColorLayers = {}
|
||||
glyphName = attrs["name"]
|
||||
for element in content:
|
||||
if isinstance(element, basestring):
|
||||
@ -111,32 +110,31 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
|
||||
layer = LayerRecord()
|
||||
layer.fromXML(element[0], element[1], element[2], ttFont)
|
||||
layers.append (layer)
|
||||
self[glyphName] = layers
|
||||
elif "value" in attrs:
|
||||
setattr(self, name, safeEval(attrs["value"]))
|
||||
self.ColorLayers[glyphName] = layers
|
||||
else: # new COLR v1 API
|
||||
from . import otTables
|
||||
|
||||
def __getitem__(self, glyphSelector):
|
||||
if isinstance(glyphSelector, int):
|
||||
# its a gid, convert to glyph name
|
||||
glyphSelector = self.getGlyphName(glyphSelector)
|
||||
if not hasattr(self, "table"):
|
||||
tableClass = getattr(otTables, self.tableTag)
|
||||
self.table = tableClass()
|
||||
self.table.fromXML(name, attrs, content, ttFont)
|
||||
self.table.populateDefaults()
|
||||
|
||||
if glyphSelector not in self.ColorLayers:
|
||||
return None
|
||||
def __getitem__(self, glyphName):
|
||||
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):
|
||||
if isinstance(glyphSelector, int):
|
||||
# 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]
|
||||
def __delitem__(self, glyphName):
|
||||
del self.ColorLayers[glyphName]
|
||||
|
||||
class LayerRecord(object):
|
||||
|
||||
@ -151,8 +149,6 @@ class LayerRecord(object):
|
||||
def fromXML(self, eltname, attrs, content, ttFont):
|
||||
for (name, value) in attrs.items():
|
||||
if name == "name":
|
||||
if isinstance(value, int):
|
||||
value = ttFont.getGlyphName(value)
|
||||
setattr(self, name, value)
|
||||
else:
|
||||
setattr(self, name, safeEval(value))
|
||||
|
@ -652,6 +652,15 @@ class BaseTable(object):
|
||||
else:
|
||||
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'):
|
||||
writer.sortCoverageLast = 1
|
||||
|
@ -13,7 +13,9 @@ from .otBase import (CountReference, FormatSwitchingBaseTable,
|
||||
OTTableReader, OTTableWriter, ValueRecordFactory)
|
||||
from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
|
||||
ContextualMorphAction, LigatureMorphAction,
|
||||
InsertionMorphAction, MorxSubtable)
|
||||
InsertionMorphAction, MorxSubtable, VariableFloat,
|
||||
VariableInt, ExtendMode as _ExtendMode)
|
||||
from itertools import zip_longest
|
||||
from functools import partial
|
||||
import struct
|
||||
import logging
|
||||
@ -134,7 +136,22 @@ class BaseConverter(object):
|
||||
self.tableClass = tableClass
|
||||
self.isCount = name.endswith("Count") or name in ['DesignAxisRecordSize', 'ValueRecordSize']
|
||||
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):
|
||||
"""Read an array of values from the reader."""
|
||||
@ -185,15 +202,22 @@ class BaseConverter(object):
|
||||
|
||||
|
||||
class SimpleValue(BaseConverter):
|
||||
@staticmethod
|
||||
def toString(value):
|
||||
return value
|
||||
@staticmethod
|
||||
def fromString(value):
|
||||
return value
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
xmlWriter.simpletag(name, attrs + [("value", value)])
|
||||
xmlWriter.simpletag(name, attrs + [("value", self.toString(value))])
|
||||
xmlWriter.newline()
|
||||
def xmlRead(self, attrs, content, font):
|
||||
return attrs["value"]
|
||||
return self.fromString(attrs["value"])
|
||||
|
||||
class IntValue(SimpleValue):
|
||||
def xmlRead(self, attrs, content, font):
|
||||
return int(attrs["value"], 0)
|
||||
@staticmethod
|
||||
def fromString(value):
|
||||
return int(value, 0)
|
||||
|
||||
class Long(IntValue):
|
||||
staticSize = 4
|
||||
@ -210,9 +234,9 @@ class ULong(IntValue):
|
||||
writer.writeULong(value)
|
||||
|
||||
class Flags32(ULong):
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
xmlWriter.simpletag(name, attrs + [("value", "0x%08X" % value)])
|
||||
xmlWriter.newline()
|
||||
@staticmethod
|
||||
def toString(value):
|
||||
return "0x%08X" % value
|
||||
|
||||
class Short(IntValue):
|
||||
staticSize = 2
|
||||
@ -303,8 +327,9 @@ class NameID(UShort):
|
||||
|
||||
|
||||
class FloatValue(SimpleValue):
|
||||
def xmlRead(self, attrs, content, font):
|
||||
return float(attrs["value"])
|
||||
@staticmethod
|
||||
def fromString(value):
|
||||
return float(value)
|
||||
|
||||
class DeciPoints(FloatValue):
|
||||
staticSize = 2
|
||||
@ -320,11 +345,12 @@ class Fixed(FloatValue):
|
||||
return fi2fl(reader.readLong(), 16)
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeLong(fl2fi(value, 16))
|
||||
def xmlRead(self, attrs, content, font):
|
||||
return str2fl(attrs["value"], 16)
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
xmlWriter.simpletag(name, attrs + [("value", fl2str(value, 16))])
|
||||
xmlWriter.newline()
|
||||
@staticmethod
|
||||
def fromString(value):
|
||||
return str2fl(value, 16)
|
||||
@staticmethod
|
||||
def toString(value):
|
||||
return fl2str(value, 16)
|
||||
|
||||
class F2Dot14(FloatValue):
|
||||
staticSize = 2
|
||||
@ -332,13 +358,14 @@ class F2Dot14(FloatValue):
|
||||
return fi2fl(reader.readShort(), 14)
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeShort(fl2fi(value, 14))
|
||||
def xmlRead(self, attrs, content, font):
|
||||
return str2fl(attrs["value"], 14)
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
xmlWriter.simpletag(name, attrs + [("value", fl2str(value, 14))])
|
||||
xmlWriter.newline()
|
||||
@staticmethod
|
||||
def fromString(value):
|
||||
return str2fl(value, 14)
|
||||
@staticmethod
|
||||
def toString(value):
|
||||
return fl2str(value, 14)
|
||||
|
||||
class Version(BaseConverter):
|
||||
class Version(SimpleValue):
|
||||
staticSize = 4
|
||||
def read(self, reader, font, tableDict):
|
||||
value = reader.readLong()
|
||||
@ -348,16 +375,12 @@ class Version(BaseConverter):
|
||||
value = fi2ve(value)
|
||||
assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
|
||||
writer.writeLong(value)
|
||||
def xmlRead(self, attrs, content, font):
|
||||
value = attrs["value"]
|
||||
value = ve2fi(value)
|
||||
return value
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
value = fi2ve(value)
|
||||
value = "0x%08x" % value
|
||||
xmlWriter.simpletag(name, attrs + [("value", value)])
|
||||
xmlWriter.newline()
|
||||
|
||||
@staticmethod
|
||||
def fromString(value):
|
||||
return ve2fi(value)
|
||||
@staticmethod
|
||||
def toString(value):
|
||||
return "0x%08x" % value
|
||||
@staticmethod
|
||||
def fromFloat(v):
|
||||
return fl2fi(v, 16)
|
||||
@ -1583,6 +1606,111 @@ class LookupFlag(UShort):
|
||||
xmlWriter.comment(" ".join(flags))
|
||||
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 = {
|
||||
# type class
|
||||
"int8": Int8,
|
||||
@ -1609,6 +1737,7 @@ converterMapping = {
|
||||
"VarIdxMapValue": VarIdxMapValue,
|
||||
"VarDataValue": VarDataValue,
|
||||
"LookupFlag": LookupFlag,
|
||||
"ExtendMode": ExtendMode,
|
||||
|
||||
# AAT
|
||||
"CIDGlyphMap": CIDGlyphMap,
|
||||
@ -1624,4 +1753,10 @@ converterMapping = {
|
||||
"STXHeader": lambda C: partial(STXHeader, tableClass=C),
|
||||
"OffsetTo": lambda C: partial(Table, 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'),
|
||||
]),
|
||||
|
||||
#
|
||||
# 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
|
||||
converter objects from otConverters.py.
|
||||
"""
|
||||
from enum import IntEnum
|
||||
import itertools
|
||||
from collections import namedtuple
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.fixedTools import otRound
|
||||
from fontTools.misc.textTools import pad, safeEval
|
||||
from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference
|
||||
import logging
|
||||
@ -1152,6 +1156,100 @@ class LigatureSubst(FormatSwitchingBaseTable):
|
||||
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
|
||||
# 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
|
||||
|
@ -1,4 +1,5 @@
|
||||
from fontTools.ttLib import newTable
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.colorLib import builder
|
||||
from fontTools.colorLib.errors import ColorLibError
|
||||
import pytest
|
||||
@ -185,3 +186,552 @@ def test_buildCPAL_invalid_color():
|
||||
),
|
||||
):
|
||||
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