You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			1718 lines
		
	
	
		
			59 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			1718 lines
		
	
	
		
			59 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
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
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def merger(celf, clazzes, attrs=(None,)):
 | 
						|
        assert celf != Merger, "Subclass Merger instead."
 | 
						|
        if "mergers" not in celf.__dict__:
 | 
						|
            celf.mergers = {}
 | 
						|
        if type(clazzes) in (type, enum.EnumMeta):
 | 
						|
            clazzes = (clazzes,)
 | 
						|
        if type(attrs) == str:
 | 
						|
            attrs = (attrs,)
 | 
						|
 | 
						|
        def wrapper(method):
 | 
						|
            assert method.__name__ == "merge"
 | 
						|
            done = []
 | 
						|
            for clazz in clazzes:
 | 
						|
                if clazz in done:
 | 
						|
                    continue  # Support multiple names of a clazz
 | 
						|
                done.append(clazz)
 | 
						|
                mergers = celf.mergers.setdefault(clazz, {})
 | 
						|
                for attr in attrs:
 | 
						|
                    assert attr not in mergers, (
 | 
						|
                        "Oops, class '%s' has merge function for '%s' defined already."
 | 
						|
                        % (clazz.__name__, attr)
 | 
						|
                    )
 | 
						|
                    mergers[attr] = method
 | 
						|
            return None
 | 
						|
 | 
						|
        return wrapper
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def mergersFor(celf, thing, _default={}):
 | 
						|
        typ = type(thing)
 | 
						|
 | 
						|
        for celf in celf.mro():
 | 
						|
            mergers = getattr(celf, "mergers", None)
 | 
						|
            if mergers is None:
 | 
						|
                break
 | 
						|
 | 
						|
            m = celf.mergers.get(typ, None)
 | 
						|
            if m is not None:
 | 
						|
                return m
 | 
						|
 | 
						|
        return _default
 | 
						|
 | 
						|
    def mergeObjects(self, out, lst, exclude=()):
 | 
						|
        if hasattr(out, "ensureDecompiled"):
 | 
						|
            out.ensureDecompiled(recurse=False)
 | 
						|
        for item in lst:
 | 
						|
            if hasattr(item, "ensureDecompiled"):
 | 
						|
                item.ensureDecompiled(recurse=False)
 | 
						|
        keys = sorted(vars(out).keys())
 | 
						|
        if not all(keys == sorted(vars(v).keys()) for v in lst):
 | 
						|
            raise KeysDiffer(
 | 
						|
                self, expected=keys, got=[sorted(vars(v).keys()) for v in lst]
 | 
						|
            )
 | 
						|
        mergers = self.mergersFor(out)
 | 
						|
        defaultMerger = mergers.get("*", self.__class__.mergeThings)
 | 
						|
        try:
 | 
						|
            for key in keys:
 | 
						|
                if key in exclude:
 | 
						|
                    continue
 | 
						|
                value = getattr(out, key)
 | 
						|
                values = [getattr(table, key) for table in lst]
 | 
						|
                mergerFunc = mergers.get(key, defaultMerger)
 | 
						|
                mergerFunc(self, value, values)
 | 
						|
        except VarLibMergeError as e:
 | 
						|
            e.stack.append("." + key)
 | 
						|
            raise
 | 
						|
 | 
						|
    def mergeLists(self, out, lst):
 | 
						|
        if not allEqualTo(out, lst, len):
 | 
						|
            raise LengthsDiffer(self, expected=len(out), got=[len(x) for x in lst])
 | 
						|
        for i, (value, values) in enumerate(zip(out, zip(*lst))):
 | 
						|
            try:
 | 
						|
                self.mergeThings(value, values)
 | 
						|
            except VarLibMergeError as e:
 | 
						|
                e.stack.append("[%d]" % i)
 | 
						|
                raise
 | 
						|
 | 
						|
    def mergeThings(self, out, lst):
 | 
						|
        if not allEqualTo(out, lst, type):
 | 
						|
            raise MismatchedTypes(
 | 
						|
                self, expected=type(out).__name__, got=[type(x).__name__ for x in lst]
 | 
						|
            )
 | 
						|
        mergerFunc = self.mergersFor(out).get(None, None)
 | 
						|
        if mergerFunc is not None:
 | 
						|
            mergerFunc(self, out, lst)
 | 
						|
        elif isinstance(out, enum.Enum):
 | 
						|
            # need to special-case Enums as have __dict__ but are not regular 'objects',
 | 
						|
            # otherwise mergeObjects/mergeThings get trapped in a RecursionError
 | 
						|
            if not allEqualTo(out, lst):
 | 
						|
                raise ShouldBeConstant(self, expected=out, got=lst)
 | 
						|
        elif hasattr(out, "__dict__"):
 | 
						|
            self.mergeObjects(out, lst)
 | 
						|
        elif isinstance(out, list):
 | 
						|
            self.mergeLists(out, lst)
 | 
						|
        else:
 | 
						|
            if not allEqualTo(out, lst):
 | 
						|
                raise ShouldBeConstant(self, expected=out, got=lst)
 | 
						|
 | 
						|
    def mergeTables(self, font, master_ttfs, tableTags):
 | 
						|
        for tag in tableTags:
 | 
						|
            if tag not in font:
 | 
						|
                continue
 | 
						|
            try:
 | 
						|
                self.ttfs = master_ttfs
 | 
						|
                self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs])
 | 
						|
            except VarLibMergeError as e:
 | 
						|
                e.stack.append(tag)
 | 
						|
                raise
 | 
						|
 | 
						|
 | 
						|
#
 | 
						|
# Aligning merger
 | 
						|
#
 | 
						|
class AligningMerger(Merger):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.GDEF, "GlyphClassDef")
 | 
						|
