# Copyright 2013 Google, Inc. All Rights Reserved. # # Google Author(s): Behdad Esfahbod, Roozbeh Pournader from fontTools import ttLib from fontTools.ttLib.tables.DefaultTable import DefaultTable from fontTools.ttLib.tables import otTables from fontTools.merge.base import add_method, mergeObjects from fontTools.merge.util import * import logging log = logging.getLogger("fontTools.merge") def mergeLookupLists(lst): # TODO Do smarter merge. return sumLists(lst) def mergeFeatures(lst): assert lst self = otTables.Feature() self.FeatureParams = None self.LookupListIndex = mergeLookupLists( [l.LookupListIndex for l in lst if l.LookupListIndex] ) self.LookupCount = len(self.LookupListIndex) return self def mergeFeatureLists(lst): d = {} for l in lst: for f in l: tag = f.FeatureTag if tag not in d: d[tag] = [] d[tag].append(f.Feature) ret = [] for tag in sorted(d.keys()): rec = otTables.FeatureRecord() rec.FeatureTag = tag rec.Feature = mergeFeatures(d[tag]) ret.append(rec) return ret def mergeLangSyses(lst): assert lst # TODO Support merging ReqFeatureIndex assert all(l.ReqFeatureIndex == 0xFFFF for l in lst) self = otTables.LangSys() self.LookupOrder = None self.ReqFeatureIndex = 0xFFFF self.FeatureIndex = mergeFeatureLists( [l.FeatureIndex for l in lst if l.FeatureIndex] ) self.FeatureCount = len(self.FeatureIndex) return self def mergeScripts(lst): assert lst if len(lst) == 1: return lst[0] langSyses = {} for sr in lst: for lsr in sr.LangSysRecord: if lsr.LangSysTag not in langSyses: langSyses[lsr.LangSysTag] = [] langSyses[lsr.LangSysTag].append(lsr.LangSys) lsrecords = [] for tag, langSys_list in sorted(langSyses.items()): lsr = otTables.LangSysRecord() lsr.LangSys = mergeLangSyses(langSys_list) lsr.LangSysTag = tag lsrecords.append(lsr) self = otTables.Script() self.LangSysRecord = lsrecords self.LangSysCount = len(lsrecords) dfltLangSyses = [s.DefaultLangSys for s in lst if s.DefaultLangSys] if dfltLangSyses: self.DefaultLangSys = mergeLangSyses(dfltLangSyses) else: self.DefaultLangSys = None return self def mergeScriptRecords(lst): d = {} for l in lst: for s in l: tag = s.ScriptTag if tag not in d: d[tag] = [] d[tag].append(s.Script) ret = [] for tag in sorted(d.keys()): rec = otTables.ScriptRecord() rec.ScriptTag = tag rec.Script = mergeScripts(d[tag]) ret.append(rec) return ret otTables.ScriptList.mergeMap = { "ScriptCount": lambda lst: None, # TODO "ScriptRecord": mergeScriptRecords, } otTables.BaseScriptList.mergeMap = { "BaseScriptCount": lambda lst: None, # TODO # TODO: Merge duplicate entries "BaseScriptRecord": lambda lst: sorted( sumLists(lst), key=lambda s: s.BaseScriptTag ), } otTables.FeatureList.mergeMap = { "FeatureCount": sum, "FeatureRecord": lambda lst: sorted(sumLists(lst), key=lambda s: s.FeatureTag), } otTables.LookupList.mergeMap = { "LookupCount": sum, "Lookup": sumLists, } otTables.Coverage.mergeMap = { "Format": min, "glyphs": sumLists, } otTables.ClassDef.mergeMap = { "Format": min, "classDefs": sumDicts, } otTables.LigCaretList.mergeMap = { "Coverage": mergeObjects, "LigGlyphCount": sum, "LigGlyph": sumLists, } otTables.AttachList.mergeMap = { "Coverage": mergeObjects, "GlyphCount": sum, "AttachPoint": sumLists, } # XXX Renumber MarkFilterSets of lookups otTables.MarkGlyphSetsDef.mergeMap = { "MarkSetTableFormat": equal, "MarkSetCount": sum, "Coverage": sumLists, } otTables.Axis.mergeMap = { "*": mergeObjects, } # XXX Fix BASE table merging otTables.BaseTagList.mergeMap = { "BaseTagCount": sum, "BaselineTag": sumLists, } otTables.GDEF.mergeMap = otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = ( otTables.BASE.mergeMap ) = otTables.JSTF.mergeMap = otTables.MATH.mergeMap = { "*": mergeObjects, "Version": max, } ttLib.getTableClass("GDEF").mergeMap = ttLib.getTableClass("GSUB").mergeMap = ( ttLib.getTableClass("GPOS").mergeMap ) = ttLib.getTableClass("BASE").mergeMap = ttLib.getTableClass( "JSTF" ).mergeMap = ttLib.getTableClass( "MATH" ).mergeMap = { "tableTag": onlyExisting(equal), # XXX clean me up "table": mergeObjects, } @add_method(ttLib.getTableClass("GSUB")) def merge(self, m, tables): assert len(tables) == len(m.duplicateGlyphsPerFont) for i, (table, dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)): if not dups: continue if table is None or table is NotImplemented: log.warning( "Have non-identical duplicates to resolve for '%s' but no GSUB. Are duplicates intended?: %s", m.fonts[i]._merger__name, dups, ) continue synthFeature = None synthLookup = None for script in table.table.ScriptList.ScriptRecord: if script.ScriptTag == "DFLT": continue # XXX for langsys in [script.Script.DefaultLangSys] + [ l.LangSys for l in script.Script.LangSysRecord ]: if langsys is None: continue # XXX Create! feature = [v for v in langsys.FeatureIndex if v.FeatureTag == "locl"] assert len(feature) <= 1 if feature: feature = feature[0] else: if not synthFeature: synthFeature = otTables.FeatureRecord() synthFeature.FeatureTag = "locl" f = synthFeature.Feature = otTables.Feature() f.FeatureParams = None f.LookupCount = 0 f.LookupListIndex = [] table.table.FeatureList.FeatureRecord.append(synthFeature) table.table.FeatureList.FeatureCount += 1 feature = synthFeature langsys.FeatureIndex.append(feature) langsys.FeatureIndex.sort(key=lambda v: v.FeatureTag) if not synthLookup: subtable = otTables.SingleSubst() subtable.mapping = dups synthLookup = otTables.Lookup() synthLookup.LookupFlag = 0 synthLookup.LookupType = 1 synthLookup.SubTableCount = 1 synthLookup.SubTable = [subtable] if table.table.LookupList is None: # mtiLib uses None as default value for LookupList, # while feaLib points to an empty array with count 0 # TODO: make them do the same table.table.LookupList = otTables.LookupList() table.table.LookupList.Lookup = [] table.table.LookupList.LookupCount = 0 table.table.LookupList.Lookup.append(synthLookup) table.table.LookupList.LookupCount += 1 if feature.Feature.LookupListIndex[:1] != [synthLookup]: feature.Feature.LookupListIndex[:0] = [synthLookup] feature.Feature.LookupCount += 1 DefaultTable.merge(self, m, tables) return self @add_method( otTables.SingleSubst, otTables.MultipleSubst, otTables.AlternateSubst, otTables.LigatureSubst, otTables.ReverseChainSingleSubst, otTables.SinglePos, otTables.PairPos, otTables.CursivePos, otTables.MarkBasePos, otTables.MarkLigPos, otTables.MarkMarkPos, ) def mapLookups(self, lookupMap): pass # Copied and trimmed down from subset.py @add_method( otTables.ContextSubst, otTables.ChainContextSubst, otTables.ContextPos, otTables.ChainContextPos, ) def __merge_classify_context(self): class ContextHelper(object): def __init__(self, klass, Format): if klass.__name__.endswith("Subst"): Typ = "Sub" Type = "Subst" else: Typ = "Pos" Type = "Pos" if klass.__name__.startswith("Chain"): Chain = "Chain" else: Chain = "" ChainTyp = Chain + Typ self.Typ = Typ self.Type = Type self.Chain = Chain self.ChainTyp = ChainTyp self.LookupRecord = Type + "LookupRecord" if Format == 1: self.Rule = ChainTyp + "Rule" self.RuleSet = ChainTyp + "RuleSet" elif Format == 2: self.Rule = ChainTyp + "ClassRule" self.RuleSet = ChainTyp + "ClassSet" if self.Format not in [1, 2, 3]: return None # Don't shoot the messenger; let it go if not hasattr(self.__class__, "_merge__ContextHelpers"): self.__class__._merge__ContextHelpers = {} if self.Format not in self.__class__._merge__ContextHelpers: helper = ContextHelper(self.__class__, self.Format) self.__class__._merge__ContextHelpers[self.Format] = helper return self.__class__._merge__ContextHelpers[self.Format] @add_method( otTables.ContextSubst, otTables.ChainContextSubst, otTables.ContextPos, otTables.ChainContextPos, ) def mapLookups(self, lookupMap): c = self.__merge_classify_context() if self.Format in [1, 2]: for rs in getattr(self, c.RuleSet): if not rs: continue for r in getattr(rs, c.Rule): if not r: continue for ll in getattr(r, c.LookupRecord): if not ll: continue ll.LookupListIndex = lookupMap[ll.LookupListIndex] elif self.Format == 3: for ll in getattr(self, c.LookupRecord): if not ll: continue ll.LookupListIndex = lookupMap[ll.LookupListIndex] else: assert 0, "unknown format: %s" % self.Format @add_method(otTables.ExtensionSubst, otTables.ExtensionPos) def mapLookups(self, lookupMap): if self.Format == 1: self.ExtSubTable.mapLookups(lookupMap) else: assert 0, "unknown format: %s" % self.Format @add_method(otTables.Lookup) def mapLookups(self, lookupMap): for st in self.SubTable: if not st: continue st.mapLookups(lookupMap) @add_method(otTables.LookupList) def mapLookups(self, lookupMap): for l in self.Lookup: if not l: continue l.mapLookups(lookupMap) @add_method(otTables.Lookup) def mapMarkFilteringSets(self, markFilteringSetMap): if self.LookupFlag & 0x0010: self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet] @add_method(otTables.LookupList) def mapMarkFilteringSets(self, markFilteringSetMap): for l in self.Lookup: if not l: continue l.mapMarkFilteringSets(markFilteringSetMap) @add_method(otTables.Feature) def mapLookups(self, lookupMap): self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex] @add_method(otTables.FeatureList) def mapLookups(self, lookupMap): for f in self.FeatureRecord: if not f or not f.Feature: continue f.Feature.mapLookups(lookupMap) @add_method(otTables.DefaultLangSys, otTables.LangSys) def mapFeatures(self, featureMap): self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex] if self.ReqFeatureIndex != 65535: self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex] @add_method(otTables.Script) def mapFeatures(self, featureMap): if self.DefaultLangSys: self.DefaultLangSys.mapFeatures(featureMap) for l in self.LangSysRecord: if not l or not l.LangSys: continue l.LangSys.mapFeatures(featureMap) @add_method(otTables.ScriptList) def mapFeatures(self, featureMap): for s in self.ScriptRecord: if not s or not s.Script: continue s.Script.mapFeatures(featureMap) def layoutPreMerge(font): # Map indices to references GDEF = font.get("GDEF") GSUB = font.get("GSUB") GPOS = font.get("GPOS") for t in [GSUB, GPOS]: if not t: continue if t.table.LookupList: lookupMap = {i: v for i, v in enumerate(t.table.LookupList.Lookup)} t.table.LookupList.mapLookups(lookupMap) t.table.FeatureList.mapLookups(lookupMap) if ( GDEF and GDEF.table.Version >= 0x00010002 and GDEF.table.MarkGlyphSetsDef ): markFilteringSetMap = { i: v for i, v in enumerate(GDEF.table.MarkGlyphSetsDef.Coverage) } t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap) if t.table.FeatureList and t.table.ScriptList: featureMap = {i: v for i, v in enumerate(t.table.FeatureList.FeatureRecord)} t.table.ScriptList.mapFeatures(featureMap) # TODO FeatureParams nameIDs def layoutPostMerge(font): # Map references back to indices GDEF = font.get("GDEF") GSUB = font.get("GSUB") GPOS = font.get("GPOS") for t in [GSUB, GPOS]: if not t: continue if t.table.FeatureList and t.table.ScriptList: # Collect unregistered (new) features. featureMap = GregariousIdentityDict(t.table.FeatureList.FeatureRecord) t.table.ScriptList.mapFeatures(featureMap) # Record used features. featureMap = AttendanceRecordingIdentityDict( t.table.FeatureList.FeatureRecord ) t.table.ScriptList.mapFeatures(featureMap) usedIndices = featureMap.s # Remove unused features t.table.FeatureList.FeatureRecord = [ f for i, f in enumerate(t.table.FeatureList.FeatureRecord) if i in usedIndices ] # Map back to indices. featureMap = NonhashableDict(t.table.FeatureList.FeatureRecord) t.table.ScriptList.mapFeatures(featureMap) t.table.FeatureList.FeatureCount = len(t.table.FeatureList.FeatureRecord) if t.table.LookupList: # Collect unregistered (new) lookups. lookupMap = GregariousIdentityDict(t.table.LookupList.Lookup) t.table.FeatureList.mapLookups(lookupMap) t.table.LookupList.mapLookups(lookupMap) # Record used lookups. lookupMap = AttendanceRecordingIdentityDict(t.table.LookupList.Lookup) t.table.FeatureList.mapLookups(lookupMap) t.table.LookupList.mapLookups(lookupMap) usedIndices = lookupMap.s # Remove unused lookups t.table.LookupList.Lookup = [ l for i, l in enumerate(t.table.LookupList.Lookup) if i in usedIndices ] # Map back to indices. lookupMap = NonhashableDict(t.table.LookupList.Lookup) t.table.FeatureList.mapLookups(lookupMap) t.table.LookupList.mapLookups(lookupMap) t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup) if GDEF and GDEF.table.Version >= 0x00010002: markFilteringSetMap = NonhashableDict( GDEF.table.MarkGlyphSetsDef.Coverage ) t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap) # TODO FeatureParams nameIDs