|
|
|
|
|
|
|
|
|
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): |
|
|
|
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 |
|
|
|
|
|
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, |
|
"ScriptRecord": mergeScriptRecords, |
|
} |
|
otTables.BaseScriptList.mergeMap = { |
|
"BaseScriptCount": lambda lst: None, |
|
|
|
"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, |
|
} |
|
|
|
|
|
otTables.MarkGlyphSetsDef.mergeMap = { |
|
"MarkSetTableFormat": equal, |
|
"MarkSetCount": sum, |
|
"Coverage": sumLists, |
|
} |
|
|
|
otTables.Axis.mergeMap = { |
|
"*": mergeObjects, |
|
} |
|
|
|
|
|
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), |
|
"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 |
|
for langsys in [script.Script.DefaultLangSys] + [ |
|
l.LangSys for l in script.Script.LangSysRecord |
|
]: |
|
if langsys is None: |
|
continue |
|
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: |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
@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 |
|
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): |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
def layoutPostMerge(font): |
|
|
|
|
|
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: |
|
|
|
featureMap = GregariousIdentityDict(t.table.FeatureList.FeatureRecord) |
|
t.table.ScriptList.mapFeatures(featureMap) |
|
|
|
|
|
featureMap = AttendanceRecordingIdentityDict( |
|
t.table.FeatureList.FeatureRecord |
|
) |
|
t.table.ScriptList.mapFeatures(featureMap) |
|
usedIndices = featureMap.s |
|
|
|
|
|
t.table.FeatureList.FeatureRecord = [ |
|
f |
|
for i, f in enumerate(t.table.FeatureList.FeatureRecord) |
|
if i in usedIndices |
|
] |
|
|
|
|
|
featureMap = NonhashableDict(t.table.FeatureList.FeatureRecord) |
|
t.table.ScriptList.mapFeatures(featureMap) |
|
|
|
t.table.FeatureList.FeatureCount = len(t.table.FeatureList.FeatureRecord) |
|
|
|
if t.table.LookupList: |
|
|
|
lookupMap = GregariousIdentityDict(t.table.LookupList.Lookup) |
|
t.table.FeatureList.mapLookups(lookupMap) |
|
t.table.LookupList.mapLookups(lookupMap) |
|
|
|
|
|
lookupMap = AttendanceRecordingIdentityDict(t.table.LookupList.Lookup) |
|
t.table.FeatureList.mapLookups(lookupMap) |
|
t.table.LookupList.mapLookups(lookupMap) |
|
usedIndices = lookupMap.s |
|
|
|
|
|
t.table.LookupList.Lookup = [ |
|
l for i, l in enumerate(t.table.LookupList.Lookup) if i in usedIndices |
|
] |
|
|
|
|
|
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) |
|
|
|
|
|
|