527 lines
16 KiB
Python
527 lines
16 KiB
Python
|
# 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
|