def merge(merger, self, lst):
 | 
						|
    if self is None:
 | 
						|
        if not allNone(lst):
 | 
						|
            raise NotANone(merger, expected=None, got=lst)
 | 
						|
        return
 | 
						|
 | 
						|
    lst = [l.classDefs for l in lst]
 | 
						|
    self.classDefs = {}
 | 
						|
    # We only care about the .classDefs
 | 
						|
    self = self.classDefs
 | 
						|
 | 
						|
    allKeys = set()
 | 
						|
    allKeys.update(*[l.keys() for l in lst])
 | 
						|
    for k in allKeys:
 | 
						|
        allValues = nonNone(l.get(k) for l in lst)
 | 
						|
        if not allEqual(allValues):
 | 
						|
            raise ShouldBeConstant(
 | 
						|
                merger, expected=allValues[0], got=lst, stack=["." + k]
 | 
						|
            )
 | 
						|
        if not allValues:
 | 
						|
            self[k] = None
 | 
						|
        else:
 | 
						|
            self[k] = allValues[0]
 | 
						|
 | 
						|
 | 
						|
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
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(otBase.ValueRecord)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    # Code below sometimes calls us with self being
 | 
						|
    # a new object. Copy it from lst and recurse.
 | 
						|
    self.__dict__ = lst[0].__dict__.copy()
 | 
						|
    merger.mergeObjects(self, lst)
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.Anchor)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    # Code below sometimes calls us with self being
 | 
						|
    # a new object. Copy it from lst and recurse.
 | 
						|
    self.__dict__ = lst[0].__dict__.copy()
 | 
						|
    merger.mergeObjects(self, lst)
 | 
						|
 | 
						|
 | 
						|
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
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.SinglePos)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0)
 | 
						|
    if not (len(lst) == 1 or (valueFormat & ~0xF == 0)):
 | 
						|
        raise UnsupportedFormat(merger, subtable="single positioning lookup")
 | 
						|
 | 
						|
    # If all have same coverage table and all are format 1,
 | 
						|
    coverageGlyphs = self.Coverage.glyphs
 | 
						|
    if all(v.Format == 1 for v in lst) and all(
 | 
						|
        coverageGlyphs == v.Coverage.glyphs for v in lst
 | 
						|
    ):
 | 
						|
        self.Value = otBase.ValueRecord(valueFormat, self.Value)
 | 
						|
        if valueFormat != 0:
 | 
						|
            # If v.Value is None, it means a kerning of 0; we want
 | 
						|
            # it to participate in the model still.
 | 
						|
            # https://github.com/fonttools/fonttools/issues/3111
 | 
						|
            merger.mergeThings(
 | 
						|
                self.Value,
 | 
						|
                [v.Value if v.Value is not None else otBase.ValueRecord() for v in lst],
 | 
						|
            )
 | 
						|
        self.ValueFormat = self.Value.getFormat()
 | 
						|
        return
 | 
						|
 | 
						|
    # Upgrade everything to Format=2
 | 
						|
    self.Format = 2
 | 
						|
    lst = [_SinglePosUpgradeToFormat2(v) for v in lst]
 | 
						|
 | 
						|
    # Align them
 | 
						|
    glyphs, padded = _merge_GlyphOrders(
 | 
						|
        merger.font, [v.Coverage.glyphs for v in lst], [v.Value for v in lst]
 | 
						|
    )
 | 
						|
 | 
						|
    self.Coverage.glyphs = glyphs
 | 
						|
    self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs]
 | 
						|
    self.ValueCount = len(self.Value)
 | 
						|
 | 
						|
    for i, values in enumerate(padded):
 | 
						|
        for j, glyph in enumerate(glyphs):
 | 
						|
            if values[j] is not None:
 | 
						|
                continue
 | 
						|
            # Fill in value from other subtables
 | 
						|
            # Note!!! This *might* result in behavior change if ValueFormat2-zeroedness
 | 
						|
            # is different between used subtable and current subtable!
 | 
						|
            # TODO(behdad) Check and warn if that happens?
 | 
						|
            v = _Lookup_SinglePos_get_effective_value(
 | 
						|
                merger, merger.lookup_subtables[i], glyph
 | 
						|
            )
 | 
						|
            if v is None:
 | 
						|
                v = otBase.ValueRecord(valueFormat)
 | 
						|
            values[j] = v
 | 
						|
 | 
						|
    merger.mergeLists(self.Value, padded)
 | 
						|
 | 
						|
    # Merge everything else; though, there shouldn't be anything else. :)
 | 
						|
    merger.mergeObjects(
 | 
						|
        self, lst, exclude=("Format", "Coverage", "Value", "ValueCount", "ValueFormat")
 | 
						|
    )
 | 
						|
    self.ValueFormat = reduce(
 | 
						|
        int.__or__, [v.getEffectiveFormat() for v in self.Value], 0
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.PairSet)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    # Align them
 | 
						|
    glyphs, padded = _merge_GlyphOrders(
 | 
						|
        merger.font,
 | 
						|
        [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst],
 | 
						|
        [vs.PairValueRecord for vs in lst],
 | 
						|
    )
 | 
						|
 | 
						|
    self.PairValueRecord = pvrs = []
 | 
						|
    for glyph in glyphs:
 | 
						|
        pvr = ot.PairValueRecord()
 | 
						|
        pvr.SecondGlyph = glyph
 | 
						|
        pvr.Value1 = (
 | 
						|
            otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None
 | 
						|
        )
 | 
						|
        pvr.Value2 = (
 | 
						|
            otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None
 | 
						|
        )
 | 
						|
        pvrs.append(pvr)
 | 
						|
    self.PairValueCount = len(self.PairValueRecord)
 | 
						|
 | 
						|
    for i, values in enumerate(padded):
 | 
						|
        for j, glyph in enumerate(glyphs):
 | 
						|
            # Fill in value from other subtables
 | 
						|
            v = ot.PairValueRecord()
 | 
						|
            v.SecondGlyph = glyph
 | 
						|
            if values[j] is not None:
 | 
						|
                vpair = values[j]
 | 
						|
            else:
 | 
						|
                vpair = _Lookup_PairPos_get_effective_value_pair(
 | 
						|
                    merger, merger.lookup_subtables[i], self._firstGlyph, glyph
 | 
						|
                )
 | 
						|
            if vpair is None:
 | 
						|
                v1, v2 = None, None
 | 
						|
            else:
 | 
						|
                v1 = getattr(vpair, "Value1", None)
 | 
						|
                v2 = getattr(vpair, "Value2", None)
 | 
						|
            v.Value1 = (
 | 
						|
                otBase.ValueRecord(merger.valueFormat1, src=v1)
 | 
						|
                if merger.valueFormat1
 | 
						|
                else None
 | 
						|
            )
 | 
						|
            v.Value2 = (
 | 
						|
                otBase.ValueRecord(merger.valueFormat2, src=v2)
 | 
						|
                if merger.valueFormat2
 | 
						|
                else None
 | 
						|
            )
 | 
						|
            values[j] = v
 | 
						|
    del self._firstGlyph
 | 
						|
 | 
						|
    merger.mergeLists(self.PairValueRecord, padded)
 | 
						|
 | 
						|
 | 
						|
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)
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.PairPos)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    merger.valueFormat1 = self.ValueFormat1 = reduce(
 | 
						|
        int.__or__, [l.ValueFormat1 for l in lst], 0
 | 
						|
    )
 | 
						|
    merger.valueFormat2 = self.ValueFormat2 = reduce(
 | 
						|
        int.__or__, [l.ValueFormat2 for l in lst], 0
 | 
						|
    )
 | 
						|
 | 
						|
    if self.Format == 1:
 | 
						|
        _PairPosFormat1_merge(self, lst, merger)
 | 
						|
    elif self.Format == 2:
 | 
						|
        _PairPosFormat2_merge(self, lst, merger)
 | 
						|
    else:
 | 
						|
        raise UnsupportedFormat(merger, subtable="pair positioning lookup")
 | 
						|
 | 
						|
    del merger.valueFormat1, merger.valueFormat2
 | 
						|
 | 
						|
    # Now examine the list of value records, and update to the union of format values,
 | 
						|
    # as merge might have created new values.
 | 
						|
    vf1 = 0
 | 
						|
    vf2 = 0
 | 
						|
    if self.Format == 1:
 | 
						|
        for pairSet in self.PairSet:
 | 
						|
            for pairValueRecord in pairSet.PairValueRecord:
 | 
						|
                pv1 = getattr(pairValueRecord, "Value1", None)
 | 
						|
                if pv1 is not None:
 | 
						|
                    vf1 |= pv1.getFormat()
 | 
						|
                pv2 = getattr(pairValueRecord, "Value2", None)
 | 
						|
                if pv2 is not None:
 | 
						|
                    vf2 |= pv2.getFormat()
 | 
						|
    elif self.Format == 2:
 | 
						|
        for class1Record in self.Class1Record:
 | 
						|
            for class2Record in class1Record.Class2Record:
 | 
						|
                pv1 = getattr(class2Record, "Value1", None)
 | 
						|
                if pv1 is not None:
 | 
						|
                    vf1 |= pv1.getFormat()
 | 
						|
                pv2 = getattr(class2Record, "Value2", None)
 | 
						|
                if pv2 is not None:
 | 
						|
                    vf2 |= pv2.getFormat()
 | 
						|
    self.ValueFormat1 = vf1
 | 
						|
    self.ValueFormat2 = vf2
 | 
						|
 | 
						|
 | 
						|
