1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708 |
- """
- 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.scalars = model.getScalars(location)
- @InstancerMerger.merger(ot.CaretValue)
- def merge(merger, self, lst):
- assert self.Format == 1
- Coords = [a.Coordinate for a in lst]
- model = merger.model
- scalars = merger.scalars
- self.Coordinate = otRound(model.interpolateFromMastersAndScalars(Coords, scalars))
- @InstancerMerger.merger(ot.Anchor)
- def merge(merger, self, lst):
- assert self.Format == 1
- XCoords = [a.XCoordinate for a in lst]
- YCoords = [a.YCoordinate for a in lst]
- model = merger.model
- scalars = merger.scalars
- self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars))
- self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars))
- @InstancerMerger.merger(otBase.ValueRecord)
- def merge(merger, self, lst):
- model = merger.model
- scalars = merger.scalars
- # TODO Handle differing valueformats
- for name, tableName in [
- ("XAdvance", "XAdvDevice"),
- ("YAdvance", "YAdvDevice"),
- ("XPlacement", "XPlaDevice"),
- ("YPlacement", "YPlaDevice"),
- ]:
- assert not hasattr(self, tableName)
- if hasattr(self, name):
- values = [getattr(a, name, 0) for a in lst]
- value = otRound(model.interpolateFromMastersAndScalars(values, scalars))
- setattr(self, name, value)
- #
- # 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)
|