diff options
Diffstat (limited to 'Lib/fontTools/merge/layout.py')
-rw-r--r-- | Lib/fontTools/merge/layout.py | 466 |
1 files changed, 466 insertions, 0 deletions
diff --git a/Lib/fontTools/merge/layout.py b/Lib/fontTools/merge/layout.py new file mode 100644 index 00000000..4bf01c37 --- /dev/null +++ b/Lib/fontTools/merge/layout.py @@ -0,0 +1,466 @@ +# 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 |