def _MarkBasePosFormat1_merge(self, lst, merger, Mark="Mark", Base="Base"):
 | 
						|
    self.ClassCount = max(l.ClassCount for l in lst)
 | 
						|
 | 
						|
    MarkCoverageGlyphs, MarkRecords = _merge_GlyphOrders(
 | 
						|
        merger.font,
 | 
						|
        [getattr(l, Mark + "Coverage").glyphs for l in lst],
 | 
						|
        [getattr(l, Mark + "Array").MarkRecord for l in lst],
 | 
						|
    )
 | 
						|
    getattr(self, Mark + "Coverage").glyphs = MarkCoverageGlyphs
 | 
						|
 | 
						|
    BaseCoverageGlyphs, BaseRecords = _merge_GlyphOrders(
 | 
						|
        merger.font,
 | 
						|
        [getattr(l, Base + "Coverage").glyphs for l in lst],
 | 
						|
        [getattr(getattr(l, Base + "Array"), Base + "Record") for l in lst],
 | 
						|
    )
 | 
						|
    getattr(self, Base + "Coverage").glyphs = BaseCoverageGlyphs
 | 
						|
 | 
						|
    # MarkArray
 | 
						|
    records = []
 | 
						|
    for g, glyphRecords in zip(MarkCoverageGlyphs, zip(*MarkRecords)):
 | 
						|
        allClasses = [r.Class for r in glyphRecords if r is not None]
 | 
						|
 | 
						|
        # TODO Right now we require that all marks have same class in
 | 
						|
        # all masters that cover them.  This is not required.
 | 
						|
        #
 | 
						|
        # We can relax that by just requiring that all marks that have
 | 
						|
        # the same class in a master, have the same class in every other
 | 
						|
        # master.  Indeed, if, say, a sparse master only covers one mark,
 | 
						|
        # that mark probably will get class 0, which would possibly be
 | 
						|
        # different from its class in other masters.
 | 
						|
        #
 | 
						|
        # We can even go further and reclassify marks to support any
 | 
						|
        # input.  But, since, it's unlikely that two marks being both,
 | 
						|
        # say, "top" in one master, and one being "top" and other being
 | 
						|
        # "top-right" in another master, we shouldn't do that, as any
 | 
						|
        # failures in that case will probably signify mistakes in the
 | 
						|
        # input masters.
 | 
						|
 | 
						|
        if not allEqual(allClasses):
 | 
						|
            raise ShouldBeConstant(merger, expected=allClasses[0], got=allClasses)
 | 
						|
        else:
 | 
						|
            rec = ot.MarkRecord()
 | 
						|
            rec.Class = allClasses[0]
 | 
						|
            allAnchors = [None if r is None else r.MarkAnchor for r in glyphRecords]
 | 
						|
            if allNone(allAnchors):
 | 
						|
                anchor = None
 | 
						|
            else:
 | 
						|
                anchor = ot.Anchor()
 | 
						|
                anchor.Format = 1
 | 
						|
                merger.mergeThings(anchor, allAnchors)
 | 
						|
            rec.MarkAnchor = anchor
 | 
						|
        records.append(rec)
 | 
						|
    array = ot.MarkArray()
 | 
						|
    array.MarkRecord = records
 | 
						|
    array.MarkCount = len(records)
 | 
						|
    setattr(self, Mark + "Array", array)
 | 
						|
 | 
						|
    # BaseArray
 | 
						|
    records = []
 | 
						|
    for g, glyphRecords in zip(BaseCoverageGlyphs, zip(*BaseRecords)):
 | 
						|
        if allNone(glyphRecords):
 | 
						|
            rec = None
 | 
						|
        else:
 | 
						|
            rec = getattr(ot, Base + "Record")()
 | 
						|
            anchors = []
 | 
						|
            setattr(rec, Base + "Anchor", anchors)
 | 
						|
            glyphAnchors = [
 | 
						|
                [] if r is None else getattr(r, Base + "Anchor") for r in glyphRecords
 | 
						|
            ]
 | 
						|
            for l in glyphAnchors:
 | 
						|
                l.extend([None] * (self.ClassCount - len(l)))
 | 
						|
            for allAnchors in zip(*glyphAnchors):
 | 
						|
                if allNone(allAnchors):
 | 
						|
                    anchor = None
 | 
						|
                else:
 | 
						|
                    anchor = ot.Anchor()
 | 
						|
                    anchor.Format = 1
 | 
						|
                    merger.mergeThings(anchor, allAnchors)
 | 
						|
                anchors.append(anchor)
 | 
						|
        records.append(rec)
 | 
						|
    array = getattr(ot, Base + "Array")()
 | 
						|
    setattr(array, Base + "Record", records)
 | 
						|
    setattr(array, Base + "Count", len(records))
 | 
						|
    setattr(self, Base + "Array", array)
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.MarkBasePos)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    if not allEqualTo(self.Format, (l.Format for l in lst)):
 | 
						|
        raise InconsistentFormats(
 | 
						|
            merger,
 | 
						|
            subtable="mark-to-base positioning lookup",
 | 
						|
            expected=self.Format,
 | 
						|
            got=[l.Format for l in lst],
 | 
						|
        )
 | 
						|
    if self.Format == 1:
 | 
						|
        _MarkBasePosFormat1_merge(self, lst, merger)
 | 
						|
    else:
 | 
						|
        raise UnsupportedFormat(merger, subtable="mark-to-base positioning lookup")
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.MarkMarkPos)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    if not allEqualTo(self.Format, (l.Format for l in lst)):
 | 
						|
        raise InconsistentFormats(
 | 
						|
            merger,
 | 
						|
            subtable="mark-to-mark positioning lookup",
 | 
						|
            expected=self.Format,
 | 
						|
            got=[l.Format for l in lst],
 | 
						|
        )
 | 
						|
    if self.Format == 1:
 | 
						|
        _MarkBasePosFormat1_merge(self, lst, merger, "Mark1", "Mark2")
 | 
						|
    else:
 | 
						|
        raise UnsupportedFormat(merger, subtable="mark-to-mark positioning lookup")
 | 
						|
 | 
						|
 | 
						|
