# 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 _get_layout_lists(table): table = table.table return ( getattr(table, "ScriptList2", None) or getattr(table, "ScriptList", None), getattr(table, "FeatureList2", None) or getattr(table, "FeatureList", None), getattr(table, "LookupList2", None) or getattr(table, "LookupList", None), ) def _get_mark_glyph_sets_def(table): table = table.table return getattr(table, "MarkGlyphSetsDef2", None) or getattr( table, "MarkGlyphSetsDef", None ) 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.LookupList2.mergeMap = otTables.LookupList.mergeMap 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.MathConstants.mergeMap = { "*": first, } otTables.MathGlyphInfo.mergeMap = { "*": mergeObjects, } otTables.MathItalicsCorrectionInfo.mergeMap = { "Coverage": mergeObjects, "ItalicsCorrectionCount": sum, "ItalicsCorrection": sumLists, } otTables.MathTopAccentAttachment.mergeMap = { "TopAccentCoverage": mergeObjects, "TopAccentAttachmentCount": sum, "TopAccentAttachment": sumLists, } otTables.MathKernInfo.mergeMap = { "MathKernCoverage": mergeObjects, "MathKernCount": sum, "MathKernInfoRecords": sumLists, } otTables.MathVariants.mergeMap = { "MinConnectorOverlap": first, "VertGlyphCoverage": mergeObjects, "HorizGlyphCoverage": mergeObjects, "VertGlyphCount": sum, "HorizGlyphCount": sum, "VertGlyphConstruction": sumLists, "HorizGlyphConstruction": 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 scriptList, featureList, lookupList = _get_layout_lists(table) for script in 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 = [] featureList.FeatureRecord.append(synthFeature) 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 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 lookupList = otTables.LookupList() lookupList.Lookup = [] lookupList.LookupCount = 0 if hasattr(table.table, "LookupList2"): table.table.LookupList2 = lookupList else: table.table.LookupList = lookupList lookupList.Lookup.append(synthLookup) 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 scriptList, featureList, lookupList = _get_layout_lists(t) if lookupList: lookupMap = {i: v for i, v in enumerate(lookupList.Lookup)} lookupList.mapLookups(lookupMap) featureList.mapLookups(lookupMap) markGlyphSetsDef = _get_mark_glyph_sets_def(GDEF) if GDEF else None if GDEF and GDEF.table.Version >= 0x00010002 and markGlyphSetsDef: markFilteringSetMap = { i: v for i, v in enumerate(markGlyphSetsDef.Coverage) } lookupList.mapMarkFilteringSets(markFilteringSetMap) if featureList and scriptList: featureMap = {i: v for i, v in enumerate(featureList.FeatureRecord)} 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 scriptList, featureList, lookupList = _get_layout_lists(t) if featureList and scriptList: # Collect unregistered (new) features. featureMap = GregariousIdentityDict(featureList.FeatureRecord) scriptList.mapFeatures(featureMap) # Record used features. featureMap = AttendanceRecordingIdentityDict(featureList.FeatureRecord) scriptList.mapFeatures(featureMap) usedIndices = featureMap.s # Remove unused features featureList.FeatureRecord = [ f for i, f in enumerate(featureList.FeatureRecord) if i in usedIndices ] # Map back to indices. featureMap = NonhashableDict(featureList.FeatureRecord) scriptList.mapFeatures(featureMap) featureList.FeatureCount = len(featureList.FeatureRecord) if lookupList: # Collect unregistered (new) lookups. lookupMap = GregariousIdentityDict(lookupList.Lookup) featureList.mapLookups(lookupMap) lookupList.mapLookups(lookupMap) # Record used lookups. lookupMap = AttendanceRecordingIdentityDict(lookupList.Lookup) featureList.mapLookups(lookupMap) lookupList.mapLookups(lookupMap) usedIndices = lookupMap.s # Remove unused lookups lookupList.Lookup = [ l for i, l in enumerate(lookupList.Lookup) if i in usedIndices ] # Map back to indices. lookupMap = NonhashableDict(lookupList.Lookup) featureList.mapLookups(lookupMap) lookupList.mapLookups(lookupMap) lookupList.LookupCount = len(lookupList.Lookup) if GDEF and GDEF.table.Version >= 0x00010002: markGlyphSetsDef = _get_mark_glyph_sets_def(GDEF) if markGlyphSetsDef: markFilteringSetMap = NonhashableDict(markGlyphSetsDef.Coverage) lookupList.mapMarkFilteringSets(markFilteringSetMap) # TODO FeatureParams nameIDs