Spaces:
Paused
Paused
| """ | |
| Merge OpenType Layout tables (GDEF / GPOS / GSUB). | |
| """ | |
| import os | |
| import copy | |
| import enum | |
| from operator import ior | |
| import logging | |
| from fontTools.colorLib.builder import MAX_PAINT_COLR_LAYER_COUNT, LayerReuseCache | |
| from fontTools.misc import classifyTools | |
| from fontTools.misc.roundTools import otRound | |
| from fontTools.misc.treeTools import build_n_ary_tree | |
| from fontTools.ttLib.tables import otTables as ot | |
| from fontTools.ttLib.tables import otBase as otBase | |
| from fontTools.ttLib.tables.otConverters import BaseFixedValue | |
| from fontTools.ttLib.tables.otTraverse import dfs_base_table | |
| from fontTools.ttLib.tables.DefaultTable import DefaultTable | |
| from fontTools.varLib import builder, models, varStore | |
| from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo, subList | |
| from fontTools.varLib.varStore import VarStoreInstancer | |
| from functools import reduce | |
| from fontTools.otlLib.builder import buildSinglePos | |
| from fontTools.otlLib.optimize.gpos import ( | |
| _compression_level_from_env, | |
| compact_pair_pos, | |
| ) | |
| log = logging.getLogger("fontTools.varLib.merger") | |
| from .errors import ( | |
| ShouldBeConstant, | |
| FoundANone, | |
| MismatchedTypes, | |
| NotANone, | |
| LengthsDiffer, | |
| KeysDiffer, | |
| InconsistentGlyphOrder, | |
| InconsistentExtensions, | |
| InconsistentFormats, | |
| UnsupportedFormat, | |
| VarLibMergeError, | |
| ) | |
| class Merger(object): | |
| def __init__(self, font=None): | |
| self.font = font | |
| # mergeTables populates this from the parent's master ttfs | |
| self.ttfs = None | |
| def merger(celf, clazzes, attrs=(None,)): | |
| assert celf != Merger, "Subclass Merger instead." | |
| if "mergers" not in celf.__dict__: | |
| celf.mergers = {} | |
| if type(clazzes) in (type, enum.EnumMeta): | |
| clazzes = (clazzes,) | |
| if type(attrs) == str: | |
| attrs = (attrs,) | |
| def wrapper(method): | |
| assert method.__name__ == "merge" | |
| done = [] | |
| for clazz in clazzes: | |
| if clazz in done: | |
| continue # Support multiple names of a clazz | |
| done.append(clazz) | |
| mergers = celf.mergers.setdefault(clazz, {}) | |
| for attr in attrs: | |
| assert attr not in mergers, ( | |
| "Oops, class '%s' has merge function for '%s' defined already." | |
| % (clazz.__name__, attr) | |
| ) | |
| mergers[attr] = method | |
| return None | |
| return wrapper | |
| def mergersFor(celf, thing, _default={}): | |
| typ = type(thing) | |
| for celf in celf.mro(): | |
| mergers = getattr(celf, "mergers", None) | |
| if mergers is None: | |
| break | |
| m = celf.mergers.get(typ, None) | |
| if m is not None: | |
| return m | |
| return _default | |
| def mergeObjects(self, out, lst, exclude=()): | |
| if hasattr(out, "ensureDecompiled"): | |
| out.ensureDecompiled(recurse=False) | |
| for item in lst: | |
| if hasattr(item, "ensureDecompiled"): | |
| item.ensureDecompiled(recurse=False) | |
| keys = sorted(vars(out).keys()) | |
| if not all(keys == sorted(vars(v).keys()) for v in lst): | |
| raise KeysDiffer( | |
| self, expected=keys, got=[sorted(vars(v).keys()) for v in lst] | |
| ) | |
| mergers = self.mergersFor(out) | |
| defaultMerger = mergers.get("*", self.__class__.mergeThings) | |
| try: | |
| for key in keys: | |
| if key in exclude: | |
| continue | |
| value = getattr(out, key) | |
| values = [getattr(table, key) for table in lst] | |
| mergerFunc = mergers.get(key, defaultMerger) | |
| mergerFunc(self, value, values) | |
| except VarLibMergeError as e: | |
| e.stack.append("." + key) | |
| raise | |
| def mergeLists(self, out, lst): | |
| if not allEqualTo(out, lst, len): | |
| raise LengthsDiffer(self, expected=len(out), got=[len(x) for x in lst]) | |
| for i, (value, values) in enumerate(zip(out, zip(*lst))): | |
| try: | |
| self.mergeThings(value, values) | |
| except VarLibMergeError as e: | |
| e.stack.append("[%d]" % i) | |
| raise | |
| def mergeThings(self, out, lst): | |
| if not allEqualTo(out, lst, type): | |
| raise MismatchedTypes( | |
| self, expected=type(out).__name__, got=[type(x).__name__ for x in lst] | |
| ) | |
| mergerFunc = self.mergersFor(out).get(None, None) | |
| if mergerFunc is not None: | |
| mergerFunc(self, out, lst) | |
| elif isinstance(out, enum.Enum): | |
| # need to special-case Enums as have __dict__ but are not regular 'objects', | |
| # otherwise mergeObjects/mergeThings get trapped in a RecursionError | |
| if not allEqualTo(out, lst): | |
| raise ShouldBeConstant(self, expected=out, got=lst) | |
| elif hasattr(out, "__dict__"): | |
| self.mergeObjects(out, lst) | |
| elif isinstance(out, list): | |
| self.mergeLists(out, lst) | |
| else: | |
| if not allEqualTo(out, lst): | |
| raise ShouldBeConstant(self, expected=out, got=lst) | |
| def mergeTables(self, font, master_ttfs, tableTags): | |
| for tag in tableTags: | |
| if tag not in font: | |
| continue | |
| try: | |
| self.ttfs = master_ttfs | |
| self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs]) | |
| except VarLibMergeError as e: | |
| e.stack.append(tag) | |
| raise | |
| # | |
| # Aligning merger | |
| # | |
| class AligningMerger(Merger): | |
| pass | |
| def merge(merger, self, lst): | |
| if self is None: | |
| if not allNone(lst): | |
| raise NotANone(merger, expected=None, got=lst) | |
| return | |
| lst = [l.classDefs for l in lst] | |
| self.classDefs = {} | |
| # We only care about the .classDefs | |
| self = self.classDefs | |
| allKeys = set() | |
| allKeys.update(*[l.keys() for l in lst]) | |
| for k in allKeys: | |
| allValues = nonNone(l.get(k) for l in lst) | |
| if not allEqual(allValues): | |
| raise ShouldBeConstant( | |
| merger, expected=allValues[0], got=lst, stack=["." + k] | |
| ) | |
| if not allValues: | |
| self[k] = None | |
| else: | |
| self[k] = allValues[0] | |
| def _SinglePosUpgradeToFormat2(self): | |
| if self.Format == 2: | |
| return self | |
| ret = ot.SinglePos() | |
| ret.Format = 2 | |
| ret.Coverage = self.Coverage | |
| ret.ValueFormat = self.ValueFormat | |
| ret.Value = [self.Value for _ in ret.Coverage.glyphs] | |
| ret.ValueCount = len(ret.Value) | |
| return ret | |
| def _merge_GlyphOrders(font, lst, values_lst=None, default=None): | |
| """Takes font and list of glyph lists (must be sorted by glyph id), and returns | |
| two things: | |
| - Combined glyph list, | |
| - If values_lst is None, return input glyph lists, but padded with None when a glyph | |
| was missing in a list. Otherwise, return values_lst list-of-list, padded with None | |
| to match combined glyph lists. | |
| """ | |
| if values_lst is None: | |
| dict_sets = [set(l) for l in lst] | |
| else: | |
| dict_sets = [{g: v for g, v in zip(l, vs)} for l, vs in zip(lst, values_lst)] | |
| combined = set() | |
| combined.update(*dict_sets) | |
| sortKey = font.getReverseGlyphMap().__getitem__ | |
| order = sorted(combined, key=sortKey) | |
| # Make sure all input glyphsets were in proper order | |
| if not all(sorted(vs, key=sortKey) == vs for vs in lst): | |
| raise InconsistentGlyphOrder() | |
| del combined | |
| paddedValues = None | |
| if values_lst is None: | |
| padded = [ | |
| [glyph if glyph in dict_set else default for glyph in order] | |
| for dict_set in dict_sets | |
| ] | |
| else: | |
| assert len(lst) == len(values_lst) | |
| padded = [ | |
| [dict_set[glyph] if glyph in dict_set else default for glyph in order] | |
| for dict_set in dict_sets | |
| ] | |
| return order, padded | |
| def merge(merger, self, lst): | |
| # Code below sometimes calls us with self being | |
| # a new object. Copy it from lst and recurse. | |
| self.__dict__ = lst[0].__dict__.copy() | |
| merger.mergeObjects(self, lst) | |
| def merge(merger, self, lst): | |
| # Code below sometimes calls us with self being | |
| # a new object. Copy it from lst and recurse. | |
| self.__dict__ = lst[0].__dict__.copy() | |
| merger.mergeObjects(self, lst) | |
| def _Lookup_SinglePos_get_effective_value(merger, subtables, glyph): | |
| for self in subtables: | |
| if ( | |
| self is None | |
| or type(self) != ot.SinglePos | |
| or self.Coverage is None | |
| or glyph not in self.Coverage.glyphs | |
| ): | |
| continue | |
| if self.Format == 1: | |
| return self.Value | |
| elif self.Format == 2: | |
| return self.Value[self.Coverage.glyphs.index(glyph)] | |
| else: | |
| raise UnsupportedFormat(merger, subtable="single positioning lookup") | |
| return None | |
| def _Lookup_PairPos_get_effective_value_pair( | |
| merger, subtables, firstGlyph, secondGlyph | |
| ): | |
| for self in subtables: | |
| if ( | |
| self is None | |
| or type(self) != ot.PairPos | |
| or self.Coverage is None | |
| or firstGlyph not in self.Coverage.glyphs | |
| ): | |
| continue | |
| if self.Format == 1: | |
| ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)] | |
| pvr = ps.PairValueRecord | |
| for rec in pvr: # TODO Speed up | |
| if rec.SecondGlyph == secondGlyph: | |
| return rec | |
| continue | |
| elif self.Format == 2: | |
| klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0) | |
| klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0) | |
| return self.Class1Record[klass1].Class2Record[klass2] | |
| else: | |
| raise UnsupportedFormat(merger, subtable="pair positioning lookup") | |
| return None | |
| def merge(merger, self, lst): | |
| self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0) | |
| if not (len(lst) == 1 or (valueFormat & ~0xF == 0)): | |
| raise UnsupportedFormat(merger, subtable="single positioning lookup") | |
| # If all have same coverage table and all are format 1, | |
| coverageGlyphs = self.Coverage.glyphs | |
| if all(v.Format == 1 for v in lst) and all( | |
| coverageGlyphs == v.Coverage.glyphs for v in lst | |
| ): | |
| self.Value = otBase.ValueRecord(valueFormat, self.Value) | |
| if valueFormat != 0: | |
| # If v.Value is None, it means a kerning of 0; we want | |
| # it to participate in the model still. | |
| # https://github.com/fonttools/fonttools/issues/3111 | |
| merger.mergeThings( | |
| self.Value, | |
| [v.Value if v.Value is not None else otBase.ValueRecord() for v in lst], | |
| ) | |
| self.ValueFormat = self.Value.getFormat() | |
| return | |
| # Upgrade everything to Format=2 | |
| self.Format = 2 | |
| lst = [_SinglePosUpgradeToFormat2(v) for v in lst] | |
| # Align them | |
| glyphs, padded = _merge_GlyphOrders( | |
| merger.font, [v.Coverage.glyphs for v in lst], [v.Value for v in lst] | |
| ) | |
| self.Coverage.glyphs = glyphs | |
| self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs] | |
| self.ValueCount = len(self.Value) | |
| for i, values in enumerate(padded): | |
| for j, glyph in enumerate(glyphs): | |
| if values[j] is not None: | |
| continue | |
| # Fill in value from other subtables | |
| # Note!!! This *might* result in behavior change if ValueFormat2-zeroedness | |
| # is different between used subtable and current subtable! | |
| # TODO(behdad) Check and warn if that happens? | |
| v = _Lookup_SinglePos_get_effective_value( | |
| merger, merger.lookup_subtables[i], glyph | |
| ) | |
| if v is None: | |
| v = otBase.ValueRecord(valueFormat) | |
| values[j] = v | |
| merger.mergeLists(self.Value, padded) | |
| # Merge everything else; though, there shouldn't be anything else. :) | |
| merger.mergeObjects( | |
| self, lst, exclude=("Format", "Coverage", "Value", "ValueCount", "ValueFormat") | |
| ) | |
| self.ValueFormat = reduce( | |
| int.__or__, [v.getEffectiveFormat() for v in self.Value], 0 | |
| ) | |
| def merge(merger, self, lst): | |
| # Align them | |
| glyphs, padded = _merge_GlyphOrders( | |
| merger.font, | |
| [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], | |
| [vs.PairValueRecord for vs in lst], | |
| ) | |
| self.PairValueRecord = pvrs = [] | |
| for glyph in glyphs: | |
| pvr = ot.PairValueRecord() | |
| pvr.SecondGlyph = glyph | |
| pvr.Value1 = ( | |
| otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None | |
| ) | |
| pvr.Value2 = ( | |
| otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None | |
| ) | |
| pvrs.append(pvr) | |
| self.PairValueCount = len(self.PairValueRecord) | |
| for i, values in enumerate(padded): | |
| for j, glyph in enumerate(glyphs): | |
| # Fill in value from other subtables | |
| v = ot.PairValueRecord() | |
| v.SecondGlyph = glyph | |
| if values[j] is not None: | |
| vpair = values[j] | |
| else: | |
| vpair = _Lookup_PairPos_get_effective_value_pair( | |
| merger, merger.lookup_subtables[i], self._firstGlyph, glyph | |
| ) | |
| if vpair is None: | |
| v1, v2 = None, None | |
| else: | |
| v1 = getattr(vpair, "Value1", None) | |
| v2 = getattr(vpair, "Value2", None) | |
| v.Value1 = ( | |
| otBase.ValueRecord(merger.valueFormat1, src=v1) | |
| if merger.valueFormat1 | |
| else None | |
| ) | |
| v.Value2 = ( | |
| otBase.ValueRecord(merger.valueFormat2, src=v2) | |
| if merger.valueFormat2 | |
| else None | |
| ) | |
| values[j] = v | |
| del self._firstGlyph | |
| merger.mergeLists(self.PairValueRecord, padded) | |
| def _PairPosFormat1_merge(self, lst, merger): | |
| assert allEqual( | |
| [l.ValueFormat2 == 0 for l in lst if l.PairSet] | |
| ), "Report bug against fonttools." | |
| # Merge everything else; makes sure Format is the same. | |
| merger.mergeObjects( | |
| self, | |
| lst, | |
| exclude=("Coverage", "PairSet", "PairSetCount", "ValueFormat1", "ValueFormat2"), | |
| ) | |
| empty = ot.PairSet() | |
| empty.PairValueRecord = [] | |
| empty.PairValueCount = 0 | |
| # Align them | |
| glyphs, padded = _merge_GlyphOrders( | |
| merger.font, | |
| [v.Coverage.glyphs for v in lst], | |
| [v.PairSet for v in lst], | |
| default=empty, | |
| ) | |
| self.Coverage.glyphs = glyphs | |
| self.PairSet = [ot.PairSet() for _ in glyphs] | |
| self.PairSetCount = len(self.PairSet) | |
| for glyph, ps in zip(glyphs, self.PairSet): | |
| ps._firstGlyph = glyph | |
| merger.mergeLists(self.PairSet, padded) | |
| def _ClassDef_invert(self, allGlyphs=None): | |
| if isinstance(self, dict): | |
| classDefs = self | |
| else: | |
| classDefs = self.classDefs if self and self.classDefs else {} | |
| m = max(classDefs.values()) if classDefs else 0 | |
| ret = [] | |
| for _ in range(m + 1): | |
| ret.append(set()) | |
| for k, v in classDefs.items(): | |
| ret[v].add(k) | |
| # Class-0 is special. It's "everything else". | |
| if allGlyphs is None: | |
| ret[0] = None | |
| else: | |
| # Limit all classes to glyphs in allGlyphs. | |
| # Collect anything without a non-zero class into class=zero. | |
| ret[0] = class0 = set(allGlyphs) | |
| for s in ret[1:]: | |
| s.intersection_update(class0) | |
| class0.difference_update(s) | |
| return ret | |
| def _ClassDef_merge_classify(lst, allGlyphses=None): | |
| self = ot.ClassDef() | |
| self.classDefs = classDefs = {} | |
| allGlyphsesWasNone = allGlyphses is None | |
| if allGlyphsesWasNone: | |
| allGlyphses = [None] * len(lst) | |
| classifier = classifyTools.Classifier() | |
| for classDef, allGlyphs in zip(lst, allGlyphses): | |
| sets = _ClassDef_invert(classDef, allGlyphs) | |
| if allGlyphs is None: | |
| sets = sets[1:] | |
| classifier.update(sets) | |
| classes = classifier.getClasses() | |
| if allGlyphsesWasNone: | |
| classes.insert(0, set()) | |
| for i, classSet in enumerate(classes): | |
| if i == 0: | |
| continue | |
| for g in classSet: | |
| classDefs[g] = i | |
| return self, classes | |
| def _PairPosFormat2_align_matrices(self, lst, font, transparent=False): | |
| matrices = [l.Class1Record for l in lst] | |
| # Align first classes | |
| self.ClassDef1, classes = _ClassDef_merge_classify( | |
| [l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst] | |
| ) | |
| self.Class1Count = len(classes) | |
| new_matrices = [] | |
| for l, matrix in zip(lst, matrices): | |
| nullRow = None | |
| coverage = set(l.Coverage.glyphs) | |
| classDef1 = l.ClassDef1.classDefs | |
| class1Records = [] | |
| for classSet in classes: | |
| exemplarGlyph = next(iter(classSet)) | |
| if exemplarGlyph not in coverage: | |
| # Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f, | |
| # Fixes https://github.com/googlei18n/fontmake/issues/470 | |
| # Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9 | |
| # when merger becomes selfless. | |
| nullRow = None | |
| if nullRow is None: | |
| nullRow = ot.Class1Record() | |
| class2records = nullRow.Class2Record = [] | |
| # TODO: When merger becomes selfless, revert e6125b353e1f54a0280ded5434b8e40d042de69f | |
| for _ in range(l.Class2Count): | |
| if transparent: | |
| rec2 = None | |
| else: | |
| rec2 = ot.Class2Record() | |
| rec2.Value1 = ( | |
| otBase.ValueRecord(self.ValueFormat1) | |
| if self.ValueFormat1 | |
| else None | |
| ) | |
| rec2.Value2 = ( | |
| otBase.ValueRecord(self.ValueFormat2) | |
| if self.ValueFormat2 | |
| else None | |
| ) | |
| class2records.append(rec2) | |
| rec1 = nullRow | |
| else: | |
| klass = classDef1.get(exemplarGlyph, 0) | |
| rec1 = matrix[klass] # TODO handle out-of-range? | |
| class1Records.append(rec1) | |
| new_matrices.append(class1Records) | |
| matrices = new_matrices | |
| del new_matrices | |
| # Align second classes | |
| self.ClassDef2, classes = _ClassDef_merge_classify([l.ClassDef2 for l in lst]) | |
| self.Class2Count = len(classes) | |
| new_matrices = [] | |
| for l, matrix in zip(lst, matrices): | |
| classDef2 = l.ClassDef2.classDefs | |
| class1Records = [] | |
| for rec1old in matrix: | |
| oldClass2Records = rec1old.Class2Record | |
| rec1new = ot.Class1Record() | |
| class2Records = rec1new.Class2Record = [] | |
| for classSet in classes: | |
| if not classSet: # class=0 | |
| rec2 = oldClass2Records[0] | |
| else: | |
| exemplarGlyph = next(iter(classSet)) | |
| klass = classDef2.get(exemplarGlyph, 0) | |
| rec2 = oldClass2Records[klass] | |
| class2Records.append(copy.deepcopy(rec2)) | |
| class1Records.append(rec1new) | |
| new_matrices.append(class1Records) | |
| matrices = new_matrices | |
| del new_matrices | |
| return matrices | |
| def _PairPosFormat2_merge(self, lst, merger): | |
| assert allEqual( | |
| [l.ValueFormat2 == 0 for l in lst if l.Class1Record] | |
| ), "Report bug against fonttools." | |
| merger.mergeObjects( | |
| self, | |
| lst, | |
| exclude=( | |
| "Coverage", | |
| "ClassDef1", | |
| "Class1Count", | |
| "ClassDef2", | |
| "Class2Count", | |
| "Class1Record", | |
| "ValueFormat1", | |
| "ValueFormat2", | |
| ), | |
| ) | |
| # Align coverages | |
| glyphs, _ = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst]) | |
| self.Coverage.glyphs = glyphs | |
| # Currently, if the coverage of PairPosFormat2 subtables are different, | |
| # we do NOT bother walking down the subtable list when filling in new | |
| # rows for alignment. As such, this is only correct if current subtable | |
| # is the last subtable in the lookup. Ensure that. | |
| # | |
| # Note that our canonicalization process merges trailing PairPosFormat2's, | |
| # so in reality this is rare. | |
| for l, subtables in zip(lst, merger.lookup_subtables): | |
| if l.Coverage.glyphs != glyphs: | |
| assert l == subtables[-1] | |
| matrices = _PairPosFormat2_align_matrices(self, lst, merger.font) | |
| self.Class1Record = list(matrices[0]) # TODO move merger to be selfless | |
| merger.mergeLists(self.Class1Record, matrices) | |
| def merge(merger, self, lst): | |
| merger.valueFormat1 = self.ValueFormat1 = reduce( | |
| int.__or__, [l.ValueFormat1 for l in lst], 0 | |
| ) | |
| merger.valueFormat2 = self.ValueFormat2 = reduce( | |
| int.__or__, [l.ValueFormat2 for l in lst], 0 | |
| ) | |
| if self.Format == 1: | |
| _PairPosFormat1_merge(self, lst, merger) | |
| elif self.Format == 2: | |
| _PairPosFormat2_merge(self, lst, merger) | |
| else: | |
| raise UnsupportedFormat(merger, subtable="pair positioning lookup") | |
| del merger.valueFormat1, merger.valueFormat2 | |
| # Now examine the list of value records, and update to the union of format values, | |
| # as merge might have created new values. | |
| vf1 = 0 | |
| vf2 = 0 | |
| if self.Format == 1: | |
| for pairSet in self.PairSet: | |
| for pairValueRecord in pairSet.PairValueRecord: | |
| pv1 = getattr(pairValueRecord, "Value1", None) | |
| if pv1 is not None: | |
| vf1 |= pv1.getFormat() | |
| pv2 = getattr(pairValueRecord, "Value2", None) | |
| if pv2 is not None: | |
| vf2 |= pv2.getFormat() | |
| elif self.Format == 2: | |
| for class1Record in self.Class1Record: | |
| for class2Record in class1Record.Class2Record: | |
| pv1 = getattr(class2Record, "Value1", None) | |
| if pv1 is not None: | |
| vf1 |= pv1.getFormat() | |
| pv2 = getattr(class2Record, "Value2", None) | |
| if pv2 is not None: | |
| vf2 |= pv2.getFormat() | |
| self.ValueFormat1 = vf1 | |
| self.ValueFormat2 = vf2 | |
| def _MarkBasePosFormat1_merge(self, lst, merger, Mark="Mark", Base="Base"): | |
| self.ClassCount = max(l.ClassCount for l in lst) | |
| MarkCoverageGlyphs, MarkRecords = _merge_GlyphOrders( | |
| merger.font, | |
| [getattr(l, Mark + "Coverage").glyphs for l in lst], | |
| [getattr(l, Mark + "Array").MarkRecord for l in lst], | |
| ) | |
| getattr(self, Mark + "Coverage").glyphs = MarkCoverageGlyphs | |
| BaseCoverageGlyphs, BaseRecords = _merge_GlyphOrders( | |
| merger.font, | |
| [getattr(l, Base + "Coverage").glyphs for l in lst], | |
| [getattr(getattr(l, Base + "Array"), Base + "Record") for l in lst], | |
| ) | |
| getattr(self, Base + "Coverage").glyphs = BaseCoverageGlyphs | |
| # MarkArray | |
| records = [] | |
| for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)): | |
| allClasses = [r.Class for r in glyphRecords if r is not None] | |
| # TODO Right now we require that all marks have same class in | |
| # all masters that cover them. This is not required. | |
| # | |
| # We can relax that by just requiring that all marks that have | |
| # the same class in a master, have the same class in every other | |
| # master. Indeed, if, say, a sparse master only covers one mark, | |
| # that mark probably will get class 0, which would possibly be | |
| # different from its class in other masters. | |
| # | |
| # We can even go further and reclassify marks to support any | |
| # input. But, since, it's unlikely that two marks being both, | |
| # say, "top" in one master, and one being "top" and other being | |
| # "top-right" in another master, we shouldn't do that, as any | |
| # failures in that case will probably signify mistakes in the | |
| # input masters. | |
| if not allEqual(allClasses): | |
| raise ShouldBeConstant(merger, expected=allClasses[0], got=allClasses) | |
| else: | |
| rec = ot.MarkRecord() | |
| rec.Class = allClasses[0] | |
| allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords] | |
| if allNone(allAnchors): | |
| anchor = None | |
| else: | |
| anchor = ot.Anchor() | |
| anchor.Format = 1 | |
| merger.mergeThings(anchor, allAnchors) | |
| rec.MarkAnchor = anchor | |
| records.append(rec) | |
| array = ot.MarkArray() | |
| array.MarkRecord = records | |
| array.MarkCount = len(records) | |
| setattr(self, Mark + "Array", array) | |
| # BaseArray | |
| records = [] | |
| for g, glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)): | |
| if allNone(glyphRecords): | |
| rec = None | |
| else: | |
| rec = getattr(ot, Base + "Record")() | |
| anchors = [] | |
| setattr(rec, Base + "Anchor", anchors) | |
| glyphAnchors = [ | |
| [] if r is None else getattr(r, Base + "Anchor") for r in glyphRecords | |
| ] | |
| for l in glyphAnchors: | |
| l.extend([None] * (self.ClassCount - len(l))) | |
| for allAnchors in zip(*glyphAnchors): | |
| if allNone(allAnchors): | |
| anchor = None | |
| else: | |
| anchor = ot.Anchor() | |
| anchor.Format = 1 | |
| merger.mergeThings(anchor, allAnchors) | |
| anchors.append(anchor) | |
| records.append(rec) | |
| array = getattr(ot, Base + "Array")() | |
| setattr(array, Base + "Record", records) | |
| setattr(array, Base + "Count", len(records)) | |
| setattr(self, Base + "Array", array) | |
| def merge(merger, self, lst): | |
| if not allEqualTo(self.Format, (l.Format for l in lst)): | |
| raise InconsistentFormats( | |
| merger, | |
| subtable="mark-to-base positioning lookup", | |
| expected=self.Format, | |
| got=[l.Format for l in lst], | |
| ) | |
| if self.Format == 1: | |
| _MarkBasePosFormat1_merge(self, lst, merger) | |
| else: | |
| raise UnsupportedFormat(merger, subtable="mark-to-base positioning lookup") | |
| def merge(merger, self, lst): | |
| if not allEqualTo(self.Format, (l.Format for l in lst)): | |
| raise InconsistentFormats( | |
| merger, | |
| subtable="mark-to-mark positioning lookup", | |
| expected=self.Format, | |
| got=[l.Format for l in lst], | |
| ) | |
| if self.Format == 1: | |
| _MarkBasePosFormat1_merge(self, lst, merger, "Mark1", "Mark2") | |
| else: | |
| raise UnsupportedFormat(merger, subtable="mark-to-mark positioning lookup") | |
| def _PairSet_flatten(lst, font): | |
| self = ot.PairSet() | |
| self.Coverage = ot.Coverage() | |
| # Align them | |
| glyphs, padded = _merge_GlyphOrders( | |
| font, | |
| [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], | |
| [vs.PairValueRecord for vs in lst], | |
| ) | |
| self.Coverage.glyphs = glyphs | |
| self.PairValueRecord = pvrs = [] | |
| for values in zip(*padded): | |
| for v in values: | |
| if v is not None: | |
| pvrs.append(v) | |
| break | |
| else: | |
| assert False | |
| self.PairValueCount = len(self.PairValueRecord) | |
| return self | |
| def _Lookup_PairPosFormat1_subtables_flatten(lst, font): | |
| assert allEqual( | |
| [l.ValueFormat2 == 0 for l in lst if l.PairSet] | |
| ), "Report bug against fonttools." | |
| self = ot.PairPos() | |
| self.Format = 1 | |
| self.Coverage = ot.Coverage() | |
| self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) | |
| self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) | |
| # Align them | |
| glyphs, padded = _merge_GlyphOrders( | |
| font, [v.Coverage.glyphs for v in lst], [v.PairSet for v in lst] | |
| ) | |
| self.Coverage.glyphs = glyphs | |
| self.PairSet = [ | |
| _PairSet_flatten([v for v in values if v is not None], font) | |
| for values in zip(*padded) | |
| ] | |
| self.PairSetCount = len(self.PairSet) | |
| return self | |
| def _Lookup_PairPosFormat2_subtables_flatten(lst, font): | |
| assert allEqual( | |
| [l.ValueFormat2 == 0 for l in lst if l.Class1Record] | |
| ), "Report bug against fonttools." | |
| self = ot.PairPos() | |
| self.Format = 2 | |
| self.Coverage = ot.Coverage() | |
| self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) | |
| self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) | |
| # Align them | |
| glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst]) | |
| self.Coverage.glyphs = glyphs | |
| matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True) | |
| matrix = self.Class1Record = [] | |
| for rows in zip(*matrices): | |
| row = ot.Class1Record() | |
| matrix.append(row) | |
| row.Class2Record = [] | |
| row = row.Class2Record | |
| for cols in zip(*list(r.Class2Record for r in rows)): | |
| col = next(iter(c for c in cols if c is not None)) | |
| row.append(col) | |
| return self | |
| def _Lookup_PairPos_subtables_canonicalize(lst, font): | |
| """Merge multiple Format1 subtables at the beginning of lst, | |
| and merge multiple consecutive Format2 subtables that have the same | |
| Class2 (ie. were split because of offset overflows). Returns new list.""" | |
| lst = list(lst) | |
| l = len(lst) | |
| i = 0 | |
| while i < l and lst[i].Format == 1: | |
| i += 1 | |
| lst[:i] = [_Lookup_PairPosFormat1_subtables_flatten(lst[:i], font)] | |
| l = len(lst) | |
| i = l | |
| while i > 0 and lst[i - 1].Format == 2: | |
| i -= 1 | |
| lst[i:] = [_Lookup_PairPosFormat2_subtables_flatten(lst[i:], font)] | |
| return lst | |
| def _Lookup_SinglePos_subtables_flatten(lst, font, min_inclusive_rec_format): | |
| glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst], None) | |
| num_glyphs = len(glyphs) | |
| new = ot.SinglePos() | |
| new.Format = 2 | |
| new.ValueFormat = min_inclusive_rec_format | |
| new.Coverage = ot.Coverage() | |
| new.Coverage.glyphs = glyphs | |
| new.ValueCount = num_glyphs | |
| new.Value = [None] * num_glyphs | |
| for singlePos in lst: | |
| if singlePos.Format == 1: | |
| val_rec = singlePos.Value | |
| for gname in singlePos.Coverage.glyphs: | |
| i = glyphs.index(gname) | |
| new.Value[i] = copy.deepcopy(val_rec) | |
| elif singlePos.Format == 2: | |
| for j, gname in enumerate(singlePos.Coverage.glyphs): | |
| val_rec = singlePos.Value[j] | |
| i = glyphs.index(gname) | |
| new.Value[i] = copy.deepcopy(val_rec) | |
| return [new] | |
| def merge(merger, self, lst): | |
| # Align them | |
| glyphs, padded = _merge_GlyphOrders( | |
| merger.font, | |
| [l.Coverage.glyphs for l in lst], | |
| [l.EntryExitRecord for l in lst], | |
| ) | |
| self.Format = 1 | |
| self.Coverage = ot.Coverage() | |
| self.Coverage.glyphs = glyphs | |
| self.EntryExitRecord = [] | |
| for _ in glyphs: | |
| rec = ot.EntryExitRecord() | |
| rec.EntryAnchor = ot.Anchor() | |
| rec.EntryAnchor.Format = 1 | |
| rec.ExitAnchor = ot.Anchor() | |
| rec.ExitAnchor.Format = 1 | |
| self.EntryExitRecord.append(rec) | |
| merger.mergeLists(self.EntryExitRecord, padded) | |
| self.EntryExitCount = len(self.EntryExitRecord) | |
| def merge(merger, self, lst): | |
| if all(master.EntryAnchor is None for master in lst): | |
| self.EntryAnchor = None | |
| if all(master.ExitAnchor is None for master in lst): | |
| self.ExitAnchor = None | |
| merger.mergeObjects(self, lst) | |
| def merge(merger, self, lst): | |
| subtables = merger.lookup_subtables = [l.SubTable for l in lst] | |
| # Remove Extension subtables | |
| for l, sts in list(zip(lst, subtables)) + [(self, self.SubTable)]: | |
| if not sts: | |
| continue | |
| if sts[0].__class__.__name__.startswith("Extension"): | |
| if not allEqual([st.__class__ for st in sts]): | |
| raise InconsistentExtensions( | |
| merger, | |
| expected="Extension", | |
| got=[st.__class__.__name__ for st in sts], | |
| ) | |
| if not allEqual([st.ExtensionLookupType for st in sts]): | |
| raise InconsistentExtensions(merger) | |
| l.LookupType = sts[0].ExtensionLookupType | |
| new_sts = [st.ExtSubTable for st in sts] | |
| del sts[:] | |
| sts.extend(new_sts) | |
| isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos) | |
| if isPairPos: | |
| # AFDKO and feaLib sometimes generate two Format1 subtables instead of one. | |
| # Merge those before continuing. | |
| # https://github.com/fonttools/fonttools/issues/719 | |
| self.SubTable = _Lookup_PairPos_subtables_canonicalize( | |
| self.SubTable, merger.font | |
| ) | |
| subtables = merger.lookup_subtables = [ | |
| _Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables | |
| ] | |
| else: | |
| isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos) | |
| if isSinglePos: | |
| numSubtables = [len(st) for st in subtables] | |
| if not all([nums == numSubtables[0] for nums in numSubtables]): | |
| # Flatten list of SinglePos subtables to single Format 2 subtable, | |
| # with all value records set to the rec format type. | |
| # We use buildSinglePos() to optimize the lookup after merging. | |
| valueFormatList = [t.ValueFormat for st in subtables for t in st] | |
| # Find the minimum value record that can accomodate all the singlePos subtables. | |
| mirf = reduce(ior, valueFormatList) | |
| self.SubTable = _Lookup_SinglePos_subtables_flatten( | |
| self.SubTable, merger.font, mirf | |
| ) | |
| subtables = merger.lookup_subtables = [ | |
| _Lookup_SinglePos_subtables_flatten(st, merger.font, mirf) | |
| for st in subtables | |
| ] | |
| flattened = True | |
| else: | |
| flattened = False | |
| merger.mergeLists(self.SubTable, subtables) | |
| self.SubTableCount = len(self.SubTable) | |
| if isPairPos: | |
| # If format-1 subtable created during canonicalization is empty, remove it. | |
| assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1 | |
| if not self.SubTable[0].Coverage.glyphs: | |
| self.SubTable.pop(0) | |
| self.SubTableCount -= 1 | |
| # If format-2 subtable created during canonicalization is empty, remove it. | |
| assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2 | |
| if not self.SubTable[-1].Coverage.glyphs: | |
| self.SubTable.pop(-1) | |
| self.SubTableCount -= 1 | |
| # Compact the merged subtables | |
| # This is a good moment to do it because the compaction should create | |
| # smaller subtables, which may prevent overflows from happening. | |
| # Keep reading the value from the ENV until ufo2ft switches to the config system | |
| level = merger.font.cfg.get( | |
| "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL", | |
| default=_compression_level_from_env(), | |
| ) | |
| if level != 0: | |
| log.info("Compacting GPOS...") | |
| self.SubTable = compact_pair_pos(merger.font, level, self.SubTable) | |
| self.SubTableCount = len(self.SubTable) | |
| elif isSinglePos and flattened: | |
| singlePosTable = self.SubTable[0] | |
| glyphs = singlePosTable.Coverage.glyphs | |
| # We know that singlePosTable is Format 2, as this is set | |
| # in _Lookup_SinglePos_subtables_flatten. | |
| singlePosMapping = { | |
| gname: valRecord for gname, valRecord in zip(glyphs, singlePosTable.Value) | |
| } | |
| self.SubTable = buildSinglePos( | |
| singlePosMapping, merger.font.getReverseGlyphMap() | |
| ) | |
| merger.mergeObjects(self, lst, exclude=["SubTable", "SubTableCount"]) | |
| del merger.lookup_subtables | |
| # | |
| # InstancerMerger | |
| # | |
| class InstancerMerger(AligningMerger): | |
| """A merger that takes multiple master fonts, and instantiates | |
| an instance.""" | |
| def __init__(self, font, model, location): | |
| Merger.__init__(self, font) | |
| self.model = model | |
| self.location = location | |
| self.masterScalars = model.getMasterScalars(location) | |
| def merge(merger, self, lst): | |
| assert self.Format == 1 | |
| Coords = [a.Coordinate for a in lst] | |
| model = merger.model | |
| masterScalars = merger.masterScalars | |
| self.Coordinate = otRound( | |
| model.interpolateFromValuesAndScalars(Coords, masterScalars) | |
| ) | |
| def merge(merger, self, lst): | |
| assert self.Format == 1 | |
| XCoords = [a.XCoordinate for a in lst] | |
| YCoords = [a.YCoordinate for a in lst] | |
| model = merger.model | |
| masterScalars = merger.masterScalars | |
| self.XCoordinate = otRound( | |
| model.interpolateFromValuesAndScalars(XCoords, masterScalars) | |
| ) | |
| self.YCoordinate = otRound( | |
| model.interpolateFromValuesAndScalars(YCoords, masterScalars) | |
| ) | |
| def merge(merger, self, lst): | |
| model = merger.model | |
| masterScalars = merger.masterScalars | |
| # TODO Handle differing valueformats | |
| for name, tableName in [ | |
| ("XAdvance", "XAdvDevice"), | |
| ("YAdvance", "YAdvDevice"), | |
| ("XPlacement", "XPlaDevice"), | |
| ("YPlacement", "YPlaDevice"), | |
| ]: | |
| assert not hasattr(self, tableName) | |
| if hasattr(self, name): | |
| values = [getattr(a, name, 0) for a in lst] | |
| value = otRound( | |
| model.interpolateFromValuesAndScalars(values, masterScalars) | |
| ) | |
| setattr(self, name, value) | |
| # | |
| # MutatorMerger | |
| # | |
| class MutatorMerger(AligningMerger): | |
| """A merger that takes a variable font, and instantiates | |
| an instance. While there's no "merging" to be done per se, | |
| the operation can benefit from many operations that the | |
| aligning merger does.""" | |
| def __init__(self, font, instancer, deleteVariations=True): | |
| Merger.__init__(self, font) | |
| self.instancer = instancer | |
| self.deleteVariations = deleteVariations | |
| def merge(merger, self, lst): | |
| # Hack till we become selfless. | |
| self.__dict__ = lst[0].__dict__.copy() | |
| if self.Format != 3: | |
| return | |
| instancer = merger.instancer | |
| dev = self.DeviceTable | |
| if merger.deleteVariations: | |
| del self.DeviceTable | |
| if dev: | |
| assert dev.DeltaFormat == 0x8000 | |
| varidx = (dev.StartSize << 16) + dev.EndSize | |
| delta = otRound(instancer[varidx]) | |
| self.Coordinate += delta | |
| if merger.deleteVariations: | |
| self.Format = 1 | |
| def merge(merger, self, lst): | |
| # Hack till we become selfless. | |
| self.__dict__ = lst[0].__dict__.copy() | |
| if self.Format != 3: | |
| return | |
| instancer = merger.instancer | |
| for v in "XY": | |
| tableName = v + "DeviceTable" | |
| if not hasattr(self, tableName): | |
| continue | |
| dev = getattr(self, tableName) | |
| if merger.deleteVariations: | |
| delattr(self, tableName) | |
| if dev is None: | |
| continue | |
| assert dev.DeltaFormat == 0x8000 | |
| varidx = (dev.StartSize << 16) + dev.EndSize | |
| delta = otRound(instancer[varidx]) | |
| attr = v + "Coordinate" | |
| setattr(self, attr, getattr(self, attr) + delta) | |
| if merger.deleteVariations: | |
| self.Format = 1 | |
| def merge(merger, self, lst): | |
| # Hack till we become selfless. | |
| self.__dict__ = lst[0].__dict__.copy() | |
| instancer = merger.instancer | |
| for name, tableName in [ | |
| ("XAdvance", "XAdvDevice"), | |
| ("YAdvance", "YAdvDevice"), | |
| ("XPlacement", "XPlaDevice"), | |
| ("YPlacement", "YPlaDevice"), | |
| ]: | |
| if not hasattr(self, tableName): | |
| continue | |
| dev = getattr(self, tableName) | |
| if merger.deleteVariations: | |
| delattr(self, tableName) | |
| if dev is None: | |
| continue | |
| assert dev.DeltaFormat == 0x8000 | |
| varidx = (dev.StartSize << 16) + dev.EndSize | |
| delta = otRound(instancer[varidx]) | |
| setattr(self, name, getattr(self, name, 0) + delta) | |
| # | |
| # VariationMerger | |
| # | |
| class VariationMerger(AligningMerger): | |
| """A merger that takes multiple master fonts, and builds a | |
| variable font.""" | |
| def __init__(self, model, axisTags, font): | |
| Merger.__init__(self, font) | |
| self.store_builder = varStore.OnlineVarStoreBuilder(axisTags) | |
| self.setModel(model) | |
| def setModel(self, model): | |
| self.model = model | |
| self.store_builder.setModel(model) | |
| def mergeThings(self, out, lst): | |
| masterModel = None | |
| origTTFs = None | |
| if None in lst: | |
| if allNone(lst): | |
| if out is not None: | |
| raise FoundANone(self, got=lst) | |
| return | |
| # temporarily subset the list of master ttfs to the ones for which | |
| # master values are not None | |
| origTTFs = self.ttfs | |
| if self.ttfs: | |
| self.ttfs = subList([v is not None for v in lst], self.ttfs) | |
| masterModel = self.model | |
| model, lst = masterModel.getSubModel(lst) | |
| self.setModel(model) | |
| super(VariationMerger, self).mergeThings(out, lst) | |
| if masterModel: | |
| self.setModel(masterModel) | |
| if origTTFs: | |
| self.ttfs = origTTFs | |
| def buildVarDevTable(store_builder, master_values): | |
| if allEqual(master_values): | |
| return master_values[0], None | |
| base, varIdx = store_builder.storeMasters(master_values) | |
| return base, builder.buildVarDevTable(varIdx) | |
| def merge(merger, self, lst): | |
| if self.Format != 1: | |
| raise UnsupportedFormat(merger, subtable="a baseline coordinate") | |
| self.Coordinate, DeviceTable = buildVarDevTable( | |
| merger.store_builder, [a.Coordinate for a in lst] | |
| ) | |
| if DeviceTable: | |
| self.Format = 3 | |
| self.DeviceTable = DeviceTable | |
| def merge(merger, self, lst): | |
| if self.Format != 1: | |
| raise UnsupportedFormat(merger, subtable="a caret") | |
| self.Coordinate, DeviceTable = buildVarDevTable( | |
| merger.store_builder, [a.Coordinate for a in lst] | |
| ) | |
| if DeviceTable: | |
| self.Format = 3 | |
| self.DeviceTable = DeviceTable | |
| def merge(merger, self, lst): | |
| if self.Format != 1: | |
| raise UnsupportedFormat(merger, subtable="an anchor") | |
| self.XCoordinate, XDeviceTable = buildVarDevTable( | |
| merger.store_builder, [a.XCoordinate for a in lst] | |
| ) | |
| self.YCoordinate, YDeviceTable = buildVarDevTable( | |
| merger.store_builder, [a.YCoordinate for a in lst] | |
| ) | |
| if XDeviceTable or YDeviceTable: | |
| self.Format = 3 | |
| self.XDeviceTable = XDeviceTable | |
| self.YDeviceTable = YDeviceTable | |
| def merge(merger, self, lst): | |
| for name, tableName in [ | |
| ("XAdvance", "XAdvDevice"), | |
| ("YAdvance", "YAdvDevice"), | |
| ("XPlacement", "XPlaDevice"), | |
| ("YPlacement", "YPlaDevice"), | |
| ]: | |
| if hasattr(self, name): | |
| value, deviceTable = buildVarDevTable( | |
| merger.store_builder, [getattr(a, name, 0) for a in lst] | |
| ) | |
| setattr(self, name, value) | |
| if deviceTable: | |
| setattr(self, tableName, deviceTable) | |
| class COLRVariationMerger(VariationMerger): | |
| """A specialized VariationMerger that takes multiple master fonts containing | |
| COLRv1 tables, and builds a variable COLR font. | |
| COLR tables are special in that variable subtables can be associated with | |
| multiple delta-set indices (via VarIndexBase). | |
| They also contain tables that must change their type (not simply the Format) | |
| as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes | |
| care of that too. | |
| """ | |
| def __init__(self, model, axisTags, font, allowLayerReuse=True): | |
| VariationMerger.__init__(self, model, axisTags, font) | |
| # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase | |
| # between variable tables with same varIdxes. | |
| self.varIndexCache = {} | |
| # flat list of all the varIdxes generated while merging | |
| self.varIdxes = [] | |
| # set of id()s of the subtables that contain variations after merging | |
| # and need to be upgraded to the associated VarType. | |
| self.varTableIds = set() | |
| # we keep these around for rebuilding a LayerList while merging PaintColrLayers | |
| self.layers = [] | |
| self.layerReuseCache = None | |
| if allowLayerReuse: | |
| self.layerReuseCache = LayerReuseCache() | |
| # flag to ensure BaseGlyphList is fully merged before LayerList gets processed | |
| self._doneBaseGlyphs = False | |
| def mergeTables(self, font, master_ttfs, tableTags=("COLR",)): | |
| if "COLR" in tableTags and "COLR" in font: | |
| # The merger modifies the destination COLR table in-place. If this contains | |
| # multiple PaintColrLayers referencing the same layers from LayerList, it's | |
| # a problem because we may risk modifying the same paint more than once, or | |
| # worse, fail while attempting to do that. | |
| # We don't know whether the master COLR table was built with layer reuse | |
| # disabled, thus to be safe we rebuild its LayerList so that it contains only | |
| # unique layers referenced from non-overlapping PaintColrLayers throughout | |
| # the base paint graphs. | |
| self.expandPaintColrLayers(font["COLR"].table) | |
| VariationMerger.mergeTables(self, font, master_ttfs, tableTags) | |
| def checkFormatEnum(self, out, lst, validate=lambda _: True): | |
| fmt = out.Format | |
| formatEnum = out.formatEnum | |
| ok = False | |
| try: | |
| fmt = formatEnum(fmt) | |
| except ValueError: | |
| pass | |
| else: | |
| ok = validate(fmt) | |
| if not ok: | |
| raise UnsupportedFormat(self, subtable=type(out).__name__, value=fmt) | |
| expected = fmt | |
| got = [] | |
| for v in lst: | |
| fmt = getattr(v, "Format", None) | |
| try: | |
| fmt = formatEnum(fmt) | |
| except ValueError: | |
| pass | |
| got.append(fmt) | |
| if not allEqualTo(expected, got): | |
| raise InconsistentFormats( | |
| self, | |
| subtable=type(out).__name__, | |
| expected=expected, | |
| got=got, | |
| ) | |
| return expected | |
| def mergeSparseDict(self, out, lst): | |
| for k in out.keys(): | |
| try: | |
| self.mergeThings(out[k], [v.get(k) for v in lst]) | |
| except VarLibMergeError as e: | |
| e.stack.append(f"[{k!r}]") | |
| raise | |
| def mergeAttrs(self, out, lst, attrs): | |
| for attr in attrs: | |
| value = getattr(out, attr) | |
| values = [getattr(item, attr) for item in lst] | |
| try: | |
| self.mergeThings(value, values) | |
| except VarLibMergeError as e: | |
| e.stack.append(f".{attr}") | |
| raise | |
| def storeMastersForAttr(self, out, lst, attr): | |
| master_values = [getattr(item, attr) for item in lst] | |
| # VarStore treats deltas for fixed-size floats as integers, so we | |
| # must convert master values to int before storing them in the builder | |
| # then back to float. | |
| is_fixed_size_float = False | |
| conv = out.getConverterByName(attr) | |
| if isinstance(conv, BaseFixedValue): | |
| is_fixed_size_float = True | |
| master_values = [conv.toInt(v) for v in master_values] | |
| baseValue = master_values[0] | |
| varIdx = ot.NO_VARIATION_INDEX | |
| if not allEqual(master_values): | |
| baseValue, varIdx = self.store_builder.storeMasters(master_values) | |
| if is_fixed_size_float: | |
| baseValue = conv.fromInt(baseValue) | |
| return baseValue, varIdx | |
| def storeVariationIndices(self, varIdxes) -> int: | |
| # try to reuse an existing VarIndexBase for the same varIdxes, or else | |
| # create a new one | |
| key = tuple(varIdxes) | |
| varIndexBase = self.varIndexCache.get(key) | |
| if varIndexBase is None: | |
| # scan for a full match anywhere in the self.varIdxes | |
| for i in range(len(self.varIdxes) - len(varIdxes) + 1): | |
| if self.varIdxes[i : i + len(varIdxes)] == varIdxes: | |
| self.varIndexCache[key] = varIndexBase = i | |
| break | |
| if varIndexBase is None: | |
| # try find a partial match at the end of the self.varIdxes | |
| for n in range(len(varIdxes) - 1, 0, -1): | |
| if self.varIdxes[-n:] == varIdxes[:n]: | |
| varIndexBase = len(self.varIdxes) - n | |
| self.varIndexCache[key] = varIndexBase | |
| self.varIdxes.extend(varIdxes[n:]) | |
| break | |
| if varIndexBase is None: | |
| # no match found, append at the end | |
| self.varIndexCache[key] = varIndexBase = len(self.varIdxes) | |
| self.varIdxes.extend(varIdxes) | |
| return varIndexBase | |
| def mergeVariableAttrs(self, out, lst, attrs) -> int: | |
| varIndexBase = ot.NO_VARIATION_INDEX | |
| varIdxes = [] | |
| for attr in attrs: | |
| baseValue, varIdx = self.storeMastersForAttr(out, lst, attr) | |
| setattr(out, attr, baseValue) | |
| varIdxes.append(varIdx) | |
| if any(v != ot.NO_VARIATION_INDEX for v in varIdxes): | |
| varIndexBase = self.storeVariationIndices(varIdxes) | |
| return varIndexBase | |
| def convertSubTablesToVarType(cls, table): | |
| for path in dfs_base_table( | |
| table, | |
| skip_root=True, | |
| predicate=lambda path: ( | |
| getattr(type(path[-1].value), "VarType", None) is not None | |
| ), | |
| ): | |
| st = path[-1] | |
| subTable = st.value | |
| varType = type(subTable).VarType | |
| newSubTable = varType() | |
| newSubTable.__dict__.update(subTable.__dict__) | |
| newSubTable.populateDefaults() | |
| parent = path[-2].value | |
| if st.index is not None: | |
| getattr(parent, st.name)[st.index] = newSubTable | |
| else: | |
| setattr(parent, st.name, newSubTable) | |
| def expandPaintColrLayers(colr): | |
| """Rebuild LayerList without PaintColrLayers reuse. | |
| Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph | |
| which are irrelevant for this); any layers referenced via PaintColrLayers are | |
| collected into a new LayerList and duplicated when reuse is detected, to ensure | |
| that all paints are distinct objects at the end of the process. | |
| PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap | |
| is left. Also, any consecutively nested PaintColrLayers are flattened. | |
| The COLR table's LayerList is replaced with the new unique layers. | |
| A side effect is also that any layer from the old LayerList which is not | |
| referenced by any PaintColrLayers is dropped. | |
| """ | |
| if not colr.LayerList: | |
| # if no LayerList, there's nothing to expand | |
| return | |
| uniqueLayerIDs = set() | |
| newLayerList = [] | |
| for rec in colr.BaseGlyphList.BaseGlyphPaintRecord: | |
| frontier = [rec.Paint] | |
| while frontier: | |
| paint = frontier.pop() | |
| if paint.Format == ot.PaintFormat.PaintColrGlyph: | |
| # don't traverse these, we treat them as constant for merging | |
| continue | |
| elif paint.Format == ot.PaintFormat.PaintColrLayers: | |
| # de-treeify any nested PaintColrLayers, append unique copies to | |
| # the new layer list and update PaintColrLayers index/count | |
| children = list(_flatten_layers(paint, colr)) | |
| first_layer_index = len(newLayerList) | |
| for layer in children: | |
| if id(layer) in uniqueLayerIDs: | |
| layer = copy.deepcopy(layer) | |
| assert id(layer) not in uniqueLayerIDs | |
| newLayerList.append(layer) | |
| uniqueLayerIDs.add(id(layer)) | |
| paint.FirstLayerIndex = first_layer_index | |
| paint.NumLayers = len(children) | |
| else: | |
| children = paint.getChildren(colr) | |
| frontier.extend(reversed(children)) | |
| # sanity check all the new layers are distinct objects | |
| assert len(newLayerList) == len(uniqueLayerIDs) | |
| colr.LayerList.Paint = newLayerList | |
| colr.LayerList.LayerCount = len(newLayerList) | |
| def merge(merger, self, lst): | |
| # ignore BaseGlyphCount, allow sparse glyph sets across masters | |
| out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord} | |
| masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst] | |
| for i, g in enumerate(out.keys()): | |
| try: | |
| # missing base glyphs don't participate in the merge | |
| merger.mergeThings(out[g], [v.get(g) for v in masters]) | |
| except VarLibMergeError as e: | |
| e.stack.append(f".BaseGlyphPaintRecord[{i}]") | |
| e.cause["location"] = f"base glyph {g!r}" | |
| raise | |
| merger._doneBaseGlyphs = True | |
| def merge(merger, self, lst): | |
| # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers | |
| # found while traversing the paint graphs rooted at BaseGlyphPaintRecords. | |
| assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList" | |
| # Simply flush the final list of layers and go home. | |
| self.LayerCount = len(merger.layers) | |
| self.Paint = merger.layers | |
| def _flatten_layers(root, colr): | |
| assert root.Format == ot.PaintFormat.PaintColrLayers | |
| for paint in root.getChildren(colr): | |
| if paint.Format == ot.PaintFormat.PaintColrLayers: | |
| yield from _flatten_layers(paint, colr) | |
| else: | |
| yield paint | |
| def _merge_PaintColrLayers(self, out, lst): | |
| # we only enforce that the (flat) number of layers is the same across all masters | |
| # but we allow FirstLayerIndex to differ to acommodate for sparse glyph sets. | |
| out_layers = list(_flatten_layers(out, self.font["COLR"].table)) | |
| # sanity check ttfs are subset to current values (see VariationMerger.mergeThings) | |
| # before matching each master PaintColrLayers to its respective COLR by position | |
| assert len(self.ttfs) == len(lst) | |
| master_layerses = [ | |
| list(_flatten_layers(lst[i], self.ttfs[i]["COLR"].table)) | |
| for i in range(len(lst)) | |
| ] | |
| try: | |
| self.mergeLists(out_layers, master_layerses) | |
| except VarLibMergeError as e: | |
| # NOTE: This attribute doesn't actually exist in PaintColrLayers but it's | |
| # handy to have it in the stack trace for debugging. | |
| e.stack.append(".Layers") | |
| raise | |
| # following block is very similar to LayerListBuilder._beforeBuildPaintColrLayers | |
| # but I couldn't find a nice way to share the code between the two... | |
| if self.layerReuseCache is not None: | |
| # successful reuse can make the list smaller | |
| out_layers = self.layerReuseCache.try_reuse(out_layers) | |
| # if the list is still too big we need to tree-fy it | |
| is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT | |
| out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT) | |
| # We now have a tree of sequences with Paint leaves. | |
| # Convert the sequences into PaintColrLayers. | |
| def listToColrLayers(paint): | |
| if isinstance(paint, list): | |
| layers = [listToColrLayers(l) for l in paint] | |
| paint = ot.Paint() | |
| paint.Format = int(ot.PaintFormat.PaintColrLayers) | |
| paint.NumLayers = len(layers) | |
| paint.FirstLayerIndex = len(self.layers) | |
| self.layers.extend(layers) | |
| if self.layerReuseCache is not None: | |
| self.layerReuseCache.add(layers, paint.FirstLayerIndex) | |
| return paint | |
| out_layers = [listToColrLayers(l) for l in out_layers] | |
| if len(out_layers) == 1 and out_layers[0].Format == ot.PaintFormat.PaintColrLayers: | |
| # special case when the reuse cache finds a single perfect PaintColrLayers match | |
| # (it can only come from a successful reuse, _flatten_layers has gotten rid of | |
| # all nested PaintColrLayers already); we assign it directly and avoid creating | |
| # an extra table | |
| out.NumLayers = out_layers[0].NumLayers | |
| out.FirstLayerIndex = out_layers[0].FirstLayerIndex | |
| else: | |
| out.NumLayers = len(out_layers) | |
| out.FirstLayerIndex = len(self.layers) | |
| self.layers.extend(out_layers) | |
| # Register our parts for reuse provided we aren't a tree | |
| # If we are a tree the leaves registered for reuse and that will suffice | |
| if self.layerReuseCache is not None and not is_tree: | |
| self.layerReuseCache.add(out_layers, out.FirstLayerIndex) | |
| def merge(merger, self, lst): | |
| fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable()) | |
| if fmt is ot.PaintFormat.PaintColrLayers: | |
| _merge_PaintColrLayers(merger, self, lst) | |
| return | |
| varFormat = fmt.as_variable() | |
| varAttrs = () | |
| if varFormat is not None: | |
| varAttrs = otBase.getVariableAttrs(type(self), varFormat) | |
| staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs) | |
| merger.mergeAttrs(self, lst, staticAttrs) | |
| varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs) | |
| subTables = [st.value for st in self.iterSubTables()] | |
| # Convert table to variable if itself has variations or any subtables have | |
| isVariable = varIndexBase != ot.NO_VARIATION_INDEX or any( | |
| id(table) in merger.varTableIds for table in subTables | |
| ) | |
| if isVariable: | |
| if varAttrs: | |
| # Some PaintVar* don't have any scalar attributes that can vary, | |
| # only indirect offsets to other variable subtables, thus have | |
| # no VarIndexBase of their own (e.g. PaintVarTransform) | |
| self.VarIndexBase = varIndexBase | |
| if subTables: | |
| # Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc. | |
| merger.convertSubTablesToVarType(self) | |
| assert varFormat is not None | |
| self.Format = int(varFormat) | |
| def merge(merger, self, lst): | |
| varType = type(self).VarType | |
| varAttrs = otBase.getVariableAttrs(varType) | |
| staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs) | |
| merger.mergeAttrs(self, lst, staticAttrs) | |
| varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs) | |
| if varIndexBase != ot.NO_VARIATION_INDEX: | |
| self.VarIndexBase = varIndexBase | |
| # mark as having variations so the parent table will convert to Var{Type} | |
| merger.varTableIds.add(id(self)) | |
| def merge(merger, self, lst): | |
| merger.mergeAttrs(self, lst, (c.name for c in self.getConverters())) | |
| if any(id(stop) in merger.varTableIds for stop in self.ColorStop): | |
| merger.convertSubTablesToVarType(self) | |
| merger.varTableIds.add(id(self)) | |
| def merge(merger, self, lst): | |
| # 'sparse' in that we allow non-default masters to omit ClipBox entries | |
| # for some/all glyphs (i.e. they don't participate) | |
| merger.mergeSparseDict(self, lst) | |