Merge pull request #1822 from fonttools/otdata-colr

Define COLR using otData; add builders for COLRv1
This commit is contained in:
Cosimo Lupo 2020-04-16 17:28:44 +01:00 committed by GitHub
commit 17bff73866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1752 additions and 161 deletions

View File

@ -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)

View File

@ -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))

View File

@ -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

View File

@ -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,
}

View File

@ -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.'),
]),
]

View File

@ -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

View File

@ -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

View 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