|
""" |
|
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, subList |
|
from fontTools.varLib.varStore import VarStoreInstancer |
|
from functools import reduce |
|
from fontTools.otlLib.builder import buildSinglePos |
|
from fontTools.otlLib.optimize.gpos import ( |
|
_compression_level_from_env, |
|
compact_pair_pos, |
|
) |
|
|
|
log = logging.getLogger("fontTools.varLib.merger") |
|
|
|
from .errors import ( |
|
ShouldBeConstant, |
|
FoundANone, |
|
MismatchedTypes, |
|
NotANone, |
|
LengthsDiffer, |
|
KeysDiffer, |
|
InconsistentGlyphOrder, |
|
InconsistentExtensions, |
|
InconsistentFormats, |
|
UnsupportedFormat, |
|
VarLibMergeError, |
|
) |
|
|
|
|
|
class Merger(object): |
|
def __init__(self, font=None): |
|
self.font = font |
|
|
|
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) in (type, enum.EnumMeta): |
|
clazzes = (clazzes,) |
|
if type(attrs) == str: |
|
attrs = (attrs,) |
|
|
|
def wrapper(method): |
|
assert method.__name__ == "merge" |
|
done = [] |
|
for clazz in clazzes: |
|
if clazz in done: |
|
continue |
|
done.append(clazz) |
|
mergers = celf.mergers.setdefault(clazz, {}) |
|
for attr in attrs: |
|
assert attr not in mergers, ( |
|
"Oops, class '%s' has merge function for '%s' defined already." |
|
% (clazz.__name__, attr) |
|
) |
|
mergers[attr] = method |
|
return None |
|
|
|
return wrapper |
|
|
|
@classmethod |
|
def mergersFor(celf, thing, _default={}): |
|
typ = type(thing) |
|
|
|
for celf in celf.mro(): |
|
mergers = getattr(celf, "mergers", None) |
|
if mergers is None: |
|
break |
|
|
|
m = celf.mergers.get(typ, None) |
|
if m is not None: |
|
return m |
|
|
|
return _default |
|
|
|
def mergeObjects(self, out, lst, exclude=()): |
|
if hasattr(out, "ensureDecompiled"): |
|
out.ensureDecompiled(recurse=False) |
|
for item in lst: |
|
if hasattr(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, got=[sorted(vars(v).keys()) for v in lst] |
|
) |
|
mergers = self.mergersFor(out) |
|
defaultMerger = mergers.get("*", self.__class__.mergeThings) |
|
try: |
|
for key in keys: |
|
if key in exclude: |
|
continue |
|
value = getattr(out, key) |
|
values = [getattr(table, key) for table in lst] |
|
mergerFunc = mergers.get(key, defaultMerger) |
|
mergerFunc(self, value, values) |
|
except VarLibMergeError as e: |
|
e.stack.append("." + key) |
|
raise |
|
|
|
def mergeLists(self, out, lst): |
|
if not allEqualTo(out, lst, len): |
|
raise LengthsDiffer(self, expected=len(out), got=[len(x) for x in lst]) |
|
for i, (value, values) in enumerate(zip(out, zip(*lst))): |
|
try: |
|
self.mergeThings(value, values) |
|
except VarLibMergeError as e: |
|
e.stack.append("[%d]" % i) |
|
raise |
|
|
|
def mergeThings(self, out, lst): |
|
if not allEqualTo(out, lst, type): |
|
raise MismatchedTypes( |
|
self, expected=type(out).__name__, got=[type(x).__name__ for x in lst] |
|
) |
|
mergerFunc = self.mergersFor(out).get(None, None) |
|
if mergerFunc is not None: |
|
mergerFunc(self, out, lst) |
|
elif isinstance(out, enum.Enum): |
|
|
|
|
|
if not allEqualTo(out, lst): |
|
raise ShouldBeConstant(self, expected=out, got=lst) |
|
elif hasattr(out, "__dict__"): |
|
self.mergeObjects(out, lst) |
|
elif isinstance(out, list): |
|
self.mergeLists(out, lst) |
|
else: |
|
if not allEqualTo(out, lst): |
|
raise ShouldBeConstant(self, expected=out, got=lst) |
|
|
|
def mergeTables(self, font, master_ttfs, tableTags): |
|
for tag in tableTags: |
|
if tag not in font: |
|
continue |
|
try: |
|
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 |
|
|
|
|
|
|
|
|
|
|
|
class AligningMerger(Merger): |
|
pass |
|
|
|
|
|
@AligningMerger.merger(ot.GDEF, "GlyphClassDef") |
|
def merge(merger, self, lst): |
|
if self is None: |
|
if not allNone(lst): |
|
raise NotANone(merger, expected=None, got=lst) |
|
return |
|
|
|
lst = [l.classDefs for l in lst] |
|
self.classDefs = {} |
|
|
|
self = self.classDefs |
|
|
|
allKeys = set() |
|
allKeys.update(*[l.keys() for l in lst]) |
|
for k in allKeys: |
|
allValues = nonNone(l.get(k) for l in lst) |
|
if not allEqual(allValues): |
|
raise ShouldBeConstant( |
|
merger, expected=allValues[0], got=lst, stack=["." + k] |
|
) |
|
if not allValues: |
|
self[k] = None |
|
else: |
|
self[k] = allValues[0] |
|
|
|
|
|
def _SinglePosUpgradeToFormat2(self): |
|
if self.Format == 2: |
|
return self |
|
|
|
ret = ot.SinglePos() |
|
ret.Format = 2 |
|
ret.Coverage = self.Coverage |
|
ret.ValueFormat = self.ValueFormat |
|
ret.Value = [self.Value for _ in ret.Coverage.glyphs] |
|
ret.ValueCount = len(ret.Value) |
|
|
|
return ret |
|
|
|
|
|
def _merge_GlyphOrders(font, lst, values_lst=None, default=None): |
|
"""Takes font and list of glyph lists (must be sorted by glyph id), and returns |
|
two things: |
|
- Combined glyph list, |
|
- If values_lst is None, return input glyph lists, but padded with None when a glyph |
|
was missing in a list. Otherwise, return values_lst list-of-list, padded with None |
|
to match combined glyph lists. |
|
""" |
|
if values_lst is None: |
|
dict_sets = [set(l) for l in lst] |
|
else: |
|
dict_sets = [{g: v for g, v in zip(l, vs)} for l, vs in zip(lst, values_lst)] |
|
combined = set() |
|
combined.update(*dict_sets) |
|
|
|
sortKey = font.getReverseGlyphMap().__getitem__ |
|
order = sorted(combined, key=sortKey) |
|
|
|
if not all(sorted(vs, key=sortKey) == vs for vs in lst): |
|
raise InconsistentGlyphOrder() |
|
del combined |
|
|
|
paddedValues = None |
|
if values_lst is None: |
|
padded = [ |
|
[glyph if glyph in dict_set else default for glyph in order] |
|
for dict_set in dict_sets |
|
] |
|
else: |
|
assert len(lst) == len(values_lst) |
|
padded = [ |
|
[dict_set[glyph] if glyph in dict_set else default for glyph in order] |
|
for dict_set in dict_sets |
|
] |
|
return order, padded |
|
|
|
|
|
@AligningMerger.merger(otBase.ValueRecord) |
|
def merge(merger, self, lst): |
|
|
|
|
|
self.__dict__ = lst[0].__dict__.copy() |
|
merger.mergeObjects(self, lst) |
|
|
|
|
|
@AligningMerger.merger(ot.Anchor) |
|
def merge(merger, self, lst): |
|
|
|
|
|
self.__dict__ = lst[0].__dict__.copy() |
|
merger.mergeObjects(self, lst) |
|
|
|
|
|
def _Lookup_SinglePos_get_effective_value(merger, subtables, glyph): |
|
for self in subtables: |
|
if ( |
|
self is None |
|
or type(self) != ot.SinglePos |
|
or self.Coverage is None |
|
or glyph not in self.Coverage.glyphs |
|
): |
|
continue |
|
if self.Format == 1: |
|
return self.Value |
|
elif self.Format == 2: |
|
return self.Value[self.Coverage.glyphs.index(glyph)] |
|
else: |
|
raise UnsupportedFormat(merger, subtable="single positioning lookup") |
|
return None |
|
|
|
|
|
def _Lookup_PairPos_get_effective_value_pair( |
|
merger, subtables, firstGlyph, secondGlyph |
|
): |
|
for self in subtables: |
|
if ( |
|
self is None |
|
or type(self) != ot.PairPos |
|
or self.Coverage is None |
|
or firstGlyph not in self.Coverage.glyphs |
|
): |
|
continue |
|
if self.Format == 1: |
|
ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)] |
|
pvr = ps.PairValueRecord |
|
for rec in pvr: |
|
if rec.SecondGlyph == secondGlyph: |
|
return rec |
|
continue |
|
elif self.Format == 2: |
|
klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0) |
|
klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0) |
|
return self.Class1Record[klass1].Class2Record[klass2] |
|
else: |
|
raise UnsupportedFormat(merger, subtable="pair positioning lookup") |
|
return None |
|
|
|
|
|
@AligningMerger.merger(ot.SinglePos) |
|
def merge(merger, self, lst): |
|
self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0) |
|
if not (len(lst) == 1 or (valueFormat & ~0xF == 0)): |
|
raise UnsupportedFormat(merger, subtable="single positioning lookup") |
|
|
|
|
|
coverageGlyphs = self.Coverage.glyphs |
|
if all(v.Format == 1 for v in lst) and all( |
|
coverageGlyphs == v.Coverage.glyphs for v in lst |
|
): |
|
self.Value = otBase.ValueRecord(valueFormat, self.Value) |
|
if valueFormat != 0: |
|
|
|
|
|
|
|
merger.mergeThings( |
|
self.Value, |
|
[v.Value if v.Value is not None else otBase.ValueRecord() for v in lst], |
|
) |
|
self.ValueFormat = self.Value.getFormat() |
|
return |
|
|
|
|
|
self.Format = 2 |
|
lst = [_SinglePosUpgradeToFormat2(v) for v in lst] |
|
|
|
|
|
glyphs, padded = _merge_GlyphOrders( |
|
merger.font, [v.Coverage.glyphs for v in lst], [v.Value for v in lst] |
|
) |
|
|
|
self.Coverage.glyphs = glyphs |
|
self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs] |
|
self.ValueCount = len(self.Value) |
|
|
|
for i, values in enumerate(padded): |
|
for j, glyph in enumerate(glyphs): |
|
if values[j] is not None: |
|
continue |
|
|
|
|
|
|
|
|
|
v = _Lookup_SinglePos_get_effective_value( |
|
merger, merger.lookup_subtables[i], glyph |
|
) |
|
if v is None: |
|
v = otBase.ValueRecord(valueFormat) |
|
values[j] = v |
|
|
|
merger.mergeLists(self.Value, padded) |
|
|
|
|
|
merger.mergeObjects( |
|
self, lst, exclude=("Format", "Coverage", "Value", "ValueCount", "ValueFormat") |
|
) |
|
self.ValueFormat = reduce( |
|
int.__or__, [v.getEffectiveFormat() for v in self.Value], 0 |
|
) |
|
|
|
|
|
@AligningMerger.merger(ot.PairSet) |
|
def merge(merger, self, lst): |
|
|
|
glyphs, padded = _merge_GlyphOrders( |
|
merger.font, |
|
[[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], |
|
[vs.PairValueRecord for vs in lst], |
|
) |
|
|
|
self.PairValueRecord = pvrs = [] |
|
for glyph in glyphs: |
|
pvr = ot.PairValueRecord() |
|
pvr.SecondGlyph = glyph |
|
pvr.Value1 = ( |
|
otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None |
|
) |
|
pvr.Value2 = ( |
|
otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None |
|
) |
|
pvrs.append(pvr) |
|
self.PairValueCount = len(self.PairValueRecord) |
|
|
|
for i, values in enumerate(padded): |
|
for j, glyph in enumerate(glyphs): |
|
|
|
v = ot.PairValueRecord() |
|
v.SecondGlyph = glyph |
|
if values[j] is not None: |
|
vpair = values[j] |
|
else: |
|
vpair = _Lookup_PairPos_get_effective_value_pair( |
|
merger, merger.lookup_subtables[i], self._firstGlyph, glyph |
|
) |
|
if vpair is None: |
|
v1, v2 = None, None |
|
else: |
|
v1 = getattr(vpair, "Value1", None) |
|
v2 = getattr(vpair, "Value2", None) |
|
v.Value1 = ( |
|
otBase.ValueRecord(merger.valueFormat1, src=v1) |
|
if merger.valueFormat1 |
|
else None |
|
) |
|
v.Value2 = ( |
|
otBase.ValueRecord(merger.valueFormat2, src=v2) |
|
if merger.valueFormat2 |
|
else None |
|
) |
|
values[j] = v |
|
del self._firstGlyph |
|
|
|
merger.mergeLists(self.PairValueRecord, padded) |
|
|
|
|
|
def _PairPosFormat1_merge(self, lst, merger): |
|
assert allEqual( |
|
[l.ValueFormat2 == 0 for l in lst if l.PairSet] |
|
), "Report bug against fonttools." |
|
|
|
|
|
merger.mergeObjects( |
|
self, |
|
lst, |
|
exclude=("Coverage", "PairSet", "PairSetCount", "ValueFormat1", "ValueFormat2"), |
|
) |
|
|
|
empty = ot.PairSet() |
|
empty.PairValueRecord = [] |
|
empty.PairValueCount = 0 |
|
|
|
|
|
glyphs, padded = _merge_GlyphOrders( |
|
merger.font, |
|
[v.Coverage.glyphs for v in lst], |
|
[v.PairSet for v in lst], |
|
default=empty, |
|
) |
|
|
|
self.Coverage.glyphs = glyphs |
|
self.PairSet = [ot.PairSet() for _ in glyphs] |
|
self.PairSetCount = len(self.PairSet) |
|
for glyph, ps in zip(glyphs, self.PairSet): |
|
ps._firstGlyph = glyph |
|
|
|
merger.mergeLists(self.PairSet, padded) |
|
|
|
|
|
def _ClassDef_invert(self, allGlyphs=None): |
|
if isinstance(self, dict): |
|
classDefs = self |
|
else: |
|
classDefs = self.classDefs if self and self.classDefs else {} |
|
m = max(classDefs.values()) if classDefs else 0 |
|
|
|
ret = [] |
|
for _ in range(m + 1): |
|
ret.append(set()) |
|
|
|
for k, v in classDefs.items(): |
|
ret[v].add(k) |
|
|
|
|
|
if allGlyphs is None: |
|
ret[0] = None |
|
else: |
|
|
|
|
|
ret[0] = class0 = set(allGlyphs) |
|
for s in ret[1:]: |
|
s.intersection_update(class0) |
|
class0.difference_update(s) |
|
|
|
return ret |
|
|
|
|
|
def _ClassDef_merge_classify(lst, allGlyphses=None): |
|
self = ot.ClassDef() |
|
self.classDefs = classDefs = {} |
|
allGlyphsesWasNone = allGlyphses is None |
|
if allGlyphsesWasNone: |
|
allGlyphses = [None] * len(lst) |
|
|
|
classifier = classifyTools.Classifier() |
|
for classDef, allGlyphs in zip(lst, allGlyphses): |
|
sets = _ClassDef_invert(classDef, allGlyphs) |
|
if allGlyphs is None: |
|
sets = sets[1:] |
|
classifier.update(sets) |
|
classes = classifier.getClasses() |
|
|
|
if allGlyphsesWasNone: |
|
classes.insert(0, set()) |
|
|
|
for i, classSet in enumerate(classes): |
|
if i == 0: |
|
continue |
|
for g in classSet: |
|
classDefs[g] = i |
|
|
|
return self, classes |
|
|
|
|
|
def _PairPosFormat2_align_matrices(self, lst, font, transparent=False): |
|
matrices = [l.Class1Record for l in lst] |
|
|
|
|
|
self.ClassDef1, classes = _ClassDef_merge_classify( |
|
[l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst] |
|
) |
|
self.Class1Count = len(classes) |
|
new_matrices = [] |
|
for l, matrix in zip(lst, matrices): |
|
nullRow = None |
|
coverage = set(l.Coverage.glyphs) |
|
classDef1 = l.ClassDef1.classDefs |
|
class1Records = [] |
|
for classSet in classes: |
|
exemplarGlyph = next(iter(classSet)) |
|
if exemplarGlyph not in coverage: |
|
|
|
|
|
|
|
|
|
nullRow = None |
|
if nullRow is None: |
|
nullRow = ot.Class1Record() |
|
class2records = nullRow.Class2Record = [] |
|
|
|
for _ in range(l.Class2Count): |
|
if transparent: |
|
rec2 = None |
|
else: |
|
rec2 = ot.Class2Record() |
|
rec2.Value1 = ( |
|
otBase.ValueRecord(self.ValueFormat1) |
|
if self.ValueFormat1 |
|
else None |
|
) |
|
rec2.Value2 = ( |
|
otBase.ValueRecord(self.ValueFormat2) |
|
if self.ValueFormat2 |
|
else None |
|
) |
|
class2records.append(rec2) |
|
rec1 = nullRow |
|
else: |
|
klass = classDef1.get(exemplarGlyph, 0) |
|
rec1 = matrix[klass] |
|
class1Records.append(rec1) |
|
new_matrices.append(class1Records) |
|
matrices = new_matrices |
|
del new_matrices |
|
|
|
|
|
self.ClassDef2, classes = _ClassDef_merge_classify([l.ClassDef2 for l in lst]) |
|
self.Class2Count = len(classes) |
|
new_matrices = [] |
|
for l, matrix in zip(lst, matrices): |
|
classDef2 = l.ClassDef2.classDefs |
|
class1Records = [] |
|
for rec1old in matrix: |
|
oldClass2Records = rec1old.Class2Record |
|
rec1new = ot.Class1Record() |
|
class2Records = rec1new.Class2Record = [] |
|
for classSet in classes: |
|
if not classSet: |
|
rec2 = oldClass2Records[0] |
|
else: |
|
exemplarGlyph = next(iter(classSet)) |
|
klass = classDef2.get(exemplarGlyph, 0) |
|
rec2 = oldClass2Records[klass] |
|
class2Records.append(copy.deepcopy(rec2)) |
|
class1Records.append(rec1new) |
|
new_matrices.append(class1Records) |
|
matrices = new_matrices |
|
del new_matrices |
|
|
|
return matrices |
|
|
|
|
|
def _PairPosFormat2_merge(self, lst, merger): |
|
assert allEqual( |
|
[l.ValueFormat2 == 0 for l in lst if l.Class1Record] |
|
), "Report bug against fonttools." |
|
|
|
merger.mergeObjects( |
|
self, |
|
lst, |
|
exclude=( |
|
"Coverage", |
|
"ClassDef1", |
|
"Class1Count", |
|
"ClassDef2", |
|
"Class2Count", |
|
"Class1Record", |
|
"ValueFormat1", |
|
"ValueFormat2", |
|
), |
|
) |
|
|
|
|
|
glyphs, _ = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst]) |
|
self.Coverage.glyphs = glyphs |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for l, subtables in zip(lst, merger.lookup_subtables): |
|
if l.Coverage.glyphs != glyphs: |
|
assert l == subtables[-1] |
|
|
|
matrices = _PairPosFormat2_align_matrices(self, lst, merger.font) |
|
|
|
self.Class1Record = list(matrices[0]) |
|
merger.mergeLists(self.Class1Record, matrices) |
|
|
|
|
|
@AligningMerger.merger(ot.PairPos) |
|
def merge(merger, self, lst): |
|
merger.valueFormat1 = self.ValueFormat1 = reduce( |
|
int.__or__, [l.ValueFormat1 for l in lst], 0 |
|
) |
|
merger.valueFormat2 = self.ValueFormat2 = reduce( |
|
int.__or__, [l.ValueFormat2 for l in lst], 0 |
|
) |
|
|
|
if self.Format == 1: |
|
_PairPosFormat1_merge(self, lst, merger) |
|
elif self.Format == 2: |
|
_PairPosFormat2_merge(self, lst, merger) |
|
else: |
|
raise UnsupportedFormat(merger, subtable="pair positioning lookup") |
|
|
|
del merger.valueFormat1, merger.valueFormat2 |
|
|
|
|
|
|
|
vf1 = 0 |
|
vf2 = 0 |
|
if self.Format == 1: |
|
for pairSet in self.PairSet: |
|
for pairValueRecord in pairSet.PairValueRecord: |
|
pv1 = getattr(pairValueRecord, "Value1", None) |
|
if pv1 is not None: |
|
vf1 |= pv1.getFormat() |
|
pv2 = getattr(pairValueRecord, "Value2", None) |
|
if pv2 is not None: |
|
vf2 |= pv2.getFormat() |
|
elif self.Format == 2: |
|
for class1Record in self.Class1Record: |
|
for class2Record in class1Record.Class2Record: |
|
pv1 = getattr(class2Record, "Value1", None) |
|
if pv1 is not None: |
|
vf1 |= pv1.getFormat() |
|
pv2 = getattr(class2Record, "Value2", None) |
|
if pv2 is not None: |
|
vf2 |= pv2.getFormat() |
|
self.ValueFormat1 = vf1 |
|
self.ValueFormat2 = vf2 |
|
|
|
|
|
def _MarkBasePosFormat1_merge(self, lst, merger, Mark="Mark", Base="Base"): |
|
self.ClassCount = max(l.ClassCount for l in lst) |
|
|
|
MarkCoverageGlyphs, MarkRecords = _merge_GlyphOrders( |
|
merger.font, |
|
[getattr(l, Mark + "Coverage").glyphs for l in lst], |
|
[getattr(l, Mark + "Array").MarkRecord for l in lst], |
|
) |
|
getattr(self, Mark + "Coverage").glyphs = MarkCoverageGlyphs |
|
|
|
BaseCoverageGlyphs, BaseRecords = _merge_GlyphOrders( |
|
merger.font, |
|
[getattr(l, Base + "Coverage").glyphs for l in lst], |
|
[getattr(getattr(l, Base + "Array"), Base + "Record") for l in lst], |
|
) |
|
getattr(self, Base + "Coverage").glyphs = BaseCoverageGlyphs |
|
|
|
|
|
records = [] |
|
for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)): |
|
allClasses = [r.Class for r in glyphRecords if r is not None] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not allEqual(allClasses): |
|
raise ShouldBeConstant(merger, expected=allClasses[0], got=allClasses) |
|
else: |
|
rec = ot.MarkRecord() |
|
rec.Class = allClasses[0] |
|
allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords] |
|
if allNone(allAnchors): |
|
anchor = None |
|
else: |
|
anchor = ot.Anchor() |
|
anchor.Format = 1 |
|
merger.mergeThings(anchor, allAnchors) |
|
rec.MarkAnchor = anchor |
|
records.append(rec) |
|
array = ot.MarkArray() |
|
array.MarkRecord = records |
|
array.MarkCount = len(records) |
|
setattr(self, Mark + "Array", array) |
|
|
|
|
|
records = [] |
|
for g, glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)): |
|
if allNone(glyphRecords): |
|
rec = None |
|
else: |
|
rec = getattr(ot, Base + "Record")() |
|
anchors = [] |
|
setattr(rec, Base + "Anchor", anchors) |
|
glyphAnchors = [ |
|
[] if r is None else getattr(r, Base + "Anchor") for r in glyphRecords |
|
] |
|
for l in glyphAnchors: |
|
l.extend([None] * (self.ClassCount - len(l))) |
|
for allAnchors in zip(*glyphAnchors): |
|
if allNone(allAnchors): |
|
anchor = None |
|
else: |
|
anchor = ot.Anchor() |
|
anchor.Format = 1 |
|
merger.mergeThings(anchor, allAnchors) |
|
anchors.append(anchor) |
|
records.append(rec) |
|
array = getattr(ot, Base + "Array")() |
|
setattr(array, Base + "Record", records) |
|
setattr(array, Base + "Count", len(records)) |
|
setattr(self, Base + "Array", array) |
|
|
|
|
|
@AligningMerger.merger(ot.MarkBasePos) |
|
def merge(merger, self, lst): |
|
if not allEqualTo(self.Format, (l.Format for l in lst)): |
|
raise InconsistentFormats( |
|
merger, |
|
subtable="mark-to-base positioning lookup", |
|
expected=self.Format, |
|
got=[l.Format for l in lst], |
|
) |
|
if self.Format == 1: |
|
_MarkBasePosFormat1_merge(self, lst, merger) |
|
else: |
|
raise UnsupportedFormat(merger, subtable="mark-to-base positioning lookup") |
|
|
|
|
|
@AligningMerger.merger(ot.MarkMarkPos) |
|
def merge(merger, self, lst): |
|
if not allEqualTo(self.Format, (l.Format for l in lst)): |
|
raise InconsistentFormats( |
|
merger, |
|
subtable="mark-to-mark positioning lookup", |
|
expected=self.Format, |
|
got=[l.Format for l in lst], |
|
) |
|
if self.Format == 1: |
|
_MarkBasePosFormat1_merge(self, lst, merger, "Mark1", "Mark2") |
|
else: |
|
raise UnsupportedFormat(merger, subtable="mark-to-mark positioning lookup") |
|
|
|
|
|
def _PairSet_flatten(lst, font): |
|
self = ot.PairSet() |
|
self.Coverage = ot.Coverage() |
|
|
|
|
|
glyphs, padded = _merge_GlyphOrders( |
|
font, |
|
[[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], |
|
[vs.PairValueRecord for vs in lst], |
|
) |
|
|
|
self.Coverage.glyphs = glyphs |
|
self.PairValueRecord = pvrs = [] |
|
for values in zip(*padded): |
|
for v in values: |
|
if v is not None: |
|
pvrs.append(v) |
|
break |
|
else: |
|
assert False |
|
self.PairValueCount = len(self.PairValueRecord) |
|
|
|
return self |
|
|
|
|
|
def _Lookup_PairPosFormat1_subtables_flatten(lst, font): |
|
assert allEqual( |
|
[l.ValueFormat2 == 0 for l in lst if l.PairSet] |
|
), "Report bug against fonttools." |
|
|
|
self = ot.PairPos() |
|
self.Format = 1 |
|
self.Coverage = ot.Coverage() |
|
self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) |
|
self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) |
|
|
|
|
|
glyphs, padded = _merge_GlyphOrders( |
|
font, [v.Coverage.glyphs for v in lst], [v.PairSet for v in lst] |
|
) |
|
|
|
self.Coverage.glyphs = glyphs |
|
self.PairSet = [ |
|
_PairSet_flatten([v for v in values if v is not None], font) |
|
for values in zip(*padded) |
|
] |
|
self.PairSetCount = len(self.PairSet) |
|
return self |
|
|
|
|
|
def _Lookup_PairPosFormat2_subtables_flatten(lst, font): |
|
assert allEqual( |
|
[l.ValueFormat2 == 0 for l in lst if l.Class1Record] |
|
), "Report bug against fonttools." |
|
|
|
self = ot.PairPos() |
|
self.Format = 2 |
|
self.Coverage = ot.Coverage() |
|
self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) |
|
self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) |
|
|
|
|
|
glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst]) |
|
self.Coverage.glyphs = glyphs |
|
|
|
matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True) |
|
|
|
matrix = self.Class1Record = [] |
|
for rows in zip(*matrices): |
|
row = ot.Class1Record() |
|
matrix.append(row) |
|
row.Class2Record = [] |
|
row = row.Class2Record |
|
for cols in zip(*list(r.Class2Record for r in rows)): |
|
col = next(iter(c for c in cols if c is not None)) |
|
row.append(col) |
|
|
|
return self |
|
|
|
|
|
def _Lookup_PairPos_subtables_canonicalize(lst, font): |
|
"""Merge multiple Format1 subtables at the beginning of lst, |
|
and merge multiple consecutive Format2 subtables that have the same |
|
Class2 (ie. were split because of offset overflows). Returns new list.""" |
|
lst = list(lst) |
|
|
|
l = len(lst) |
|
i = 0 |
|
while i < l and lst[i].Format == 1: |
|
i += 1 |
|
lst[:i] = [_Lookup_PairPosFormat1_subtables_flatten(lst[:i], font)] |
|
|
|
l = len(lst) |
|
i = l |
|
while i > 0 and lst[i - 1].Format == 2: |
|
i -= 1 |
|
lst[i:] = [_Lookup_PairPosFormat2_subtables_flatten(lst[i:], font)] |
|
|
|
return lst |
|
|
|
|
|
def _Lookup_SinglePos_subtables_flatten(lst, font, min_inclusive_rec_format): |
|
glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst], None) |
|
num_glyphs = len(glyphs) |
|
new = ot.SinglePos() |
|
new.Format = 2 |
|
new.ValueFormat = min_inclusive_rec_format |
|
new.Coverage = ot.Coverage() |
|
new.Coverage.glyphs = glyphs |
|
new.ValueCount = num_glyphs |
|
new.Value = [None] * num_glyphs |
|
for singlePos in lst: |
|
if singlePos.Format == 1: |
|
val_rec = singlePos.Value |
|
for gname in singlePos.Coverage.glyphs: |
|
i = glyphs.index(gname) |
|
new.Value[i] = copy.deepcopy(val_rec) |
|
elif singlePos.Format == 2: |
|
for j, gname in enumerate(singlePos.Coverage.glyphs): |
|
val_rec = singlePos.Value[j] |
|
i = glyphs.index(gname) |
|
new.Value[i] = copy.deepcopy(val_rec) |
|
return [new] |
|
|
|
|
|
@AligningMerger.merger(ot.CursivePos) |
|
def merge(merger, self, lst): |
|
|
|
glyphs, padded = _merge_GlyphOrders( |
|
merger.font, |
|
[l.Coverage.glyphs for l in lst], |
|
[l.EntryExitRecord for l in lst], |
|
) |
|
|
|
self.Format = 1 |
|
self.Coverage = ot.Coverage() |
|
self.Coverage.glyphs = glyphs |
|
self.EntryExitRecord = [] |
|
for _ in glyphs: |
|
rec = ot.EntryExitRecord() |
|
rec.EntryAnchor = ot.Anchor() |
|
rec.EntryAnchor.Format = 1 |
|
rec.ExitAnchor = ot.Anchor() |
|
rec.ExitAnchor.Format = 1 |
|
self.EntryExitRecord.append(rec) |
|
merger.mergeLists(self.EntryExitRecord, padded) |
|
self.EntryExitCount = len(self.EntryExitRecord) |
|
|
|
|
|
@AligningMerger.merger(ot.EntryExitRecord) |
|
def merge(merger, self, lst): |
|
if all(master.EntryAnchor is None for master in lst): |
|
self.EntryAnchor = None |
|
if all(master.ExitAnchor is None for master in lst): |
|
self.ExitAnchor = None |
|
merger.mergeObjects(self, lst) |
|
|
|
|
|
@AligningMerger.merger(ot.Lookup) |
|
def merge(merger, self, lst): |
|
subtables = merger.lookup_subtables = [l.SubTable for l in lst] |
|
|
|
|
|
for l, sts in list(zip(lst, subtables)) + [(self, self.SubTable)]: |
|
if not sts: |
|
continue |
|
if sts[0].__class__.__name__.startswith("Extension"): |
|
if not allEqual([st.__class__ for st in sts]): |
|
raise InconsistentExtensions( |
|
merger, |
|
expected="Extension", |
|
got=[st.__class__.__name__ for st in sts], |
|
) |
|
if not allEqual([st.ExtensionLookupType for st in sts]): |
|
raise InconsistentExtensions(merger) |
|
l.LookupType = sts[0].ExtensionLookupType |
|
new_sts = [st.ExtSubTable for st in sts] |
|
del sts[:] |
|
sts.extend(new_sts) |
|
|
|
isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos) |
|
|
|
if isPairPos: |
|
|
|
|
|
|
|
self.SubTable = _Lookup_PairPos_subtables_canonicalize( |
|
self.SubTable, merger.font |
|
) |
|
subtables = merger.lookup_subtables = [ |
|
_Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables |
|
] |
|
else: |
|
isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos) |
|
if isSinglePos: |
|
numSubtables = [len(st) for st in subtables] |
|
if not all([nums == numSubtables[0] for nums in numSubtables]): |
|
|
|
|
|
|
|
valueFormatList = [t.ValueFormat for st in subtables for t in st] |
|
|
|
mirf = reduce(ior, valueFormatList) |
|
self.SubTable = _Lookup_SinglePos_subtables_flatten( |
|
self.SubTable, merger.font, mirf |
|
) |
|
subtables = merger.lookup_subtables = [ |
|
_Lookup_SinglePos_subtables_flatten(st, merger.font, mirf) |
|
for st in subtables |
|
] |
|
flattened = True |
|
else: |
|
flattened = False |
|
|
|
merger.mergeLists(self.SubTable, subtables) |
|
self.SubTableCount = len(self.SubTable) |
|
|
|
if isPairPos: |
|
|
|
assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1 |
|
if not self.SubTable[0].Coverage.glyphs: |
|
self.SubTable.pop(0) |
|
self.SubTableCount -= 1 |
|
|
|
|
|
assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2 |
|
if not self.SubTable[-1].Coverage.glyphs: |
|
self.SubTable.pop(-1) |
|
self.SubTableCount -= 1 |
|
|
|
|
|
|
|
|
|
|
|
level = merger.font.cfg.get( |
|
"fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL", |
|
default=_compression_level_from_env(), |
|
) |
|
if level != 0: |
|
log.info("Compacting GPOS...") |
|
self.SubTable = compact_pair_pos(merger.font, level, self.SubTable) |
|
self.SubTableCount = len(self.SubTable) |
|
|
|
elif isSinglePos and flattened: |
|
singlePosTable = self.SubTable[0] |
|
glyphs = singlePosTable.Coverage.glyphs |
|
|
|
|
|
singlePosMapping = { |
|
gname: valRecord for gname, valRecord in zip(glyphs, singlePosTable.Value) |
|
} |
|
self.SubTable = buildSinglePos( |
|
singlePosMapping, merger.font.getReverseGlyphMap() |
|
) |
|
merger.mergeObjects(self, lst, exclude=["SubTable", "SubTableCount"]) |
|
|
|
del merger.lookup_subtables |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InstancerMerger(AligningMerger): |
|
"""A merger that takes multiple master fonts, and instantiates |
|
an instance.""" |
|
|
|
def __init__(self, font, model, location): |
|
Merger.__init__(self, font) |
|
self.model = model |
|
self.location = location |
|
self.masterScalars = model.getMasterScalars(location) |
|
|
|
|
|
@InstancerMerger.merger(ot.CaretValue) |
|
def merge(merger, self, lst): |
|
assert self.Format == 1 |
|
Coords = [a.Coordinate for a in lst] |
|
model = merger.model |
|
masterScalars = merger.masterScalars |
|
self.Coordinate = otRound( |
|
model.interpolateFromValuesAndScalars(Coords, masterScalars) |
|
) |
|
|
|
|
|
@InstancerMerger.merger(ot.Anchor) |
|
def merge(merger, self, lst): |
|
assert self.Format == 1 |
|
XCoords = [a.XCoordinate for a in lst] |
|
YCoords = [a.YCoordinate for a in lst] |
|
model = merger.model |
|
masterScalars = merger.masterScalars |
|
self.XCoordinate = otRound( |
|
model.interpolateFromValuesAndScalars(XCoords, masterScalars) |
|
) |
|
self.YCoordinate = otRound( |
|
model.interpolateFromValuesAndScalars(YCoords, masterScalars) |
|
) |
|
|
|
|
|
@InstancerMerger.merger(otBase.ValueRecord) |
|
def merge(merger, self, lst): |
|
model = merger.model |
|
masterScalars = merger.masterScalars |
|
|
|
for name, tableName in [ |
|
("XAdvance", "XAdvDevice"), |
|
("YAdvance", "YAdvDevice"), |
|
("XPlacement", "XPlaDevice"), |
|
("YPlacement", "YPlaDevice"), |
|
]: |
|
assert not hasattr(self, tableName) |
|
|
|
if hasattr(self, name): |
|
values = [getattr(a, name, 0) for a in lst] |
|
value = otRound( |
|
model.interpolateFromValuesAndScalars(values, masterScalars) |
|
) |
|
setattr(self, name, value) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MutatorMerger(AligningMerger): |
|
"""A merger that takes a variable font, and instantiates |
|
an instance. While there's no "merging" to be done per se, |
|
the operation can benefit from many operations that the |
|
aligning merger does.""" |
|
|
|
def __init__(self, font, instancer, deleteVariations=True): |
|
Merger.__init__(self, font) |
|
self.instancer = instancer |
|
self.deleteVariations = deleteVariations |
|
|
|
|
|
@MutatorMerger.merger(ot.CaretValue) |
|
def merge(merger, self, lst): |
|
|
|
self.__dict__ = lst[0].__dict__.copy() |
|
|
|
if self.Format != 3: |
|
return |
|
|
|
instancer = merger.instancer |
|
dev = self.DeviceTable |
|
if merger.deleteVariations: |
|
del self.DeviceTable |
|
if dev: |
|
assert dev.DeltaFormat == 0x8000 |
|
varidx = (dev.StartSize << 16) + dev.EndSize |
|
delta = otRound(instancer[varidx]) |
|
self.Coordinate += delta |
|
|
|
if merger.deleteVariations: |
|
self.Format = 1 |
|
|
|
|
|
@MutatorMerger.merger(ot.Anchor) |
|
def merge(merger, self, lst): |
|
|
|
self.__dict__ = lst[0].__dict__.copy() |
|
|
|
if self.Format != 3: |
|
return |
|
|
|
instancer = merger.instancer |
|
for v in "XY": |
|
tableName = v + "DeviceTable" |
|
if not hasattr(self, tableName): |
|
continue |
|
dev = getattr(self, tableName) |
|
if merger.deleteVariations: |
|
delattr(self, tableName) |
|
if dev is None: |
|
continue |
|
|
|
assert dev.DeltaFormat == 0x8000 |
|
varidx = (dev.StartSize << 16) + dev.EndSize |
|
delta = otRound(instancer[varidx]) |
|
|
|
attr = v + "Coordinate" |
|
setattr(self, attr, getattr(self, attr) + delta) |
|
|
|
if merger.deleteVariations: |
|
self.Format = 1 |
|
|
|
|
|
@MutatorMerger.merger(otBase.ValueRecord) |
|
def merge(merger, self, lst): |
|
|
|
self.__dict__ = lst[0].__dict__.copy() |
|
|
|
instancer = merger.instancer |
|
for name, tableName in [ |
|
("XAdvance", "XAdvDevice"), |
|
("YAdvance", "YAdvDevice"), |
|
("XPlacement", "XPlaDevice"), |
|
("YPlacement", "YPlaDevice"), |
|
]: |
|
if not hasattr(self, tableName): |
|
continue |
|
dev = getattr(self, tableName) |
|
if merger.deleteVariations: |
|
delattr(self, tableName) |
|
if dev is None: |
|
continue |
|
|
|
assert dev.DeltaFormat == 0x8000 |
|
varidx = (dev.StartSize << 16) + dev.EndSize |
|
delta = otRound(instancer[varidx]) |
|
|
|
setattr(self, name, getattr(self, name, 0) + delta) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VariationMerger(AligningMerger): |
|
"""A merger that takes multiple master fonts, and builds a |
|
variable font.""" |
|
|
|
def __init__(self, model, axisTags, font): |
|
Merger.__init__(self, font) |
|
self.store_builder = varStore.OnlineVarStoreBuilder(axisTags) |
|
self.setModel(model) |
|
|
|
def setModel(self, model): |
|
self.model = model |
|
self.store_builder.setModel(model) |
|
|
|
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 |
|
|
|
|
|
|
|
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) |
|
|
|
super(VariationMerger, self).mergeThings(out, lst) |
|
|
|
if masterModel: |
|
self.setModel(masterModel) |
|
if origTTFs: |
|
self.ttfs = origTTFs |
|
|
|
|
|
def buildVarDevTable(store_builder, master_values): |
|
if allEqual(master_values): |
|
return master_values[0], None |
|
base, varIdx = store_builder.storeMasters(master_values) |
|
return base, builder.buildVarDevTable(varIdx) |
|
|
|
|
|
@VariationMerger.merger(ot.BaseCoord) |
|
def merge(merger, self, lst): |
|
if self.Format != 1: |
|
raise UnsupportedFormat(merger, subtable="a baseline coordinate") |
|
self.Coordinate, DeviceTable = buildVarDevTable( |
|
merger.store_builder, [a.Coordinate for a in lst] |
|
) |
|
if DeviceTable: |
|
self.Format = 3 |
|
self.DeviceTable = DeviceTable |
|
|
|
|
|
@VariationMerger.merger(ot.CaretValue) |
|
def merge(merger, self, lst): |
|
if self.Format != 1: |
|
raise UnsupportedFormat(merger, subtable="a caret") |
|
self.Coordinate, DeviceTable = buildVarDevTable( |
|
merger.store_builder, [a.Coordinate for a in lst] |
|
) |
|
if DeviceTable: |
|
self.Format = 3 |
|
self.DeviceTable = DeviceTable |
|
|
|
|
|
@VariationMerger.merger(ot.Anchor) |
|
def merge(merger, self, lst): |
|
if self.Format != 1: |
|
raise UnsupportedFormat(merger, subtable="an anchor") |
|
self.XCoordinate, XDeviceTable = buildVarDevTable( |
|
merger.store_builder, [a.XCoordinate for a in lst] |
|
) |
|
self.YCoordinate, YDeviceTable = buildVarDevTable( |
|
merger.store_builder, [a.YCoordinate for a in lst] |
|
) |
|
if XDeviceTable or YDeviceTable: |
|
self.Format = 3 |
|
self.XDeviceTable = XDeviceTable |
|
self.YDeviceTable = YDeviceTable |
|
|
|
|
|
@VariationMerger.merger(otBase.ValueRecord) |
|
def merge(merger, self, lst): |
|
for name, tableName in [ |
|
("XAdvance", "XAdvDevice"), |
|
("YAdvance", "YAdvDevice"), |
|
("XPlacement", "XPlaDevice"), |
|
("YPlacement", "YPlaDevice"), |
|
]: |
|
if hasattr(self, name): |
|
value, deviceTable = buildVarDevTable( |
|
merger.store_builder, [getattr(a, name, 0) for a in 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) |
|
|
|
|
|
self.varIndexCache = {} |
|
|
|
self.varIdxes = [] |
|
|
|
|
|
self.varTableIds = set() |
|
|
|
self.layers = [] |
|
self.layerReuseCache = None |
|
if allowLayerReuse: |
|
self.layerReuseCache = LayerReuseCache() |
|
|
|
self._doneBaseGlyphs = False |
|
|
|
def mergeTables(self, font, master_ttfs, tableTags=("COLR",)): |
|
if "COLR" in tableTags and "COLR" in font: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
key = tuple(varIdxes) |
|
varIndexBase = self.varIndexCache.get(key) |
|
|
|
if varIndexBase is None: |
|
|
|
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: |
|
|
|
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: |
|
|
|
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: |
|
|
|
return |
|
uniqueLayerIDs = set() |
|
newLayerList = [] |
|
for rec in colr.BaseGlyphList.BaseGlyphPaintRecord: |
|
frontier = [rec.Paint] |
|
while frontier: |
|
paint = frontier.pop() |
|
if paint.Format == ot.PaintFormat.PaintColrGlyph: |
|
|
|
continue |
|
elif paint.Format == ot.PaintFormat.PaintColrLayers: |
|
|
|
|
|
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)) |
|
|
|
assert len(newLayerList) == len(uniqueLayerIDs) |
|
colr.LayerList.Paint = newLayerList |
|
colr.LayerList.LayerCount = len(newLayerList) |
|
|
|
|
|
@COLRVariationMerger.merger(ot.BaseGlyphList) |
|
def merge(merger, self, lst): |
|
|
|
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: |
|
|
|
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): |
|
|
|
|
|
assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList" |
|
|
|
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): |
|
|
|
|
|
|
|
out_layers = list(_flatten_layers(out, self.font["COLR"].table)) |
|
|
|
|
|
|
|
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: |
|
|
|
|
|
e.stack.append(".Layers") |
|
raise |
|
|
|
|
|
|
|
|
|
if self.layerReuseCache is not None: |
|
|
|
out_layers = self.layerReuseCache.try_reuse(out_layers) |
|
|
|
|
|
is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT |
|
out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT) |
|
|
|
|
|
|
|
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.extend(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: |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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()] |
|
|
|
|
|
isVariable = varIndexBase != ot.NO_VARIATION_INDEX or any( |
|
id(table) in merger.varTableIds for table in subTables |
|
) |
|
|
|
if isVariable: |
|
if varAttrs: |
|
|
|
|
|
|
|
self.VarIndexBase = varIndexBase |
|
|
|
if subTables: |
|
|
|
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 |
|
|
|
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): |
|
|
|
|
|
merger.mergeSparseDict(self, lst) |
|
|