def _PairSet_flatten(lst, font):
 | 
						|
    self = ot.PairSet()
 | 
						|
    self.Coverage = ot.Coverage()
 | 
						|
 | 
						|
    # 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]
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.CursivePos)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    # Align them
 | 
						|
    glyphs, padded = _merge_GlyphOrders(
 | 
						|
        merger.font,
 | 
						|
        [l.Coverage.glyphs for l in lst],
 | 
						|
        [l.EntryExitRecord for l in lst],
 | 
						|
    )
 | 
						|
 | 
						|
    self.Format = 1
 | 
						|
    self.Coverage = ot.Coverage()
 | 
						|
    self.Coverage.glyphs = glyphs
 | 
						|
    self.EntryExitRecord = []
 | 
						|
    for _ in glyphs:
 | 
						|
        rec = ot.EntryExitRecord()
 | 
						|
        rec.EntryAnchor = ot.Anchor()
 | 
						|
        rec.EntryAnchor.Format = 1
 | 
						|
        rec.ExitAnchor = ot.Anchor()
 | 
						|
        rec.ExitAnchor.Format = 1
 | 
						|
        self.EntryExitRecord.append(rec)
 | 
						|
    merger.mergeLists(self.EntryExitRecord, padded)
 | 
						|
    self.EntryExitCount = len(self.EntryExitRecord)
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.EntryExitRecord)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    if all(master.EntryAnchor is None for master in lst):
 | 
						|
        self.EntryAnchor = None
 | 
						|
    if all(master.ExitAnchor is None for master in lst):
 | 
						|
        self.ExitAnchor = None
 | 
						|
    merger.mergeObjects(self, lst)
 | 
						|
 | 
						|
 | 
						|
