# 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: 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