Spaces:
Paused
Paused
| # 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, | |
| } | |
| 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 | |
| def mapLookups(self, lookupMap): | |
| pass | |
| # Copied and trimmed down from subset.py | |
| 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] | |
| 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 | |
| def mapLookups(self, lookupMap): | |
| if self.Format == 1: | |
| self.ExtSubTable.mapLookups(lookupMap) | |
| else: | |
| assert 0, "unknown format: %s" % self.Format | |
| def mapLookups(self, lookupMap): | |
| for st in self.SubTable: | |
| if not st: | |
| continue | |
| st.mapLookups(lookupMap) | |
| def mapLookups(self, lookupMap): | |
| for l in self.Lookup: | |
| if not l: | |
| continue | |
| l.mapLookups(lookupMap) | |
| def mapMarkFilteringSets(self, markFilteringSetMap): | |
| if self.LookupFlag & 0x0010: | |
| self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet] | |
| def mapMarkFilteringSets(self, markFilteringSetMap): | |
| for l in self.Lookup: | |
| if not l: | |
| continue | |
| l.mapMarkFilteringSets(markFilteringSetMap) | |
| def mapLookups(self, lookupMap): | |
| self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex] | |
| def mapLookups(self, lookupMap): | |
| for f in self.FeatureRecord: | |
| if not f or not f.Feature: | |
| continue | |
| f.Feature.mapLookups(lookupMap) | |
| def mapFeatures(self, featureMap): | |
| self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex] | |
| if self.ReqFeatureIndex != 65535: | |
| self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex] | |
| 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) | |
| 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 | |