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