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:
Cosimo Lupo 2022-07-06 10:55:50 +01:00 committed by GitHub
commit 2a07518b70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 3861 additions and 300 deletions

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@ -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>",
]

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

View File

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

View File

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

View 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

File diff suppressed because it is too large Load Diff

View File

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