xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/merger.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""
2Merge OpenType Layout tables (GDEF / GPOS / GSUB).
3"""
4
5import os
6import copy
7import enum
8from operator import ior
9import logging
10from fontTools.colorLib.builder import MAX_PAINT_COLR_LAYER_COUNT, LayerReuseCache
11from fontTools.misc import classifyTools
12from fontTools.misc.roundTools import otRound
13from fontTools.misc.treeTools import build_n_ary_tree
14from fontTools.ttLib.tables import otTables as ot
15from fontTools.ttLib.tables import otBase as otBase
16from fontTools.ttLib.tables.otConverters import BaseFixedValue
17from fontTools.ttLib.tables.otTraverse import dfs_base_table
18from fontTools.ttLib.tables.DefaultTable import DefaultTable
19from fontTools.varLib import builder, models, varStore
20from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo, subList
21from fontTools.varLib.varStore import VarStoreInstancer
22from functools import reduce
23from fontTools.otlLib.builder import buildSinglePos
24from fontTools.otlLib.optimize.gpos import (
25    _compression_level_from_env,
26    compact_pair_pos,
27)
28
29log = logging.getLogger("fontTools.varLib.merger")
30
31from .errors import (
32    ShouldBeConstant,
33    FoundANone,
34    MismatchedTypes,
35    NotANone,
36    LengthsDiffer,
37    KeysDiffer,
38    InconsistentGlyphOrder,
39    InconsistentExtensions,
40    InconsistentFormats,
41    UnsupportedFormat,
42    VarLibMergeError,
43)
44
45
46class Merger(object):
47    def __init__(self, font=None):
48        self.font = font
49        # mergeTables populates this from the parent's master ttfs
50        self.ttfs = None
51
52    @classmethod
53    def merger(celf, clazzes, attrs=(None,)):
54        assert celf != Merger, "Subclass Merger instead."
55        if "mergers" not in celf.__dict__:
56            celf.mergers = {}
57        if type(clazzes) in (type, enum.EnumMeta):
58            clazzes = (clazzes,)
59        if type(attrs) == str:
60            attrs = (attrs,)
61
62        def wrapper(method):
63            assert method.__name__ == "merge"
64            done = []
65            for clazz in clazzes:
66                if clazz in done:
67                    continue  # Support multiple names of a clazz
68                done.append(clazz)
69                mergers = celf.mergers.setdefault(clazz, {})
70                for attr in attrs:
71                    assert attr not in mergers, (
72                        "Oops, class '%s' has merge function for '%s' defined already."
73                        % (clazz.__name__, attr)
74                    )
75                    mergers[attr] = method
76            return None
77
78        return wrapper
79
80    @classmethod
81    def mergersFor(celf, thing, _default={}):
82        typ = type(thing)
83
84        for celf in celf.mro():
85            mergers = getattr(celf, "mergers", None)
86            if mergers is None:
87                break
88
89            m = celf.mergers.get(typ, None)
90            if m is not None:
91                return m
92
93        return _default
94
95    def mergeObjects(self, out, lst, exclude=()):
96        if hasattr(out, "ensureDecompiled"):
97            out.ensureDecompiled(recurse=False)
98        for item in lst:
99            if hasattr(item, "ensureDecompiled"):
100                item.ensureDecompiled(recurse=False)
101        keys = sorted(vars(out).keys())
102        if not all(keys == sorted(vars(v).keys()) for v in lst):
103            raise KeysDiffer(
104                self, expected=keys, got=[sorted(vars(v).keys()) for v in lst]
105            )
106        mergers = self.mergersFor(out)
107        defaultMerger = mergers.get("*", self.__class__.mergeThings)
108        try:
109            for key in keys:
110                if key in exclude:
111                    continue
112                value = getattr(out, key)
113                values = [getattr(table, key) for table in lst]
114                mergerFunc = mergers.get(key, defaultMerger)
115                mergerFunc(self, value, values)
116        except VarLibMergeError as e:
117            e.stack.append("." + key)
118            raise
119
120    def mergeLists(self, out, lst):
121        if not allEqualTo(out, lst, len):
122            raise LengthsDiffer(self, expected=len(out), got=[len(x) for x in lst])
123        for i, (value, values) in enumerate(zip(out, zip(*lst))):
124            try:
125                self.mergeThings(value, values)
126            except VarLibMergeError as e:
127                e.stack.append("[%d]" % i)
128                raise
129
130    def mergeThings(self, out, lst):
131        if not allEqualTo(out, lst, type):
132            raise MismatchedTypes(
133                self, expected=type(out).__name__, got=[type(x).__name__ for x in lst]
134            )
135        mergerFunc = self.mergersFor(out).get(None, None)
136        if mergerFunc is not None:
137            mergerFunc(self, out, lst)
138        elif isinstance(out, enum.Enum):
139            # need to special-case Enums as have __dict__ but are not regular 'objects',
140            # otherwise mergeObjects/mergeThings get trapped in a RecursionError
141            if not allEqualTo(out, lst):
142                raise ShouldBeConstant(self, expected=out, got=lst)
143        elif hasattr(out, "__dict__"):
144            self.mergeObjects(out, lst)
145        elif isinstance(out, list):
146            self.mergeLists(out, lst)
147        else:
148            if not allEqualTo(out, lst):
149                raise ShouldBeConstant(self, expected=out, got=lst)
150
151    def mergeTables(self, font, master_ttfs, tableTags):
152        for tag in tableTags:
153            if tag not in font:
154                continue
155            try:
156                self.ttfs = master_ttfs
157                self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs])
158            except VarLibMergeError as e:
159                e.stack.append(tag)
160                raise
161
162
163#
164# Aligning merger
165#
166class AligningMerger(Merger):
167    pass
168
169
170@AligningMerger.merger(ot.GDEF, "GlyphClassDef")
171def merge(merger, self, lst):
172    if self is None:
173        if not allNone(lst):
174            raise NotANone(merger, expected=None, got=lst)
175        return
176
177    lst = [l.classDefs for l in lst]
178    self.classDefs = {}
179    # We only care about the .classDefs
180    self = self.classDefs
181
182    allKeys = set()
183    allKeys.update(*[l.keys() for l in lst])
184    for k in allKeys:
185        allValues = nonNone(l.get(k) for l in lst)
186        if not allEqual(allValues):
187            raise ShouldBeConstant(
188                merger, expected=allValues[0], got=lst, stack=["." + k]
189            )
190        if not allValues:
191            self[k] = None
192        else:
193            self[k] = allValues[0]
194
195
196def _SinglePosUpgradeToFormat2(self):
197    if self.Format == 2:
198        return self
199
200    ret = ot.SinglePos()
201    ret.Format = 2
202    ret.Coverage = self.Coverage
203    ret.ValueFormat = self.ValueFormat
204    ret.Value = [self.Value for _ in ret.Coverage.glyphs]
205    ret.ValueCount = len(ret.Value)
206
207    return ret
208
209
210def _merge_GlyphOrders(font, lst, values_lst=None, default=None):
211    """Takes font and list of glyph lists (must be sorted by glyph id), and returns
212    two things:
213    - Combined glyph list,
214    - If values_lst is None, return input glyph lists, but padded with None when a glyph
215      was missing in a list.  Otherwise, return values_lst list-of-list, padded with None
216      to match combined glyph lists.
217    """
218    if values_lst is None:
219        dict_sets = [set(l) for l in lst]
220    else:
221        dict_sets = [{g: v for g, v in zip(l, vs)} for l, vs in zip(lst, values_lst)]
222    combined = set()
223    combined.update(*dict_sets)
224
225    sortKey = font.getReverseGlyphMap().__getitem__
226    order = sorted(combined, key=sortKey)
227    # Make sure all input glyphsets were in proper order
228    if not all(sorted(vs, key=sortKey) == vs for vs in lst):
229        raise InconsistentGlyphOrder()
230    del combined
231
232    paddedValues = None
233    if values_lst is None:
234        padded = [
235            [glyph if glyph in dict_set else default for glyph in order]
236            for dict_set in dict_sets
237        ]
238    else:
239        assert len(lst) == len(values_lst)
240        padded = [
241            [dict_set[glyph] if glyph in dict_set else default for glyph in order]
242            for dict_set in dict_sets
243        ]
244    return order, padded
245
246
247@AligningMerger.merger(otBase.ValueRecord)
248def merge(merger, self, lst):
249    # Code below sometimes calls us with self being
250    # a new object. Copy it from lst and recurse.
251    self.__dict__ = lst[0].__dict__.copy()
252    merger.mergeObjects(self, lst)
253
254
255@AligningMerger.merger(ot.Anchor)
256def merge(merger, self, lst):
257    # Code below sometimes calls us with self being
258    # a new object. Copy it from lst and recurse.
259    self.__dict__ = lst[0].__dict__.copy()
260    merger.mergeObjects(self, lst)
261
262
263def _Lookup_SinglePos_get_effective_value(merger, subtables, glyph):
264    for self in subtables:
265        if (
266            self is None
267            or type(self) != ot.SinglePos
268            or self.Coverage is None
269            or glyph not in self.Coverage.glyphs
270        ):
271            continue
272        if self.Format == 1:
273            return self.Value
274        elif self.Format == 2:
275            return self.Value[self.Coverage.glyphs.index(glyph)]
276        else:
277            raise UnsupportedFormat(merger, subtable="single positioning lookup")
278    return None
279
280
281def _Lookup_PairPos_get_effective_value_pair(
282    merger, subtables, firstGlyph, secondGlyph
283):
284    for self in subtables:
285        if (
286            self is None
287            or type(self) != ot.PairPos
288            or self.Coverage is None
289            or firstGlyph not in self.Coverage.glyphs
290        ):
291            continue
292        if self.Format == 1:
293            ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)]
294            pvr = ps.PairValueRecord
295            for rec in pvr:  # TODO Speed up
296                if rec.SecondGlyph == secondGlyph:
297                    return rec
298            continue
299        elif self.Format == 2:
300            klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0)
301            klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0)
302            return self.Class1Record[klass1].Class2Record[klass2]
303        else:
304            raise UnsupportedFormat(merger, subtable="pair positioning lookup")
305    return None
306
307
308@AligningMerger.merger(ot.SinglePos)
309def merge(merger, self, lst):
310    self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0)
311    if not (len(lst) == 1 or (valueFormat & ~0xF == 0)):
312        raise UnsupportedFormat(merger, subtable="single positioning lookup")
313
314    # If all have same coverage table and all are format 1,
315    coverageGlyphs = self.Coverage.glyphs
316    if all(v.Format == 1 for v in lst) and all(
317        coverageGlyphs == v.Coverage.glyphs for v in lst
318    ):
319        self.Value = otBase.ValueRecord(valueFormat, self.Value)
320        if valueFormat != 0:
321            # If v.Value is None, it means a kerning of 0; we want
322            # it to participate in the model still.
323            # https://github.com/fonttools/fonttools/issues/3111
324            merger.mergeThings(
325                self.Value,
326                [v.Value if v.Value is not None else otBase.ValueRecord() for v in lst],
327            )
328        self.ValueFormat = self.Value.getFormat()
329        return
330
331    # Upgrade everything to Format=2
332    self.Format = 2
333    lst = [_SinglePosUpgradeToFormat2(v) for v in lst]
334
335    # Align them
336    glyphs, padded = _merge_GlyphOrders(
337        merger.font, [v.Coverage.glyphs for v in lst], [v.Value for v in lst]
338    )
339
340    self.Coverage.glyphs = glyphs
341    self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs]
342    self.ValueCount = len(self.Value)
343
344    for i, values in enumerate(padded):
345        for j, glyph in enumerate(glyphs):
346            if values[j] is not None:
347                continue
348            # Fill in value from other subtables
349            # Note!!! This *might* result in behavior change if ValueFormat2-zeroedness
350            # is different between used subtable and current subtable!
351            # TODO(behdad) Check and warn if that happens?
352            v = _Lookup_SinglePos_get_effective_value(
353                merger, merger.lookup_subtables[i], glyph
354            )
355            if v is None:
356                v = otBase.ValueRecord(valueFormat)
357            values[j] = v
358
359    merger.mergeLists(self.Value, padded)
360
361    # Merge everything else; though, there shouldn't be anything else. :)
362    merger.mergeObjects(
363        self, lst, exclude=("Format", "Coverage", "Value", "ValueCount", "ValueFormat")
364    )
365    self.ValueFormat = reduce(
366        int.__or__, [v.getEffectiveFormat() for v in self.Value], 0
367    )
368
369
370@AligningMerger.merger(ot.PairSet)
371def merge(merger, self, lst):
372    # Align them
373    glyphs, padded = _merge_GlyphOrders(
374        merger.font,
375        [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
376        [vs.PairValueRecord for vs in lst],
377    )
378
379    self.PairValueRecord = pvrs = []
380    for glyph in glyphs:
381        pvr = ot.PairValueRecord()
382        pvr.SecondGlyph = glyph
383        pvr.Value1 = (
384            otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None
385        )
386        pvr.Value2 = (
387            otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None
388        )
389        pvrs.append(pvr)
390    self.PairValueCount = len(self.PairValueRecord)
391
392    for i, values in enumerate(padded):
393        for j, glyph in enumerate(glyphs):
394            # Fill in value from other subtables
395            v = ot.PairValueRecord()
396            v.SecondGlyph = glyph
397            if values[j] is not None:
398                vpair = values[j]
399            else:
400                vpair = _Lookup_PairPos_get_effective_value_pair(
401                    merger, merger.lookup_subtables[i], self._firstGlyph, glyph
402                )
403            if vpair is None:
404                v1, v2 = None, None
405            else:
406                v1 = getattr(vpair, "Value1", None)
407                v2 = getattr(vpair, "Value2", None)
408            v.Value1 = (
409                otBase.ValueRecord(merger.valueFormat1, src=v1)
410                if merger.valueFormat1
411                else None
412            )
413            v.Value2 = (
414                otBase.ValueRecord(merger.valueFormat2, src=v2)
415                if merger.valueFormat2
416                else None
417            )
418            values[j] = v
419    del self._firstGlyph
420
421    merger.mergeLists(self.PairValueRecord, padded)
422
423
424def _PairPosFormat1_merge(self, lst, merger):
425    assert allEqual(
426        [l.ValueFormat2 == 0 for l in lst if l.PairSet]
427    ), "Report bug against fonttools."
428
429    # Merge everything else; makes sure Format is the same.
430    merger.mergeObjects(
431        self,
432        lst,
433        exclude=("Coverage", "PairSet", "PairSetCount", "ValueFormat1", "ValueFormat2"),
434    )
435
436    empty = ot.PairSet()
437    empty.PairValueRecord = []
438    empty.PairValueCount = 0
439
440    # Align them
441    glyphs, padded = _merge_GlyphOrders(
442        merger.font,
443        [v.Coverage.glyphs for v in lst],
444        [v.PairSet for v in lst],
445        default=empty,
446    )
447
448    self.Coverage.glyphs = glyphs
449    self.PairSet = [ot.PairSet() for _ in glyphs]
450    self.PairSetCount = len(self.PairSet)
451    for glyph, ps in zip(glyphs, self.PairSet):
452        ps._firstGlyph = glyph
453
454    merger.mergeLists(self.PairSet, padded)
455
456
457def _ClassDef_invert(self, allGlyphs=None):
458    if isinstance(self, dict):
459        classDefs = self
460    else:
461        classDefs = self.classDefs if self and self.classDefs else {}
462    m = max(classDefs.values()) if classDefs else 0
463
464    ret = []
465    for _ in range(m + 1):
466        ret.append(set())
467
468    for k, v in classDefs.items():
469        ret[v].add(k)
470
471    # Class-0 is special.  It's "everything else".
472    if allGlyphs is None:
473        ret[0] = None
474    else:
475        # Limit all classes to glyphs in allGlyphs.
476        # Collect anything without a non-zero class into class=zero.
477        ret[0] = class0 = set(allGlyphs)
478        for s in ret[1:]:
479            s.intersection_update(class0)
480            class0.difference_update(s)
481
482    return ret
483
484
485def _ClassDef_merge_classify(lst, allGlyphses=None):
486    self = ot.ClassDef()
487    self.classDefs = classDefs = {}
488    allGlyphsesWasNone = allGlyphses is None
489    if allGlyphsesWasNone:
490        allGlyphses = [None] * len(lst)
491
492    classifier = classifyTools.Classifier()
493    for classDef, allGlyphs in zip(lst, allGlyphses):
494        sets = _ClassDef_invert(classDef, allGlyphs)
495        if allGlyphs is None:
496            sets = sets[1:]
497        classifier.update(sets)
498    classes = classifier.getClasses()
499
500    if allGlyphsesWasNone:
501        classes.insert(0, set())
502
503    for i, classSet in enumerate(classes):
504        if i == 0:
505            continue
506        for g in classSet:
507            classDefs[g] = i
508
509    return self, classes
510
511
512def _PairPosFormat2_align_matrices(self, lst, font, transparent=False):
513    matrices = [l.Class1Record for l in lst]
514
515    # Align first classes
516    self.ClassDef1, classes = _ClassDef_merge_classify(
517        [l.ClassDef1 for l in lst], [l.Coverage.glyphs for l in lst]
518    )
519    self.Class1Count = len(classes)
520    new_matrices = []
521    for l, matrix in zip(lst, matrices):
522        nullRow = None
523        coverage = set(l.Coverage.glyphs)
524        classDef1 = l.ClassDef1.classDefs
525        class1Records = []
526        for classSet in classes:
527            exemplarGlyph = next(iter(classSet))
528            if exemplarGlyph not in coverage:
529                # Follow-up to e6125b353e1f54a0280ded5434b8e40d042de69f,
530                # Fixes https://github.com/googlei18n/fontmake/issues/470
531                # Again, revert 8d441779e5afc664960d848f62c7acdbfc71d7b9
532                # when merger becomes selfless.
533                nullRow = None
534                if nullRow is None:
535                    nullRow = ot.Class1Record()
536                    class2records = nullRow.Class2Record = []
537                    # TODO: When merger becomes selfless, revert e6125b353e1f54a0280ded5434b8e40d042de69f
538                    for _ in range(l.Class2Count):
539                        if transparent:
540                            rec2 = None
541                        else:
542                            rec2 = ot.Class2Record()
543                            rec2.Value1 = (
544                                otBase.ValueRecord(self.ValueFormat1)
545                                if self.ValueFormat1
546                                else None
547                            )
548                            rec2.Value2 = (
549                                otBase.ValueRecord(self.ValueFormat2)
550                                if self.ValueFormat2
551                                else None
552                            )
553                        class2records.append(rec2)
554                rec1 = nullRow
555            else:
556                klass = classDef1.get(exemplarGlyph, 0)
557                rec1 = matrix[klass]  # TODO handle out-of-range?
558            class1Records.append(rec1)
559        new_matrices.append(class1Records)
560    matrices = new_matrices
561    del new_matrices
562
563    # Align second classes
564    self.ClassDef2, classes = _ClassDef_merge_classify([l.ClassDef2 for l in lst])
565    self.Class2Count = len(classes)
566    new_matrices = []
567    for l, matrix in zip(lst, matrices):
568        classDef2 = l.ClassDef2.classDefs
569        class1Records = []
570        for rec1old in matrix:
571            oldClass2Records = rec1old.Class2Record
572            rec1new = ot.Class1Record()
573            class2Records = rec1new.Class2Record = []
574            for classSet in classes:
575                if not classSet:  # class=0
576                    rec2 = oldClass2Records[0]
577                else:
578                    exemplarGlyph = next(iter(classSet))
579                    klass = classDef2.get(exemplarGlyph, 0)
580                    rec2 = oldClass2Records[klass]
581                class2Records.append(copy.deepcopy(rec2))
582            class1Records.append(rec1new)
583        new_matrices.append(class1Records)
584    matrices = new_matrices
585    del new_matrices
586
587    return matrices
588
589
590def _PairPosFormat2_merge(self, lst, merger):
591    assert allEqual(
592        [l.ValueFormat2 == 0 for l in lst if l.Class1Record]
593    ), "Report bug against fonttools."
594
595    merger.mergeObjects(
596        self,
597        lst,
598        exclude=(
599            "Coverage",
600            "ClassDef1",
601            "Class1Count",
602            "ClassDef2",
603            "Class2Count",
604            "Class1Record",
605            "ValueFormat1",
606            "ValueFormat2",
607        ),
608    )
609
610    # Align coverages
611    glyphs, _ = _merge_GlyphOrders(merger.font, [v.Coverage.glyphs for v in lst])
612    self.Coverage.glyphs = glyphs
613
614    # Currently, if the coverage of PairPosFormat2 subtables are different,
615    # we do NOT bother walking down the subtable list when filling in new
616    # rows for alignment.  As such, this is only correct if current subtable
617    # is the last subtable in the lookup.  Ensure that.
618    #
619    # Note that our canonicalization process merges trailing PairPosFormat2's,
620    # so in reality this is rare.
621    for l, subtables in zip(lst, merger.lookup_subtables):
622        if l.Coverage.glyphs != glyphs:
623            assert l == subtables[-1]
624
625    matrices = _PairPosFormat2_align_matrices(self, lst, merger.font)
626
627    self.Class1Record = list(matrices[0])  # TODO move merger to be selfless
628    merger.mergeLists(self.Class1Record, matrices)
629
630
631@AligningMerger.merger(ot.PairPos)
632def merge(merger, self, lst):
633    merger.valueFormat1 = self.ValueFormat1 = reduce(
634        int.__or__, [l.ValueFormat1 for l in lst], 0
635    )
636    merger.valueFormat2 = self.ValueFormat2 = reduce(
637        int.__or__, [l.ValueFormat2 for l in lst], 0
638    )
639
640    if self.Format == 1:
641        _PairPosFormat1_merge(self, lst, merger)
642    elif self.Format == 2:
643        _PairPosFormat2_merge(self, lst, merger)
644    else:
645        raise UnsupportedFormat(merger, subtable="pair positioning lookup")
646
647    del merger.valueFormat1, merger.valueFormat2
648
649    # Now examine the list of value records, and update to the union of format values,
650    # as merge might have created new values.
651    vf1 = 0
652    vf2 = 0
653    if self.Format == 1:
654        for pairSet in self.PairSet:
655            for pairValueRecord in pairSet.PairValueRecord:
656                pv1 = getattr(pairValueRecord, "Value1", None)
657                if pv1 is not None:
658                    vf1 |= pv1.getFormat()
659                pv2 = getattr(pairValueRecord, "Value2", None)
660                if pv2 is not None:
661                    vf2 |= pv2.getFormat()
662    elif self.Format == 2:
663        for class1Record in self.Class1Record:
664            for class2Record in class1Record.Class2Record:
665                pv1 = getattr(class2Record, "Value1", None)
666                if pv1 is not None:
667                    vf1 |= pv1.getFormat()
668                pv2 = getattr(class2Record, "Value2", None)
669                if pv2 is not None:
670                    vf2 |= pv2.getFormat()
671    self.ValueFormat1 = vf1
672    self.ValueFormat2 = vf2
673
674
675def _MarkBasePosFormat1_merge(self, lst, merger, Mark="Mark", Base="Base"):
676    self.ClassCount = max(l.ClassCount for l in lst)
677
678    MarkCoverageGlyphs, MarkRecords = _merge_GlyphOrders(
679        merger.font,
680        [getattr(l, Mark + "Coverage").glyphs for l in lst],
681        [getattr(l, Mark + "Array").MarkRecord for l in lst],
682    )
683    getattr(self, Mark + "Coverage").glyphs = MarkCoverageGlyphs
684
685    BaseCoverageGlyphs, BaseRecords = _merge_GlyphOrders(
686        merger.font,
687        [getattr(l, Base + "Coverage").glyphs for l in lst],
688        [getattr(getattr(l, Base + "Array"), Base + "Record") for l in lst],
689    )
690    getattr(self, Base + "Coverage").glyphs = BaseCoverageGlyphs
691
692    # MarkArray
693    records = []
694    for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)):
695        allClasses = [r.Class for r in glyphRecords if r is not None]
696
697        # TODO Right now we require that all marks have same class in
698        # all masters that cover them.  This is not required.
699        #
700        # We can relax that by just requiring that all marks that have
701        # the same class in a master, have the same class in every other
702        # master.  Indeed, if, say, a sparse master only covers one mark,
703        # that mark probably will get class 0, which would possibly be
704        # different from its class in other masters.
705        #
706        # We can even go further and reclassify marks to support any
707        # input.  But, since, it's unlikely that two marks being both,
708        # say, "top" in one master, and one being "top" and other being
709        # "top-right" in another master, we shouldn't do that, as any
710        # failures in that case will probably signify mistakes in the
711        # input masters.
712
713        if not allEqual(allClasses):
714            raise ShouldBeConstant(merger, expected=allClasses[0], got=allClasses)
715        else:
716            rec = ot.MarkRecord()
717            rec.Class = allClasses[0]
718            allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords]
719            if allNone(allAnchors):
720                anchor = None
721            else:
722                anchor = ot.Anchor()
723                anchor.Format = 1
724                merger.mergeThings(anchor, allAnchors)
725            rec.MarkAnchor = anchor
726        records.append(rec)
727    array = ot.MarkArray()
728    array.MarkRecord = records
729    array.MarkCount = len(records)
730    setattr(self, Mark + "Array", array)
731
732    # BaseArray
733    records = []
734    for g, glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)):
735        if allNone(glyphRecords):
736            rec = None
737        else:
738            rec = getattr(ot, Base + "Record")()
739            anchors = []
740            setattr(rec, Base + "Anchor", anchors)
741            glyphAnchors = [
742                [] if r is None else getattr(r, Base + "Anchor") for r in glyphRecords
743            ]
744            for l in glyphAnchors:
745                l.extend([None] * (self.ClassCount - len(l)))
746            for allAnchors in zip(*glyphAnchors):
747                if allNone(allAnchors):
748                    anchor = None
749                else:
750                    anchor = ot.Anchor()
751                    anchor.Format = 1
752                    merger.mergeThings(anchor, allAnchors)
753                anchors.append(anchor)
754        records.append(rec)
755    array = getattr(ot, Base + "Array")()
756    setattr(array, Base + "Record", records)
757    setattr(array, Base + "Count", len(records))
758    setattr(self, Base + "Array", array)
759
760
761@AligningMerger.merger(ot.MarkBasePos)
762def merge(merger, self, lst):
763    if not allEqualTo(self.Format, (l.Format for l in lst)):
764        raise InconsistentFormats(
765            merger,
766            subtable="mark-to-base positioning lookup",
767            expected=self.Format,
768            got=[l.Format for l in lst],
769        )
770    if self.Format == 1:
771        _MarkBasePosFormat1_merge(self, lst, merger)
772    else:
773        raise UnsupportedFormat(merger, subtable="mark-to-base positioning lookup")
774
775
776@AligningMerger.merger(ot.MarkMarkPos)
777def merge(merger, self, lst):
778    if not allEqualTo(self.Format, (l.Format for l in lst)):
779        raise InconsistentFormats(
780            merger,
781            subtable="mark-to-mark positioning lookup",
782            expected=self.Format,
783            got=[l.Format for l in lst],
784        )
785    if self.Format == 1:
786        _MarkBasePosFormat1_merge(self, lst, merger, "Mark1", "Mark2")
787    else:
788        raise UnsupportedFormat(merger, subtable="mark-to-mark positioning lookup")
789
790
791def _PairSet_flatten(lst, font):
792    self = ot.PairSet()
793    self.Coverage = ot.Coverage()
794
795    # Align them
796    glyphs, padded = _merge_GlyphOrders(
797        font,
798        [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
799        [vs.PairValueRecord for vs in lst],
800    )
801
802    self.Coverage.glyphs = glyphs
803    self.PairValueRecord = pvrs = []
804    for values in zip(*padded):
805        for v in values:
806            if v is not None:
807                pvrs.append(v)
808                break
809        else:
810            assert False
811    self.PairValueCount = len(self.PairValueRecord)
812
813    return self
814
815
816def _Lookup_PairPosFormat1_subtables_flatten(lst, font):
817    assert allEqual(
818        [l.ValueFormat2 == 0 for l in lst if l.PairSet]
819    ), "Report bug against fonttools."
820
821    self = ot.PairPos()
822    self.Format = 1
823    self.Coverage = ot.Coverage()
824    self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0)
825    self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0)
826
827    # Align them
828    glyphs, padded = _merge_GlyphOrders(
829        font, [v.Coverage.glyphs for v in lst], [v.PairSet for v in lst]
830    )
831
832    self.Coverage.glyphs = glyphs
833    self.PairSet = [
834        _PairSet_flatten([v for v in values if v is not None], font)
835        for values in zip(*padded)
836    ]
837    self.PairSetCount = len(self.PairSet)
838    return self
839
840
841def _Lookup_PairPosFormat2_subtables_flatten(lst, font):
842    assert allEqual(
843        [l.ValueFormat2 == 0 for l in lst if l.Class1Record]
844    ), "Report bug against fonttools."
845
846    self = ot.PairPos()
847    self.Format = 2
848    self.Coverage = ot.Coverage()
849    self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0)
850    self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0)
851
852    # Align them
853    glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst])
854    self.Coverage.glyphs = glyphs
855
856    matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True)
857
858    matrix = self.Class1Record = []
859    for rows in zip(*matrices):
860        row = ot.Class1Record()
861        matrix.append(row)
862        row.Class2Record = []
863        row = row.Class2Record
864        for cols in zip(*list(r.Class2Record for r in rows)):
865            col = next(iter(c for c in cols if c is not None))
866            row.append(col)
867
868    return self
869
870
871def _Lookup_PairPos_subtables_canonicalize(lst, font):
872    """Merge multiple Format1 subtables at the beginning of lst,
873    and merge multiple consecutive Format2 subtables that have the same
874    Class2 (ie. were split because of offset overflows).  Returns new list."""
875    lst = list(lst)
876
877    l = len(lst)
878    i = 0
879    while i < l and lst[i].Format == 1:
880        i += 1
881    lst[:i] = [_Lookup_PairPosFormat1_subtables_flatten(lst[:i], font)]
882
883    l = len(lst)
884    i = l
885    while i > 0 and lst[i - 1].Format == 2:
886        i -= 1
887    lst[i:] = [_Lookup_PairPosFormat2_subtables_flatten(lst[i:], font)]
888
889    return lst
890
891
892def _Lookup_SinglePos_subtables_flatten(lst, font, min_inclusive_rec_format):
893    glyphs, _ = _merge_GlyphOrders(font, [v.Coverage.glyphs for v in lst], None)
894    num_glyphs = len(glyphs)
895    new = ot.SinglePos()
896    new.Format = 2
897    new.ValueFormat = min_inclusive_rec_format
898    new.Coverage = ot.Coverage()
899    new.Coverage.glyphs = glyphs
900    new.ValueCount = num_glyphs
901    new.Value = [None] * num_glyphs
902    for singlePos in lst:
903        if singlePos.Format == 1:
904            val_rec = singlePos.Value
905            for gname in singlePos.Coverage.glyphs:
906                i = glyphs.index(gname)
907                new.Value[i] = copy.deepcopy(val_rec)
908        elif singlePos.Format == 2:
909            for j, gname in enumerate(singlePos.Coverage.glyphs):
910                val_rec = singlePos.Value[j]
911                i = glyphs.index(gname)
912                new.Value[i] = copy.deepcopy(val_rec)
913    return [new]
914
915
916@AligningMerger.merger(ot.CursivePos)
917def merge(merger, self, lst):
918    # Align them
919    glyphs, padded = _merge_GlyphOrders(
920        merger.font,
921        [l.Coverage.glyphs for l in lst],
922        [l.EntryExitRecord for l in lst],
923    )
924
925    self.Format = 1
926    self.Coverage = ot.Coverage()
927    self.Coverage.glyphs = glyphs
928    self.EntryExitRecord = []
929    for _ in glyphs:
930        rec = ot.EntryExitRecord()
931        rec.EntryAnchor = ot.Anchor()
932        rec.EntryAnchor.Format = 1
933        rec.ExitAnchor = ot.Anchor()
934        rec.ExitAnchor.Format = 1
935        self.EntryExitRecord.append(rec)
936    merger.mergeLists(self.EntryExitRecord, padded)
937    self.EntryExitCount = len(self.EntryExitRecord)
938
939
940@AligningMerger.merger(ot.EntryExitRecord)
941def merge(merger, self, lst):
942    if all(master.EntryAnchor is None for master in lst):
943        self.EntryAnchor = None
944    if all(master.ExitAnchor is None for master in lst):
945        self.ExitAnchor = None
946    merger.mergeObjects(self, lst)
947
948
949@AligningMerger.merger(ot.Lookup)
950def merge(merger, self, lst):
951    subtables = merger.lookup_subtables = [l.SubTable for l in lst]
952
953    # Remove Extension subtables
954    for l, sts in list(zip(lst, subtables)) + [(self, self.SubTable)]:
955        if not sts:
956            continue
957        if sts[0].__class__.__name__.startswith("Extension"):
958            if not allEqual([st.__class__ for st in sts]):
959                raise InconsistentExtensions(
960                    merger,
961                    expected="Extension",
962                    got=[st.__class__.__name__ for st in sts],
963                )
964            if not allEqual([st.ExtensionLookupType for st in sts]):
965                raise InconsistentExtensions(merger)
966            l.LookupType = sts[0].ExtensionLookupType
967            new_sts = [st.ExtSubTable for st in sts]
968            del sts[:]
969            sts.extend(new_sts)
970
971    isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos)
972
973    if isPairPos:
974        # AFDKO and feaLib sometimes generate two Format1 subtables instead of one.
975        # Merge those before continuing.
976        # https://github.com/fonttools/fonttools/issues/719
977        self.SubTable = _Lookup_PairPos_subtables_canonicalize(
978            self.SubTable, merger.font
979        )
980        subtables = merger.lookup_subtables = [
981            _Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables
982        ]
983    else:
984        isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos)
985        if isSinglePos:
986            numSubtables = [len(st) for st in subtables]
987            if not all([nums == numSubtables[0] for nums in numSubtables]):
988                # Flatten list of SinglePos subtables to single Format 2 subtable,
989                # with all value records set to the rec format type.
990                # We use buildSinglePos() to optimize the lookup after merging.
991                valueFormatList = [t.ValueFormat for st in subtables for t in st]
992                # Find the minimum value record that can accomodate all the singlePos subtables.
993                mirf = reduce(ior, valueFormatList)
994                self.SubTable = _Lookup_SinglePos_subtables_flatten(
995                    self.SubTable, merger.font, mirf
996                )
997                subtables = merger.lookup_subtables = [
998                    _Lookup_SinglePos_subtables_flatten(st, merger.font, mirf)
999                    for st in subtables
1000                ]
1001                flattened = True
1002            else:
1003                flattened = False
1004
1005    merger.mergeLists(self.SubTable, subtables)
1006    self.SubTableCount = len(self.SubTable)
1007
1008    if isPairPos:
1009        # If format-1 subtable created during canonicalization is empty, remove it.
1010        assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1
1011        if not self.SubTable[0].Coverage.glyphs:
1012            self.SubTable.pop(0)
1013            self.SubTableCount -= 1
1014
1015        # If format-2 subtable created during canonicalization is empty, remove it.
1016        assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2
1017        if not self.SubTable[-1].Coverage.glyphs:
1018            self.SubTable.pop(-1)
1019            self.SubTableCount -= 1
1020
1021        # Compact the merged subtables
1022        # This is a good moment to do it because the compaction should create
1023        # smaller subtables, which may prevent overflows from happening.
1024        # Keep reading the value from the ENV until ufo2ft switches to the config system
1025        level = merger.font.cfg.get(
1026            "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
1027            default=_compression_level_from_env(),
1028        )
1029        if level != 0:
1030            log.info("Compacting GPOS...")
1031            self.SubTable = compact_pair_pos(merger.font, level, self.SubTable)
1032            self.SubTableCount = len(self.SubTable)
1033
1034    elif isSinglePos and flattened:
1035        singlePosTable = self.SubTable[0]
1036        glyphs = singlePosTable.Coverage.glyphs
1037        # We know that singlePosTable is Format 2, as this is set
1038        # in _Lookup_SinglePos_subtables_flatten.
1039        singlePosMapping = {
1040            gname: valRecord for gname, valRecord in zip(glyphs, singlePosTable.Value)
1041        }
1042        self.SubTable = buildSinglePos(
1043            singlePosMapping, merger.font.getReverseGlyphMap()
1044        )
1045    merger.mergeObjects(self, lst, exclude=["SubTable", "SubTableCount"])
1046
1047    del merger.lookup_subtables
1048
1049
1050#
1051# InstancerMerger
1052#
1053
1054
1055class InstancerMerger(AligningMerger):
1056    """A merger that takes multiple master fonts, and instantiates
1057    an instance."""
1058
1059    def __init__(self, font, model, location):
1060        Merger.__init__(self, font)
1061        self.model = model
1062        self.location = location
1063        self.masterScalars = model.getMasterScalars(location)
1064
1065
1066@InstancerMerger.merger(ot.CaretValue)
1067def merge(merger, self, lst):
1068    assert self.Format == 1
1069    Coords = [a.Coordinate for a in lst]
1070    model = merger.model
1071    masterScalars = merger.masterScalars
1072    self.Coordinate = otRound(
1073        model.interpolateFromValuesAndScalars(Coords, masterScalars)
1074    )
1075
1076
1077@InstancerMerger.merger(ot.Anchor)
1078def merge(merger, self, lst):
1079    assert self.Format == 1
1080    XCoords = [a.XCoordinate for a in lst]
1081    YCoords = [a.YCoordinate for a in lst]
1082    model = merger.model
1083    masterScalars = merger.masterScalars
1084    self.XCoordinate = otRound(
1085        model.interpolateFromValuesAndScalars(XCoords, masterScalars)
1086    )
1087    self.YCoordinate = otRound(
1088        model.interpolateFromValuesAndScalars(YCoords, masterScalars)
1089    )
1090
1091
1092@InstancerMerger.merger(otBase.ValueRecord)
1093def merge(merger, self, lst):
1094    model = merger.model
1095    masterScalars = merger.masterScalars
1096    # TODO Handle differing valueformats
1097    for name, tableName in [
1098        ("XAdvance", "XAdvDevice"),
1099        ("YAdvance", "YAdvDevice"),
1100        ("XPlacement", "XPlaDevice"),
1101        ("YPlacement", "YPlaDevice"),
1102    ]:
1103        assert not hasattr(self, tableName)
1104
1105        if hasattr(self, name):
1106            values = [getattr(a, name, 0) for a in lst]
1107            value = otRound(
1108                model.interpolateFromValuesAndScalars(values, masterScalars)
1109            )
1110            setattr(self, name, value)
1111
1112
1113#
1114# MutatorMerger
1115#
1116
1117
1118class MutatorMerger(AligningMerger):
1119    """A merger that takes a variable font, and instantiates
1120    an instance.  While there's no "merging" to be done per se,
1121    the operation can benefit from many operations that the
1122    aligning merger does."""
1123
1124    def __init__(self, font, instancer, deleteVariations=True):
1125        Merger.__init__(self, font)
1126        self.instancer = instancer
1127        self.deleteVariations = deleteVariations
1128
1129
1130@MutatorMerger.merger(ot.CaretValue)
1131def merge(merger, self, lst):
1132    # Hack till we become selfless.
1133    self.__dict__ = lst[0].__dict__.copy()
1134
1135    if self.Format != 3:
1136        return
1137
1138    instancer = merger.instancer
1139    dev = self.DeviceTable
1140    if merger.deleteVariations:
1141        del self.DeviceTable
1142    if dev:
1143        assert dev.DeltaFormat == 0x8000
1144        varidx = (dev.StartSize << 16) + dev.EndSize
1145        delta = otRound(instancer[varidx])
1146        self.Coordinate += delta
1147
1148    if merger.deleteVariations:
1149        self.Format = 1
1150
1151
1152@MutatorMerger.merger(ot.Anchor)
1153def merge(merger, self, lst):
1154    # Hack till we become selfless.
1155    self.__dict__ = lst[0].__dict__.copy()
1156
1157    if self.Format != 3:
1158        return
1159
1160    instancer = merger.instancer
1161    for v in "XY":
1162        tableName = v + "DeviceTable"
1163        if not hasattr(self, tableName):
1164            continue
1165        dev = getattr(self, tableName)
1166        if merger.deleteVariations:
1167            delattr(self, tableName)
1168        if dev is None:
1169            continue
1170
1171        assert dev.DeltaFormat == 0x8000
1172        varidx = (dev.StartSize << 16) + dev.EndSize
1173        delta = otRound(instancer[varidx])
1174
1175        attr = v + "Coordinate"
1176        setattr(self, attr, getattr(self, attr) + delta)
1177
1178    if merger.deleteVariations:
1179        self.Format = 1
1180
1181
1182@MutatorMerger.merger(otBase.ValueRecord)
1183def merge(merger, self, lst):
1184    # Hack till we become selfless.
1185    self.__dict__ = lst[0].__dict__.copy()
1186
1187    instancer = merger.instancer
1188    for name, tableName in [
1189        ("XAdvance", "XAdvDevice"),
1190        ("YAdvance", "YAdvDevice"),
1191        ("XPlacement", "XPlaDevice"),
1192        ("YPlacement", "YPlaDevice"),
1193    ]:
1194        if not hasattr(self, tableName):
1195            continue
1196        dev = getattr(self, tableName)
1197        if merger.deleteVariations:
1198            delattr(self, tableName)
1199        if dev is None:
1200            continue
1201
1202        assert dev.DeltaFormat == 0x8000
1203        varidx = (dev.StartSize << 16) + dev.EndSize
1204        delta = otRound(instancer[varidx])
1205
1206        setattr(self, name, getattr(self, name, 0) + delta)
1207
1208
1209#
1210# VariationMerger
1211#
1212
1213
1214class VariationMerger(AligningMerger):
1215    """A merger that takes multiple master fonts, and builds a
1216    variable font."""
1217
1218    def __init__(self, model, axisTags, font):
1219        Merger.__init__(self, font)
1220        self.store_builder = varStore.OnlineVarStoreBuilder(axisTags)
1221        self.setModel(model)
1222
1223    def setModel(self, model):
1224        self.model = model
1225        self.store_builder.setModel(model)
1226
1227    def mergeThings(self, out, lst):
1228        masterModel = None
1229        origTTFs = None
1230        if None in lst:
1231            if allNone(lst):
1232                if out is not None:
1233                    raise FoundANone(self, got=lst)
1234                return
1235
1236            # temporarily subset the list of master ttfs to the ones for which
1237            # master values are not None
1238            origTTFs = self.ttfs
1239            if self.ttfs:
1240                self.ttfs = subList([v is not None for v in lst], self.ttfs)
1241
1242            masterModel = self.model
1243            model, lst = masterModel.getSubModel(lst)
1244            self.setModel(model)
1245
1246        super(VariationMerger, self).mergeThings(out, lst)
1247
1248        if masterModel:
1249            self.setModel(masterModel)
1250        if origTTFs:
1251            self.ttfs = origTTFs
1252
1253
1254def buildVarDevTable(store_builder, master_values):
1255    if allEqual(master_values):
1256        return master_values[0], None
1257    base, varIdx = store_builder.storeMasters(master_values)
1258    return base, builder.buildVarDevTable(varIdx)
1259
1260
1261@VariationMerger.merger(ot.BaseCoord)
1262def merge(merger, self, lst):
1263    if self.Format != 1:
1264        raise UnsupportedFormat(merger, subtable="a baseline coordinate")
1265    self.Coordinate, DeviceTable = buildVarDevTable(
1266        merger.store_builder, [a.Coordinate for a in lst]
1267    )
1268    if DeviceTable:
1269        self.Format = 3
1270        self.DeviceTable = DeviceTable
1271
1272
1273@VariationMerger.merger(ot.CaretValue)
1274def merge(merger, self, lst):
1275    if self.Format != 1:
1276        raise UnsupportedFormat(merger, subtable="a caret")
1277    self.Coordinate, DeviceTable = buildVarDevTable(
1278        merger.store_builder, [a.Coordinate for a in lst]
1279    )
1280    if DeviceTable:
1281        self.Format = 3
1282        self.DeviceTable = DeviceTable
1283
1284
1285@VariationMerger.merger(ot.Anchor)
1286def merge(merger, self, lst):
1287    if self.Format != 1:
1288        raise UnsupportedFormat(merger, subtable="an anchor")
1289    self.XCoordinate, XDeviceTable = buildVarDevTable(
1290        merger.store_builder, [a.XCoordinate for a in lst]
1291    )
1292    self.YCoordinate, YDeviceTable = buildVarDevTable(
1293        merger.store_builder, [a.YCoordinate for a in lst]
1294    )
1295    if XDeviceTable or YDeviceTable:
1296        self.Format = 3
1297        self.XDeviceTable = XDeviceTable
1298        self.YDeviceTable = YDeviceTable
1299
1300
1301@VariationMerger.merger(otBase.ValueRecord)
1302def merge(merger, self, lst):
1303    for name, tableName in [
1304        ("XAdvance", "XAdvDevice"),
1305        ("YAdvance", "YAdvDevice"),
1306        ("XPlacement", "XPlaDevice"),
1307        ("YPlacement", "YPlaDevice"),
1308    ]:
1309        if hasattr(self, name):
1310            value, deviceTable = buildVarDevTable(
1311                merger.store_builder, [getattr(a, name, 0) for a in lst]
1312            )
1313            setattr(self, name, value)
1314            if deviceTable:
1315                setattr(self, tableName, deviceTable)
1316
1317
1318class COLRVariationMerger(VariationMerger):
1319    """A specialized VariationMerger that takes multiple master fonts containing
1320    COLRv1 tables, and builds a variable COLR font.
1321
1322    COLR tables are special in that variable subtables can be associated with
1323    multiple delta-set indices (via VarIndexBase).
1324    They also contain tables that must change their type (not simply the Format)
1325    as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes
1326    care of that too.
1327    """
1328
1329    def __init__(self, model, axisTags, font, allowLayerReuse=True):
1330        VariationMerger.__init__(self, model, axisTags, font)
1331        # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase
1332        # between variable tables with same varIdxes.
1333        self.varIndexCache = {}
1334        # flat list of all the varIdxes generated while merging
1335        self.varIdxes = []
1336        # set of id()s of the subtables that contain variations after merging
1337        # and need to be upgraded to the associated VarType.
1338        self.varTableIds = set()
1339        # we keep these around for rebuilding a LayerList while merging PaintColrLayers
1340        self.layers = []
1341        self.layerReuseCache = None
1342        if allowLayerReuse:
1343            self.layerReuseCache = LayerReuseCache()
1344        # flag to ensure BaseGlyphList is fully merged before LayerList gets processed
1345        self._doneBaseGlyphs = False
1346
1347    def mergeTables(self, font, master_ttfs, tableTags=("COLR",)):
1348        if "COLR" in tableTags and "COLR" in font:
1349            # The merger modifies the destination COLR table in-place. If this contains
1350            # multiple PaintColrLayers referencing the same layers from LayerList, it's
1351            # a problem because we may risk modifying the same paint more than once, or
1352            # worse, fail while attempting to do that.
1353            # We don't know whether the master COLR table was built with layer reuse
1354            # disabled, thus to be safe we rebuild its LayerList so that it contains only
1355            # unique layers referenced from non-overlapping PaintColrLayers throughout
1356            # the base paint graphs.
1357            self.expandPaintColrLayers(font["COLR"].table)
1358        VariationMerger.mergeTables(self, font, master_ttfs, tableTags)
1359
1360    def checkFormatEnum(self, out, lst, validate=lambda _: True):
1361        fmt = out.Format
1362        formatEnum = out.formatEnum
1363        ok = False
1364        try:
1365            fmt = formatEnum(fmt)
1366        except ValueError:
1367            pass
1368        else:
1369            ok = validate(fmt)
1370        if not ok:
1371            raise UnsupportedFormat(self, subtable=type(out).__name__, value=fmt)
1372        expected = fmt
1373        got = []
1374        for v in lst:
1375            fmt = getattr(v, "Format", None)
1376            try:
1377                fmt = formatEnum(fmt)
1378            except ValueError:
1379                pass
1380            got.append(fmt)
1381        if not allEqualTo(expected, got):
1382            raise InconsistentFormats(
1383                self,
1384                subtable=type(out).__name__,
1385                expected=expected,
1386                got=got,
1387            )
1388        return expected
1389
1390    def mergeSparseDict(self, out, lst):
1391        for k in out.keys():
1392            try:
1393                self.mergeThings(out[k], [v.get(k) for v in lst])
1394            except VarLibMergeError as e:
1395                e.stack.append(f"[{k!r}]")
1396                raise
1397
1398    def mergeAttrs(self, out, lst, attrs):
1399        for attr in attrs:
1400            value = getattr(out, attr)
1401            values = [getattr(item, attr) for item in lst]
1402            try:
1403                self.mergeThings(value, values)
1404            except VarLibMergeError as e:
1405                e.stack.append(f".{attr}")
1406                raise
1407
1408    def storeMastersForAttr(self, out, lst, attr):
1409        master_values = [getattr(item, attr) for item in lst]
1410
1411        # VarStore treats deltas for fixed-size floats as integers, so we
1412        # must convert master values to int before storing them in the builder
1413        # then back to float.
1414        is_fixed_size_float = False
1415        conv = out.getConverterByName(attr)
1416        if isinstance(conv, BaseFixedValue):
1417            is_fixed_size_float = True
1418            master_values = [conv.toInt(v) for v in master_values]
1419
1420        baseValue = master_values[0]
1421        varIdx = ot.NO_VARIATION_INDEX
1422        if not allEqual(master_values):
1423            baseValue, varIdx = self.store_builder.storeMasters(master_values)
1424
1425        if is_fixed_size_float:
1426            baseValue = conv.fromInt(baseValue)
1427
1428        return baseValue, varIdx
1429
1430    def storeVariationIndices(self, varIdxes) -> int:
1431        # try to reuse an existing VarIndexBase for the same varIdxes, or else
1432        # create a new one
1433        key = tuple(varIdxes)
1434        varIndexBase = self.varIndexCache.get(key)
1435
1436        if varIndexBase is None:
1437            # scan for a full match anywhere in the self.varIdxes
1438            for i in range(len(self.varIdxes) - len(varIdxes) + 1):
1439                if self.varIdxes[i : i + len(varIdxes)] == varIdxes:
1440                    self.varIndexCache[key] = varIndexBase = i
1441                    break
1442
1443        if varIndexBase is None:
1444            # try find a partial match at the end of the self.varIdxes
1445            for n in range(len(varIdxes) - 1, 0, -1):
1446                if self.varIdxes[-n:] == varIdxes[:n]:
1447                    varIndexBase = len(self.varIdxes) - n
1448                    self.varIndexCache[key] = varIndexBase
1449                    self.varIdxes.extend(varIdxes[n:])
1450                    break
1451
1452        if varIndexBase is None:
1453            # no match found, append at the end
1454            self.varIndexCache[key] = varIndexBase = len(self.varIdxes)
1455            self.varIdxes.extend(varIdxes)
1456
1457        return varIndexBase
1458
1459    def mergeVariableAttrs(self, out, lst, attrs) -> int:
1460        varIndexBase = ot.NO_VARIATION_INDEX
1461        varIdxes = []
1462        for attr in attrs:
1463            baseValue, varIdx = self.storeMastersForAttr(out, lst, attr)
1464            setattr(out, attr, baseValue)
1465            varIdxes.append(varIdx)
1466
1467        if any(v != ot.NO_VARIATION_INDEX for v in varIdxes):
1468            varIndexBase = self.storeVariationIndices(varIdxes)
1469
1470        return varIndexBase
1471
1472    @classmethod
1473    def convertSubTablesToVarType(cls, table):
1474        for path in dfs_base_table(
1475            table,
1476            skip_root=True,
1477            predicate=lambda path: (
1478                getattr(type(path[-1].value), "VarType", None) is not None
1479            ),
1480        ):
1481            st = path[-1]
1482            subTable = st.value
1483            varType = type(subTable).VarType
1484            newSubTable = varType()
1485            newSubTable.__dict__.update(subTable.__dict__)
1486            newSubTable.populateDefaults()
1487            parent = path[-2].value
1488            if st.index is not None:
1489                getattr(parent, st.name)[st.index] = newSubTable
1490            else:
1491                setattr(parent, st.name, newSubTable)
1492
1493    @staticmethod
1494    def expandPaintColrLayers(colr):
1495        """Rebuild LayerList without PaintColrLayers reuse.
1496
1497        Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph
1498        which are irrelevant for this); any layers referenced via PaintColrLayers are
1499        collected into a new LayerList and duplicated when reuse is detected, to ensure
1500        that all paints are distinct objects at the end of the process.
1501        PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap
1502        is left. Also, any consecutively nested PaintColrLayers are flattened.
1503        The COLR table's LayerList is replaced with the new unique layers.
1504        A side effect is also that any layer from the old LayerList which is not
1505        referenced by any PaintColrLayers is dropped.
1506        """
1507        if not colr.LayerList:
1508            # if no LayerList, there's nothing to expand
1509            return
1510        uniqueLayerIDs = set()
1511        newLayerList = []
1512        for rec in colr.BaseGlyphList.BaseGlyphPaintRecord:
1513            frontier = [rec.Paint]
1514            while frontier:
1515                paint = frontier.pop()
1516                if paint.Format == ot.PaintFormat.PaintColrGlyph:
1517                    # don't traverse these, we treat them as constant for merging
1518                    continue
1519                elif paint.Format == ot.PaintFormat.PaintColrLayers:
1520                    # de-treeify any nested PaintColrLayers, append unique copies to
1521                    # the new layer list and update PaintColrLayers index/count
1522                    children = list(_flatten_layers(paint, colr))
1523                    first_layer_index = len(newLayerList)
1524                    for layer in children:
1525                        if id(layer) in uniqueLayerIDs:
1526                            layer = copy.deepcopy(layer)
1527                            assert id(layer) not in uniqueLayerIDs
1528                        newLayerList.append(layer)
1529                        uniqueLayerIDs.add(id(layer))
1530                    paint.FirstLayerIndex = first_layer_index
1531                    paint.NumLayers = len(children)
1532                else:
1533                    children = paint.getChildren(colr)
1534                frontier.extend(reversed(children))
1535        # sanity check all the new layers are distinct objects
1536        assert len(newLayerList) == len(uniqueLayerIDs)
1537        colr.LayerList.Paint = newLayerList
1538        colr.LayerList.LayerCount = len(newLayerList)
1539
1540
1541@COLRVariationMerger.merger(ot.BaseGlyphList)
1542def merge(merger, self, lst):
1543    # ignore BaseGlyphCount, allow sparse glyph sets across masters
1544    out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord}
1545    masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst]
1546
1547    for i, g in enumerate(out.keys()):
1548        try:
1549            # missing base glyphs don't participate in the merge
1550            merger.mergeThings(out[g], [v.get(g) for v in masters])
1551        except VarLibMergeError as e:
1552            e.stack.append(f".BaseGlyphPaintRecord[{i}]")
1553            e.cause["location"] = f"base glyph {g!r}"
1554            raise
1555
1556    merger._doneBaseGlyphs = True
1557
1558
1559@COLRVariationMerger.merger(ot.LayerList)
1560def merge(merger, self, lst):
1561    # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers
1562    # found while traversing the paint graphs rooted at BaseGlyphPaintRecords.
1563    assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList"
1564    # Simply flush the final list of layers and go home.
1565    self.LayerCount = len(merger.layers)
1566    self.Paint = merger.layers
1567
1568
1569def _flatten_layers(root, colr):
1570    assert root.Format == ot.PaintFormat.PaintColrLayers
1571    for paint in root.getChildren(colr):
1572        if paint.Format == ot.PaintFormat.PaintColrLayers:
1573            yield from _flatten_layers(paint, colr)
1574        else:
1575            yield paint
1576
1577
1578def _merge_PaintColrLayers(self, out, lst):
1579    # we only enforce that the (flat) number of layers is the same across all masters
1580    # but we allow FirstLayerIndex to differ to acommodate for sparse glyph sets.
1581
1582    out_layers = list(_flatten_layers(out, self.font["COLR"].table))
1583
1584    # sanity check ttfs are subset to current values (see VariationMerger.mergeThings)
1585    # before matching each master PaintColrLayers to its respective COLR by position
1586    assert len(self.ttfs) == len(lst)
1587    master_layerses = [
1588        list(_flatten_layers(lst[i], self.ttfs[i]["COLR"].table))
1589        for i in range(len(lst))
1590    ]
1591
1592    try:
1593        self.mergeLists(out_layers, master_layerses)
1594    except VarLibMergeError as e:
1595        # NOTE: This attribute doesn't actually exist in PaintColrLayers but it's
1596        # handy to have it in the stack trace for debugging.
1597        e.stack.append(".Layers")
1598        raise
1599
1600    # following block is very similar to LayerListBuilder._beforeBuildPaintColrLayers
1601    # but I couldn't find a nice way to share the code between the two...
1602
1603    if self.layerReuseCache is not None:
1604        # successful reuse can make the list smaller
1605        out_layers = self.layerReuseCache.try_reuse(out_layers)
1606
1607    # if the list is still too big we need to tree-fy it
1608    is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT
1609    out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT)
1610
1611    # We now have a tree of sequences with Paint leaves.
1612    # Convert the sequences into PaintColrLayers.
1613    def listToColrLayers(paint):
1614        if isinstance(paint, list):
1615            layers = [listToColrLayers(l) for l in paint]
1616            paint = ot.Paint()
1617            paint.Format = int(ot.PaintFormat.PaintColrLayers)
1618            paint.NumLayers = len(layers)
1619            paint.FirstLayerIndex = len(self.layers)
1620            self.layers.extend(layers)
1621            if self.layerReuseCache is not None:
1622                self.layerReuseCache.add(layers, paint.FirstLayerIndex)
1623        return paint
1624
1625    out_layers = [listToColrLayers(l) for l in out_layers]
1626
1627    if len(out_layers) == 1 and out_layers[0].Format == ot.PaintFormat.PaintColrLayers:
1628        # special case when the reuse cache finds a single perfect PaintColrLayers match
1629        # (it can only come from a successful reuse, _flatten_layers has gotten rid of
1630        # all nested PaintColrLayers already); we assign it directly and avoid creating
1631        # an extra table
1632        out.NumLayers = out_layers[0].NumLayers
1633        out.FirstLayerIndex = out_layers[0].FirstLayerIndex
1634    else:
1635        out.NumLayers = len(out_layers)
1636        out.FirstLayerIndex = len(self.layers)
1637
1638        self.layers.extend(out_layers)
1639
1640        # Register our parts for reuse provided we aren't a tree
1641        # If we are a tree the leaves registered for reuse and that will suffice
1642        if self.layerReuseCache is not None and not is_tree:
1643            self.layerReuseCache.add(out_layers, out.FirstLayerIndex)
1644
1645
1646@COLRVariationMerger.merger((ot.Paint, ot.ClipBox))
1647def merge(merger, self, lst):
1648    fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable())
1649
1650    if fmt is ot.PaintFormat.PaintColrLayers:
1651        _merge_PaintColrLayers(merger, self, lst)
1652        return
1653
1654    varFormat = fmt.as_variable()
1655
1656    varAttrs = ()
1657    if varFormat is not None:
1658        varAttrs = otBase.getVariableAttrs(type(self), varFormat)
1659    staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
1660
1661    merger.mergeAttrs(self, lst, staticAttrs)
1662
1663    varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
1664
1665    subTables = [st.value for st in self.iterSubTables()]
1666
1667    # Convert table to variable if itself has variations or any subtables have
1668    isVariable = varIndexBase != ot.NO_VARIATION_INDEX or any(
1669        id(table) in merger.varTableIds for table in subTables
1670    )
1671
1672    if isVariable:
1673        if varAttrs:
1674            # Some PaintVar* don't have any scalar attributes that can vary,
1675            # only indirect offsets to other variable subtables, thus have
1676            # no VarIndexBase of their own (e.g. PaintVarTransform)
1677            self.VarIndexBase = varIndexBase
1678
1679        if subTables:
1680            # Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc.
1681            merger.convertSubTablesToVarType(self)
1682
1683        assert varFormat is not None
1684        self.Format = int(varFormat)
1685
1686
1687@COLRVariationMerger.merger((ot.Affine2x3, ot.ColorStop))
1688def merge(merger, self, lst):
1689    varType = type(self).VarType
1690
1691    varAttrs = otBase.getVariableAttrs(varType)
1692    staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
1693
1694    merger.mergeAttrs(self, lst, staticAttrs)
1695
1696    varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
1697
1698    if varIndexBase != ot.NO_VARIATION_INDEX:
1699        self.VarIndexBase = varIndexBase
1700        # mark as having variations so the parent table will convert to Var{Type}
1701        merger.varTableIds.add(id(self))
1702
1703
1704@COLRVariationMerger.merger(ot.ColorLine)
1705def merge(merger, self, lst):
1706    merger.mergeAttrs(self, lst, (c.name for c in self.getConverters()))
1707
1708    if any(id(stop) in merger.varTableIds for stop in self.ColorStop):
1709        merger.convertSubTablesToVarType(self)
1710        merger.varTableIds.add(id(self))
1711
1712
1713@COLRVariationMerger.merger(ot.ClipList, "clips")
1714def merge(merger, self, lst):
1715    # 'sparse' in that we allow non-default masters to omit ClipBox entries
1716    # for some/all glyphs (i.e. they don't participate)
1717    merger.mergeSparseDict(self, lst)
1718