Spaces:
Running
Running
# 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, | |
} | |
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 | |
def mapLookups(self, lookupMap): | |
pass | |
# Copied and trimmed down from subset.py | |
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] | |
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 | |
def mapLookups(self, lookupMap): | |
if self.Format == 1: | |
self.ExtSubTable.mapLookups(lookupMap) | |
else: | |
assert 0, "unknown format: %s" % self.Format | |
def mapLookups(self, lookupMap): | |
for st in self.SubTable: | |
if not st: | |
continue | |
st.mapLookups(lookupMap) | |
def mapLookups(self, lookupMap): | |
for l in self.Lookup: | |
if not l: | |
continue | |
l.mapLookups(lookupMap) | |
def mapMarkFilteringSets(self, markFilteringSetMap): | |
if self.LookupFlag & 0x0010: | |
self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet] | |
def mapMarkFilteringSets(self, markFilteringSetMap): | |
for l in self.Lookup: | |
if not l: | |
continue | |
l.mapMarkFilteringSets(markFilteringSetMap) | |
def mapLookups(self, lookupMap): | |
self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex] | |
def mapLookups(self, lookupMap): | |
for f in self.FeatureRecord: | |
if not f or not f.Feature: | |
continue | |
f.Feature.mapLookups(lookupMap) | |
def mapFeatures(self, featureMap): | |
self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex] | |
if self.ReqFeatureIndex != 65535: | |
self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex] | |
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) | |
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 | |