@AligningMerger.merger(ot.Lookup)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    subtables = merger.lookup_subtables = [l.SubTable for l in lst]
 | 
						|
 | 
						|
    # Remove Extension subtables
 | 
						|
    for l, sts in list(zip(lst, subtables)) + [(self, self.SubTable)]:
 | 
						|
        if not sts:
 | 
						|
            continue
 | 
						|
        if sts[0].__class__.__name__.startswith("Extension"):
 | 
						|
            if not allEqual([st.__class__ for st in sts]):
 | 
						|
                raise InconsistentExtensions(
 | 
						|
                    merger,
 | 
						|
                    expected="Extension",
 | 
						|
                    got=[st.__class__.__name__ for st in sts],
 | 
						|
                )
 | 
						|
            if not allEqual([st.ExtensionLookupType for st in sts]):
 | 
						|
                raise InconsistentExtensions(merger)
 | 
						|
            l.LookupType = sts[0].ExtensionLookupType
 | 
						|
            new_sts = [st.ExtSubTable for st in sts]
 | 
						|
            del sts[:]
 | 
						|
            sts.extend(new_sts)
 | 
						|
 | 
						|
    isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos)
 | 
						|
 | 
						|
    if isPairPos:
 | 
						|
        # AFDKO and feaLib sometimes generate two Format1 subtables instead of one.
 | 
						|
        # Merge those before continuing.
 | 
						|
        # https://github.com/fonttools/fonttools/issues/719
 | 
						|
        self.SubTable = _Lookup_PairPos_subtables_canonicalize(
 | 
						|
            self.SubTable, merger.font
 | 
						|
        )
 | 
						|
        subtables = merger.lookup_subtables = [
 | 
						|
            _Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables
 | 
						|
        ]
 | 
						|
    else:
 | 
						|
        isSinglePos = self.SubTable and isinstance(self.SubTable[0], ot.SinglePos)
 | 
						|
        if isSinglePos:
 | 
						|
            numSubtables = [len(st) for st in subtables]
 | 
						|
            if not all([nums == numSubtables[0] for nums in numSubtables]):
 | 
						|
                # Flatten list of SinglePos subtables to single Format 2 subtable,
 | 
						|
                # with all value records set to the rec format type.
 | 
						|
                # We use buildSinglePos() to optimize the lookup after merging.
 | 
						|
                valueFormatList = [t.ValueFormat for st in subtables for t in st]
 | 
						|
                # Find the minimum value record that can accomodate all the singlePos subtables.
 | 
						|
                mirf = reduce(ior, valueFormatList)
 | 
						|
                self.SubTable = _Lookup_SinglePos_subtables_flatten(
 | 
						|
                    self.SubTable, merger.font, mirf
 | 
						|
                )
 | 
						|
                subtables = merger.lookup_subtables = [
 | 
						|
                    _Lookup_SinglePos_subtables_flatten(st, merger.font, mirf)
 | 
						|
                    for st in subtables
 | 
						|
                ]
 | 
						|
                flattened = True
 | 
						|
            else:
 | 
						|
                flattened = False
 | 
						|
 | 
						|
    merger.mergeLists(self.SubTable, subtables)
 | 
						|
    self.SubTableCount = len(self.SubTable)
 | 
						|
 | 
						|
    if isPairPos:
 | 
						|
        # If format-1 subtable created during canonicalization is empty, remove it.
 | 
						|
        assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1
 | 
						|
        if not self.SubTable[0].Coverage.glyphs:
 | 
						|
            self.SubTable.pop(0)
 | 
						|
            self.SubTableCount -= 1
 | 
						|
 | 
						|
        # If format-2 subtable created during canonicalization is empty, remove it.
 | 
						|
        assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2
 | 
						|
        if not self.SubTable[-1].Coverage.glyphs:
 | 
						|
            self.SubTable.pop(-1)
 | 
						|
            self.SubTableCount -= 1
 | 
						|
 | 
						|
        # Compact the merged subtables
 | 
						|
        # This is a good moment to do it because the compaction should create
 | 
						|
        # smaller subtables, which may prevent overflows from happening.
 | 
						|
        # Keep reading the value from the ENV until ufo2ft switches to the config system
 | 
						|
        level = merger.font.cfg.get(
 | 
						|
            "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
 | 
						|
            default=_compression_level_from_env(),
 | 
						|
        )
 | 
						|
        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)
 | 
						|
 | 
						|
 | 
						|
