Merge pull request #2660 from fonttools/variable-colr
[varLib] add support for building variable COLR from set of master COLRv1 tables
This commit is contained in:
commit
2a07518b70
@ -23,6 +23,7 @@ from typing import (
|
||||
)
|
||||
from fontTools.misc.arrayTools import intRect
|
||||
from fontTools.misc.fixedTools import fixedToFloat
|
||||
from fontTools.misc.treeTools import build_n_ary_tree
|
||||
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
|
||||
@ -186,10 +187,12 @@ def populateCOLRv0(
|
||||
def buildCOLR(
|
||||
colorGlyphs: _ColorGlyphsDict,
|
||||
version: Optional[int] = None,
|
||||
*,
|
||||
glyphMap: Optional[Mapping[str, int]] = None,
|
||||
varStore: Optional[ot.VarStore] = None,
|
||||
varIndexMap: Optional[ot.DeltaSetIndexMap] = None,
|
||||
clipBoxes: Optional[Dict[str, _ClipBoxInput]] = None,
|
||||
allowLayerReuse: bool = True,
|
||||
) -> C_O_L_R_.table_C_O_L_R_:
|
||||
"""Build COLR table from color layers mapping.
|
||||
|
||||
@ -231,7 +234,11 @@ def buildCOLR(
|
||||
|
||||
populateCOLRv0(colr, colorGlyphsV0, glyphMap)
|
||||
|
||||
colr.LayerList, colr.BaseGlyphList = buildColrV1(colorGlyphsV1, glyphMap)
|
||||
colr.LayerList, colr.BaseGlyphList = buildColrV1(
|
||||
colorGlyphsV1,
|
||||
glyphMap,
|
||||
allowLayerReuse=allowLayerReuse,
|
||||
)
|
||||
|
||||
if version is None:
|
||||
version = 1 if (varStore or colorGlyphsV1) else 0
|
||||
@ -242,9 +249,6 @@ def buildCOLR(
|
||||
if version == 0:
|
||||
self.ColorLayers = self._decompileColorLayersV0(colr)
|
||||
else:
|
||||
clipBoxes = {
|
||||
name: clipBoxes[name] for name in clipBoxes or {} if name in colorGlyphsV1
|
||||
}
|
||||
colr.ClipList = buildClipList(clipBoxes) if clipBoxes else None
|
||||
colr.VarIndexMap = varIndexMap
|
||||
colr.VarStore = varStore
|
||||
@ -443,29 +447,16 @@ def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
|
||||
yield (lbound, ubound)
|
||||
|
||||
|
||||
class LayerListBuilder:
|
||||
layers: List[ot.Paint]
|
||||
class LayerReuseCache:
|
||||
reusePool: Mapping[Tuple[Any, ...], int]
|
||||
tuples: Mapping[int, Tuple[Any, ...]]
|
||||
keepAlive: List[ot.Paint] # we need id to remain valid
|
||||
|
||||
def __init__(self):
|
||||
self.layers = []
|
||||
self.reusePool = {}
|
||||
self.tuples = {}
|
||||
self.keepAlive = []
|
||||
|
||||
# We need to intercept construction of PaintColrLayers
|
||||
callbacks = _buildPaintCallbacks()
|
||||
callbacks[
|
||||
(
|
||||
BuildCallback.BEFORE_BUILD,
|
||||
ot.Paint,
|
||||
ot.PaintFormat.PaintColrLayers,
|
||||
)
|
||||
] = self._beforeBuildPaintColrLayers
|
||||
self.tableBuilder = TableBuilder(callbacks)
|
||||
|
||||
def _paint_tuple(self, paint: ot.Paint):
|
||||
# start simple, who even cares about cyclic graphs or interesting field types
|
||||
def _tuple_safe(value):
|
||||
@ -491,25 +482,7 @@ class LayerListBuilder:
|
||||
def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
|
||||
return tuple(self._paint_tuple(p) for p in paints)
|
||||
|
||||
# COLR layers is unusual in that it modifies shared state
|
||||
# so we need a callback into an object
|
||||
def _beforeBuildPaintColrLayers(self, dest, source):
|
||||
# Sketchy gymnastics: a sequence input will have dropped it's layers
|
||||
# into NumLayers; get it back
|
||||
if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
|
||||
layers = source["NumLayers"]
|
||||
else:
|
||||
layers = source["Layers"]
|
||||
|
||||
# Convert maps seqs or whatever into typed objects
|
||||
layers = [self.buildPaint(l) for l in layers]
|
||||
|
||||
# No reason to have a colr layers with just one entry
|
||||
if len(layers) == 1:
|
||||
return layers[0], {}
|
||||
|
||||
# Look for reuse, with preference to longer sequences
|
||||
# This may make the layer list smaller
|
||||
def try_reuse(self, layers: List[ot.Paint]) -> List[ot.Paint]:
|
||||
found_reuse = True
|
||||
while found_reuse:
|
||||
found_reuse = False
|
||||
@ -532,10 +505,63 @@ class LayerListBuilder:
|
||||
layers = layers[:lbound] + [new_slice] + layers[ubound:]
|
||||
found_reuse = True
|
||||
break
|
||||
return layers
|
||||
|
||||
def add(self, layers: List[ot.Paint], first_layer_index: int):
|
||||
for lbound, ubound in _reuse_ranges(len(layers)):
|
||||
self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
|
||||
lbound + first_layer_index
|
||||
)
|
||||
|
||||
|
||||
class LayerListBuilder:
|
||||
layers: List[ot.Paint]
|
||||
cache: LayerReuseCache
|
||||
allowLayerReuse: bool
|
||||
|
||||
def __init__(self, *, allowLayerReuse=True):
|
||||
self.layers = []
|
||||
if allowLayerReuse:
|
||||
self.cache = LayerReuseCache()
|
||||
else:
|
||||
self.cache = None
|
||||
|
||||
# We need to intercept construction of PaintColrLayers
|
||||
callbacks = _buildPaintCallbacks()
|
||||
callbacks[
|
||||
(
|
||||
BuildCallback.BEFORE_BUILD,
|
||||
ot.Paint,
|
||||
ot.PaintFormat.PaintColrLayers,
|
||||
)
|
||||
] = self._beforeBuildPaintColrLayers
|
||||
self.tableBuilder = TableBuilder(callbacks)
|
||||
|
||||
# COLR layers is unusual in that it modifies shared state
|
||||
# so we need a callback into an object
|
||||
def _beforeBuildPaintColrLayers(self, dest, source):
|
||||
# Sketchy gymnastics: a sequence input will have dropped it's layers
|
||||
# into NumLayers; get it back
|
||||
if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
|
||||
layers = source["NumLayers"]
|
||||
else:
|
||||
layers = source["Layers"]
|
||||
|
||||
# Convert maps seqs or whatever into typed objects
|
||||
layers = [self.buildPaint(l) for l in layers]
|
||||
|
||||
# No reason to have a colr layers with just one entry
|
||||
if len(layers) == 1:
|
||||
return layers[0], {}
|
||||
|
||||
if self.cache is not None:
|
||||
# Look for reuse, with preference to longer sequences
|
||||
# This may make the layer list smaller
|
||||
layers = self.cache.try_reuse(layers)
|
||||
|
||||
# The layer list is now final; if it's too big we need to tree it
|
||||
is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
|
||||
layers = _build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
|
||||
layers = build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
|
||||
|
||||
# We now have a tree of sequences with Paint leaves.
|
||||
# Convert the sequences into PaintColrLayers.
|
||||
@ -563,11 +589,8 @@ class LayerListBuilder:
|
||||
|
||||
# Register our parts for reuse provided we aren't a tree
|
||||
# If we are a tree the leaves registered for reuse and that will suffice
|
||||
if not is_tree:
|
||||
for lbound, ubound in _reuse_ranges(len(layers)):
|
||||
self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
|
||||
lbound + paint.FirstLayerIndex
|
||||
)
|
||||
if self.cache is not None and not is_tree:
|
||||
self.cache.add(layers, paint.FirstLayerIndex)
|
||||
|
||||
# we've fully built dest; empty source prevents generalized build from kicking in
|
||||
return paint, {}
|
||||
@ -603,6 +626,8 @@ def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
|
||||
def buildColrV1(
|
||||
colorGlyphs: _ColorGlyphsDict,
|
||||
glyphMap: Optional[Mapping[str, int]] = None,
|
||||
*,
|
||||
allowLayerReuse: bool = True,
|
||||
) -> Tuple[Optional[ot.LayerList], ot.BaseGlyphList]:
|
||||
if glyphMap is not None:
|
||||
colorGlyphItems = sorted(
|
||||
@ -613,7 +638,7 @@ def buildColrV1(
|
||||
|
||||
errors = {}
|
||||
baseGlyphs = []
|
||||
layerBuilder = LayerListBuilder()
|
||||
layerBuilder = LayerListBuilder(allowLayerReuse=allowLayerReuse)
|
||||
for baseGlyph, paint in colorGlyphItems:
|
||||
try:
|
||||
baseGlyphs.append(buildBaseGlyphPaintRecord(baseGlyph, layerBuilder, paint))
|
||||
@ -632,45 +657,3 @@ def buildColrV1(
|
||||
glyphs.BaseGlyphCount = len(baseGlyphs)
|
||||
glyphs.BaseGlyphPaintRecord = baseGlyphs
|
||||
return (layers, glyphs)
|
||||
|
||||
|
||||
def _build_n_ary_tree(leaves, n):
|
||||
"""Build N-ary tree from sequence of leaf nodes.
|
||||
|
||||
Return a list of lists where each non-leaf node is a list containing
|
||||
max n nodes.
|
||||
"""
|
||||
if not leaves:
|
||||
return []
|
||||
|
||||
assert n > 1
|
||||
|
||||
depth = ceil(log(len(leaves), n))
|
||||
|
||||
if depth <= 1:
|
||||
return list(leaves)
|
||||
|
||||
# Fully populate complete subtrees of root until we have enough leaves left
|
||||
root = []
|
||||
unassigned = None
|
||||
full_step = n ** (depth - 1)
|
||||
for i in range(0, len(leaves), full_step):
|
||||
subtree = leaves[i : i + full_step]
|
||||
if len(subtree) < full_step:
|
||||
unassigned = subtree
|
||||
break
|
||||
while len(subtree) > n:
|
||||
subtree = [subtree[k : k + n] for k in range(0, len(subtree), n)]
|
||||
root.append(subtree)
|
||||
|
||||
if unassigned:
|
||||
# Recurse to fill the last subtree, which is the only partially populated one
|
||||
subtree = _build_n_ary_tree(unassigned, n)
|
||||
if len(subtree) <= n - len(root):
|
||||
# replace last subtree with its children if they can still fit
|
||||
root.extend(subtree)
|
||||
else:
|
||||
root.append(subtree)
|
||||
assert len(root) <= n
|
||||
|
||||
return root
|
||||
|
@ -13,12 +13,12 @@ def unbuildColrV1(layerList, baseGlyphList):
|
||||
}
|
||||
|
||||
|
||||
def _flatten(lst):
|
||||
for el in lst:
|
||||
if isinstance(el, list):
|
||||
yield from _flatten(el)
|
||||
def _flatten_layers(lst):
|
||||
for paint in lst:
|
||||
if paint["Format"] == ot.PaintFormat.PaintColrLayers:
|
||||
yield from _flatten_layers(paint["Layers"])
|
||||
else:
|
||||
yield el
|
||||
yield paint
|
||||
|
||||
|
||||
class LayerListUnbuilder:
|
||||
@ -41,7 +41,7 @@ class LayerListUnbuilder:
|
||||
assert source["Format"] == ot.PaintFormat.PaintColrLayers
|
||||
|
||||
layers = list(
|
||||
_flatten(
|
||||
_flatten_layers(
|
||||
[
|
||||
self.unbuildPaint(childPaint)
|
||||
for childPaint in self.layers[
|
||||
|
@ -838,6 +838,7 @@ class FontBuilder(object):
|
||||
varStore=None,
|
||||
varIndexMap=None,
|
||||
clipBoxes=None,
|
||||
allowLayerReuse=True,
|
||||
):
|
||||
"""Build new COLR table using color layers dictionary.
|
||||
|
||||
@ -853,6 +854,7 @@ class FontBuilder(object):
|
||||
varStore=varStore,
|
||||
varIndexMap=varIndexMap,
|
||||
clipBoxes=clipBoxes,
|
||||
allowLayerReuse=allowLayerReuse,
|
||||
)
|
||||
|
||||
def setupCPAL(
|
||||
|
45
Lib/fontTools/misc/treeTools.py
Normal file
45
Lib/fontTools/misc/treeTools.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""Generic tools for working with trees."""
|
||||
|
||||
from math import ceil, log
|
||||
|
||||
|
||||
def build_n_ary_tree(leaves, n):
|
||||
"""Build N-ary tree from sequence of leaf nodes.
|
||||
|
||||
Return a list of lists where each non-leaf node is a list containing
|
||||
max n nodes.
|
||||
"""
|
||||
if not leaves:
|
||||
return []
|
||||
|
||||
assert n > 1
|
||||
|
||||
depth = ceil(log(len(leaves), n))
|
||||
|
||||
if depth <= 1:
|
||||
return list(leaves)
|
||||
|
||||
# Fully populate complete subtrees of root until we have enough leaves left
|
||||
root = []
|
||||
unassigned = None
|
||||
full_step = n ** (depth - 1)
|
||||
for i in range(0, len(leaves), full_step):
|
||||
subtree = leaves[i : i + full_step]
|
||||
if len(subtree) < full_step:
|
||||
unassigned = subtree
|
||||
break
|
||||
while len(subtree) > n:
|
||||
subtree = [subtree[k : k + n] for k in range(0, len(subtree), n)]
|
||||
root.append(subtree)
|
||||
|
||||
if unassigned:
|
||||
# Recurse to fill the last subtree, which is the only partially populated one
|
||||
subtree = build_n_ary_tree(unassigned, n)
|
||||
if len(subtree) <= n - len(root):
|
||||
# replace last subtree with its children if they can still fit
|
||||
root.extend(subtree)
|
||||
else:
|
||||
root.append(subtree)
|
||||
assert len(root) <= n
|
||||
|
||||
return root
|
@ -6,7 +6,8 @@ import sys
|
||||
import array
|
||||
import struct
|
||||
import logging
|
||||
from typing import Iterator, NamedTuple, Optional
|
||||
from functools import lru_cache
|
||||
from typing import Iterator, NamedTuple, Optional, Tuple
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -220,8 +221,8 @@ class BaseTTXConverter(DefaultTable):
|
||||
self.table.fromXML(name, attrs, content, font)
|
||||
self.table.populateDefaults()
|
||||
|
||||
def ensureDecompiled(self):
|
||||
self.table.ensureDecompiled(recurse=True)
|
||||
def ensureDecompiled(self, recurse=True):
|
||||
self.table.ensureDecompiled(recurse=recurse)
|
||||
|
||||
|
||||
# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928
|
||||
@ -864,6 +865,9 @@ class BaseTable(object):
|
||||
#elif not conv.isCount:
|
||||
# # Warn?
|
||||
# pass
|
||||
if hasattr(conv, "DEFAULT"):
|
||||
# OptionalValue converters (e.g. VarIndex)
|
||||
setattr(self, conv.name, conv.DEFAULT)
|
||||
|
||||
def decompile(self, reader, font):
|
||||
self.readFormat(reader)
|
||||
@ -1098,6 +1102,10 @@ class BaseTable(object):
|
||||
if isinstance(v, BaseTable)
|
||||
)
|
||||
|
||||
# instance (not @class)method for consistency with FormatSwitchingBaseTable
|
||||
def getVariableAttrs(self):
|
||||
return getVariableAttrs(self.__class__)
|
||||
|
||||
|
||||
class FormatSwitchingBaseTable(BaseTable):
|
||||
|
||||
@ -1132,6 +1140,9 @@ class FormatSwitchingBaseTable(BaseTable):
|
||||
def toXML(self, xmlWriter, font, attrs=None, name=None):
|
||||
BaseTable.toXML(self, xmlWriter, font, attrs, name)
|
||||
|
||||
def getVariableAttrs(self):
|
||||
return getVariableAttrs(self.__class__, self.Format)
|
||||
|
||||
|
||||
class UInt8FormatSwitchingBaseTable(FormatSwitchingBaseTable):
|
||||
def readFormat(self, reader):
|
||||
@ -1153,6 +1164,33 @@ def getFormatSwitchingBaseTableClass(formatType):
|
||||
raise TypeError(f"Unsupported format type: {formatType!r}")
|
||||
|
||||
|
||||
# memoize since these are parsed from otData.py, thus stay constant
|
||||
@lru_cache()
|
||||
def getVariableAttrs(cls: BaseTable, fmt: Optional[int] = None) -> Tuple[str]:
|
||||
"""Return sequence of variable table field names (can be empty).
|
||||
|
||||
Attributes are deemed "variable" when their otData.py's description contain
|
||||
'VarIndexBase + {offset}', e.g. COLRv1 PaintVar* tables.
|
||||
"""
|
||||
if not issubclass(cls, BaseTable):
|
||||
raise TypeError(cls)
|
||||
if issubclass(cls, FormatSwitchingBaseTable):
|
||||
if fmt is None:
|
||||
raise TypeError(f"'fmt' is required for format-switching {cls.__name__}")
|
||||
converters = cls.convertersByName[fmt]
|
||||
else:
|
||||
converters = cls.convertersByName
|
||||
# assume if no 'VarIndexBase' field is present, table has no variable fields
|
||||
if "VarIndexBase" not in converters:
|
||||
return ()
|
||||
varAttrs = {}
|
||||
for name, conv in converters.items():
|
||||
offset = conv.getVarIndexOffset()
|
||||
if offset is not None:
|
||||
varAttrs[name] = offset
|
||||
return tuple(sorted(varAttrs, key=varAttrs.__getitem__))
|
||||
|
||||
|
||||
#
|
||||
# Support for ValueRecords
|
||||
#
|
||||
|
@ -15,10 +15,13 @@ from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
|
||||
ContextualMorphAction, LigatureMorphAction,
|
||||
InsertionMorphAction, MorxSubtable,
|
||||
ExtendMode as _ExtendMode,
|
||||
CompositeMode as _CompositeMode)
|
||||
CompositeMode as _CompositeMode,
|
||||
NO_VARIATION_INDEX)
|
||||
from itertools import zip_longest
|
||||
from functools import partial
|
||||
import re
|
||||
import struct
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
|
||||
@ -60,7 +63,7 @@ def buildConverters(tableSpec, tableNamespace):
|
||||
else:
|
||||
converterClass = eval(tp, tableNamespace, converterMapping)
|
||||
|
||||
conv = converterClass(name, repeat, aux)
|
||||
conv = converterClass(name, repeat, aux, description=descr)
|
||||
|
||||
if conv.tableClass:
|
||||
# A "template" such as OffsetTo(AType) knowss the table class already
|
||||
@ -136,7 +139,7 @@ class BaseConverter(object):
|
||||
"""Base class for converter objects. Apart from the constructor, this
|
||||
is an abstract class."""
|
||||
|
||||
def __init__(self, name, repeat, aux, tableClass=None):
|
||||
def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
|
||||
self.name = name
|
||||
self.repeat = repeat
|
||||
self.aux = aux
|
||||
@ -159,6 +162,7 @@ class BaseConverter(object):
|
||||
"BaseGlyphRecordCount",
|
||||
"LayerRecordCount",
|
||||
]
|
||||
self.description = description
|
||||
|
||||
def readArray(self, reader, font, tableDict, count):
|
||||
"""Read an array of values from the reader."""
|
||||
@ -211,6 +215,15 @@ class BaseConverter(object):
|
||||
"""Write a value to XML."""
|
||||
raise NotImplementedError(self)
|
||||
|
||||
varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)")
|
||||
|
||||
def getVarIndexOffset(self) -> Optional[int]:
|
||||
"""If description has `VarIndexBase + {offset}`, return the offset else None."""
|
||||
m = self.varIndexBasePlusOffsetRE.search(self.description)
|
||||
if not m:
|
||||
return None
|
||||
return int(m.group(1))
|
||||
|
||||
|
||||
class SimpleValue(BaseConverter):
|
||||
@staticmethod
|
||||
@ -270,7 +283,7 @@ class Flags32(ULong):
|
||||
return "0x%08X" % value
|
||||
|
||||
class VarIndex(OptionalValue, ULong):
|
||||
DEFAULT = 0xFFFFFFFF
|
||||
DEFAULT = NO_VARIATION_INDEX
|
||||
|
||||
class Short(IntValue):
|
||||
staticSize = 2
|
||||
@ -402,40 +415,50 @@ class DeciPoints(FloatValue):
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeUShort(round(value * 10))
|
||||
|
||||
class Fixed(FloatValue):
|
||||
staticSize = 4
|
||||
class BaseFixedValue(FloatValue):
|
||||
staticSize = NotImplemented
|
||||
precisionBits = NotImplemented
|
||||
readerMethod = NotImplemented
|
||||
writerMethod = NotImplemented
|
||||
def read(self, reader, font, tableDict):
|
||||
return fi2fl(reader.readLong(), 16)
|
||||
return self.fromInt(getattr(reader, self.readerMethod)())
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeLong(fl2fi(value, 16))
|
||||
@staticmethod
|
||||
def fromString(value):
|
||||
return str2fl(value, 16)
|
||||
@staticmethod
|
||||
def toString(value):
|
||||
return fl2str(value, 16)
|
||||
getattr(writer, self.writerMethod)(self.toInt(value))
|
||||
@classmethod
|
||||
def fromInt(cls, value):
|
||||
return fi2fl(value, cls.precisionBits)
|
||||
@classmethod
|
||||
def toInt(cls, value):
|
||||
return fl2fi(value, cls.precisionBits)
|
||||
@classmethod
|
||||
def fromString(cls, value):
|
||||
return str2fl(value, cls.precisionBits)
|
||||
@classmethod
|
||||
def toString(cls, value):
|
||||
return fl2str(value, cls.precisionBits)
|
||||
|
||||
class F2Dot14(FloatValue):
|
||||
class Fixed(BaseFixedValue):
|
||||
staticSize = 4
|
||||
precisionBits = 16
|
||||
readerMethod = "readLong"
|
||||
writerMethod = "writeLong"
|
||||
|
||||
class F2Dot14(BaseFixedValue):
|
||||
staticSize = 2
|
||||
def read(self, reader, font, tableDict):
|
||||
return fi2fl(reader.readShort(), 14)
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeShort(fl2fi(value, 14))
|
||||
@staticmethod
|
||||
def fromString(value):
|
||||
return str2fl(value, 14)
|
||||
@staticmethod
|
||||
def toString(value):
|
||||
return fl2str(value, 14)
|
||||
precisionBits = 14
|
||||
readerMethod = "readShort"
|
||||
writerMethod = "writeShort"
|
||||
|
||||
class Angle(F2Dot14):
|
||||
# angles are specified in degrees, and encoded as F2Dot14 fractions of half
|
||||
# circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc.
|
||||
factor = 1.0/(1<<14) * 180 # 0.010986328125
|
||||
def read(self, reader, font, tableDict):
|
||||
return super().read(reader, font, tableDict) * 180
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
super().write(writer, font, tableDict, value / 180, repeatIndex=repeatIndex)
|
||||
@classmethod
|
||||
def fromInt(cls, value):
|
||||
return super().fromInt(value) * 180
|
||||
@classmethod
|
||||
def toInt(cls, value):
|
||||
return super().toInt(value / 180)
|
||||
@classmethod
|
||||
def fromString(cls, value):
|
||||
# quantize to nearest multiples of minimum fixed-precision angle
|
||||
@ -686,8 +709,10 @@ class FeatureParams(Table):
|
||||
|
||||
class ValueFormat(IntValue):
|
||||
staticSize = 2
|
||||
def __init__(self, name, repeat, aux, tableClass=None):
|
||||
BaseConverter.__init__(self, name, repeat, aux, tableClass)
|
||||
def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
|
||||
BaseConverter.__init__(
|
||||
self, name, repeat, aux, tableClass, description=description
|
||||
)
|
||||
self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1")
|
||||
def read(self, reader, font, tableDict):
|
||||
format = reader.readUShort()
|
||||
@ -720,8 +745,10 @@ class ValueRecord(ValueFormat):
|
||||
class AATLookup(BaseConverter):
|
||||
BIN_SEARCH_HEADER_SIZE = 10
|
||||
|
||||
def __init__(self, name, repeat, aux, tableClass):
|
||||
BaseConverter.__init__(self, name, repeat, aux, tableClass)
|
||||
def __init__(self, name, repeat, aux, tableClass, *, description=""):
|
||||
BaseConverter.__init__(
|
||||
self, name, repeat, aux, tableClass, description=description
|
||||
)
|
||||
if issubclass(self.tableClass, SimpleValue):
|
||||
self.converter = self.tableClass(name='Value', repeat=None, aux=None)
|
||||
else:
|
||||
@ -1019,8 +1046,10 @@ class MorxSubtableConverter(BaseConverter):
|
||||
val: key for key, val in _PROCESSING_ORDERS.items()
|
||||
}
|
||||
|
||||
def __init__(self, name, repeat, aux):
|
||||
BaseConverter.__init__(self, name, repeat, aux)
|
||||
def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
|
||||
BaseConverter.__init__(
|
||||
self, name, repeat, aux, tableClass, description=description
|
||||
)
|
||||
|
||||
def _setTextDirectionFromCoverageFlags(self, flags, subtable):
|
||||
if (flags & 0x20) != 0:
|
||||
@ -1140,8 +1169,10 @@ class MorxSubtableConverter(BaseConverter):
|
||||
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader
|
||||
# TODO: Untangle the implementation of the various lookup-specific formats.
|
||||
class STXHeader(BaseConverter):
|
||||
def __init__(self, name, repeat, aux, tableClass):
|
||||
BaseConverter.__init__(self, name, repeat, aux, tableClass)
|
||||
def __init__(self, name, repeat, aux, tableClass, *, description=""):
|
||||
BaseConverter.__init__(
|
||||
self, name, repeat, aux, tableClass, description=description
|
||||
)
|
||||
assert issubclass(self.tableClass, AATAction)
|
||||
self.classLookup = AATLookup("GlyphClasses", None, None, UShort)
|
||||
if issubclass(self.tableClass, ContextualMorphAction):
|
||||
|
@ -1623,10 +1623,10 @@ otData = [
|
||||
|
||||
('ClipBoxFormat2', [
|
||||
('uint8', 'Format', None, None, 'Format for variable ClipBox: set to 2.'),
|
||||
('int16', 'xMin', None, None, 'Minimum x of clip box.'),
|
||||
('int16', 'yMin', None, None, 'Minimum y of clip box.'),
|
||||
('int16', 'xMax', None, None, 'Maximum x of clip box.'),
|
||||
('int16', 'yMax', None, None, 'Maximum y of clip box.'),
|
||||
('int16', 'xMin', None, None, 'Minimum x of clip box. VarIndexBase + 0.'),
|
||||
('int16', 'yMin', None, None, 'Minimum y of clip box. VarIndexBase + 1.'),
|
||||
('int16', 'xMax', None, None, 'Maximum x of clip box. VarIndexBase + 2.'),
|
||||
('int16', 'yMax', None, None, 'Maximum y of clip box. VarIndexBase + 3.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1648,12 +1648,12 @@ otData = [
|
||||
('Fixed', 'dy', None, None, 'Translation in y direction'),
|
||||
]),
|
||||
('VarAffine2x3', [
|
||||
('Fixed', 'xx', None, None, 'x-part of x basis vector'),
|
||||
('Fixed', 'yx', None, None, 'y-part of x basis vector'),
|
||||
('Fixed', 'xy', None, None, 'x-part of y basis vector'),
|
||||
('Fixed', 'yy', None, None, 'y-part of y basis vector'),
|
||||
('Fixed', 'dx', None, None, 'Translation in x direction'),
|
||||
('Fixed', 'dy', None, None, 'Translation in y direction'),
|
||||
('Fixed', 'xx', None, None, 'x-part of x basis vector. VarIndexBase + 0.'),
|
||||
('Fixed', 'yx', None, None, 'y-part of x basis vector. VarIndexBase + 1.'),
|
||||
('Fixed', 'xy', None, None, 'x-part of y basis vector. VarIndexBase + 2.'),
|
||||
('Fixed', 'yy', None, None, 'y-part of y basis vector. VarIndexBase + 3.'),
|
||||
('Fixed', 'dx', None, None, 'Translation in x direction. VarIndexBase + 4.'),
|
||||
('Fixed', 'dy', None, None, 'Translation in y direction. VarIndexBase + 5.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1663,9 +1663,9 @@ otData = [
|
||||
('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
|
||||
]),
|
||||
('VarColorStop', [
|
||||
('F2Dot14', 'StopOffset', None, None, 'VarIndexBase + 0'),
|
||||
('F2Dot14', 'StopOffset', None, None, 'VarIndexBase + 0.'),
|
||||
('uint16', 'PaletteIndex', None, None, 'Index for a CPAL palette entry.'),
|
||||
('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 1'),
|
||||
('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 1.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1697,7 +1697,7 @@ otData = [
|
||||
('PaintFormat3', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 3'),
|
||||
('uint16', 'PaletteIndex', None, None, 'Index for a CPAL palette entry.'),
|
||||
('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 0'),
|
||||
('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 0.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1716,12 +1716,12 @@ otData = [
|
||||
('PaintFormat5', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
|
||||
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarLinearGradient table) to VarColorLine subtable.'),
|
||||
('int16', 'x0', None, None, ''),
|
||||
('int16', 'y0', None, None, ''),
|
||||
('int16', 'x1', None, None, ''),
|
||||
('int16', 'y1', None, None, ''),
|
||||
('int16', 'x2', None, None, ''),
|
||||
('int16', 'y2', None, None, ''),
|
||||
('int16', 'x0', None, None, 'VarIndexBase + 0.'),
|
||||
('int16', 'y0', None, None, 'VarIndexBase + 1.'),
|
||||
('int16', 'x1', None, None, 'VarIndexBase + 2.'),
|
||||
('int16', 'y1', None, None, 'VarIndexBase + 3.'),
|
||||
('int16', 'x2', None, None, 'VarIndexBase + 4.'),
|
||||
('int16', 'y2', None, None, 'VarIndexBase + 5.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1740,12 +1740,12 @@ otData = [
|
||||
('PaintFormat7', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 7'),
|
||||
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarRadialGradient table) to VarColorLine subtable.'),
|
||||
('int16', 'x0', None, None, ''),
|
||||
('int16', 'y0', None, None, ''),
|
||||
('uint16', 'r0', None, None, ''),
|
||||
('int16', 'x1', None, None, ''),
|
||||
('int16', 'y1', None, None, ''),
|
||||
('uint16', 'r1', None, None, ''),
|
||||
('int16', 'x0', None, None, 'VarIndexBase + 0.'),
|
||||
('int16', 'y0', None, None, 'VarIndexBase + 1.'),
|
||||
('uint16', 'r0', None, None, 'VarIndexBase + 2.'),
|
||||
('int16', 'x1', None, None, 'VarIndexBase + 3.'),
|
||||
('int16', 'y1', None, None, 'VarIndexBase + 4.'),
|
||||
('uint16', 'r1', None, None, 'VarIndexBase + 5.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1762,10 +1762,10 @@ otData = [
|
||||
('PaintFormat9', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 9'),
|
||||
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarSweepGradient table) to VarColorLine subtable.'),
|
||||
('int16', 'centerX', None, None, 'Center x coordinate.'),
|
||||
('int16', 'centerY', None, None, 'Center y coordinate.'),
|
||||
('Angle', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
|
||||
('Angle', 'endAngle', None, None, 'End of the angular range of the gradient.'),
|
||||
('int16', 'centerX', None, None, 'Center x coordinate. VarIndexBase + 0.'),
|
||||
('int16', 'centerY', None, None, 'Center y coordinate. VarIndexBase + 1.'),
|
||||
('Angle', 'startAngle', None, None, 'Start of the angular range of the gradient. VarIndexBase + 2.'),
|
||||
('Angle', 'endAngle', None, None, 'End of the angular range of the gradient. VarIndexBase + 3.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1806,8 +1806,8 @@ otData = [
|
||||
('PaintFormat15', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 15'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarTranslate table) to Paint subtable.'),
|
||||
('int16', 'dx', None, None, 'Translation in x direction.'),
|
||||
('int16', 'dy', None, None, 'Translation in y direction.'),
|
||||
('int16', 'dx', None, None, 'Translation in x direction. VarIndexBase + 0.'),
|
||||
('int16', 'dy', None, None, 'Translation in y direction. VarIndexBase + 1.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1822,8 +1822,8 @@ otData = [
|
||||
('PaintFormat17', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 17'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScale table) to Paint subtable.'),
|
||||
('F2Dot14', 'scaleX', None, None, ''),
|
||||
('F2Dot14', 'scaleY', None, None, ''),
|
||||
('F2Dot14', 'scaleX', None, None, 'VarIndexBase + 0.'),
|
||||
('F2Dot14', 'scaleY', None, None, 'VarIndexBase + 1.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1840,10 +1840,10 @@ otData = [
|
||||
('PaintFormat19', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 19'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScaleAroundCenter table) to Paint subtable.'),
|
||||
('F2Dot14', 'scaleX', None, None, ''),
|
||||
('F2Dot14', 'scaleY', None, None, ''),
|
||||
('int16', 'centerX', None, None, ''),
|
||||
('int16', 'centerY', None, None, ''),
|
||||
('F2Dot14', 'scaleX', None, None, 'VarIndexBase + 0.'),
|
||||
('F2Dot14', 'scaleY', None, None, 'VarIndexBase + 1.'),
|
||||
('int16', 'centerX', None, None, 'VarIndexBase + 2.'),
|
||||
('int16', 'centerY', None, None, 'VarIndexBase + 3.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1857,7 +1857,7 @@ otData = [
|
||||
('PaintFormat21', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 21'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScaleUniform table) to Paint subtable.'),
|
||||
('F2Dot14', 'scale', None, None, ''),
|
||||
('F2Dot14', 'scale', None, None, 'VarIndexBase + 0.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1873,9 +1873,9 @@ otData = [
|
||||
('PaintFormat23', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 23'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScaleUniformAroundCenter table) to Paint subtable.'),
|
||||
('F2Dot14', 'scale', None, None, ''),
|
||||
('int16', 'centerX', None, None, ''),
|
||||
('int16', 'centerY', None, None, ''),
|
||||
('F2Dot14', 'scale', None, None, 'VarIndexBase + 0'),
|
||||
('int16', 'centerX', None, None, 'VarIndexBase + 1'),
|
||||
('int16', 'centerY', None, None, 'VarIndexBase + 2'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1889,7 +1889,7 @@ otData = [
|
||||
('PaintFormat25', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 25'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarRotate table) to Paint subtable.'),
|
||||
('Angle', 'angle', None, None, ''),
|
||||
('Angle', 'angle', None, None, 'VarIndexBase + 0.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1905,9 +1905,9 @@ otData = [
|
||||
('PaintFormat27', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 27'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarRotateAroundCenter table) to Paint subtable.'),
|
||||
('Angle', 'angle', None, None, ''),
|
||||
('int16', 'centerX', None, None, ''),
|
||||
('int16', 'centerY', None, None, ''),
|
||||
('Angle', 'angle', None, None, 'VarIndexBase + 0.'),
|
||||
('int16', 'centerX', None, None, 'VarIndexBase + 1.'),
|
||||
('int16', 'centerY', None, None, 'VarIndexBase + 2.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1922,8 +1922,8 @@ otData = [
|
||||
('PaintFormat29', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 29'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarSkew table) to Paint subtable.'),
|
||||
('Angle', 'xSkewAngle', None, None, ''),
|
||||
('Angle', 'ySkewAngle', None, None, ''),
|
||||
('Angle', 'xSkewAngle', None, None, 'VarIndexBase + 0.'),
|
||||
('Angle', 'ySkewAngle', None, None, 'VarIndexBase + 1.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
@ -1940,10 +1940,10 @@ otData = [
|
||||
('PaintFormat31', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 31'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarSkewAroundCenter table) to Paint subtable.'),
|
||||
('Angle', 'xSkewAngle', None, None, ''),
|
||||
('Angle', 'ySkewAngle', None, None, ''),
|
||||
('int16', 'centerX', None, None, ''),
|
||||
('int16', 'centerY', None, None, ''),
|
||||
('Angle', 'xSkewAngle', None, None, 'VarIndexBase + 0.'),
|
||||
('Angle', 'ySkewAngle', None, None, 'VarIndexBase + 1.'),
|
||||
('int16', 'centerX', None, None, 'VarIndexBase + 2.'),
|
||||
('int16', 'centerY', None, None, 'VarIndexBase + 3.'),
|
||||
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
|
||||
]),
|
||||
|
||||
|
@ -600,6 +600,11 @@ class Coverage(FormatSwitchingBaseTable):
|
||||
glyphs.append(attrs["value"])
|
||||
|
||||
|
||||
# The special 0xFFFFFFFF delta-set index is used to indicate that there
|
||||
# is no variation data in the ItemVariationStore for a given variable field
|
||||
NO_VARIATION_INDEX = 0xFFFFFFFF
|
||||
|
||||
|
||||
class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
|
||||
|
||||
def populateDefaults(self, propagator=None):
|
||||
@ -647,12 +652,19 @@ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
|
||||
return rawTable
|
||||
|
||||
def toXML2(self, xmlWriter, font):
|
||||
# Make xml dump less verbose, by omitting no-op entries like:
|
||||
# <Map index="..." outer="65535" inner="65535"/>
|
||||
xmlWriter.comment(
|
||||
"Omitted values default to 0xFFFF/0xFFFF (no variations)"
|
||||
)
|
||||
xmlWriter.newline()
|
||||
for i, value in enumerate(getattr(self, "mapping", [])):
|
||||
attrs = (
|
||||
('index', i),
|
||||
('outer', value >> 16),
|
||||
('inner', value & 0xFFFF),
|
||||
)
|
||||
attrs = [('index', i)]
|
||||
if value != NO_VARIATION_INDEX:
|
||||
attrs.extend([
|
||||
('outer', value >> 16),
|
||||
('inner', value & 0xFFFF),
|
||||
])
|
||||
xmlWriter.simpletag("Map", attrs)
|
||||
xmlWriter.newline()
|
||||
|
||||
@ -661,8 +673,8 @@ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
|
||||
if mapping is None:
|
||||
self.mapping = mapping = []
|
||||
index = safeEval(attrs['index'])
|
||||
outer = safeEval(attrs['outer'])
|
||||
inner = safeEval(attrs['inner'])
|
||||
outer = safeEval(attrs.get('outer', '0xFFFF'))
|
||||
inner = safeEval(attrs.get('inner', '0xFFFF'))
|
||||
assert inner <= 0xFFFF
|
||||
mapping.insert(index, (outer << 16) | inner)
|
||||
|
||||
@ -1257,7 +1269,19 @@ class BaseGlyphList(BaseTable):
|
||||
return self.__dict__.copy()
|
||||
|
||||
|
||||
class ClipBoxFormat(IntEnum):
|
||||
Static = 1
|
||||
Variable = 2
|
||||
|
||||
def is_variable(self):
|
||||
return self is self.Variable
|
||||
|
||||
def as_variable(self):
|
||||
return self.Variable
|
||||
|
||||
|
||||
class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
|
||||
formatEnum = ClipBoxFormat
|
||||
|
||||
def as_tuple(self):
|
||||
return tuple(getattr(self, conv.name) for conv in self.getConverters())
|
||||
@ -1492,12 +1516,24 @@ class PaintFormat(IntEnum):
|
||||
PaintVarSkewAroundCenter = 31
|
||||
PaintComposite = 32
|
||||
|
||||
def is_variable(self):
|
||||
return self.name.startswith("PaintVar")
|
||||
|
||||
def as_variable(self):
|
||||
if self.is_variable():
|
||||
return self
|
||||
try:
|
||||
return PaintFormat.__members__[f"PaintVar{self.name[5:]}"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
class Paint(getFormatSwitchingBaseTableClass("uint8")):
|
||||
formatEnum = PaintFormat
|
||||
|
||||
def getFormatName(self):
|
||||
try:
|
||||
return PaintFormat(self.Format).name
|
||||
return self.formatEnum(self.Format).name
|
||||
except ValueError:
|
||||
raise NotImplementedError(f"Unknown Paint format: {self.Format}")
|
||||
|
||||
@ -1962,6 +1998,14 @@ def _buildClasses():
|
||||
cls.DontShare = True
|
||||
namespace[name] = cls
|
||||
|
||||
# link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.)
|
||||
for name, _ in otData:
|
||||
if name.startswith("Var") and len(name) > 3 and name[3:] in namespace:
|
||||
varType = namespace[name]
|
||||
noVarType = namespace[name[3:]]
|
||||
varType.NoVarType = noVarType
|
||||
noVarType.VarType = varType
|
||||
|
||||
for base, alts in _equivalents.items():
|
||||
base = namespace[base]
|
||||
for alt in alts:
|
||||
|
137
Lib/fontTools/ttLib/tables/otTraverse.py
Normal file
137
Lib/fontTools/ttLib/tables/otTraverse.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""Methods for traversing trees of otData-driven OpenType tables."""
|
||||
from collections import deque
|
||||
from typing import Callable, Deque, Iterable, List, Optional, Tuple
|
||||
from .otBase import BaseTable
|
||||
|
||||
|
||||
__all__ = [
|
||||
"bfs_base_table",
|
||||
"dfs_base_table",
|
||||
"SubTablePath",
|
||||
]
|
||||
|
||||
|
||||
class SubTablePath(Tuple[BaseTable.SubTableEntry, ...]):
|
||||
|
||||
def __str__(self) -> str:
|
||||
path_parts = []
|
||||
for entry in self:
|
||||
path_part = entry.name
|
||||
if entry.index is not None:
|
||||
path_part += f"[{entry.index}]"
|
||||
path_parts.append(path_part)
|
||||
return ".".join(path_parts)
|
||||
|
||||
|
||||
# Given f(current frontier, new entries) add new entries to frontier
|
||||
AddToFrontierFn = Callable[[Deque[SubTablePath], List[SubTablePath]], None]
|
||||
|
||||
|
||||
def dfs_base_table(
|
||||
root: BaseTable,
|
||||
root_accessor: Optional[str] = None,
|
||||
skip_root: bool = False,
|
||||
predicate: Optional[Callable[[SubTablePath], bool]] = None,
|
||||
) -> Iterable[SubTablePath]:
|
||||
"""Depth-first search tree of BaseTables.
|
||||
|
||||
Args:
|
||||
root (BaseTable): the root of the tree.
|
||||
root_accessor (Optional[str]): attribute name for the root table, if any (mostly
|
||||
useful for debugging).
|
||||
skip_root (Optional[bool]): if True, the root itself is not visited, only its
|
||||
children.
|
||||
predicate (Optional[Callable[[SubTablePath], bool]]): function to filter out
|
||||
paths. If True, the path is yielded and its subtables are added to the
|
||||
queue. If False, the path is skipped and its subtables are not traversed.
|
||||
|
||||
Yields:
|
||||
SubTablePath: tuples of BaseTable.SubTableEntry(name, table, index) namedtuples
|
||||
for each of the nodes in the tree. The last entry in a path is the current
|
||||
subtable, whereas preceding ones refer to its parent tables all the way up to
|
||||
the root.
|
||||
"""
|
||||
yield from _traverse_ot_data(
|
||||
root,
|
||||
root_accessor,
|
||||
skip_root,
|
||||
predicate,
|
||||
lambda frontier, new: frontier.extendleft(reversed(new)),
|
||||
)
|
||||
|
||||
|
||||
def bfs_base_table(
|
||||
root: BaseTable,
|
||||
root_accessor: Optional[str] = None,
|
||||
skip_root: bool = False,
|
||||
predicate: Optional[Callable[[SubTablePath], bool]] = None,
|
||||
) -> Iterable[SubTablePath]:
|
||||
"""Breadth-first search tree of BaseTables.
|
||||
|
||||
Args:
|
||||
root (BaseTable): the root of the tree.
|
||||
root_accessor (Optional[str]): attribute name for the root table, if any (mostly
|
||||
useful for debugging).
|
||||
skip_root (Optional[bool]): if True, the root itself is not visited, only its
|
||||
children.
|
||||
predicate (Optional[Callable[[SubTablePath], bool]]): function to filter out
|
||||
paths. If True, the path is yielded and its subtables are added to the
|
||||
queue. If False, the path is skipped and its subtables are not traversed.
|
||||
|
||||
Yields:
|
||||
SubTablePath: tuples of BaseTable.SubTableEntry(name, table, index) namedtuples
|
||||
for each of the nodes in the tree. The last entry in a path is the current
|
||||
subtable, whereas preceding ones refer to its parent tables all the way up to
|
||||
the root.
|
||||
"""
|
||||
yield from _traverse_ot_data(
|
||||
root,
|
||||
root_accessor,
|
||||
skip_root,
|
||||
predicate,
|
||||
lambda frontier, new: frontier.extend(new),
|
||||
)
|
||||
|
||||
|
||||
def _traverse_ot_data(
|
||||
root: BaseTable,
|
||||
root_accessor: Optional[str],
|
||||
skip_root: bool,
|
||||
predicate: Optional[Callable[[SubTablePath], bool]],
|
||||
add_to_frontier_fn: AddToFrontierFn,
|
||||
) -> Iterable[SubTablePath]:
|
||||
# no visited because general otData cannot cycle (forward-offset only)
|
||||
if root_accessor is None:
|
||||
root_accessor = type(root).__name__
|
||||
|
||||
if predicate is None:
|
||||
|
||||
def predicate(path):
|
||||
return True
|
||||
|
||||
frontier: Deque[SubTablePath] = deque()
|
||||
|
||||
root_entry = BaseTable.SubTableEntry(root_accessor, root)
|
||||
if not skip_root:
|
||||
frontier.append((root_entry,))
|
||||
else:
|
||||
add_to_frontier_fn(
|
||||
frontier,
|
||||
[(root_entry, subtable_entry) for subtable_entry in root.iterSubTables()],
|
||||
)
|
||||
|
||||
while frontier:
|
||||
# path is (value, attr_name) tuples. attr_name is attr of parent to get value
|
||||
path = frontier.popleft()
|
||||
current = path[-1].value
|
||||
|
||||
if not predicate(path):
|
||||
continue
|
||||
|
||||
yield SubTablePath(path)
|
||||
|
||||
new_entries = [
|
||||
path + (subtable_entry,) for subtable_entry in current.iterSubTables()
|
||||
]
|
||||
|
||||
add_to_frontier_fn(frontier, new_entries)
|
@ -30,13 +30,15 @@ from fontTools.ttLib.tables.TupleVariation import TupleVariation
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.ttLib.tables.otBase import OTTableWriter
|
||||
from fontTools.varLib import builder, models, varStore
|
||||
from fontTools.varLib.merger import VariationMerger
|
||||
from fontTools.varLib.merger import VariationMerger, COLRVariationMerger
|
||||
from fontTools.varLib.mvar import MVAR_ENTRIES
|
||||
from fontTools.varLib.iup import iup_delta_optimize
|
||||
from fontTools.varLib.featureVars import addFeatureVariations
|
||||
from fontTools.designspaceLib import DesignSpaceDocument, InstanceDescriptor
|
||||
from fontTools.designspaceLib.split import splitInterpolable, splitVariableFonts
|
||||
from fontTools.varLib.stat import buildVFStatTable
|
||||
from fontTools.colorLib.builder import buildColrV1
|
||||
from fontTools.colorLib.unbuilder import unbuildColrV1
|
||||
from functools import partial
|
||||
from collections import OrderedDict, namedtuple
|
||||
import os.path
|
||||
@ -711,6 +713,19 @@ def _add_CFF2(varFont, model, master_fonts):
|
||||
merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
|
||||
|
||||
|
||||
def _add_COLR(font, model, master_fonts, axisTags, colr_layer_reuse=True):
|
||||
merger = COLRVariationMerger(model, axisTags, font, allowLayerReuse=colr_layer_reuse)
|
||||
merger.mergeTables(font, master_fonts)
|
||||
store = merger.store_builder.finish()
|
||||
|
||||
colr = font["COLR"].table
|
||||
if store:
|
||||
mapping = store.optimize()
|
||||
colr.VarStore = store
|
||||
varIdxes = [mapping[v] for v in merger.varIdxes]
|
||||
colr.VarIndexMap = builder.buildDeltaSetIndexMap(varIdxes)
|
||||
|
||||
|
||||
def load_designspace(designspace):
|
||||
# TODO: remove this and always assume 'designspace' is a DesignSpaceDocument,
|
||||
# never a file path, as that's already handled by caller
|
||||
@ -865,7 +880,14 @@ def set_default_weight_width_slant(font, location):
|
||||
font["post"].italicAngle = italicAngle
|
||||
|
||||
|
||||
def build_many(designspace: DesignSpaceDocument, master_finder=lambda s:s, exclude=[], optimize=True, skip_vf=lambda vf_name: False):
|
||||
def build_many(
|
||||
designspace: DesignSpaceDocument,
|
||||
master_finder=lambda s:s,
|
||||
exclude=[],
|
||||
optimize=True,
|
||||
skip_vf=lambda vf_name: False,
|
||||
colr_layer_reuse=True,
|
||||
):
|
||||
"""
|
||||
Build variable fonts from a designspace file, version 5 which can define
|
||||
several VFs, or version 4 which has implicitly one VF covering the whole doc.
|
||||
@ -897,7 +919,13 @@ def build_many(designspace: DesignSpaceDocument, master_finder=lambda s:s, exclu
|
||||
res[name] = vf
|
||||
return res
|
||||
|
||||
def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
|
||||
def build(
|
||||
designspace,
|
||||
master_finder=lambda s:s,
|
||||
exclude=[],
|
||||
optimize=True,
|
||||
colr_layer_reuse=True,
|
||||
):
|
||||
"""
|
||||
Build variation font from a designspace file.
|
||||
|
||||
@ -975,6 +1003,8 @@ def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
|
||||
post.formatType = 2.0
|
||||
post.extraNames = []
|
||||
post.mapping = {}
|
||||
if 'COLR' not in exclude and 'COLR' in vf and vf['COLR'].version > 0:
|
||||
_add_COLR(vf, model, master_fonts, axisTags, colr_layer_reuse)
|
||||
|
||||
set_default_weight_width_slant(
|
||||
vf, location={axis.axisTag: axis.defaultValue for axis in vf["fvar"].axes}
|
||||
@ -1082,6 +1112,12 @@ def main(args=None):
|
||||
action='store_false',
|
||||
help='do not perform IUP optimization'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-colr-layer-reuse',
|
||||
dest='colr_layer_reuse',
|
||||
action='store_false',
|
||||
help='do not rebuild variable COLR table to optimize COLR layer reuse',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--master-finder',
|
||||
default='master_ttf_interpolatable/{stem}.ttf',
|
||||
@ -1120,7 +1156,8 @@ def main(args=None):
|
||||
designspace_filename,
|
||||
finder,
|
||||
exclude=options.exclude,
|
||||
optimize=options.optimize
|
||||
optimize=options.optimize,
|
||||
colr_layer_reuse=options.colr_layer_reuse,
|
||||
)
|
||||
|
||||
outfile = options.outfile
|
||||
|
@ -42,7 +42,10 @@ class VarLibMergeError(VarLibError):
|
||||
index = [x == self.cause["expected"] for x in self.cause["got"]].index(
|
||||
False
|
||||
)
|
||||
return index, self._master_name(index)
|
||||
master_name = self._master_name(index)
|
||||
if "location" in self.cause:
|
||||
master_name = f"{master_name} ({self.cause['location']})"
|
||||
return index, master_name
|
||||
return None, None
|
||||
|
||||
@property
|
||||
@ -50,7 +53,7 @@ class VarLibMergeError(VarLibError):
|
||||
if "expected" in self.cause and "got" in self.cause:
|
||||
offender_index, offender = self.offender
|
||||
got = self.cause["got"][offender_index]
|
||||
return f"Expected to see {self.stack[0]}=={self.cause['expected']}, instead saw {got}\n"
|
||||
return f"Expected to see {self.stack[0]}=={self.cause['expected']!r}, instead saw {got!r}\n"
|
||||
return ""
|
||||
|
||||
def __str__(self):
|
||||
@ -138,9 +141,17 @@ class InconsistentExtensions(VarLibMergeError):
|
||||
class UnsupportedFormat(VarLibMergeError):
|
||||
"""an OpenType subtable (%s) had a format I didn't expect"""
|
||||
|
||||
def __init__(self, merger=None, **kwargs):
|
||||
super().__init__(merger, **kwargs)
|
||||
if not self.stack:
|
||||
self.stack = [".Format"]
|
||||
|
||||
@property
|
||||
def reason(self):
|
||||
return self.__doc__ % self.cause["subtable"]
|
||||
s = self.__doc__ % self.cause["subtable"]
|
||||
if "value" in self.cause:
|
||||
s += f" ({self.cause['value']!r})"
|
||||
return s
|
||||
|
||||
|
||||
class InconsistentFormats(UnsupportedFormat):
|
||||
|
@ -3,15 +3,20 @@ Merge OpenType Layout tables (GDEF / GPOS / GSUB).
|
||||
"""
|
||||
import os
|
||||
import copy
|
||||
import enum
|
||||
from operator import ior
|
||||
import logging
|
||||
from fontTools.colorLib.builder import MAX_PAINT_COLR_LAYER_COUNT, LayerReuseCache
|
||||
from fontTools.misc import classifyTools
|
||||
from fontTools.misc.roundTools import otRound
|
||||
from fontTools.misc.treeTools import build_n_ary_tree
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.ttLib.tables import otBase as otBase
|
||||
from fontTools.ttLib.tables.otConverters import BaseFixedValue
|
||||
from fontTools.ttLib.tables.otTraverse import dfs_base_table
|
||||
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
||||
from fontTools.varLib import builder, models, varStore
|
||||
from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo
|
||||
from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo, subList
|
||||
from fontTools.varLib.varStore import VarStoreInstancer
|
||||
from functools import reduce
|
||||
from fontTools.otlLib.builder import buildSinglePos
|
||||
@ -39,13 +44,15 @@ class Merger(object):
|
||||
|
||||
def __init__(self, font=None):
|
||||
self.font = font
|
||||
# mergeTables populates this from the parent's master ttfs
|
||||
self.ttfs = None
|
||||
|
||||
@classmethod
|
||||
def merger(celf, clazzes, attrs=(None,)):
|
||||
assert celf != Merger, 'Subclass Merger instead.'
|
||||
if 'mergers' not in celf.__dict__:
|
||||
celf.mergers = {}
|
||||
if type(clazzes) == type:
|
||||
if type(clazzes) in (type, enum.EnumMeta):
|
||||
clazzes = (clazzes,)
|
||||
if type(attrs) == str:
|
||||
attrs = (attrs,)
|
||||
@ -81,10 +88,10 @@ class Merger(object):
|
||||
|
||||
def mergeObjects(self, out, lst, exclude=()):
|
||||
if hasattr(out, "ensureDecompiled"):
|
||||
out.ensureDecompiled()
|
||||
out.ensureDecompiled(recurse=False)
|
||||
for item in lst:
|
||||
if hasattr(item, "ensureDecompiled"):
|
||||
item.ensureDecompiled()
|
||||
item.ensureDecompiled(recurse=False)
|
||||
keys = sorted(vars(out).keys())
|
||||
if not all(keys == sorted(vars(v).keys()) for v in lst):
|
||||
raise KeysDiffer(self, expected=keys,
|
||||
@ -122,6 +129,11 @@ class Merger(object):
|
||||
mergerFunc = self.mergersFor(out).get(None, None)
|
||||
if mergerFunc is not None:
|
||||
mergerFunc(self, out, lst)
|
||||
elif isinstance(out, enum.Enum):
|
||||
# need to special-case Enums as have __dict__ but are not regular 'objects',
|
||||
# otherwise mergeObjects/mergeThings get trapped in a RecursionError
|
||||
if not allEqualTo(out, lst):
|
||||
raise ShouldBeConstant(self, expected=out, got=lst)
|
||||
elif hasattr(out, '__dict__'):
|
||||
self.mergeObjects(out, lst)
|
||||
elif isinstance(out, list):
|
||||
@ -134,9 +146,8 @@ class Merger(object):
|
||||
for tag in tableTags:
|
||||
if tag not in font: continue
|
||||
try:
|
||||
self.ttfs = [m for m in master_ttfs if tag in m]
|
||||
self.mergeThings(font[tag], [m[tag] if tag in m else None
|
||||
for m in master_ttfs])
|
||||
self.ttfs = master_ttfs
|
||||
self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs])
|
||||
except VarLibMergeError as e:
|
||||
e.stack.append(tag)
|
||||
raise
|
||||
@ -1036,11 +1047,19 @@ class VariationMerger(AligningMerger):
|
||||
|
||||
def mergeThings(self, out, lst):
|
||||
masterModel = None
|
||||
origTTFs = None
|
||||
if None in lst:
|
||||
if allNone(lst):
|
||||
if out is not None:
|
||||
raise FoundANone(self, got=lst)
|
||||
return
|
||||
|
||||
# temporarily subset the list of master ttfs to the ones for which
|
||||
# master values are not None
|
||||
origTTFs = self.ttfs
|
||||
if self.ttfs:
|
||||
self.ttfs = subList([v is not None for v in lst], self.ttfs)
|
||||
|
||||
masterModel = self.model
|
||||
model, lst = masterModel.getSubModel(lst)
|
||||
self.setModel(model)
|
||||
@ -1049,6 +1068,8 @@ class VariationMerger(AligningMerger):
|
||||
|
||||
if masterModel:
|
||||
self.setModel(masterModel)
|
||||
if origTTFs:
|
||||
self.ttfs = origTTFs
|
||||
|
||||
|
||||
def buildVarDevTable(store_builder, master_values):
|
||||
@ -1099,3 +1120,408 @@ def merge(merger, self, lst):
|
||||
setattr(self, name, value)
|
||||
if deviceTable:
|
||||
setattr(self, tableName, deviceTable)
|
||||
|
||||
|
||||
class COLRVariationMerger(VariationMerger):
|
||||
"""A specialized VariationMerger that takes multiple master fonts containing
|
||||
COLRv1 tables, and builds a variable COLR font.
|
||||
|
||||
COLR tables are special in that variable subtables can be associated with
|
||||
multiple delta-set indices (via VarIndexBase).
|
||||
They also contain tables that must change their type (not simply the Format)
|
||||
as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes
|
||||
care of that too.
|
||||
"""
|
||||
|
||||
def __init__(self, model, axisTags, font, allowLayerReuse=True):
|
||||
VariationMerger.__init__(self, model, axisTags, font)
|
||||
# maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase
|
||||
# between variable tables with same varIdxes.
|
||||
self.varIndexCache = {}
|
||||
# flat list of all the varIdxes generated while merging
|
||||
self.varIdxes = []
|
||||
# set of id()s of the subtables that contain variations after merging
|
||||
# and need to be upgraded to the associated VarType.
|
||||
self.varTableIds = set()
|
||||
# we keep these around for rebuilding a LayerList while merging PaintColrLayers
|
||||
self.layers = []
|
||||
self.layerReuseCache = None
|
||||
if allowLayerReuse:
|
||||
self.layerReuseCache = LayerReuseCache()
|
||||
# flag to ensure BaseGlyphList is fully merged before LayerList gets processed
|
||||
self._doneBaseGlyphs = False
|
||||
|
||||
def mergeTables(self, font, master_ttfs, tableTags=("COLR",)):
|
||||
if "COLR" in tableTags and "COLR" in font:
|
||||
# The merger modifies the destination COLR table in-place. If this contains
|
||||
# multiple PaintColrLayers referencing the same layers from LayerList, it's
|
||||
# a problem because we may risk modifying the same paint more than once, or
|
||||
# worse, fail while attempting to do that.
|
||||
# We don't know whether the master COLR table was built with layer reuse
|
||||
# disabled, thus to be safe we rebuild its LayerList so that it contains only
|
||||
# unique layers referenced from non-overlapping PaintColrLayers throughout
|
||||
# the base paint graphs.
|
||||
self.expandPaintColrLayers(font["COLR"].table)
|
||||
VariationMerger.mergeTables(self, font, master_ttfs, tableTags)
|
||||
|
||||
def checkFormatEnum(self, out, lst, validate=lambda _: True):
|
||||
fmt = out.Format
|
||||
formatEnum = out.formatEnum
|
||||
ok = False
|
||||
try:
|
||||
fmt = formatEnum(fmt)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
ok = validate(fmt)
|
||||
if not ok:
|
||||
raise UnsupportedFormat(
|
||||
self, subtable=type(out).__name__, value=fmt
|
||||
)
|
||||
expected = fmt
|
||||
got = []
|
||||
for v in lst:
|
||||
fmt = getattr(v, "Format", None)
|
||||
try:
|
||||
fmt = formatEnum(fmt)
|
||||
except ValueError:
|
||||
pass
|
||||
got.append(fmt)
|
||||
if not allEqualTo(expected, got):
|
||||
raise InconsistentFormats(
|
||||
self,
|
||||
subtable=type(out).__name__,
|
||||
expected=expected,
|
||||
got=got,
|
||||
)
|
||||
return expected
|
||||
|
||||
def mergeSparseDict(self, out, lst):
|
||||
for k in out.keys():
|
||||
try:
|
||||
self.mergeThings(out[k], [v.get(k) for v in lst])
|
||||
except VarLibMergeError as e:
|
||||
e.stack.append(f"[{k!r}]")
|
||||
raise
|
||||
|
||||
def mergeAttrs(self, out, lst, attrs):
|
||||
for attr in attrs:
|
||||
value = getattr(out, attr)
|
||||
values = [getattr(item, attr) for item in lst]
|
||||
try:
|
||||
self.mergeThings(value, values)
|
||||
except VarLibMergeError as e:
|
||||
e.stack.append(f".{attr}")
|
||||
raise
|
||||
|
||||
def storeMastersForAttr(self, out, lst, attr):
|
||||
master_values = [getattr(item, attr) for item in lst]
|
||||
|
||||
# VarStore treats deltas for fixed-size floats as integers, so we
|
||||
# must convert master values to int before storing them in the builder
|
||||
# then back to float.
|
||||
is_fixed_size_float = False
|
||||
conv = out.getConverterByName(attr)
|
||||
if isinstance(conv, BaseFixedValue):
|
||||
is_fixed_size_float = True
|
||||
master_values = [conv.toInt(v) for v in master_values]
|
||||
|
||||
baseValue = master_values[0]
|
||||
varIdx = ot.NO_VARIATION_INDEX
|
||||
if not allEqual(master_values):
|
||||
baseValue, varIdx = self.store_builder.storeMasters(master_values)
|
||||
|
||||
if is_fixed_size_float:
|
||||
baseValue = conv.fromInt(baseValue)
|
||||
|
||||
return baseValue, varIdx
|
||||
|
||||
def storeVariationIndices(self, varIdxes) -> int:
|
||||
# try to reuse an existing VarIndexBase for the same varIdxes, or else
|
||||
# create a new one
|
||||
key = tuple(varIdxes)
|
||||
varIndexBase = self.varIndexCache.get(key)
|
||||
|
||||
if varIndexBase is None:
|
||||
# scan for a full match anywhere in the self.varIdxes
|
||||
for i in range(len(self.varIdxes) - len(varIdxes) + 1):
|
||||
if self.varIdxes[i:i+len(varIdxes)] == varIdxes:
|
||||
self.varIndexCache[key] = varIndexBase = i
|
||||
break
|
||||
|
||||
if varIndexBase is None:
|
||||
# try find a partial match at the end of the self.varIdxes
|
||||
for n in range(len(varIdxes)-1, 0, -1):
|
||||
if self.varIdxes[-n:] == varIdxes[:n]:
|
||||
varIndexBase = len(self.varIdxes) - n
|
||||
self.varIndexCache[key] = varIndexBase
|
||||
self.varIdxes.extend(varIdxes[n:])
|
||||
break
|
||||
|
||||
if varIndexBase is None:
|
||||
# no match found, append at the end
|
||||
self.varIndexCache[key] = varIndexBase = len(self.varIdxes)
|
||||
self.varIdxes.extend(varIdxes)
|
||||
|
||||
return varIndexBase
|
||||
|
||||
def mergeVariableAttrs(self, out, lst, attrs) -> int:
|
||||
varIndexBase = ot.NO_VARIATION_INDEX
|
||||
varIdxes = []
|
||||
for attr in attrs:
|
||||
baseValue, varIdx = self.storeMastersForAttr(out, lst, attr)
|
||||
setattr(out, attr, baseValue)
|
||||
varIdxes.append(varIdx)
|
||||
|
||||
if any(v != ot.NO_VARIATION_INDEX for v in varIdxes):
|
||||
varIndexBase = self.storeVariationIndices(varIdxes)
|
||||
|
||||
return varIndexBase
|
||||
|
||||
@classmethod
|
||||
def convertSubTablesToVarType(cls, table):
|
||||
for path in dfs_base_table(
|
||||
table,
|
||||
skip_root=True,
|
||||
predicate=lambda path: (
|
||||
getattr(type(path[-1].value), "VarType", None) is not None
|
||||
)
|
||||
):
|
||||
st = path[-1]
|
||||
subTable = st.value
|
||||
varType = type(subTable).VarType
|
||||
newSubTable = varType()
|
||||
newSubTable.__dict__.update(subTable.__dict__)
|
||||
newSubTable.populateDefaults()
|
||||
parent = path[-2].value
|
||||
if st.index is not None:
|
||||
getattr(parent, st.name)[st.index] = newSubTable
|
||||
else:
|
||||
setattr(parent, st.name, newSubTable)
|
||||
|
||||
@staticmethod
|
||||
def expandPaintColrLayers(colr):
|
||||
"""Rebuild LayerList without PaintColrLayers reuse.
|
||||
|
||||
Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph
|
||||
which are irrelevant for this); any layers referenced via PaintColrLayers are
|
||||
collected into a new LayerList and duplicated when reuse is detected, to ensure
|
||||
that all paints are distinct objects at the end of the process.
|
||||
PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap
|
||||
is left. Also, any consecutively nested PaintColrLayers are flattened.
|
||||
The COLR table's LayerList is replaced with the new unique layers.
|
||||
A side effect is also that any layer from the old LayerList which is not
|
||||
referenced by any PaintColrLayers is dropped.
|
||||
"""
|
||||
if not colr.LayerList:
|
||||
# if no LayerList, there's nothing to expand
|
||||
return
|
||||
uniqueLayerIDs = set()
|
||||
newLayerList = []
|
||||
for rec in colr.BaseGlyphList.BaseGlyphPaintRecord:
|
||||
frontier = [rec.Paint]
|
||||
while frontier:
|
||||
paint = frontier.pop()
|
||||
if paint.Format == ot.PaintFormat.PaintColrGlyph:
|
||||
# don't traverse these, we treat them as constant for merging
|
||||
continue
|
||||
elif paint.Format == ot.PaintFormat.PaintColrLayers:
|
||||
# de-treeify any nested PaintColrLayers, append unique copies to
|
||||
# the new layer list and update PaintColrLayers index/count
|
||||
children = list(_flatten_layers(paint, colr))
|
||||
first_layer_index = len(newLayerList)
|
||||
for layer in children:
|
||||
if id(layer) in uniqueLayerIDs:
|
||||
layer = copy.deepcopy(layer)
|
||||
assert id(layer) not in uniqueLayerIDs
|
||||
newLayerList.append(layer)
|
||||
uniqueLayerIDs.add(id(layer))
|
||||
paint.FirstLayerIndex = first_layer_index
|
||||
paint.NumLayers = len(children)
|
||||
else:
|
||||
children = paint.getChildren(colr)
|
||||
frontier.extend(reversed(children))
|
||||
# sanity check all the new layers are distinct objects
|
||||
assert len(newLayerList) == len(uniqueLayerIDs)
|
||||
colr.LayerList.Paint = newLayerList
|
||||
colr.LayerList.LayerCount = len(newLayerList)
|
||||
|
||||
|
||||
@COLRVariationMerger.merger(ot.BaseGlyphList)
|
||||
def merge(merger, self, lst):
|
||||
# ignore BaseGlyphCount, allow sparse glyph sets across masters
|
||||
out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord}
|
||||
masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst]
|
||||
|
||||
for i, g in enumerate(out.keys()):
|
||||
try:
|
||||
# missing base glyphs don't participate in the merge
|
||||
merger.mergeThings(out[g], [v.get(g) for v in masters])
|
||||
except VarLibMergeError as e:
|
||||
e.stack.append(f".BaseGlyphPaintRecord[{i}]")
|
||||
e.cause["location"] = f"base glyph {g!r}"
|
||||
raise
|
||||
|
||||
merger._doneBaseGlyphs = True
|
||||
|
||||
|
||||
@COLRVariationMerger.merger(ot.LayerList)
|
||||
def merge(merger, self, lst):
|
||||
# nothing to merge for LayerList, assuming we have already merged all PaintColrLayers
|
||||
# found while traversing the paint graphs rooted at BaseGlyphPaintRecords.
|
||||
assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList"
|
||||
# Simply flush the final list of layers and go home.
|
||||
self.LayerCount = len(merger.layers)
|
||||
self.Paint = merger.layers
|
||||
|
||||
|
||||
def _flatten_layers(root, colr):
|
||||
assert root.Format == ot.PaintFormat.PaintColrLayers
|
||||
for paint in root.getChildren(colr):
|
||||
if paint.Format == ot.PaintFormat.PaintColrLayers:
|
||||
yield from _flatten_layers(paint, colr)
|
||||
else:
|
||||
yield paint
|
||||
|
||||
|
||||
def _merge_PaintColrLayers(self, out, lst):
|
||||
# we only enforce that the (flat) number of layers is the same across all masters
|
||||
# but we allow FirstLayerIndex to differ to acommodate for sparse glyph sets.
|
||||
|
||||
out_layers = list(_flatten_layers(out, self.font["COLR"].table))
|
||||
|
||||
# sanity check ttfs are subset to current values (see VariationMerger.mergeThings)
|
||||
# before matching each master PaintColrLayers to its respective COLR by position
|
||||
assert len(self.ttfs) == len(lst)
|
||||
master_layerses = [
|
||||
list(_flatten_layers(lst[i], self.ttfs[i]["COLR"].table))
|
||||
for i in range(len(lst))
|
||||
]
|
||||
|
||||
try:
|
||||
self.mergeLists(out_layers, master_layerses)
|
||||
except VarLibMergeError as e:
|
||||
# NOTE: This attribute doesn't actually exist in PaintColrLayers but it's
|
||||
# handy to have it in the stack trace for debugging.
|
||||
e.stack.append(".Layers")
|
||||
raise
|
||||
|
||||
# following block is very similar to LayerListBuilder._beforeBuildPaintColrLayers
|
||||
# but I couldn't find a nice way to share the code between the two...
|
||||
|
||||
if self.layerReuseCache is not None:
|
||||
# successful reuse can make the list smaller
|
||||
out_layers = self.layerReuseCache.try_reuse(out_layers)
|
||||
|
||||
# if the list is still too big we need to tree-fy it
|
||||
is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT
|
||||
out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT)
|
||||
|
||||
# We now have a tree of sequences with Paint leaves.
|
||||
# Convert the sequences into PaintColrLayers.
|
||||
def listToColrLayers(paint):
|
||||
if isinstance(paint, list):
|
||||
layers = [listToColrLayers(l) for l in paint]
|
||||
paint = ot.Paint()
|
||||
paint.Format = int(ot.PaintFormat.PaintColrLayers)
|
||||
paint.NumLayers = len(layers)
|
||||
paint.FirstLayerIndex = len(self.layers)
|
||||
self.layers.exend(layers)
|
||||
if self.layerReuseCache is not None:
|
||||
self.layerReuseCache.add(layers, paint.FirstLayerIndex)
|
||||
return paint
|
||||
|
||||
out_layers = [listToColrLayers(l) for l in out_layers]
|
||||
|
||||
if len(out_layers) == 1 and out_layers[0].Format == ot.PaintFormat.PaintColrLayers:
|
||||
# special case when the reuse cache finds a single perfect PaintColrLayers match
|
||||
# (it can only come from a successful reuse, _flatten_layers has gotten rid of
|
||||
# all nested PaintColrLayers already); we assign it directly and avoid creating
|
||||
# an extra table
|
||||
out.NumLayers = out_layers[0].NumLayers
|
||||
out.FirstLayerIndex = out_layers[0].FirstLayerIndex
|
||||
else:
|
||||
out.NumLayers = len(out_layers)
|
||||
out.FirstLayerIndex = len(self.layers)
|
||||
|
||||
self.layers.extend(out_layers)
|
||||
|
||||
# Register our parts for reuse provided we aren't a tree
|
||||
# If we are a tree the leaves registered for reuse and that will suffice
|
||||
if self.layerReuseCache is not None and not is_tree:
|
||||
self.layerReuseCache.add(out_layers, out.FirstLayerIndex)
|
||||
|
||||
|
||||
@COLRVariationMerger.merger((ot.Paint, ot.ClipBox))
|
||||
def merge(merger, self, lst):
|
||||
fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable())
|
||||
|
||||
if fmt is ot.PaintFormat.PaintColrLayers:
|
||||
_merge_PaintColrLayers(merger, self, lst)
|
||||
return
|
||||
|
||||
varFormat = fmt.as_variable()
|
||||
|
||||
varAttrs = ()
|
||||
if varFormat is not None:
|
||||
varAttrs = otBase.getVariableAttrs(type(self), varFormat)
|
||||
staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
|
||||
|
||||
merger.mergeAttrs(self, lst, staticAttrs)
|
||||
|
||||
varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
|
||||
|
||||
subTables = [st.value for st in self.iterSubTables()]
|
||||
|
||||
# Convert table to variable if itself has variations or any subtables have
|
||||
isVariable = (
|
||||
varIndexBase != ot.NO_VARIATION_INDEX
|
||||
or any(id(table) in merger.varTableIds for table in subTables)
|
||||
)
|
||||
|
||||
if isVariable:
|
||||
if varAttrs:
|
||||
# Some PaintVar* don't have any scalar attributes that can vary,
|
||||
# only indirect offsets to other variable subtables, thus have
|
||||
# no VarIndexBase of their own (e.g. PaintVarTransform)
|
||||
self.VarIndexBase = varIndexBase
|
||||
|
||||
if subTables:
|
||||
# Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc.
|
||||
merger.convertSubTablesToVarType(self)
|
||||
|
||||
assert varFormat is not None
|
||||
self.Format = int(varFormat)
|
||||
|
||||
|
||||
@COLRVariationMerger.merger((ot.Affine2x3, ot.ColorStop))
|
||||
def merge(merger, self, lst):
|
||||
varType = type(self).VarType
|
||||
|
||||
varAttrs = otBase.getVariableAttrs(varType)
|
||||
staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
|
||||
|
||||
merger.mergeAttrs(self, lst, staticAttrs)
|
||||
|
||||
varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
|
||||
|
||||
if varIndexBase != ot.NO_VARIATION_INDEX:
|
||||
self.VarIndexBase = varIndexBase
|
||||
# mark as having variations so the parent table will convert to Var{Type}
|
||||
merger.varTableIds.add(id(self))
|
||||
|
||||
|
||||
@COLRVariationMerger.merger(ot.ColorLine)
|
||||
def merge(merger, self, lst):
|
||||
merger.mergeAttrs(self, lst, (c.name for c in self.getConverters()))
|
||||
|
||||
if any(id(stop) in merger.varTableIds for stop in self.ColorStop):
|
||||
merger.convertSubTablesToVarType(self)
|
||||
merger.varTableIds.add(id(self))
|
||||
|
||||
|
||||
@COLRVariationMerger.merger(ot.ClipList, "clips")
|
||||
def merge(merger, self, lst):
|
||||
# 'sparse' in that we allow non-default masters to omit ClipBox entries
|
||||
# for some/all glyphs (i.e. they don't participate)
|
||||
merger.mergeSparseDict(self, lst)
|
||||
|
@ -7,7 +7,7 @@ from functools import partial
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
NO_VARIATION_INDEX = 0xFFFFFFFF
|
||||
NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX
|
||||
ot.VarStore.NO_VARIATION_INDEX = NO_VARIATION_INDEX
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ from fontTools.ttLib import newTable
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.colorLib import builder
|
||||
from fontTools.colorLib.geometry import round_start_circle_stable_containment, Circle
|
||||
from fontTools.colorLib.builder import LayerListBuilder, _build_n_ary_tree
|
||||
from fontTools.colorLib.builder import LayerListBuilder
|
||||
from fontTools.colorLib.table_builder import TableBuilder
|
||||
from fontTools.colorLib.errors import ColorLibError
|
||||
import pytest
|
||||
@ -1678,7 +1678,7 @@ class BuildCOLRTest(object):
|
||||
clipBoxes={
|
||||
"a": (0, 0, 1000, 1000, 0), # optional 5th: varIndexBase
|
||||
"c": (-100.8, -200.4, 1100.1, 1200.5), # floats get rounded
|
||||
"e": (0, 0, 10, 10), # missing base glyph 'e' is ignored
|
||||
"e": (0, 0, 10, 10), # 'e' does _not_ get ignored despite being missing
|
||||
},
|
||||
)
|
||||
|
||||
@ -1689,9 +1689,11 @@ class BuildCOLRTest(object):
|
||||
] == [
|
||||
("a", (0, 0, 1000, 1000, 0)),
|
||||
("c", (-101, -201, 1101, 1201)),
|
||||
("e", (0, 0, 10, 10)),
|
||||
]
|
||||
assert clipBoxes["a"].Format == 2
|
||||
assert clipBoxes["c"].Format == 1
|
||||
assert clipBoxes["e"].Format == 1
|
||||
|
||||
def test_duplicate_base_glyphs(self):
|
||||
# If > 1 base glyphs refer to equivalent list of layers we expect them to share
|
||||
@ -1778,81 +1780,3 @@ class TrickyRadialGradientTest:
|
||||
)
|
||||
def test_nudge_start_circle_position(self, c0, r0, c1, r1, inside, expected):
|
||||
assert self.round_start_circle(c0, r0, c1, r1, inside) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lst, n, expected",
|
||||
[
|
||||
([0], 2, [0]),
|
||||
([0, 1], 2, [0, 1]),
|
||||
([0, 1, 2], 2, [[0, 1], 2]),
|
||||
([0, 1, 2], 3, [0, 1, 2]),
|
||||
([0, 1, 2, 3], 2, [[0, 1], [2, 3]]),
|
||||
([0, 1, 2, 3], 3, [[0, 1, 2], 3]),
|
||||
([0, 1, 2, 3, 4], 3, [[0, 1, 2], 3, 4]),
|
||||
([0, 1, 2, 3, 4, 5], 3, [[0, 1, 2], [3, 4, 5]]),
|
||||
(list(range(7)), 3, [[0, 1, 2], [3, 4, 5], 6]),
|
||||
(list(range(8)), 3, [[0, 1, 2], [3, 4, 5], [6, 7]]),
|
||||
(list(range(9)), 3, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]),
|
||||
(list(range(10)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9]),
|
||||
(list(range(11)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9, 10]),
|
||||
(list(range(12)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11]]),
|
||||
(list(range(13)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], 12]),
|
||||
(
|
||||
list(range(14)),
|
||||
3,
|
||||
[[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], 12, 13]],
|
||||
),
|
||||
(
|
||||
list(range(15)),
|
||||
3,
|
||||
[[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], [12, 13, 14]],
|
||||
),
|
||||
(
|
||||
list(range(16)),
|
||||
3,
|
||||
[[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], [12, 13, 14], 15]],
|
||||
),
|
||||
(
|
||||
list(range(23)),
|
||||
3,
|
||||
[
|
||||
[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
|
||||
[[9, 10, 11], [12, 13, 14], [15, 16, 17]],
|
||||
[[18, 19, 20], 21, 22],
|
||||
],
|
||||
),
|
||||
(
|
||||
list(range(27)),
|
||||
3,
|
||||
[
|
||||
[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
|
||||
[[9, 10, 11], [12, 13, 14], [15, 16, 17]],
|
||||
[[18, 19, 20], [21, 22, 23], [24, 25, 26]],
|
||||
],
|
||||
),
|
||||
(
|
||||
list(range(28)),
|
||||
3,
|
||||
[
|
||||
[
|
||||
[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
|
||||
[[9, 10, 11], [12, 13, 14], [15, 16, 17]],
|
||||
[[18, 19, 20], [21, 22, 23], [24, 25, 26]],
|
||||
],
|
||||
27,
|
||||
],
|
||||
),
|
||||
(list(range(257)), 256, [list(range(256)), 256]),
|
||||
(list(range(258)), 256, [list(range(256)), 256, 257]),
|
||||
(list(range(512)), 256, [list(range(256)), list(range(256, 512))]),
|
||||
(list(range(512 + 1)), 256, [list(range(256)), list(range(256, 512)), 512]),
|
||||
(
|
||||
list(range(256 ** 2)),
|
||||
256,
|
||||
[list(range(k * 256, k * 256 + 256)) for k in range(256)],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_build_n_ary_tree(lst, n, expected):
|
||||
assert _build_n_ary_tree(lst, n) == expected
|
||||
|
@ -221,7 +221,26 @@ TEST_COLOR_GLYPHS = {
|
||||
"Glyph": "glyph00012",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
# When PaintColrLayers contains more than 255 layers, we build a tree
|
||||
# of nested PaintColrLayers of max 255 items (NumLayers field is a uint8).
|
||||
# Below we test that unbuildColrV1 restores a flat list of layers without
|
||||
# nested PaintColrLayers.
|
||||
"glyph00017": {
|
||||
"Format": int(ot.PaintFormat.PaintColrLayers),
|
||||
"Layers": [
|
||||
{
|
||||
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintSolid),
|
||||
"PaletteIndex": i,
|
||||
"Alpha": 1.0,
|
||||
},
|
||||
"Glyph": "glyph{str(18 + i).zfill(5)}",
|
||||
}
|
||||
for i in range(256)
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -230,7 +249,8 @@ def test_unbuildColrV1():
|
||||
colorGlyphs = unbuildColrV1(layers, baseGlyphs)
|
||||
assert colorGlyphs == TEST_COLOR_GLYPHS
|
||||
|
||||
|
||||
def test_unbuildColrV1_noLayers():
|
||||
_, baseGlyphsV1 = buildColrV1(TEST_COLOR_GLYPHS)
|
||||
# Just looking to see we don't crash
|
||||
unbuildColrV1(None, baseGlyphsV1)
|
||||
unbuildColrV1(None, baseGlyphsV1)
|
||||
|
80
Tests/misc/treeTools_test.py
Normal file
80
Tests/misc/treeTools_test.py
Normal file
@ -0,0 +1,80 @@
|
||||
from fontTools.misc.treeTools import build_n_ary_tree
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lst, n, expected",
|
||||
[
|
||||
([0], 2, [0]),
|
||||
([0, 1], 2, [0, 1]),
|
||||
([0, 1, 2], 2, [[0, 1], 2]),
|
||||
([0, 1, 2], 3, [0, 1, 2]),
|
||||
([0, 1, 2, 3], 2, [[0, 1], [2, 3]]),
|
||||
([0, 1, 2, 3], 3, [[0, 1, 2], 3]),
|
||||
([0, 1, 2, 3, 4], 3, [[0, 1, 2], 3, 4]),
|
||||
([0, 1, 2, 3, 4, 5], 3, [[0, 1, 2], [3, 4, 5]]),
|
||||
(list(range(7)), 3, [[0, 1, 2], [3, 4, 5], 6]),
|
||||
(list(range(8)), 3, [[0, 1, 2], [3, 4, 5], [6, 7]]),
|
||||
(list(range(9)), 3, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]),
|
||||
(list(range(10)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9]),
|
||||
(list(range(11)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9, 10]),
|
||||
(list(range(12)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11]]),
|
||||
(list(range(13)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], 12]),
|
||||
(
|
||||
list(range(14)),
|
||||
3,
|
||||
[[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], 12, 13]],
|
||||
),
|
||||
(
|
||||
list(range(15)),
|
||||
3,
|
||||
[[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], [12, 13, 14]],
|
||||
),
|
||||
(
|
||||
list(range(16)),
|
||||
3,
|
||||
[[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], [12, 13, 14], 15]],
|
||||
),
|
||||
(
|
||||
list(range(23)),
|
||||
3,
|
||||
[
|
||||
[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
|
||||
[[9, 10, 11], [12, 13, 14], [15, 16, 17]],
|
||||
[[18, 19, 20], 21, 22],
|
||||
],
|
||||
),
|
||||
(
|
||||
list(range(27)),
|
||||
3,
|
||||
[
|
||||
[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
|
||||
[[9, 10, 11], [12, 13, 14], [15, 16, 17]],
|
||||
[[18, 19, 20], [21, 22, 23], [24, 25, 26]],
|
||||
],
|
||||
),
|
||||
(
|
||||
list(range(28)),
|
||||
3,
|
||||
[
|
||||
[
|
||||
[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
|
||||
[[9, 10, 11], [12, 13, 14], [15, 16, 17]],
|
||||
[[18, 19, 20], [21, 22, 23], [24, 25, 26]],
|
||||
],
|
||||
27,
|
||||
],
|
||||
),
|
||||
(list(range(257)), 256, [list(range(256)), 256]),
|
||||
(list(range(258)), 256, [list(range(256)), 256, 257]),
|
||||
(list(range(512)), 256, [list(range(256)), list(range(256, 512))]),
|
||||
(list(range(512 + 1)), 256, [list(range(256)), list(range(256, 512)), 512]),
|
||||
(
|
||||
list(range(256 ** 2)),
|
||||
256,
|
||||
[list(range(k * 256, k * 256 + 256)) for k in range(256)],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_build_n_ary_tree(lst, n, expected):
|
||||
assert build_n_ary_tree(lst, n) == expected
|
@ -532,17 +532,18 @@ COLR_V1_XML = [
|
||||
|
||||
COLR_V1_VAR_XML = [
|
||||
'<VarIndexMap Format="0">',
|
||||
' <Map index="0" outer="1" inner="1"/>',
|
||||
' <Map index="1" outer="1" inner="0"/>',
|
||||
' <Map index="2" outer="1" inner="0"/>',
|
||||
' <Map index="3" outer="1" inner="1"/>',
|
||||
' <Map index="4" outer="1" inner="0"/>',
|
||||
' <Map index="5" outer="1" inner="0"/>',
|
||||
' <!-- Omitted values default to 0xFFFF/0xFFFF (no variations) -->',
|
||||
' <Map index="0" outer="1" inner="0"/>',
|
||||
' <Map index="1"/>',
|
||||
' <Map index="2"/>',
|
||||
' <Map index="3" outer="1" inner="0"/>',
|
||||
' <Map index="4"/>',
|
||||
' <Map index="5"/>',
|
||||
' <Map index="6" outer="0" inner="2"/>',
|
||||
' <Map index="7" outer="0" inner="0"/>',
|
||||
' <Map index="8" outer="0" inner="1"/>',
|
||||
' <Map index="9" outer="1" inner="0"/>',
|
||||
' <Map index="10" outer="1" inner="0"/>',
|
||||
' <Map index="9"/>',
|
||||
' <Map index="10"/>',
|
||||
' <Map index="11" outer="0" inner="3"/>',
|
||||
' <Map index="12" outer="0" inner="3"/>',
|
||||
"</VarIndexMap>",
|
||||
@ -571,12 +572,11 @@ COLR_V1_VAR_XML = [
|
||||
' <Item index="3" value="[500]"/>',
|
||||
" </VarData>",
|
||||
' <VarData index="1">',
|
||||
" <!-- ItemCount=2 -->",
|
||||
" <!-- ItemCount=1 -->",
|
||||
' <NumShorts value="32769"/>',
|
||||
" <!-- VarRegionCount=1 -->",
|
||||
' <VarRegionIndex index="0" value="0"/>',
|
||||
' <Item index="0" value="[0]"/>',
|
||||
' <Item index="1" value="[65536]"/>',
|
||||
' <Item index="0" value="[65536]"/>',
|
||||
" </VarData>",
|
||||
"</VarStore>",
|
||||
]
|
||||
|
18
Tests/varLib/data/TestVariableCOLR.designspace
Normal file
18
Tests/varLib/data/TestVariableCOLR.designspace
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<designspace format="5.0">
|
||||
<axes>
|
||||
<axis tag="wght" name="Weight" minimum="400" maximum="700" default="400"/>
|
||||
</axes>
|
||||
<sources>
|
||||
<source filename="master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx" name="Regular">
|
||||
<location>
|
||||
<dimension name="Weight" xvalue="400"/>
|
||||
</location>
|
||||
</source>
|
||||
<source filename="master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx" name="Bold">
|
||||
<location>
|
||||
<dimension name="Weight" xvalue="700"/>
|
||||
</location>
|
||||
</source>
|
||||
</sources>
|
||||
</designspace>
|
@ -0,0 +1,357 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.33">
|
||||
|
||||
<GlyphOrder>
|
||||
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||
<GlyphID id="0" name=".notdef"/>
|
||||
<GlyphID id="1" name=".space"/>
|
||||
<GlyphID id="2" name="A"/>
|
||||
<GlyphID id="3" name="B"/>
|
||||
<GlyphID id="4" name="A.0"/>
|
||||
</GlyphOrder>
|
||||
|
||||
<head>
|
||||
<!-- Most of this table will be recalculated by the compiler -->
|
||||
<tableVersion value="1.0"/>
|
||||
<fontRevision value="1.0"/>
|
||||
<checkSumAdjustment value="0x92daf67f"/>
|
||||
<magicNumber value="0x5f0f3cf5"/>
|
||||
<flags value="00000000 00000011"/>
|
||||
<unitsPerEm value="1024"/>
|
||||
<created value="Tue Jul 5 12:29:44 2022"/>
|
||||
<modified value="Tue Jul 5 12:29:44 2022"/>
|
||||
<xMin value="51"/>
|
||||
<yMin value="-250"/>
|
||||
<xMax value="878"/>
|
||||
<yMax value="950"/>
|
||||
<macStyle value="00000000 00000001"/>
|
||||
<lowestRecPPEM value="6"/>
|
||||
<fontDirectionHint value="2"/>
|
||||
<indexToLocFormat value="0"/>
|
||||
<glyphDataFormat value="0"/>
|
||||
</head>
|
||||
|
||||
<hhea>
|
||||
<tableVersion value="0x00010000"/>
|
||||
<ascent value="950"/>
|
||||
<descent value="-250"/>
|
||||
<lineGap value="0"/>
|
||||
<advanceWidthMax value="1275"/>
|
||||
<minLeftSideBearing value="51"/>
|
||||
<minRightSideBearing value="397"/>
|
||||
<xMaxExtent value="878"/>
|
||||
<caretSlopeRise value="1"/>
|
||||
<caretSlopeRun value="0"/>
|
||||
<caretOffset value="0"/>
|
||||
<reserved0 value="0"/>
|
||||
<reserved1 value="0"/>
|
||||
<reserved2 value="0"/>
|
||||
<reserved3 value="0"/>
|
||||
<metricDataFormat value="0"/>
|
||||
<numberOfHMetrics value="1"/>
|
||||
</hhea>
|
||||
|
||||
<maxp>
|
||||
<!-- Most of this table will be recalculated by the compiler -->
|
||||
<tableVersion value="0x10000"/>
|
||||
<numGlyphs value="5"/>
|
||||
<maxPoints value="16"/>
|
||||
<maxContours value="2"/>
|
||||
<maxCompositePoints value="0"/>
|
||||
<maxCompositeContours value="0"/>
|
||||
<maxZones value="1"/>
|
||||
<maxTwilightPoints value="0"/>
|
||||
<maxStorage value="0"/>
|
||||
<maxFunctionDefs value="0"/>
|
||||
<maxInstructionDefs value="0"/>
|
||||
<maxStackElements value="0"/>
|
||||
<maxSizeOfInstructions value="0"/>
|
||||
<maxComponentElements value="0"/>
|
||||
<maxComponentDepth value="0"/>
|
||||
</maxp>
|
||||
|
||||
<OS_2>
|
||||
<!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
|
||||
will be recalculated by the compiler -->
|
||||
<version value="4"/>
|
||||
<xAvgCharWidth value="1275"/>
|
||||
<usWeightClass value="400"/>
|
||||
<usWidthClass value="5"/>
|
||||
<fsType value="00000000 00000100"/>
|
||||
<ySubscriptXSize value="666"/>
|
||||
<ySubscriptYSize value="614"/>
|
||||
<ySubscriptXOffset value="0"/>
|
||||
<ySubscriptYOffset value="77"/>
|
||||
<ySuperscriptXSize value="666"/>
|
||||
<ySuperscriptYSize value="614"/>
|
||||
<ySuperscriptXOffset value="0"/>
|
||||
<ySuperscriptYOffset value="358"/>
|
||||
<yStrikeoutSize value="51"/>
|
||||
<yStrikeoutPosition value="307"/>
|
||||
<sFamilyClass value="0"/>
|
||||
<panose>
|
||||
<bFamilyType value="0"/>
|
||||
<bSerifStyle value="0"/>
|
||||
<bWeight value="0"/>
|
||||
<bProportion value="0"/>
|
||||
<bContrast value="0"/>
|
||||
<bStrokeVariation value="0"/>
|
||||
<bArmStyle value="0"/>
|
||||
<bLetterForm value="0"/>
|
||||
<bMidline value="0"/>
|
||||
<bXHeight value="0"/>
|
||||
</panose>
|
||||
<ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
|
||||
<ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
|
||||
<ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
|
||||
<ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
|
||||
<achVendID value="NONE"/>
|
||||
<fsSelection value="00000000 10100000"/>
|
||||
<usFirstCharIndex value="32"/>
|
||||
<usLastCharIndex value="66"/>
|
||||
<sTypoAscender value="950"/>
|
||||
<sTypoDescender value="-250"/>
|
||||
<sTypoLineGap value="0"/>
|
||||
<usWinAscent value="950"/>
|
||||
<usWinDescent value="250"/>
|
||||
<ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
|
||||
<ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
|
||||
<sxHeight value="512"/>
|
||||
<sCapHeight value="717"/>
|
||||
<usDefaultChar value="0"/>
|
||||
<usBreakChar value="32"/>
|
||||
<usMaxContext value="0"/>
|
||||
</OS_2>
|
||||
|
||||
<hmtx>
|
||||
<mtx name=".notdef" width="1275" lsb="51"/>
|
||||
<mtx name=".space" width="1275" lsb="0"/>
|
||||
<mtx name="A" width="1275" lsb="0"/>
|
||||
<mtx name="A.0" width="1275" lsb="398"/>
|
||||
<mtx name="B" width="1275" lsb="0"/>
|
||||
</hmtx>
|
||||
|
||||
<cmap>
|
||||
<tableVersion version="0"/>
|
||||
<cmap_format_4 platformID="0" platEncID="3" language="0">
|
||||
<map code="0x20" name=".space"/><!-- SPACE -->
|
||||
<map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
|
||||
<map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
|
||||
</cmap_format_4>
|
||||
<cmap_format_4 platformID="3" platEncID="1" language="0">
|
||||
<map code="0x20" name=".space"/><!-- SPACE -->
|
||||
<map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
|
||||
<map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
|
||||
</cmap_format_4>
|
||||
</cmap>
|
||||
|
||||
<loca>
|
||||
<!-- The 'loca' table will be calculated by the compiler -->
|
||||
</loca>
|
||||
|
||||
<glyf>
|
||||
|
||||
<!-- The xMin, yMin, xMax and yMax values
|
||||
will be recalculated by the compiler. -->
|
||||
|
||||
<TTGlyph name=".notdef" xMin="51" yMin="-250" xMax="461" yMax="950">
|
||||
<contour>
|
||||
<pt x="51" y="-250" on="1"/>
|
||||
<pt x="51" y="950" on="1"/>
|
||||
<pt x="461" y="950" on="1"/>
|
||||
<pt x="461" y="-250" on="1"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<pt x="102" y="-199" on="1"/>
|
||||
<pt x="410" y="-199" on="1"/>
|
||||
<pt x="410" y="899" on="1"/>
|
||||
<pt x="102" y="899" on="1"/>
|
||||
</contour>
|
||||
<instructions/>
|
||||
</TTGlyph>
|
||||
|
||||
<TTGlyph name=".space"/><!-- contains no outline data -->
|
||||
|
||||
<TTGlyph name="A"/><!-- contains no outline data -->
|
||||
|
||||
<TTGlyph name="A.0" xMin="398" yMin="110" xMax="878" yMax="590">
|
||||
<contour>
|
||||
<pt x="878" y="350" on="1"/>
|
||||
<pt x="878" y="416" on="0"/>
|
||||
<pt x="813" y="525" on="0"/>
|
||||
<pt x="704" y="590" on="0"/>
|
||||
<pt x="638" y="590" on="1"/>
|
||||
<pt x="571" y="590" on="0"/>
|
||||
<pt x="462" y="525" on="0"/>
|
||||
<pt x="398" y="416" on="0"/>
|
||||
<pt x="398" y="350" on="1"/>
|
||||
<pt x="398" y="284" on="0"/>
|
||||
<pt x="462" y="175" on="0"/>
|
||||
<pt x="571" y="110" on="0"/>
|
||||
<pt x="638" y="110" on="1"/>
|
||||
<pt x="704" y="110" on="0"/>
|
||||
<pt x="813" y="175" on="0"/>
|
||||
<pt x="878" y="284" on="0"/>
|
||||
</contour>
|
||||
<instructions/>
|
||||
</TTGlyph>
|
||||
|
||||
<TTGlyph name="B"/><!-- contains no outline data -->
|
||||
|
||||
</glyf>
|
||||
|
||||
<name>
|
||||
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
|
||||
An Emoji Family
|
||||
</namerecord>
|
||||
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
|
||||
Bold
|
||||
</namerecord>
|
||||
<namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
|
||||
1.000;NONE;AnEmojiFamily-Bold
|
||||
</namerecord>
|
||||
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
|
||||
An Emoji Family Bold
|
||||
</namerecord>
|
||||
<namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
|
||||
Version 1.000
|
||||
</namerecord>
|
||||
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
|
||||
AnEmojiFamily-Bold
|
||||
</namerecord>
|
||||
</name>
|
||||
|
||||
<post>
|
||||
<formatType value="2.0"/>
|
||||
<italicAngle value="0.0"/>
|
||||
<underlinePosition value="-77"/>
|
||||
<underlineThickness value="51"/>
|
||||
<isFixedPitch value="0"/>
|
||||
<minMemType42 value="0"/>
|
||||
<maxMemType42 value="0"/>
|
||||
<minMemType1 value="0"/>
|
||||
<maxMemType1 value="0"/>
|
||||
<psNames>
|
||||
<!-- This file uses unique glyph names based on the information
|
||||
found in the 'post' table. Since these names might not be unique,
|
||||
we have to invent artificial names in case of clashes. In order to
|
||||
be able to retain the original information, we need a name to
|
||||
ps name mapping for those cases where they differ. That's what
|
||||
you see below.
|
||||
-->
|
||||
</psNames>
|
||||
<extraNames>
|
||||
<!-- following are the name that are not taken from the standard Mac glyph order -->
|
||||
<psName name=".space"/>
|
||||
<psName name="A.0"/>
|
||||
</extraNames>
|
||||
</post>
|
||||
|
||||
<COLR>
|
||||
<Version value="1"/>
|
||||
<!-- BaseGlyphRecordCount=0 -->
|
||||
<!-- LayerRecordCount=0 -->
|
||||
<BaseGlyphList>
|
||||
<!-- BaseGlyphCount=2 -->
|
||||
<BaseGlyphPaintRecord index="0">
|
||||
<BaseGlyph value="A"/>
|
||||
<Paint Format="1"><!-- PaintColrLayers -->
|
||||
<NumLayers value="3"/>
|
||||
<FirstLayerIndex value="0"/>
|
||||
</Paint>
|
||||
</BaseGlyphPaintRecord>
|
||||
<BaseGlyphPaintRecord index="1">
|
||||
<BaseGlyph value="B"/>
|
||||
<Paint Format="1"><!-- PaintColrLayers -->
|
||||
<NumLayers value="2"/>
|
||||
<FirstLayerIndex value="3"/>
|
||||
</Paint>
|
||||
</BaseGlyphPaintRecord>
|
||||
</BaseGlyphList>
|
||||
<LayerList>
|
||||
<!-- LayerCount=5 -->
|
||||
<Paint index="0" Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="0"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<Paint index="1" Format="14"><!-- PaintTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="2"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="100"/>
|
||||
<dy value="-120"/>
|
||||
</Paint>
|
||||
<Paint index="2" Format="14"><!-- PaintTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="1"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="0"/>
|
||||
<dy value="-240"/>
|
||||
</Paint>
|
||||
<Paint index="3" Format="14"><!-- PaintTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="2"/>
|
||||
<Alpha value="0.5"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="0"/>
|
||||
<dy value="-120"/>
|
||||
</Paint>
|
||||
<Paint index="4" Format="14"><!-- PaintTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="1"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="0"/>
|
||||
<dy value="-240"/>
|
||||
</Paint>
|
||||
</LayerList>
|
||||
<ClipList Format="1">
|
||||
<Clip>
|
||||
<Glyph value="A"/>
|
||||
<ClipBox Format="1">
|
||||
<xMin value="380"/>
|
||||
<yMin value="-140"/>
|
||||
<xMax value="980"/>
|
||||
<yMax value="600"/>
|
||||
</ClipBox>
|
||||
</Clip>
|
||||
<Clip>
|
||||
<Glyph value="B"/>
|
||||
<ClipBox Format="1">
|
||||
<xMin value="380"/>
|
||||
<yMin value="-140"/>
|
||||
<xMax value="880"/>
|
||||
<yMax value="480"/>
|
||||
</ClipBox>
|
||||
</Clip>
|
||||
</ClipList>
|
||||
</COLR>
|
||||
|
||||
<CPAL>
|
||||
<version value="0"/>
|
||||
<numPaletteEntries value="3"/>
|
||||
<palette index="0">
|
||||
<color index="0" value="#0000FFFF"/>
|
||||
<color index="1" value="#008000FF"/>
|
||||
<color index="2" value="#FF0000FF"/>
|
||||
</palette>
|
||||
</CPAL>
|
||||
|
||||
</ttFont>
|
@ -0,0 +1,335 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.33">
|
||||
|
||||
<GlyphOrder>
|
||||
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||
<GlyphID id="0" name=".notdef"/>
|
||||
<GlyphID id="1" name=".space"/>
|
||||
<GlyphID id="2" name="A"/>
|
||||
<GlyphID id="3" name="B"/>
|
||||
<GlyphID id="4" name="A.0"/>
|
||||
</GlyphOrder>
|
||||
|
||||
<head>
|
||||
<!-- Most of this table will be recalculated by the compiler -->
|
||||
<tableVersion value="1.0"/>
|
||||
<fontRevision value="1.0"/>
|
||||
<checkSumAdjustment value="0x49d0234e"/>
|
||||
<magicNumber value="0x5f0f3cf5"/>
|
||||
<flags value="00000000 00000011"/>
|
||||
<unitsPerEm value="1024"/>
|
||||
<created value="Tue Jul 5 12:29:44 2022"/>
|
||||
<modified value="Tue Jul 5 12:29:44 2022"/>
|
||||
<xMin value="51"/>
|
||||
<yMin value="-250"/>
|
||||
<xMax value="878"/>
|
||||
<yMax value="950"/>
|
||||
<macStyle value="00000000 00000000"/>
|
||||
<lowestRecPPEM value="6"/>
|
||||
<fontDirectionHint value="2"/>
|
||||
<indexToLocFormat value="0"/>
|
||||
<glyphDataFormat value="0"/>
|
||||
</head>
|
||||
|
||||
<hhea>
|
||||
<tableVersion value="0x00010000"/>
|
||||
<ascent value="950"/>
|
||||
<descent value="-250"/>
|
||||
<lineGap value="0"/>
|
||||
<advanceWidthMax value="1275"/>
|
||||
<minLeftSideBearing value="51"/>
|
||||
<minRightSideBearing value="397"/>
|
||||
<xMaxExtent value="878"/>
|
||||
<caretSlopeRise value="1"/>
|
||||
<caretSlopeRun value="0"/>
|
||||
<caretOffset value="0"/>
|
||||
<reserved0 value="0"/>
|
||||
<reserved1 value="0"/>
|
||||
<reserved2 value="0"/>
|
||||
<reserved3 value="0"/>
|
||||
<metricDataFormat value="0"/>
|
||||
<numberOfHMetrics value="1"/>
|
||||
</hhea>
|
||||
|
||||
<maxp>
|
||||
<!-- Most of this table will be recalculated by the compiler -->
|
||||
<tableVersion value="0x10000"/>
|
||||
<numGlyphs value="5"/>
|
||||
<maxPoints value="16"/>
|
||||
<maxContours value="2"/>
|
||||
<maxCompositePoints value="0"/>
|
||||
<maxCompositeContours value="0"/>
|
||||
<maxZones value="1"/>
|
||||
<maxTwilightPoints value="0"/>
|
||||
<maxStorage value="0"/>
|
||||
<maxFunctionDefs value="0"/>
|
||||
<maxInstructionDefs value="0"/>
|
||||
<maxStackElements value="0"/>
|
||||
<maxSizeOfInstructions value="0"/>
|
||||
<maxComponentElements value="0"/>
|
||||
<maxComponentDepth value="0"/>
|
||||
</maxp>
|
||||
|
||||
<OS_2>
|
||||
<!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
|
||||
will be recalculated by the compiler -->
|
||||
<version value="4"/>
|
||||
<xAvgCharWidth value="1275"/>
|
||||
<usWeightClass value="400"/>
|
||||
<usWidthClass value="5"/>
|
||||
<fsType value="00000000 00000100"/>
|
||||
<ySubscriptXSize value="666"/>
|
||||
<ySubscriptYSize value="614"/>
|
||||
<ySubscriptXOffset value="0"/>
|
||||
<ySubscriptYOffset value="77"/>
|
||||
<ySuperscriptXSize value="666"/>
|
||||
<ySuperscriptYSize value="614"/>
|
||||
<ySuperscriptXOffset value="0"/>
|
||||
<ySuperscriptYOffset value="358"/>
|
||||
<yStrikeoutSize value="51"/>
|
||||
<yStrikeoutPosition value="307"/>
|
||||
<sFamilyClass value="0"/>
|
||||
<panose>
|
||||
<bFamilyType value="0"/>
|
||||
<bSerifStyle value="0"/>
|
||||
<bWeight value="0"/>
|
||||
<bProportion value="0"/>
|
||||
<bContrast value="0"/>
|
||||
<bStrokeVariation value="0"/>
|
||||
<bArmStyle value="0"/>
|
||||
<bLetterForm value="0"/>
|
||||
<bMidline value="0"/>
|
||||
<bXHeight value="0"/>
|
||||
</panose>
|
||||
<ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
|
||||
<ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
|
||||
<ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
|
||||
<ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
|
||||
<achVendID value="NONE"/>
|
||||
<fsSelection value="00000000 11000000"/>
|
||||
<usFirstCharIndex value="32"/>
|
||||
<usLastCharIndex value="66"/>
|
||||
<sTypoAscender value="950"/>
|
||||
<sTypoDescender value="-250"/>
|
||||
<sTypoLineGap value="0"/>
|
||||
<usWinAscent value="950"/>
|
||||
<usWinDescent value="250"/>
|
||||
<ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
|
||||
<ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
|
||||
<sxHeight value="512"/>
|
||||
<sCapHeight value="717"/>
|
||||
<usDefaultChar value="0"/>
|
||||
<usBreakChar value="32"/>
|
||||
<usMaxContext value="0"/>
|
||||
</OS_2>
|
||||
|
||||
<hmtx>
|
||||
<mtx name=".notdef" width="1275" lsb="51"/>
|
||||
<mtx name=".space" width="1275" lsb="0"/>
|
||||
<mtx name="A" width="1275" lsb="0"/>
|
||||
<mtx name="A.0" width="1275" lsb="398"/>
|
||||
<mtx name="B" width="1275" lsb="0"/>
|
||||
</hmtx>
|
||||
|
||||
<cmap>
|
||||
<tableVersion version="0"/>
|
||||
<cmap_format_4 platformID="0" platEncID="3" language="0">
|
||||
<map code="0x20" name=".space"/><!-- SPACE -->
|
||||
<map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
|
||||
<map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
|
||||
</cmap_format_4>
|
||||
<cmap_format_4 platformID="3" platEncID="1" language="0">
|
||||
<map code="0x20" name=".space"/><!-- SPACE -->
|
||||
<map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
|
||||
<map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
|
||||
</cmap_format_4>
|
||||
</cmap>
|
||||
|
||||
<loca>
|
||||
<!-- The 'loca' table will be calculated by the compiler -->
|
||||
</loca>
|
||||
|
||||
<glyf>
|
||||
|
||||
<!-- The xMin, yMin, xMax and yMax values
|
||||
will be recalculated by the compiler. -->
|
||||
|
||||
<TTGlyph name=".notdef" xMin="51" yMin="-250" xMax="461" yMax="950">
|
||||
<contour>
|
||||
<pt x="51" y="-250" on="1"/>
|
||||
<pt x="51" y="950" on="1"/>
|
||||
<pt x="461" y="950" on="1"/>
|
||||
<pt x="461" y="-250" on="1"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<pt x="102" y="-199" on="1"/>
|
||||
<pt x="410" y="-199" on="1"/>
|
||||
<pt x="410" y="899" on="1"/>
|
||||
<pt x="102" y="899" on="1"/>
|
||||
</contour>
|
||||
<instructions/>
|
||||
</TTGlyph>
|
||||
|
||||
<TTGlyph name=".space"/><!-- contains no outline data -->
|
||||
|
||||
<TTGlyph name="A"/><!-- contains no outline data -->
|
||||
|
||||
<TTGlyph name="A.0" xMin="398" yMin="110" xMax="878" yMax="590">
|
||||
<contour>
|
||||
<pt x="878" y="350" on="1"/>
|
||||
<pt x="878" y="416" on="0"/>
|
||||
<pt x="813" y="525" on="0"/>
|
||||
<pt x="704" y="590" on="0"/>
|
||||
<pt x="638" y="590" on="1"/>
|
||||
<pt x="571" y="590" on="0"/>
|
||||
<pt x="462" y="525" on="0"/>
|
||||
<pt x="398" y="416" on="0"/>
|
||||
<pt x="398" y="350" on="1"/>
|
||||
<pt x="398" y="284" on="0"/>
|
||||
<pt x="462" y="175" on="0"/>
|
||||
<pt x="571" y="110" on="0"/>
|
||||
<pt x="638" y="110" on="1"/>
|
||||
<pt x="704" y="110" on="0"/>
|
||||
<pt x="813" y="175" on="0"/>
|
||||
<pt x="878" y="284" on="0"/>
|
||||
</contour>
|
||||
<instructions/>
|
||||
</TTGlyph>
|
||||
|
||||
<TTGlyph name="B"/><!-- contains no outline data -->
|
||||
|
||||
</glyf>
|
||||
|
||||
<name>
|
||||
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
|
||||
An Emoji Family
|
||||
</namerecord>
|
||||
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
|
||||
Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
|
||||
1.000;NONE;AnEmojiFamily-Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
|
||||
An Emoji Family Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
|
||||
Version 1.000
|
||||
</namerecord>
|
||||
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
|
||||
AnEmojiFamily-Regular
|
||||
</namerecord>
|
||||
</name>
|
||||
|
||||
<post>
|
||||
<formatType value="2.0"/>
|
||||
<italicAngle value="0.0"/>
|
||||
<underlinePosition value="-77"/>
|
||||
<underlineThickness value="51"/>
|
||||
<isFixedPitch value="0"/>
|
||||
<minMemType42 value="0"/>
|
||||
<maxMemType42 value="0"/>
|
||||
<minMemType1 value="0"/>
|
||||
<maxMemType1 value="0"/>
|
||||
<psNames>
|
||||
<!-- This file uses unique glyph names based on the information
|
||||
found in the 'post' table. Since these names might not be unique,
|
||||
we have to invent artificial names in case of clashes. In order to
|
||||
be able to retain the original information, we need a name to
|
||||
ps name mapping for those cases where they differ. That's what
|
||||
you see below.
|
||||
-->
|
||||
</psNames>
|
||||
<extraNames>
|
||||
<!-- following are the name that are not taken from the standard Mac glyph order -->
|
||||
<psName name=".space"/>
|
||||
<psName name="A.0"/>
|
||||
</extraNames>
|
||||
</post>
|
||||
|
||||
<COLR>
|
||||
<Version value="1"/>
|
||||
<!-- BaseGlyphRecordCount=0 -->
|
||||
<!-- LayerRecordCount=0 -->
|
||||
<BaseGlyphList>
|
||||
<!-- BaseGlyphCount=2 -->
|
||||
<BaseGlyphPaintRecord index="0">
|
||||
<BaseGlyph value="A"/>
|
||||
<Paint Format="1"><!-- PaintColrLayers -->
|
||||
<NumLayers value="3"/>
|
||||
<FirstLayerIndex value="0"/>
|
||||
</Paint>
|
||||
</BaseGlyphPaintRecord>
|
||||
<BaseGlyphPaintRecord index="1">
|
||||
<BaseGlyph value="B"/>
|
||||
<Paint Format="1"><!-- PaintColrLayers -->
|
||||
<NumLayers value="2"/>
|
||||
<FirstLayerIndex value="1"/>
|
||||
</Paint>
|
||||
</BaseGlyphPaintRecord>
|
||||
</BaseGlyphList>
|
||||
<LayerList>
|
||||
<!-- LayerCount=3 -->
|
||||
<Paint index="0" Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="0"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<Paint index="1" Format="14"><!-- PaintTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="2"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="0"/>
|
||||
<dy value="-120"/>
|
||||
</Paint>
|
||||
<Paint index="2" Format="14"><!-- PaintTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="1"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="0"/>
|
||||
<dy value="-240"/>
|
||||
</Paint>
|
||||
</LayerList>
|
||||
<ClipList Format="1">
|
||||
<Clip>
|
||||
<Glyph value="A"/>
|
||||
<ClipBox Format="1">
|
||||
<xMin value="380"/>
|
||||
<yMin value="-140"/>
|
||||
<xMax value="880"/>
|
||||
<yMax value="600"/>
|
||||
</ClipBox>
|
||||
</Clip>
|
||||
<Clip>
|
||||
<Glyph value="B"/>
|
||||
<ClipBox Format="1">
|
||||
<xMin value="380"/>
|
||||
<yMin value="-140"/>
|
||||
<xMax value="880"/>
|
||||
<yMax value="480"/>
|
||||
</ClipBox>
|
||||
</Clip>
|
||||
</ClipList>
|
||||
</COLR>
|
||||
|
||||
<CPAL>
|
||||
<version value="0"/>
|
||||
<numPaletteEntries value="3"/>
|
||||
<palette index="0">
|
||||
<color index="0" value="#0000FFFF"/>
|
||||
<color index="1" value="#008000FF"/>
|
||||
<color index="2" value="#FF0000FF"/>
|
||||
</palette>
|
||||
</CPAL>
|
||||
|
||||
</ttFont>
|
220
Tests/varLib/data/test_results/TestVariableCOLR-VF.ttx
Normal file
220
Tests/varLib/data/test_results/TestVariableCOLR-VF.ttx
Normal file
@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.33">
|
||||
|
||||
<GlyphOrder>
|
||||
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||
<GlyphID id="0" name=".notdef"/>
|
||||
<GlyphID id="1" name=".space"/>
|
||||
<GlyphID id="2" name="A"/>
|
||||
<GlyphID id="3" name="B"/>
|
||||
<GlyphID id="4" name="A.0"/>
|
||||
</GlyphOrder>
|
||||
|
||||
<fvar>
|
||||
|
||||
<!-- Weight -->
|
||||
<Axis>
|
||||
<AxisTag>wght</AxisTag>
|
||||
<Flags>0x0</Flags>
|
||||
<MinValue>400.0</MinValue>
|
||||
<DefaultValue>400.0</DefaultValue>
|
||||
<MaxValue>700.0</MaxValue>
|
||||
<AxisNameID>256</AxisNameID>
|
||||
</Axis>
|
||||
</fvar>
|
||||
|
||||
<glyf>
|
||||
|
||||
<!-- The xMin, yMin, xMax and yMax values
|
||||
will be recalculated by the compiler. -->
|
||||
|
||||
<TTGlyph name=".notdef" xMin="51" yMin="-250" xMax="461" yMax="950">
|
||||
<contour>
|
||||
<pt x="51" y="-250" on="1"/>
|
||||
<pt x="51" y="950" on="1"/>
|
||||
<pt x="461" y="950" on="1"/>
|
||||
<pt x="461" y="-250" on="1"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<pt x="102" y="-199" on="1"/>
|
||||
<pt x="410" y="-199" on="1"/>
|
||||
<pt x="410" y="899" on="1"/>
|
||||
<pt x="102" y="899" on="1"/>
|
||||
</contour>
|
||||
<instructions/>
|
||||
</TTGlyph>
|
||||
|
||||
<TTGlyph name=".space"/><!-- contains no outline data -->
|
||||
|
||||
<TTGlyph name="A"/><!-- contains no outline data -->
|
||||
|
||||
<TTGlyph name="A.0" xMin="398" yMin="110" xMax="878" yMax="590">
|
||||
<contour>
|
||||
<pt x="878" y="350" on="1"/>
|
||||
<pt x="878" y="416" on="0"/>
|
||||
<pt x="813" y="525" on="0"/>
|
||||
<pt x="704" y="590" on="0"/>
|
||||
<pt x="638" y="590" on="1"/>
|
||||
<pt x="571" y="590" on="0"/>
|
||||
<pt x="462" y="525" on="0"/>
|
||||
<pt x="398" y="416" on="0"/>
|
||||
<pt x="398" y="350" on="1"/>
|
||||
<pt x="398" y="284" on="0"/>
|
||||
<pt x="462" y="175" on="0"/>
|
||||
<pt x="571" y="110" on="0"/>
|
||||
<pt x="638" y="110" on="1"/>
|
||||
<pt x="704" y="110" on="0"/>
|
||||
<pt x="813" y="175" on="0"/>
|
||||
<pt x="878" y="284" on="0"/>
|
||||
</contour>
|
||||
<instructions/>
|
||||
</TTGlyph>
|
||||
|
||||
<TTGlyph name="B"/><!-- contains no outline data -->
|
||||
|
||||
</glyf>
|
||||
|
||||
<COLR>
|
||||
<Version value="1"/>
|
||||
<!-- BaseGlyphRecordCount=0 -->
|
||||
<!-- LayerRecordCount=0 -->
|
||||
<BaseGlyphList>
|
||||
<!-- BaseGlyphCount=2 -->
|
||||
<BaseGlyphPaintRecord index="0">
|
||||
<BaseGlyph value="A"/>
|
||||
<Paint Format="1"><!-- PaintColrLayers -->
|
||||
<NumLayers value="3"/>
|
||||
<FirstLayerIndex value="0"/>
|
||||
</Paint>
|
||||
</BaseGlyphPaintRecord>
|
||||
<BaseGlyphPaintRecord index="1">
|
||||
<BaseGlyph value="B"/>
|
||||
<Paint Format="1"><!-- PaintColrLayers -->
|
||||
<NumLayers value="2"/>
|
||||
<FirstLayerIndex value="3"/>
|
||||
</Paint>
|
||||
</BaseGlyphPaintRecord>
|
||||
</BaseGlyphList>
|
||||
<LayerList>
|
||||
<!-- LayerCount=5 -->
|
||||
<Paint index="0" Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="0"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<Paint index="1" Format="15"><!-- PaintVarTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="2"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="0"/>
|
||||
<dy value="-120"/>
|
||||
<VarIndexBase value="0"/>
|
||||
</Paint>
|
||||
<Paint index="2" Format="14"><!-- PaintTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="1"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="0"/>
|
||||
<dy value="-240"/>
|
||||
</Paint>
|
||||
<Paint index="3" Format="14"><!-- PaintTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="3"><!-- PaintVarSolid -->
|
||||
<PaletteIndex value="2"/>
|
||||
<Alpha value="1.0"/>
|
||||
<VarIndexBase value="2"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="0"/>
|
||||
<dy value="-120"/>
|
||||
</Paint>
|
||||
<Paint index="4" Format="14"><!-- PaintTranslate -->
|
||||
<Paint Format="10"><!-- PaintGlyph -->
|
||||
<Paint Format="2"><!-- PaintSolid -->
|
||||
<PaletteIndex value="1"/>
|
||||
<Alpha value="1.0"/>
|
||||
</Paint>
|
||||
<Glyph value="A.0"/>
|
||||
</Paint>
|
||||
<dx value="0"/>
|
||||
<dy value="-240"/>
|
||||
</Paint>
|
||||
</LayerList>
|
||||
<ClipList Format="1">
|
||||
<Clip>
|
||||
<Glyph value="A"/>
|
||||
<ClipBox Format="2">
|
||||
<xMin value="380"/>
|
||||
<yMin value="-140"/>
|
||||
<xMax value="880"/>
|
||||
<yMax value="600"/>
|
||||
<VarIndexBase value="3"/>
|
||||
</ClipBox>
|
||||
</Clip>
|
||||
<Clip>
|
||||
<Glyph value="B"/>
|
||||
<ClipBox Format="1">
|
||||
<xMin value="380"/>
|
||||
<yMin value="-140"/>
|
||||
<xMax value="880"/>
|
||||
<yMax value="480"/>
|
||||
</ClipBox>
|
||||
</Clip>
|
||||
</ClipList>
|
||||
<VarIndexMap Format="0">
|
||||
<!-- Omitted values default to 0xFFFF/0xFFFF (no variations) -->
|
||||
<Map index="0" outer="0" inner="1"/>
|
||||
<Map index="1"/>
|
||||
<Map index="2" outer="0" inner="0"/>
|
||||
<Map index="3"/>
|
||||
<Map index="4"/>
|
||||
<Map index="5" outer="0" inner="1"/>
|
||||
<Map index="6"/>
|
||||
</VarIndexMap>
|
||||
<VarStore Format="1">
|
||||
<Format value="1"/>
|
||||
<VarRegionList>
|
||||
<!-- RegionAxisCount=1 -->
|
||||
<!-- RegionCount=1 -->
|
||||
<Region index="0">
|
||||
<VarRegionAxis index="0">
|
||||
<StartCoord value="0.0"/>
|
||||
<PeakCoord value="1.0"/>
|
||||
<EndCoord value="1.0"/>
|
||||
</VarRegionAxis>
|
||||
</Region>
|
||||
</VarRegionList>
|
||||
<!-- VarDataCount=1 -->
|
||||
<VarData index="0">
|
||||
<!-- ItemCount=2 -->
|
||||
<NumShorts value="1"/>
|
||||
<!-- VarRegionCount=1 -->
|
||||
<VarRegionIndex index="0" value="0"/>
|
||||
<Item index="0" value="[-8192]"/>
|
||||
<Item index="1" value="[100]"/>
|
||||
</VarData>
|
||||
</VarStore>
|
||||
</COLR>
|
||||
|
||||
<CPAL>
|
||||
<version value="0"/>
|
||||
<numPaletteEntries value="3"/>
|
||||
<palette index="0">
|
||||
<color index="0" value="#0000FFFF"/>
|
||||
<color index="1" value="#008000FF"/>
|
||||
<color index="2" value="#FF0000FF"/>
|
||||
</palette>
|
||||
</CPAL>
|
||||
|
||||
</ttFont>
|
1844
Tests/varLib/merger_test.py
Normal file
1844
Tests/varLib/merger_test.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -841,7 +841,7 @@ Got: kern.
|
||||
def test_varlib_build_incompatible_lookup_types(self):
|
||||
with pytest.raises(
|
||||
varLibErrors.MismatchedTypes,
|
||||
match = r"MarkBasePos, instead saw PairPos"
|
||||
match = r"'MarkBasePos', instead saw 'PairPos'"
|
||||
):
|
||||
self._run_varlib_build_test(
|
||||
designspace_name="IncompatibleLookupTypes",
|
||||
@ -871,6 +871,15 @@ Expected to see .ScriptCount==1, instead saw 0"""
|
||||
save_before_dump=True,
|
||||
)
|
||||
|
||||
def test_varlib_build_variable_colr(self):
|
||||
self._run_varlib_build_test(
|
||||
designspace_name='TestVariableCOLR',
|
||||
font_name='TestVariableCOLR',
|
||||
tables=["GlyphOrder", "fvar", "glyf", "COLR", "CPAL"],
|
||||
expected_ttx_name='TestVariableCOLR-VF',
|
||||
save_before_dump=True,
|
||||
)
|
||||
|
||||
def test_load_masters_layerName_without_required_font():
|
||||
ds = DesignSpaceDocument()
|
||||
s = SourceDescriptor()
|
||||
|
Loading…
x
Reference in New Issue
Block a user