diff options
Diffstat (limited to 'Lib/fontTools/varLib/merger.py')
-rw-r--r-- | Lib/fontTools/varLib/merger.py | 2762 |
1 files changed, 1464 insertions, 1298 deletions
diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py index c9a1d3e3..b2c34016 100644 --- a/Lib/fontTools/varLib/merger.py +++ b/Lib/fontTools/varLib/merger.py @@ -21,8 +21,8 @@ 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, + _compression_level_from_env, + compact_pair_pos, ) log = logging.getLogger("fontTools.varLib.merger") @@ -41,1502 +41,1668 @@ from .errors import ( VarLibMergeError, ) + class Merger(object): + def __init__(self, font=None): + self.font = font + # mergeTables populates this from the parent's master ttfs + self.ttfs = None + + @classmethod + def merger(celf, clazzes, attrs=(None,)): + assert celf != Merger, "Subclass Merger instead." + if "mergers" not in celf.__dict__: + celf.mergers = {} + if type(clazzes) 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 + + @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): + # 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 - def __init__(self, font=None): - self.font = font - # mergeTables populates this from the parent's master ttfs - self.ttfs = None - - @classmethod - def merger(celf, clazzes, attrs=(None,)): - assert celf != Merger, 'Subclass Merger instead.' - if 'mergers' not in celf.__dict__: - celf.mergers = {} - if type(clazzes) 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 - - @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): - # 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 + 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 = {} - # 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] + 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 + 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) - 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 - 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 + """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 + @AligningMerger.merger(otBase.ValueRecord) 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) + # 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) + @AligningMerger.merger(ot.Anchor) 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) + # 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 + 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 + @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") - - # 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: - merger.mergeThings(self.Value, [v.Value 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) + 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 + ) + @AligningMerger.merger(ot.PairSet) 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) + # 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." + 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) - # 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 +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 - # Align them - glyphs, padded = _merge_GlyphOrders(merger.font, - [v.Coverage.glyphs for v in lst], - [v.PairSet for v in lst], - default=empty) + ret = [] + for _ in range(m + 1): + ret.append(set()) - 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 + for k, v in classDefs.items(): + ret[v].add(k) - merger.mergeLists(self.PairSet, padded) + # 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) -def _ClassDef_invert(self, allGlyphs=None): + return ret - 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()) +def _ClassDef_merge_classify(lst, allGlyphses=None): + self = ot.ClassDef() + self.classDefs = classDefs = {} + allGlyphsesWasNone = allGlyphses is None + if allGlyphsesWasNone: + allGlyphses = [None] * len(lst) - for k,v in classDefs.items(): - ret[v].add(k) + 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() - # 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) + if allGlyphsesWasNone: + classes.insert(0, set()) - return ret + for i, classSet in enumerate(classes): + if i == 0: + continue + for g in classSet: + classDefs[g] = i + + return self, classes -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 - 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) + 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) + @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 - - # 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) + 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) + @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") + 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") + 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." +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 - 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]) +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 - 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." + 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) - 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 - # 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) - 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) - 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 - 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) + """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 = 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)] + 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 - 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] + 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): + # 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) + + +@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] - - # 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(), + 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 ) - 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 + 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.""" + """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.scalars = model.getScalars(location) - def __init__(self, font, model, location): - Merger.__init__(self, font) - self.model = model - self.location = location - self.scalars = model.getScalars(location) @InstancerMerger.merger(ot.CaretValue) def merge(merger, self, lst): - assert self.Format == 1 - Coords = [a.Coordinate for a in lst] - model = merger.model - scalars = merger.scalars - self.Coordinate = otRound(model.interpolateFromMastersAndScalars(Coords, scalars)) + assert self.Format == 1 + Coords = [a.Coordinate for a in lst] + model = merger.model + scalars = merger.scalars + self.Coordinate = otRound(model.interpolateFromMastersAndScalars(Coords, scalars)) + @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 - scalars = merger.scalars - self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars)) - self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars)) + assert self.Format == 1 + XCoords = [a.XCoordinate for a in lst] + YCoords = [a.YCoordinate for a in lst] + model = merger.model + scalars = merger.scalars + self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars)) + self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars)) + @InstancerMerger.merger(otBase.ValueRecord) def merge(merger, self, lst): - model = merger.model - scalars = merger.scalars - # 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.interpolateFromMastersAndScalars(values, scalars)) - setattr(self, name, value) + model = merger.model + scalars = merger.scalars + # 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.interpolateFromMastersAndScalars(values, scalars)) + 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.""" + """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 __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): + # Hack till we become selfless. + self.__dict__ = lst[0].__dict__.copy() - # Hack till we become selfless. - self.__dict__ = lst[0].__dict__.copy() + if self.Format != 3: + return - 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 - 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 - if merger.deleteVariations: - self.Format = 1 @MutatorMerger.merger(ot.Anchor) def merge(merger, self, lst): + # Hack till we become selfless. + self.__dict__ = lst[0].__dict__.copy() - # Hack till we become selfless. - self.__dict__ = lst[0].__dict__.copy() + if self.Format != 3: + return - 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 - 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]) - assert dev.DeltaFormat == 0x8000 - varidx = (dev.StartSize << 16) + dev.EndSize - delta = otRound(instancer[varidx]) + attr = v + "Coordinate" + setattr(self, attr, getattr(self, attr) + delta) - attr = v+'Coordinate' - setattr(self, attr, getattr(self, attr) + delta) + if merger.deleteVariations: + self.Format = 1 - if merger.deleteVariations: - self.Format = 1 @MutatorMerger.merger(otBase.ValueRecord) 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) + # 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.""" + """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 __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 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 + 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) + # 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) + masterModel = self.model + model, lst = masterModel.getSubModel(lst) + self.setModel(model) - super(VariationMerger, self).mergeThings(out, lst) + super(VariationMerger, self).mergeThings(out, lst) - if masterModel: - self.setModel(masterModel) - if origTTFs: - self.ttfs = origTTFs + 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) + 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 + 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 + 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 + 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) + 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 - - @classmethod - def convertSubTablesToVarType(cls, table): - for path in dfs_base_table( - table, - skip_root=True, - predicate=lambda path: ( - getattr(type(path[-1].value), "VarType", None) is not None - ) - ): - st = path[-1] - subTable = st.value - varType = type(subTable).VarType - newSubTable = varType() - newSubTable.__dict__.update(subTable.__dict__) - newSubTable.populateDefaults() - parent = path[-2].value - if st.index is not None: - getattr(parent, st.name)[st.index] = newSubTable - else: - setattr(parent, st.name, newSubTable) - - @staticmethod - def expandPaintColrLayers(colr): - """Rebuild LayerList without PaintColrLayers reuse. - - Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph - which are irrelevant for this); any layers referenced via PaintColrLayers are - collected into a new LayerList and duplicated when reuse is detected, to ensure - that all paints are distinct objects at the end of the process. - PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap - is left. Also, any consecutively nested PaintColrLayers are flattened. - The COLR table's LayerList is replaced with the new unique layers. - A side effect is also that any layer from the old LayerList which is not - referenced by any PaintColrLayers is dropped. - """ - if not colr.LayerList: - # if no LayerList, there's nothing to expand - return - uniqueLayerIDs = set() - newLayerList = [] - for rec in colr.BaseGlyphList.BaseGlyphPaintRecord: - frontier = [rec.Paint] - while frontier: - paint = frontier.pop() - if paint.Format == ot.PaintFormat.PaintColrGlyph: - # don't traverse these, we treat them as constant for merging - continue - elif paint.Format == ot.PaintFormat.PaintColrLayers: - # de-treeify any nested PaintColrLayers, append unique copies to - # the new layer list and update PaintColrLayers index/count - children = list(_flatten_layers(paint, colr)) - first_layer_index = len(newLayerList) - for layer in children: - if id(layer) in uniqueLayerIDs: - layer = copy.deepcopy(layer) - assert id(layer) not in uniqueLayerIDs - newLayerList.append(layer) - uniqueLayerIDs.add(id(layer)) - paint.FirstLayerIndex = first_layer_index - paint.NumLayers = len(children) - else: - children = paint.getChildren(colr) - frontier.extend(reversed(children)) - # sanity check all the new layers are distinct objects - assert len(newLayerList) == len(uniqueLayerIDs) - colr.LayerList.Paint = newLayerList - colr.LayerList.LayerCount = len(newLayerList) + """A specialized VariationMerger that takes multiple master fonts containing + COLRv1 tables, and builds a variable COLR font. + + COLR tables are special in that variable subtables can be associated with + multiple delta-set indices (via VarIndexBase). + They also contain tables that must change their type (not simply the Format) + as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes + care of that too. + """ + + def __init__(self, model, axisTags, font, allowLayerReuse=True): + VariationMerger.__init__(self, model, axisTags, font) + # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase + # between variable tables with same varIdxes. + self.varIndexCache = {} + # flat list of all the varIdxes generated while merging + self.varIdxes = [] + # set of id()s of the subtables that contain variations after merging + # and need to be upgraded to the associated VarType. + self.varTableIds = set() + # we keep these around for rebuilding a LayerList while merging PaintColrLayers + self.layers = [] + self.layerReuseCache = None + if allowLayerReuse: + self.layerReuseCache = LayerReuseCache() + # flag to ensure BaseGlyphList is fully merged before LayerList gets processed + self._doneBaseGlyphs = False + + def mergeTables(self, font, master_ttfs, tableTags=("COLR",)): + if "COLR" in tableTags and "COLR" in font: + # The merger modifies the destination COLR table in-place. If this contains + # multiple PaintColrLayers referencing the same layers from LayerList, it's + # a problem because we may risk modifying the same paint more than once, or + # worse, fail while attempting to do that. + # We don't know whether the master COLR table was built with layer reuse + # disabled, thus to be safe we rebuild its LayerList so that it contains only + # unique layers referenced from non-overlapping PaintColrLayers throughout + # the base paint graphs. + self.expandPaintColrLayers(font["COLR"].table) + VariationMerger.mergeTables(self, font, master_ttfs, tableTags) + + def checkFormatEnum(self, out, lst, validate=lambda _: True): + fmt = out.Format + formatEnum = out.formatEnum + ok = False + try: + fmt = formatEnum(fmt) + except ValueError: + pass + else: + ok = validate(fmt) + if not ok: + raise UnsupportedFormat(self, subtable=type(out).__name__, value=fmt) + expected = fmt + got = [] + for v in lst: + fmt = getattr(v, "Format", None) + try: + fmt = formatEnum(fmt) + except ValueError: + pass + got.append(fmt) + if not allEqualTo(expected, got): + raise InconsistentFormats( + self, + subtable=type(out).__name__, + expected=expected, + got=got, + ) + return expected + + def mergeSparseDict(self, out, lst): + for k in out.keys(): + try: + self.mergeThings(out[k], [v.get(k) for v in lst]) + except VarLibMergeError as e: + e.stack.append(f"[{k!r}]") + raise + + def mergeAttrs(self, out, lst, attrs): + for attr in attrs: + value = getattr(out, attr) + values = [getattr(item, attr) for item in lst] + try: + self.mergeThings(value, values) + except VarLibMergeError as e: + e.stack.append(f".{attr}") + raise + + def storeMastersForAttr(self, out, lst, attr): + master_values = [getattr(item, attr) for item in lst] + + # VarStore treats deltas for fixed-size floats as integers, so we + # must convert master values to int before storing them in the builder + # then back to float. + is_fixed_size_float = False + conv = out.getConverterByName(attr) + if isinstance(conv, BaseFixedValue): + is_fixed_size_float = True + master_values = [conv.toInt(v) for v in master_values] + + baseValue = master_values[0] + varIdx = ot.NO_VARIATION_INDEX + if not allEqual(master_values): + baseValue, varIdx = self.store_builder.storeMasters(master_values) + + if is_fixed_size_float: + baseValue = conv.fromInt(baseValue) + + return baseValue, varIdx + + def storeVariationIndices(self, varIdxes) -> int: + # try to reuse an existing VarIndexBase for the same varIdxes, or else + # create a new one + key = tuple(varIdxes) + varIndexBase = self.varIndexCache.get(key) + + if varIndexBase is None: + # scan for a full match anywhere in the self.varIdxes + for i in range(len(self.varIdxes) - len(varIdxes) + 1): + if self.varIdxes[i : i + len(varIdxes)] == varIdxes: + self.varIndexCache[key] = varIndexBase = i + break + + if varIndexBase is None: + # try find a partial match at the end of the self.varIdxes + for n in range(len(varIdxes) - 1, 0, -1): + if self.varIdxes[-n:] == varIdxes[:n]: + varIndexBase = len(self.varIdxes) - n + self.varIndexCache[key] = varIndexBase + self.varIdxes.extend(varIdxes[n:]) + break + + if varIndexBase is None: + # no match found, append at the end + self.varIndexCache[key] = varIndexBase = len(self.varIdxes) + self.varIdxes.extend(varIdxes) + + return varIndexBase + + def mergeVariableAttrs(self, out, lst, attrs) -> int: + varIndexBase = ot.NO_VARIATION_INDEX + varIdxes = [] + for attr in attrs: + baseValue, varIdx = self.storeMastersForAttr(out, lst, attr) + setattr(out, attr, baseValue) + varIdxes.append(varIdx) + + if any(v != ot.NO_VARIATION_INDEX for v in varIdxes): + varIndexBase = self.storeVariationIndices(varIdxes) + + return varIndexBase + + @classmethod + def convertSubTablesToVarType(cls, table): + for path in dfs_base_table( + table, + skip_root=True, + predicate=lambda path: ( + getattr(type(path[-1].value), "VarType", None) is not None + ), + ): + st = path[-1] + subTable = st.value + varType = type(subTable).VarType + newSubTable = varType() + newSubTable.__dict__.update(subTable.__dict__) + newSubTable.populateDefaults() + parent = path[-2].value + if st.index is not None: + getattr(parent, st.name)[st.index] = newSubTable + else: + setattr(parent, st.name, newSubTable) + + @staticmethod + def expandPaintColrLayers(colr): + """Rebuild LayerList without PaintColrLayers reuse. + + Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph + which are irrelevant for this); any layers referenced via PaintColrLayers are + collected into a new LayerList and duplicated when reuse is detected, to ensure + that all paints are distinct objects at the end of the process. + PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap + is left. Also, any consecutively nested PaintColrLayers are flattened. + The COLR table's LayerList is replaced with the new unique layers. + A side effect is also that any layer from the old LayerList which is not + referenced by any PaintColrLayers is dropped. + """ + if not colr.LayerList: + # if no LayerList, there's nothing to expand + return + uniqueLayerIDs = set() + newLayerList = [] + for rec in colr.BaseGlyphList.BaseGlyphPaintRecord: + frontier = [rec.Paint] + while frontier: + paint = frontier.pop() + if paint.Format == ot.PaintFormat.PaintColrGlyph: + # don't traverse these, we treat them as constant for merging + continue + elif paint.Format == ot.PaintFormat.PaintColrLayers: + # de-treeify any nested PaintColrLayers, append unique copies to + # the new layer list and update PaintColrLayers index/count + children = list(_flatten_layers(paint, colr)) + first_layer_index = len(newLayerList) + for layer in children: + if id(layer) in uniqueLayerIDs: + layer = copy.deepcopy(layer) + assert id(layer) not in uniqueLayerIDs + newLayerList.append(layer) + uniqueLayerIDs.add(id(layer)) + paint.FirstLayerIndex = first_layer_index + paint.NumLayers = len(children) + else: + children = paint.getChildren(colr) + frontier.extend(reversed(children)) + # sanity check all the new layers are distinct objects + assert len(newLayerList) == len(uniqueLayerIDs) + colr.LayerList.Paint = newLayerList + colr.LayerList.LayerCount = len(newLayerList) @COLRVariationMerger.merger(ot.BaseGlyphList) def merge(merger, self, lst): - # ignore BaseGlyphCount, allow sparse glyph sets across masters - out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord} - masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst] + # 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 + 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 + merger._doneBaseGlyphs = True @COLRVariationMerger.merger(ot.LayerList) def merge(merger, self, lst): - # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers - # found while traversing the paint graphs rooted at BaseGlyphPaintRecords. - assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList" - # Simply flush the final list of layers and go home. - self.LayerCount = len(merger.layers) - self.Paint = merger.layers + # 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 + 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) + # 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) @COLRVariationMerger.merger((ot.Paint, ot.ClipBox)) def merge(merger, self, lst): - fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable()) + fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable()) - if fmt is ot.PaintFormat.PaintColrLayers: - _merge_PaintColrLayers(merger, self, lst) - return + if fmt is ot.PaintFormat.PaintColrLayers: + _merge_PaintColrLayers(merger, self, lst) + return - varFormat = fmt.as_variable() + 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) + 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) + merger.mergeAttrs(self, lst, staticAttrs) - varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs) + varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs) - subTables = [st.value for st in self.iterSubTables()] + 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) - ) + # 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 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) + if subTables: + # Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc. + merger.convertSubTablesToVarType(self) - assert varFormat is not None - self.Format = int(varFormat) + assert varFormat is not None + self.Format = int(varFormat) @COLRVariationMerger.merger((ot.Affine2x3, ot.ColorStop)) def merge(merger, self, lst): - varType = type(self).VarType + varType = type(self).VarType - varAttrs = otBase.getVariableAttrs(varType) - staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs) + varAttrs = otBase.getVariableAttrs(varType) + staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs) - merger.mergeAttrs(self, lst, staticAttrs) + merger.mergeAttrs(self, lst, staticAttrs) - varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs) + 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)) + if varIndexBase != ot.NO_VARIATION_INDEX: + self.VarIndexBase = varIndexBase + # mark as having variations so the parent table will convert to Var{Type} + merger.varTableIds.add(id(self)) @COLRVariationMerger.merger(ot.ColorLine) def merge(merger, self, lst): - merger.mergeAttrs(self, lst, (c.name for c in self.getConverters())) + 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)) + if any(id(stop) in merger.varTableIds for stop in self.ColorStop): + merger.convertSubTablesToVarType(self) + merger.varTableIds.add(id(self)) @COLRVariationMerger.merger(ot.ClipList, "clips") def merge(merger, self, lst): - # 'sparse' in that we allow non-default masters to omit ClipBox entries - # for some/all glyphs (i.e. they don't participate) - merger.mergeSparseDict(self, lst) + # '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) |