@InstancerMerger.merger(ot.CaretValue)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    assert self.Format == 1
 | 
						|
    Coords = [a.Coordinate for a in lst]
 | 
						|
    model = merger.model
 | 
						|
    masterScalars = merger.masterScalars
 | 
						|
    self.Coordinate = otRound(
 | 
						|
        model.interpolateFromValuesAndScalars(Coords, masterScalars)
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@InstancerMerger.merger(ot.Anchor)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    assert self.Format == 1
 | 
						|
    XCoords = [a.XCoordinate for a in lst]
 | 
						|
    YCoords = [a.YCoordinate for a in lst]
 | 
						|
    model = merger.model
 | 
						|
    masterScalars = merger.masterScalars
 | 
						|
    self.XCoordinate = otRound(
 | 
						|
        model.interpolateFromValuesAndScalars(XCoords, masterScalars)
 | 
						|
    )
 | 
						|
    self.YCoordinate = otRound(
 | 
						|
        model.interpolateFromValuesAndScalars(YCoords, masterScalars)
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@InstancerMerger.merger(otBase.ValueRecord)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    model = merger.model
 | 
						|
    masterScalars = merger.masterScalars
 | 
						|
    # 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
 | 
						|
 | 
						|
 | 
						|
@MutatorMerger.merger(ot.CaretValue)
 | 
						|
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
 | 
						|
 | 
						|
 | 
						|
@MutatorMerger.merger(ot.Anchor)
 | 
						|
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
 | 
						|
 | 
						|
 | 
						|
@MutatorMerger.merger(otBase.ValueRecord)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    # Hack till we become selfless.
 | 
						|
    self.__dict__ = lst[0].__dict__.copy()
 | 
						|
 | 
						|
    instancer = merger.instancer
 | 
						|
    for name, tableName in [
 | 
						|
        ("XAdvance", "XAdvDevice"),
 | 
						|
        ("YAdvance", "YAdvDevice"),
 | 
						|
        ("XPlacement", "XPlaDevice"),
 | 
						|
        ("YPlacement", "YPlaDevice"),
 | 
						|
    ]:
 | 
						|
        if not hasattr(self, tableName):
 | 
						|
            continue
 | 
						|
        dev = getattr(self, tableName)
 | 
						|
        if merger.deleteVariations:
 | 
						|
            delattr(self, tableName)
 | 
						|
        if dev is None:
 | 
						|
            continue
 | 
						|
 | 
						|
        assert dev.DeltaFormat == 0x8000
 | 
						|
        varidx = (dev.StartSize << 16) + dev.EndSize
 | 
						|
        delta = otRound(instancer[varidx])
 | 
						|
 | 
						|
        setattr(self, name, getattr(self, name, 0) + delta)
 | 
						|
 | 
						|
 | 
						|
#
 | 
						|
# 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)
 | 
						|
 | 
						|
 | 
						|
@VariationMerger.merger(ot.BaseCoord)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    if self.Format != 1:
 | 
						|
        raise UnsupportedFormat(merger, subtable="a baseline coordinate")
 | 
						|
    self.Coordinate, DeviceTable = buildVarDevTable(
 | 
						|
        merger.store_builder, [a.Coordinate for a in lst]
 | 
						|
    )
 | 
						|
    if DeviceTable:
 | 
						|
        self.Format = 3
 | 
						|
        self.DeviceTable = DeviceTable
 | 
						|
 | 
						|
 | 
						|
@VariationMerger.merger(ot.CaretValue)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    if self.Format != 1:
 | 
						|
        raise UnsupportedFormat(merger, subtable="a caret")
 | 
						|
    self.Coordinate, DeviceTable = buildVarDevTable(
 | 
						|
        merger.store_builder, [a.Coordinate for a in lst]
 | 
						|
    )
 | 
						|
    if DeviceTable:
 | 
						|
        self.Format = 3
 | 
						|
        self.DeviceTable = DeviceTable
 | 
						|
 | 
						|
 | 
						|
@VariationMerger.merger(ot.Anchor)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    if self.Format != 1:
 | 
						|
        raise UnsupportedFormat(merger, subtable="an anchor")
 | 
						|
    self.XCoordinate, XDeviceTable = buildVarDevTable(
 | 
						|
        merger.store_builder, [a.XCoordinate for a in lst]
 | 
						|
    )
 | 
						|
    self.YCoordinate, YDeviceTable = buildVarDevTable(
 | 
						|
        merger.store_builder, [a.YCoordinate for a in lst]
 | 
						|
    )
 | 
						|
    if XDeviceTable or YDeviceTable:
 | 
						|
        self.Format = 3
 | 
						|
        self.XDeviceTable = XDeviceTable
 | 
						|
        self.YDeviceTable = YDeviceTable
 | 
						|
 | 
						|
 | 
						|
@VariationMerger.merger(otBase.ValueRecord)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    for name, tableName in [
 | 
						|
        ("XAdvance", "XAdvDevice"),
 | 
						|
        ("YAdvance", "YAdvDevice"),
 | 
						|
        ("XPlacement", "XPlaDevice"),
 | 
						|
        ("YPlacement", "YPlaDevice"),
 | 
						|
    ]:
 | 
						|
        if hasattr(self, name):
 | 
						|
            value, deviceTable = buildVarDevTable(
 | 
						|
                merger.store_builder, [getattr(a, name, 0) for a in lst]
 | 
						|
            )
 | 
						|
            setattr(self, name, value)
 | 
						|
            if deviceTable:
 | 
						|
                setattr(self, tableName, deviceTable)
 | 
						|
 | 
						|
 | 
						|
class COLRVariationMerger(VariationMerger):
 | 
						|
    """A specialized VariationMerger that takes multiple master fonts containing
 | 
						|
    COLRv1 tables, and builds a variable COLR font.
 | 
						|
 | 
						|
    COLR tables are special in that variable subtables can be associated with
 | 
						|
    multiple delta-set indices (via VarIndexBase).
 | 
						|
    They also contain tables that must change their type (not simply the Format)
 | 
						|
    as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes
 | 
						|
    care of that too.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, model, axisTags, font, allowLayerReuse=True):
 | 
						|
        VariationMerger.__init__(self, model, axisTags, font)
 | 
						|
        # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase
 | 
						|
        # between variable tables with same varIdxes.
 | 
						|
        self.varIndexCache = {}
 | 
						|
        # flat list of all the varIdxes generated while merging
 | 
						|
        self.varIdxes = []
 | 
						|
        # set of id()s of the subtables that contain variations after merging
 | 
						|
        # and need to be upgraded to the associated VarType.
 | 
						|
        self.varTableIds = set()
 | 
						|
        # we keep these around for rebuilding a LayerList while merging PaintColrLayers
 | 
						|
        self.layers = []
 | 
						|
        self.layerReuseCache = None
 | 
						|
        if allowLayerReuse:
 | 
						|
            self.layerReuseCache = LayerReuseCache()
 | 
						|
        # flag to ensure BaseGlyphList is fully merged before LayerList gets processed
 | 
						|
        self._doneBaseGlyphs = False
 | 
						|
 | 
						|
    def mergeTables(self, font, master_ttfs, tableTags=("COLR",)):
 | 
						|
        if "COLR" in tableTags and "COLR" in font:
 | 
						|
            # The merger modifies the destination COLR table in-place. If this contains
 | 
						|
            # multiple PaintColrLayers referencing the same layers from LayerList, it's
 | 
						|
            # a problem because we may risk modifying the same paint more than once, or
 | 
						|
            # worse, fail while attempting to do that.
 | 
						|
            # We don't know whether the master COLR table was built with layer reuse
 | 
						|
            # disabled, thus to be safe we rebuild its LayerList so that it contains only
 | 
						|
            # unique layers referenced from non-overlapping PaintColrLayers throughout
 | 
						|
            # the base paint graphs.
 | 
						|
            self.expandPaintColrLayers(font["COLR"].table)
 | 
						|
        VariationMerger.mergeTables(self, font, master_ttfs, tableTags)
 | 
						|
 | 
						|
    def checkFormatEnum(self, out, lst, validate=lambda _: True):
 | 
						|
        fmt = out.Format
 | 
						|
        formatEnum = out.formatEnum
 | 
						|
        ok = False
 | 
						|
        try:
 | 
						|
            fmt = formatEnum(fmt)
 | 
						|
        except ValueError:
 | 
						|
            pass
 | 
						|
        else:
 | 
						|
            ok = validate(fmt)
 | 
						|
        if not ok:
 | 
						|
            raise UnsupportedFormat(self, subtable=type(out).__name__, value=fmt)
 | 
						|
        expected = fmt
 | 
						|
        got = []
 | 
						|
        for v in lst:
 | 
						|
            fmt = getattr(v, "Format", None)
 | 
						|
            try:
 | 
						|
                fmt = formatEnum(fmt)
 | 
						|
            except ValueError:
 | 
						|
                pass
 | 
						|
            got.append(fmt)
 | 
						|
        if not allEqualTo(expected, got):
 | 
						|
            raise InconsistentFormats(
 | 
						|
                self,
 | 
						|
                subtable=type(out).__name__,
 | 
						|
                expected=expected,
 | 
						|
                got=got,
 | 
						|
            )
 | 
						|
        return expected
 | 
						|
 | 
						|
    def mergeSparseDict(self, out, lst):
 | 
						|
        for k in out.keys():
 | 
						|
            try:
 | 
						|
                self.mergeThings(out[k], [v.get(k) for v in lst])
 | 
						|
            except VarLibMergeError as e:
 | 
						|
                e.stack.append(f"[{k!r}]")
 | 
						|
                raise
 | 
						|
 | 
						|
    def mergeAttrs(self, out, lst, attrs):
 | 
						|
        for attr in attrs:
 | 
						|
            value = getattr(out, attr)
 | 
						|
            values = [getattr(item, attr) for item in lst]
 | 
						|
            try:
 | 
						|
                self.mergeThings(value, values)
 | 
						|
            except VarLibMergeError as e:
 | 
						|
                e.stack.append(f".{attr}")
 | 
						|
                raise
 | 
						|
 | 
						|
    def storeMastersForAttr(self, out, lst, attr):
 | 
						|
        master_values = [getattr(item, attr) for item in lst]
 | 
						|
 | 
						|
        # VarStore treats deltas for fixed-size floats as integers, so we
 | 
						|
        # must convert master values to int before storing them in the builder
 | 
						|
        # then back to float.
 | 
						|
        is_fixed_size_float = False
 | 
						|
        conv = out.getConverterByName(attr)
 | 
						|
        if isinstance(conv, BaseFixedValue):
 | 
						|
            is_fixed_size_float = True
 | 
						|
            master_values = [conv.toInt(v) for v in master_values]
 | 
						|
 | 
						|
        baseValue = master_values[0]
 | 
						|
        varIdx = ot.NO_VARIATION_INDEX
 | 
						|
        if not allEqual(master_values):
 | 
						|
            baseValue, varIdx = self.store_builder.storeMasters(master_values)
 | 
						|
 | 
						|
        if is_fixed_size_float:
 | 
						|
            baseValue = conv.fromInt(baseValue)
 | 
						|
 | 
						|
        return baseValue, varIdx
 | 
						|
 | 
						|
    def storeVariationIndices(self, varIdxes) -> int:
 | 
						|
        # try to reuse an existing VarIndexBase for the same varIdxes, or else
 | 
						|
        # create a new one
 | 
						|
        key = tuple(varIdxes)
 | 
						|
        varIndexBase = self.varIndexCache.get(key)
 | 
						|
 | 
						|
        if varIndexBase is None:
 | 
						|
            # scan for a full match anywhere in the self.varIdxes
 | 
						|
            for i in range(len(self.varIdxes) - len(varIdxes) + 1):
 | 
						|
                if self.varIdxes[i : i + len(varIdxes)] == varIdxes:
 | 
						|
                    self.varIndexCache[key] = varIndexBase = i
 | 
						|
                    break
 | 
						|
 | 
						|
        if varIndexBase is None:
 | 
						|
            # try find a partial match at the end of the self.varIdxes
 | 
						|
            for n in range(len(varIdxes) - 1, 0, -1):
 | 
						|
                if self.varIdxes[-n:] == varIdxes[:n]:
 | 
						|
                    varIndexBase = len(self.varIdxes) - n
 | 
						|
                    self.varIndexCache[key] = varIndexBase
 | 
						|
                    self.varIdxes.extend(varIdxes[n:])
 | 
						|
                    break
 | 
						|
 | 
						|
        if varIndexBase is None:
 | 
						|
            # no match found, append at the end
 | 
						|
            self.varIndexCache[key] = varIndexBase = len(self.varIdxes)
 | 
						|
            self.varIdxes.extend(varIdxes)
 | 
						|
 | 
						|
        return varIndexBase
 | 
						|
 | 
						|
    def mergeVariableAttrs(self, out, lst, attrs) -> int:
 | 
						|
        varIndexBase = ot.NO_VARIATION_INDEX
 | 
						|
        varIdxes = []
 | 
						|
        for attr in attrs:
 | 
						|
            baseValue, varIdx = self.storeMastersForAttr(out, lst, attr)
 | 
						|
            setattr(out, attr, baseValue)
 | 
						|
            varIdxes.append(varIdx)
 | 
						|
 | 
						|
        if any(v != ot.NO_VARIATION_INDEX for v in varIdxes):
 | 
						|
            varIndexBase = self.storeVariationIndices(varIdxes)
 | 
						|
 | 
						|
        return varIndexBase
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def convertSubTablesToVarType(cls, table):
 | 
						|
        for path in dfs_base_table(
 | 
						|
            table,
 | 
						|
            skip_root=True,
 | 
						|
            predicate=lambda path: (
 | 
						|
                getattr(type(path[-1].value), "VarType", None) is not None
 | 
						|
            ),
 | 
						|
        ):
 | 
						|
            st = path[-1]
 | 
						|
            subTable = st.value
 | 
						|
            varType = type(subTable).VarType
 | 
						|
            newSubTable = varType()
 | 
						|
            newSubTable.__dict__.update(subTable.__dict__)
 | 
						|
            newSubTable.populateDefaults()
 | 
						|
            parent = path[-2].value
 | 
						|
            if st.index is not None:
 | 
						|
                getattr(parent, st.name)[st.index] = newSubTable
 | 
						|
            else:
 | 
						|
                setattr(parent, st.name, newSubTable)
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def expandPaintColrLayers(colr):
 | 
						|
        """Rebuild LayerList without PaintColrLayers reuse.
 | 
						|
 | 
						|
        Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph
 | 
						|
        which are irrelevant for this); any layers referenced via PaintColrLayers are
 | 
						|
        collected into a new LayerList and duplicated when reuse is detected, to ensure
 | 
						|
        that all paints are distinct objects at the end of the process.
 | 
						|
        PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap
 | 
						|
        is left. Also, any consecutively nested PaintColrLayers are flattened.
 | 
						|
        The COLR table's LayerList is replaced with the new unique layers.
 | 
						|
        A side effect is also that any layer from the old LayerList which is not
 | 
						|
        referenced by any PaintColrLayers is dropped.
 | 
						|
        """
 | 
						|
        if not colr.LayerList:
 | 
						|
            # if no LayerList, there's nothing to expand
 | 
						|
            return
 | 
						|
        uniqueLayerIDs = set()
 | 
						|
        newLayerList = []
 | 
						|
        for rec in colr.BaseGlyphList.BaseGlyphPaintRecord:
 | 
						|
            frontier = [rec.Paint]
 | 
						|
            while frontier:
 | 
						|
                paint = frontier.pop()
 | 
						|
                if paint.Format == ot.PaintFormat.PaintColrGlyph:
 | 
						|
                    # don't traverse these, we treat them as constant for merging
 | 
						|
                    continue
 | 
						|
                elif paint.Format == ot.PaintFormat.PaintColrLayers:
 | 
						|
                    # de-treeify any nested PaintColrLayers, append unique copies to
 | 
						|
                    # the new layer list and update PaintColrLayers index/count
 | 
						|
                    children = list(_flatten_layers(paint, colr))
 | 
						|
                    first_layer_index = len(newLayerList)
 | 
						|
                    for layer in children:
 | 
						|
                        if id(layer) in uniqueLayerIDs:
 | 
						|
                            layer = copy.deepcopy(layer)
 | 
						|
                            assert id(layer) not in uniqueLayerIDs
 | 
						|
                        newLayerList.append(layer)
 | 
						|
                        uniqueLayerIDs.add(id(layer))
 | 
						|
                    paint.FirstLayerIndex = first_layer_index
 | 
						|
                    paint.NumLayers = len(children)
 | 
						|
                else:
 | 
						|
                    children = paint.getChildren(colr)
 | 
						|
                frontier.extend(reversed(children))
 | 
						|
        # sanity check all the new layers are distinct objects
 | 
						|
        assert len(newLayerList) == len(uniqueLayerIDs)
 | 
						|
        colr.LayerList.Paint = newLayerList
 | 
						|
        colr.LayerList.LayerCount = len(newLayerList)
 | 
						|
 | 
						|
 | 
						|
@COLRVariationMerger.merger(ot.BaseGlyphList)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    # ignore BaseGlyphCount, allow sparse glyph sets across masters
 | 
						|
    out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord}
 | 
						|
    masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst]
 | 
						|
 | 
						|
    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
 | 
						|
 | 
						|
 | 
						|
@COLRVariationMerger.merger(ot.LayerList)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers
 | 
						|
    # found while traversing the paint graphs rooted at BaseGlyphPaintRecords.
 | 
						|
    assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList"
 | 
						|
    # Simply flush the final list of layers and go home.
 | 
						|
    self.LayerCount = len(merger.layers)
 | 
						|
    self.Paint = merger.layers
 | 
						|
 | 
						|
 | 
						|
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)
 | 
						|
 | 
						|
 | 
						|
@COLRVariationMerger.merger((ot.Paint, ot.ClipBox))
 | 
						|
def merge(merger, self, lst):
 | 
						|
    fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable())
 | 
						|
 | 
						|
    if fmt is ot.PaintFormat.PaintColrLayers:
 | 
						|
        _merge_PaintColrLayers(merger, self, lst)
 | 
						|
        return
 | 
						|
 | 
						|
    varFormat = fmt.as_variable()
 | 
						|
 | 
						|
    varAttrs = ()
 | 
						|
    if varFormat is not None:
 | 
						|
        varAttrs = otBase.getVariableAttrs(type(self), varFormat)
 | 
						|
    staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
 | 
						|
 | 
						|
    merger.mergeAttrs(self, lst, staticAttrs)
 | 
						|
 | 
						|
    varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
 | 
						|
 | 
						|
    subTables = [st.value for st in self.iterSubTables()]
 | 
						|
 | 
						|
    # 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)
 | 
						|
 | 
						|
 | 
						|
@COLRVariationMerger.merger((ot.Affine2x3, ot.ColorStop))
 | 
						|
def merge(merger, self, lst):
 | 
						|
    varType = type(self).VarType
 | 
						|
 | 
						|
    varAttrs = otBase.getVariableAttrs(varType)
 | 
						|
    staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
 | 
						|
 | 
						|
    merger.mergeAttrs(self, lst, staticAttrs)
 | 
						|
 | 
						|
    varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
 | 
						|
 | 
						|
    if varIndexBase != ot.NO_VARIATION_INDEX:
 | 
						|
        self.VarIndexBase = varIndexBase
 | 
						|
        # mark as having variations so the parent table will convert to Var{Type}
 | 
						|
        merger.varTableIds.add(id(self))
 | 
						|
 | 
						|
 | 
						|
@COLRVariationMerger.merger(ot.ColorLine)
 | 
						|
def merge(merger, self, lst):
 | 
						|
    merger.mergeAttrs(self, lst, (c.name for c in self.getConverters()))
 | 
						|
 | 
						|
    if any(id(stop) in merger.varTableIds for stop in self.ColorStop):
 | 
						|
        merger.convertSubTablesToVarType(self)
 | 
						|
        merger.varTableIds.add(id(self))
 | 
						|
 | 
						|
 | 
						|
@COLRVariationMerger.merger(ot.ClipList, "clips")
 | 
						|
def merge(merger, self, lst):
 | 
						|
    # 'sparse' in that we allow non-default masters to omit ClipBox entries
 | 
						|
    # for some/all glyphs (i.e. they don't participate)
 | 
						|
    merger.mergeSparseDict(self, lst)
 |