Spaces:
Running
Running
""" | |
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 | |
# mergeTables populates this from the parent's master ttfs | |
self.ttfs = None | |
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 # Support multiple names of a clazz | |
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 | |
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): | |
# 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): | |
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 | |
# | |
# Aligning merger | |
# | |
class AligningMerger(Merger): | |
pass | |
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 = {} | |
# We only care about the .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) | |
# Make sure all input glyphsets were in proper order | |
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 | |
def merge(merger, self, lst): | |
# Code below sometimes calls us with self being | |
# a new object. Copy it from lst and recurse. | |
self.__dict__ = lst[0].__dict__.copy() | |
merger.mergeObjects(self, lst) | |
def merge(merger, self, lst): | |
# Code below sometimes calls us with self being | |
# a new object. Copy it from lst and recurse. | |
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: # TODO Speed up | |
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 | |
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") | |
# If all have same coverage table and all are format 1, | |
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: | |
# If v.Value is None, it means a kerning of 0; we want | |
# it to participate in the model still. | |
# https://github.com/fonttools/fonttools/issues/3111 | |
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 | |
# Upgrade everything to Format=2 | |
self.Format = 2 | |
lst = [_SinglePosUpgradeToFormat2(v) for v in lst] | |
# Align them | |
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 | |
# Fill in value from other subtables | |
# Note!!! This *might* result in behavior change if ValueFormat2-zeroedness | |
# is different between used subtable and current subtable! | |
# TODO(behdad) Check and warn if that happens? | |
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) | |
# Merge everything else; though, there shouldn't be anything else. :) | |
merger.mergeObjects( | |
self, lst, exclude=("Format", "Coverage", "Value", "ValueCount", "ValueFormat") | |
) | |
self.ValueFormat = reduce( | |
int.__or__, [v.getEffectiveFormat() for v in self.Value], 0 | |
) | |
def merge(merger, self, lst): | |
# Align them | |
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): | |
# Fill in value from other subtables | |
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." | |
# Merge everything else; makes sure Format is the same. | |
merger.mergeObjects( | |
self, | |
lst, | |
exclude=("Coverage", "PairSet", "PairSetCount", "ValueFormat1", "ValueFormat2"), | |
) | |
empty = ot.PairSet() | |
empty.PairValueRecord = [] | |
empty.PairValueCount = 0 | |
# Align them | |
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) | |
# Class-0 is special. It's "everything else". | |
if allGlyphs is None: | |
ret[0] = None | |
else: | |
# Limit all classes to glyphs in allGlyphs. | |
# Collect anything without a non-zero class into class=zero. | |
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] | |
# Align first classes | |
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: | |
# Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f, | |
# Fixes https://github.com/googlei18n/fontmake/issues/470 | |
# Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9 | |
# when merger becomes selfless. | |
nullRow = None | |
if nullRow is None: | |
nullRow = ot.Class1Record() | |
class2records = nullRow.Class2Record = [] | |
# TODO: When merger becomes selfless, revert e6125b353e1f54a0280ded5434b8e40d042de69f | |
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] # TODO handle out-of-range? | |
class1Records.append(rec1) | |
new_matrices.append(class1Records) | |
matrices = new_matrices | |
del new_matrices | |
# Align second classes | |
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: # class=0 | |
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", | |
), | |
) | |
# Align coverages | |
glyphs, _ = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst]) | |
self.Coverage.glyphs = glyphs | |
# Currently, if the coverage of PairPosFormat2 subtables are different, | |
# we do NOT bother walking down the subtable list when filling in new | |
# rows for alignment. As such, this is only correct if current subtable | |
# is the last subtable in the lookup. Ensure that. | |
# | |
# Note that our canonicalization process merges trailing PairPosFormat2's, | |
# so in reality this is rare. | |
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]) # TODO move merger to be selfless | |
merger.mergeLists(self.Class1Record, matrices) | |
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 | |
# Now examine the list of value records, and update to the union of format values, | |
# as merge might have created new values. | |
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 | |
# MarkArray | |
records = [] | |
for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)): | |
allClasses = [r.Class for r in glyphRecords if r is not None] | |
# TODO Right now we require that all marks have same class in | |
# all masters that cover them. This is not required. | |
# | |
# We can relax that by just requiring that all marks that have | |
# the same class in a master, have the same class in every other | |
# master. Indeed, if, say, a sparse master only covers one mark, | |
# that mark probably will get class 0, which would possibly be | |
# different from its class in other masters. | |
# | |
# We can even go further and reclassify marks to support any | |
# input. But, since, it's unlikely that two marks being both, | |
# say, "top" in one master, and one being "top" and other being | |
# "top-right" in another master, we shouldn't do that, as any | |
# failures in that case will probably signify mistakes in the | |
# input masters. | |
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) | |
# BaseArray | |
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) | |
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") | |
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() | |
# Align them | |
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) | |
# Align them | |
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) | |
# Align them | |
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] | |
def merge(merger, self, lst): | |
# Align them | |
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) | |
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) | |
def merge(merger, self, lst): | |
subtables = merger.lookup_subtables = [l.SubTable for l in lst] | |
# Remove Extension subtables | |
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: | |
# AFDKO and feaLib sometimes generate two Format1 subtables instead of one. | |
# Merge those before continuing. | |
# https://github.com/fonttools/fonttools/issues/719 | |
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]): | |
# Flatten list of SinglePos subtables to single Format 2 subtable, | |
# with all value records set to the rec format type. | |
# We use buildSinglePos() to optimize the lookup after merging. | |
valueFormatList = [t.ValueFormat for st in subtables for t in st] | |
# Find the minimum value record that can accomodate all the singlePos subtables. | |
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: | |
# If format-1 subtable created during canonicalization is empty, remove it. | |
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 | |
# If format-2 subtable created during canonicalization is empty, remove it. | |
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 | |
# Compact the merged subtables | |
# This is a good moment to do it because the compaction should create | |
# smaller subtables, which may prevent overflows from happening. | |
# Keep reading the value from the ENV until ufo2ft switches to the config system | |
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 | |
# We know that singlePosTable is Format 2, as this is set | |
# in _Lookup_SinglePos_subtables_flatten. | |
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 | |
# | |
# InstancerMerger | |
# | |
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) | |
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) | |
) | |
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) | |
) | |
def merge(merger, self, lst): | |
model = merger.model | |
masterScalars = merger.masterScalars | |
# TODO Handle differing valueformats | |
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) | |
# | |
# MutatorMerger | |
# | |
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 | |
def merge(merger, self, lst): | |
# Hack till we become selfless. | |
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 | |
def merge(merger, self, lst): | |
# Hack till we become selfless. | |
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 | |
def merge(merger, self, lst): | |
# Hack till we become selfless. | |
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) | |
# | |
# VariationMerger | |
# | |
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 | |
# 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) | |
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) | |
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 | |
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 | |
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 | |
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) | |
# 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 | |
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) | |
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) | |
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 | |
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.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: | |
# 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) | |
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) | |
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)) | |
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)) | |
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) | |