aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/merge/layout.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/merge/layout.py')
-rw-r--r--Lib/fontTools/merge/layout.py466
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