Spaces:
Sleeping
Sleeping
#!/usr/bin/python | |
# FontDame-to-FontTools for OpenType Layout tables | |
# | |
# Source language spec is available at: | |
# http://monotype.github.io/OpenType_Table_Source/otl_source.html | |
# https://github.com/Monotype/OpenType_Table_Source/ | |
from fontTools import ttLib | |
from fontTools.ttLib.tables._c_m_a_p import cmap_classes | |
from fontTools.ttLib.tables import otTables as ot | |
from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict | |
from fontTools.otlLib import builder as otl | |
from contextlib import contextmanager | |
from fontTools.ttLib import newTable | |
from fontTools.feaLib.lookupDebugInfo import LOOKUP_DEBUG_ENV_VAR, LOOKUP_DEBUG_INFO_KEY | |
from operator import setitem | |
import os | |
import logging | |
class MtiLibError(Exception): | |
pass | |
class ReferenceNotFoundError(MtiLibError): | |
pass | |
class FeatureNotFoundError(ReferenceNotFoundError): | |
pass | |
class LookupNotFoundError(ReferenceNotFoundError): | |
pass | |
log = logging.getLogger("fontTools.mtiLib") | |
def makeGlyph(s): | |
if s[:2] in ["U ", "u "]: | |
return ttLib.TTFont._makeGlyphName(int(s[2:], 16)) | |
elif s[:2] == "# ": | |
return "glyph%.5d" % int(s[2:]) | |
assert s.find(" ") < 0, "Space found in glyph name: %s" % s | |
assert s, "Glyph name is empty" | |
return s | |
def makeGlyphs(l): | |
return [makeGlyph(g) for g in l] | |
def mapLookup(sym, mapping): | |
# Lookups are addressed by name. So resolved them using a map if available. | |
# Fallback to parsing as lookup index if a map isn't provided. | |
if mapping is not None: | |
try: | |
idx = mapping[sym] | |
except KeyError: | |
raise LookupNotFoundError(sym) | |
else: | |
idx = int(sym) | |
return idx | |
def mapFeature(sym, mapping): | |
# Features are referenced by index according the spec. So, if symbol is an | |
# integer, use it directly. Otherwise look up in the map if provided. | |
try: | |
idx = int(sym) | |
except ValueError: | |
try: | |
idx = mapping[sym] | |
except KeyError: | |
raise FeatureNotFoundError(sym) | |
return idx | |
def setReference(mapper, mapping, sym, setter, collection, key): | |
try: | |
mapped = mapper(sym, mapping) | |
except ReferenceNotFoundError as e: | |
try: | |
if mapping is not None: | |
mapping.addDeferredMapping( | |
lambda ref: setter(collection, key, ref), sym, e | |
) | |
return | |
except AttributeError: | |
pass | |
raise | |
setter(collection, key, mapped) | |
class DeferredMapping(dict): | |
def __init__(self): | |
self._deferredMappings = [] | |
def addDeferredMapping(self, setter, sym, e): | |
log.debug("Adding deferred mapping for symbol '%s' %s", sym, type(e).__name__) | |
self._deferredMappings.append((setter, sym, e)) | |
def applyDeferredMappings(self): | |
for setter, sym, e in self._deferredMappings: | |
log.debug( | |
"Applying deferred mapping for symbol '%s' %s", sym, type(e).__name__ | |
) | |
try: | |
mapped = self[sym] | |
except KeyError: | |
raise e | |
setter(mapped) | |
log.debug("Set to %s", mapped) | |
self._deferredMappings = [] | |
def parseScriptList(lines, featureMap=None): | |
self = ot.ScriptList() | |
records = [] | |
with lines.between("script table"): | |
for line in lines: | |
while len(line) < 4: | |
line.append("") | |
scriptTag, langSysTag, defaultFeature, features = line | |
log.debug("Adding script %s language-system %s", scriptTag, langSysTag) | |
langSys = ot.LangSys() | |
langSys.LookupOrder = None | |
if defaultFeature: | |
setReference( | |
mapFeature, | |
featureMap, | |
defaultFeature, | |
setattr, | |
langSys, | |
"ReqFeatureIndex", | |
) | |
else: | |
langSys.ReqFeatureIndex = 0xFFFF | |
syms = stripSplitComma(features) | |
langSys.FeatureIndex = theList = [3] * len(syms) | |
for i, sym in enumerate(syms): | |
setReference(mapFeature, featureMap, sym, setitem, theList, i) | |
langSys.FeatureCount = len(langSys.FeatureIndex) | |
script = [s for s in records if s.ScriptTag == scriptTag] | |
if script: | |
script = script[0].Script | |
else: | |
scriptRec = ot.ScriptRecord() | |
scriptRec.ScriptTag = scriptTag + " " * (4 - len(scriptTag)) | |
scriptRec.Script = ot.Script() | |
records.append(scriptRec) | |
script = scriptRec.Script | |
script.DefaultLangSys = None | |
script.LangSysRecord = [] | |
script.LangSysCount = 0 | |
if langSysTag == "default": | |
script.DefaultLangSys = langSys | |
else: | |
langSysRec = ot.LangSysRecord() | |
langSysRec.LangSysTag = langSysTag + " " * (4 - len(langSysTag)) | |
langSysRec.LangSys = langSys | |
script.LangSysRecord.append(langSysRec) | |
script.LangSysCount = len(script.LangSysRecord) | |
for script in records: | |
script.Script.LangSysRecord = sorted( | |
script.Script.LangSysRecord, key=lambda rec: rec.LangSysTag | |
) | |
self.ScriptRecord = sorted(records, key=lambda rec: rec.ScriptTag) | |
self.ScriptCount = len(self.ScriptRecord) | |
return self | |
def parseFeatureList(lines, lookupMap=None, featureMap=None): | |
self = ot.FeatureList() | |
self.FeatureRecord = [] | |
with lines.between("feature table"): | |
for line in lines: | |
name, featureTag, lookups = line | |
if featureMap is not None: | |
assert name not in featureMap, "Duplicate feature name: %s" % name | |
featureMap[name] = len(self.FeatureRecord) | |
# If feature name is integer, make sure it matches its index. | |
try: | |
assert int(name) == len(self.FeatureRecord), "%d %d" % ( | |
name, | |
len(self.FeatureRecord), | |
) | |
except ValueError: | |
pass | |
featureRec = ot.FeatureRecord() | |
featureRec.FeatureTag = featureTag | |
featureRec.Feature = ot.Feature() | |
self.FeatureRecord.append(featureRec) | |
feature = featureRec.Feature | |
feature.FeatureParams = None | |
syms = stripSplitComma(lookups) | |
feature.LookupListIndex = theList = [None] * len(syms) | |
for i, sym in enumerate(syms): | |
setReference(mapLookup, lookupMap, sym, setitem, theList, i) | |
feature.LookupCount = len(feature.LookupListIndex) | |
self.FeatureCount = len(self.FeatureRecord) | |
return self | |
def parseLookupFlags(lines): | |
flags = 0 | |
filterset = None | |
allFlags = [ | |
"righttoleft", | |
"ignorebaseglyphs", | |
"ignoreligatures", | |
"ignoremarks", | |
"markattachmenttype", | |
"markfiltertype", | |
] | |
while lines.peeks()[0].lower() in allFlags: | |
line = next(lines) | |
flag = { | |
"righttoleft": 0x0001, | |
"ignorebaseglyphs": 0x0002, | |
"ignoreligatures": 0x0004, | |
"ignoremarks": 0x0008, | |
}.get(line[0].lower()) | |
if flag: | |
assert line[1].lower() in ["yes", "no"], line[1] | |
if line[1].lower() == "yes": | |
flags |= flag | |
continue | |
if line[0].lower() == "markattachmenttype": | |
flags |= int(line[1]) << 8 | |
continue | |
if line[0].lower() == "markfiltertype": | |
flags |= 0x10 | |
filterset = int(line[1]) | |
return flags, filterset | |
def parseSingleSubst(lines, font, _lookupMap=None): | |
mapping = {} | |
for line in lines: | |
assert len(line) == 2, line | |
line = makeGlyphs(line) | |
mapping[line[0]] = line[1] | |
return otl.buildSingleSubstSubtable(mapping) | |
def parseMultiple(lines, font, _lookupMap=None): | |
mapping = {} | |
for line in lines: | |
line = makeGlyphs(line) | |
mapping[line[0]] = line[1:] | |
return otl.buildMultipleSubstSubtable(mapping) | |
def parseAlternate(lines, font, _lookupMap=None): | |
mapping = {} | |
for line in lines: | |
line = makeGlyphs(line) | |
mapping[line[0]] = line[1:] | |
return otl.buildAlternateSubstSubtable(mapping) | |
def parseLigature(lines, font, _lookupMap=None): | |
mapping = {} | |
for line in lines: | |
assert len(line) >= 2, line | |
line = makeGlyphs(line) | |
mapping[tuple(line[1:])] = line[0] | |
return otl.buildLigatureSubstSubtable(mapping) | |
def parseSinglePos(lines, font, _lookupMap=None): | |
values = {} | |
for line in lines: | |
assert len(line) == 3, line | |
w = line[0].title().replace(" ", "") | |
assert w in valueRecordFormatDict | |
g = makeGlyph(line[1]) | |
v = int(line[2]) | |
if g not in values: | |
values[g] = ValueRecord() | |
assert not hasattr(values[g], w), (g, w) | |
setattr(values[g], w, v) | |
return otl.buildSinglePosSubtable(values, font.getReverseGlyphMap()) | |
def parsePair(lines, font, _lookupMap=None): | |
self = ot.PairPos() | |
self.ValueFormat1 = self.ValueFormat2 = 0 | |
typ = lines.peeks()[0].split()[0].lower() | |
if typ in ("left", "right"): | |
self.Format = 1 | |
values = {} | |
for line in lines: | |
assert len(line) == 4, line | |
side = line[0].split()[0].lower() | |
assert side in ("left", "right"), side | |
what = line[0][len(side) :].title().replace(" ", "") | |
mask = valueRecordFormatDict[what][0] | |
glyph1, glyph2 = makeGlyphs(line[1:3]) | |
value = int(line[3]) | |
if not glyph1 in values: | |
values[glyph1] = {} | |
if not glyph2 in values[glyph1]: | |
values[glyph1][glyph2] = (ValueRecord(), ValueRecord()) | |
rec2 = values[glyph1][glyph2] | |
if side == "left": | |
self.ValueFormat1 |= mask | |
vr = rec2[0] | |
else: | |
self.ValueFormat2 |= mask | |
vr = rec2[1] | |
assert not hasattr(vr, what), (vr, what) | |
setattr(vr, what, value) | |
self.Coverage = makeCoverage(set(values.keys()), font) | |
self.PairSet = [] | |
for glyph1 in self.Coverage.glyphs: | |
values1 = values[glyph1] | |
pairset = ot.PairSet() | |
records = pairset.PairValueRecord = [] | |
for glyph2 in sorted(values1.keys(), key=font.getGlyphID): | |
values2 = values1[glyph2] | |
pair = ot.PairValueRecord() | |
pair.SecondGlyph = glyph2 | |
pair.Value1 = values2[0] | |
pair.Value2 = values2[1] if self.ValueFormat2 else None | |
records.append(pair) | |
pairset.PairValueCount = len(pairset.PairValueRecord) | |
self.PairSet.append(pairset) | |
self.PairSetCount = len(self.PairSet) | |
elif typ.endswith("class"): | |
self.Format = 2 | |
classDefs = [None, None] | |
while lines.peeks()[0].endswith("class definition begin"): | |
typ = lines.peek()[0][: -len("class definition begin")].lower() | |
idx, klass = { | |
"first": (0, ot.ClassDef1), | |
"second": (1, ot.ClassDef2), | |
}[typ] | |
assert classDefs[idx] is None | |
classDefs[idx] = parseClassDef(lines, font, klass=klass) | |
self.ClassDef1, self.ClassDef2 = classDefs | |
self.Class1Count, self.Class2Count = ( | |
1 + max(c.classDefs.values()) for c in classDefs | |
) | |
self.Class1Record = [ot.Class1Record() for i in range(self.Class1Count)] | |
for rec1 in self.Class1Record: | |
rec1.Class2Record = [ot.Class2Record() for j in range(self.Class2Count)] | |
for rec2 in rec1.Class2Record: | |
rec2.Value1 = ValueRecord() | |
rec2.Value2 = ValueRecord() | |
for line in lines: | |
assert len(line) == 4, line | |
side = line[0].split()[0].lower() | |
assert side in ("left", "right"), side | |
what = line[0][len(side) :].title().replace(" ", "") | |
mask = valueRecordFormatDict[what][0] | |
class1, class2, value = (int(x) for x in line[1:4]) | |
rec2 = self.Class1Record[class1].Class2Record[class2] | |
if side == "left": | |
self.ValueFormat1 |= mask | |
vr = rec2.Value1 | |
else: | |
self.ValueFormat2 |= mask | |
vr = rec2.Value2 | |
assert not hasattr(vr, what), (vr, what) | |
setattr(vr, what, value) | |
for rec1 in self.Class1Record: | |
for rec2 in rec1.Class2Record: | |
rec2.Value1 = ValueRecord(self.ValueFormat1, rec2.Value1) | |
rec2.Value2 = ( | |
ValueRecord(self.ValueFormat2, rec2.Value2) | |
if self.ValueFormat2 | |
else None | |
) | |
self.Coverage = makeCoverage(set(self.ClassDef1.classDefs.keys()), font) | |
else: | |
assert 0, typ | |
return self | |
def parseKernset(lines, font, _lookupMap=None): | |
typ = lines.peeks()[0].split()[0].lower() | |
if typ in ("left", "right"): | |
with lines.until( | |
("firstclass definition begin", "secondclass definition begin") | |
): | |
return parsePair(lines, font) | |
return parsePair(lines, font) | |
def makeAnchor(data, klass=ot.Anchor): | |
assert len(data) <= 2 | |
anchor = klass() | |
anchor.Format = 1 | |
anchor.XCoordinate, anchor.YCoordinate = intSplitComma(data[0]) | |
if len(data) > 1 and data[1] != "": | |
anchor.Format = 2 | |
anchor.AnchorPoint = int(data[1]) | |
return anchor | |
def parseCursive(lines, font, _lookupMap=None): | |
records = {} | |
for line in lines: | |
assert len(line) in [3, 4], line | |
idx, klass = { | |
"entry": (0, ot.EntryAnchor), | |
"exit": (1, ot.ExitAnchor), | |
}[line[0]] | |
glyph = makeGlyph(line[1]) | |
if glyph not in records: | |
records[glyph] = [None, None] | |
assert records[glyph][idx] is None, (glyph, idx) | |
records[glyph][idx] = makeAnchor(line[2:], klass) | |
return otl.buildCursivePosSubtable(records, font.getReverseGlyphMap()) | |
def makeMarkRecords(data, coverage, c): | |
records = [] | |
for glyph in coverage.glyphs: | |
klass, anchor = data[glyph] | |
record = c.MarkRecordClass() | |
record.Class = klass | |
setattr(record, c.MarkAnchor, anchor) | |
records.append(record) | |
return records | |
def makeBaseRecords(data, coverage, c, classCount): | |
records = [] | |
idx = {} | |
for glyph in coverage.glyphs: | |
idx[glyph] = len(records) | |
record = c.BaseRecordClass() | |
anchors = [None] * classCount | |
setattr(record, c.BaseAnchor, anchors) | |
records.append(record) | |
for (glyph, klass), anchor in data.items(): | |
record = records[idx[glyph]] | |
anchors = getattr(record, c.BaseAnchor) | |
assert anchors[klass] is None, (glyph, klass) | |
anchors[klass] = anchor | |
return records | |
def makeLigatureRecords(data, coverage, c, classCount): | |
records = [None] * len(coverage.glyphs) | |
idx = {g: i for i, g in enumerate(coverage.glyphs)} | |
for (glyph, klass, compIdx, compCount), anchor in data.items(): | |
record = records[idx[glyph]] | |
if record is None: | |
record = records[idx[glyph]] = ot.LigatureAttach() | |
record.ComponentCount = compCount | |
record.ComponentRecord = [ot.ComponentRecord() for i in range(compCount)] | |
for compRec in record.ComponentRecord: | |
compRec.LigatureAnchor = [None] * classCount | |
assert record.ComponentCount == compCount, ( | |
glyph, | |
record.ComponentCount, | |
compCount, | |
) | |
anchors = record.ComponentRecord[compIdx - 1].LigatureAnchor | |
assert anchors[klass] is None, (glyph, compIdx, klass) | |
anchors[klass] = anchor | |
return records | |
def parseMarkToSomething(lines, font, c): | |
self = c.Type() | |
self.Format = 1 | |
markData = {} | |
baseData = {} | |
Data = { | |
"mark": (markData, c.MarkAnchorClass), | |
"base": (baseData, c.BaseAnchorClass), | |
"ligature": (baseData, c.BaseAnchorClass), | |
} | |
maxKlass = 0 | |
for line in lines: | |
typ = line[0] | |
assert typ in ("mark", "base", "ligature") | |
glyph = makeGlyph(line[1]) | |
data, anchorClass = Data[typ] | |
extraItems = 2 if typ == "ligature" else 0 | |
extras = tuple(int(i) for i in line[2 : 2 + extraItems]) | |
klass = int(line[2 + extraItems]) | |
anchor = makeAnchor(line[3 + extraItems :], anchorClass) | |
if typ == "mark": | |
key, value = glyph, (klass, anchor) | |
else: | |
key, value = ((glyph, klass) + extras), anchor | |
assert key not in data, key | |
data[key] = value | |
maxKlass = max(maxKlass, klass) | |
# Mark | |
markCoverage = makeCoverage(set(markData.keys()), font, c.MarkCoverageClass) | |
markArray = c.MarkArrayClass() | |
markRecords = makeMarkRecords(markData, markCoverage, c) | |
setattr(markArray, c.MarkRecord, markRecords) | |
setattr(markArray, c.MarkCount, len(markRecords)) | |
setattr(self, c.MarkCoverage, markCoverage) | |
setattr(self, c.MarkArray, markArray) | |
self.ClassCount = maxKlass + 1 | |
# Base | |
self.classCount = 0 if not baseData else 1 + max(k[1] for k, v in baseData.items()) | |
baseCoverage = makeCoverage( | |
set([k[0] for k in baseData.keys()]), font, c.BaseCoverageClass | |
) | |
baseArray = c.BaseArrayClass() | |
if c.Base == "Ligature": | |
baseRecords = makeLigatureRecords(baseData, baseCoverage, c, self.classCount) | |
else: | |
baseRecords = makeBaseRecords(baseData, baseCoverage, c, self.classCount) | |
setattr(baseArray, c.BaseRecord, baseRecords) | |
setattr(baseArray, c.BaseCount, len(baseRecords)) | |
setattr(self, c.BaseCoverage, baseCoverage) | |
setattr(self, c.BaseArray, baseArray) | |
return self | |
class MarkHelper(object): | |
def __init__(self): | |
for Which in ("Mark", "Base"): | |
for What in ("Coverage", "Array", "Count", "Record", "Anchor"): | |
key = Which + What | |
if Which == "Mark" and What in ("Count", "Record", "Anchor"): | |
value = key | |
else: | |
value = getattr(self, Which) + What | |
if value == "LigatureRecord": | |
value = "LigatureAttach" | |
setattr(self, key, value) | |
if What != "Count": | |
klass = getattr(ot, value) | |
setattr(self, key + "Class", klass) | |
class MarkToBaseHelper(MarkHelper): | |
Mark = "Mark" | |
Base = "Base" | |
Type = ot.MarkBasePos | |
class MarkToMarkHelper(MarkHelper): | |
Mark = "Mark1" | |
Base = "Mark2" | |
Type = ot.MarkMarkPos | |
class MarkToLigatureHelper(MarkHelper): | |
Mark = "Mark" | |
Base = "Ligature" | |
Type = ot.MarkLigPos | |
def parseMarkToBase(lines, font, _lookupMap=None): | |
return parseMarkToSomething(lines, font, MarkToBaseHelper()) | |
def parseMarkToMark(lines, font, _lookupMap=None): | |
return parseMarkToSomething(lines, font, MarkToMarkHelper()) | |
def parseMarkToLigature(lines, font, _lookupMap=None): | |
return parseMarkToSomething(lines, font, MarkToLigatureHelper()) | |
def stripSplitComma(line): | |
return [s.strip() for s in line.split(",")] if line else [] | |
def intSplitComma(line): | |
return [int(i) for i in line.split(",")] if line else [] | |
# Copied from fontTools.subset | |
class ContextHelper(object): | |
def __init__(self, klassName, Format): | |
if klassName.endswith("Subst"): | |
Typ = "Sub" | |
Type = "Subst" | |
else: | |
Typ = "Pos" | |
Type = "Pos" | |
if klassName.startswith("Chain"): | |
Chain = "Chain" | |
InputIdx = 1 | |
DataLen = 3 | |
else: | |
Chain = "" | |
InputIdx = 0 | |
DataLen = 1 | |
ChainTyp = Chain + Typ | |
self.Typ = Typ | |
self.Type = Type | |
self.Chain = Chain | |
self.ChainTyp = ChainTyp | |
self.InputIdx = InputIdx | |
self.DataLen = DataLen | |
self.LookupRecord = Type + "LookupRecord" | |
if Format == 1: | |
Coverage = lambda r: r.Coverage | |
ChainCoverage = lambda r: r.Coverage | |
ContextData = lambda r: (None,) | |
ChainContextData = lambda r: (None, None, None) | |
SetContextData = None | |
SetChainContextData = None | |
RuleData = lambda r: (r.Input,) | |
ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead) | |
def SetRuleData(r, d): | |
(r.Input,) = d | |
(r.GlyphCount,) = (len(x) + 1 for x in d) | |
def ChainSetRuleData(r, d): | |
(r.Backtrack, r.Input, r.LookAhead) = d | |
( | |
r.BacktrackGlyphCount, | |
r.InputGlyphCount, | |
r.LookAheadGlyphCount, | |
) = (len(d[0]), len(d[1]) + 1, len(d[2])) | |
elif Format == 2: | |
Coverage = lambda r: r.Coverage | |
ChainCoverage = lambda r: r.Coverage | |
ContextData = lambda r: (r.ClassDef,) | |
ChainContextData = lambda r: ( | |
r.BacktrackClassDef, | |
r.InputClassDef, | |
r.LookAheadClassDef, | |
) | |
def SetContextData(r, d): | |
(r.ClassDef,) = d | |
def SetChainContextData(r, d): | |
(r.BacktrackClassDef, r.InputClassDef, r.LookAheadClassDef) = d | |
RuleData = lambda r: (r.Class,) | |
ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead) | |
def SetRuleData(r, d): | |
(r.Class,) = d | |
(r.GlyphCount,) = (len(x) + 1 for x in d) | |
def ChainSetRuleData(r, d): | |
(r.Backtrack, r.Input, r.LookAhead) = d | |
( | |
r.BacktrackGlyphCount, | |
r.InputGlyphCount, | |
r.LookAheadGlyphCount, | |
) = (len(d[0]), len(d[1]) + 1, len(d[2])) | |
elif Format == 3: | |
Coverage = lambda r: r.Coverage[0] | |
ChainCoverage = lambda r: r.InputCoverage[0] | |
ContextData = None | |
ChainContextData = None | |
SetContextData = None | |
SetChainContextData = None | |
RuleData = lambda r: r.Coverage | |
ChainRuleData = lambda r: ( | |
r.BacktrackCoverage + r.InputCoverage + r.LookAheadCoverage | |
) | |
def SetRuleData(r, d): | |
(r.Coverage,) = d | |
(r.GlyphCount,) = (len(x) for x in d) | |
def ChainSetRuleData(r, d): | |
(r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d | |
( | |
r.BacktrackGlyphCount, | |
r.InputGlyphCount, | |
r.LookAheadGlyphCount, | |
) = (len(x) for x in d) | |
else: | |
assert 0, "unknown format: %s" % Format | |
if Chain: | |
self.Coverage = ChainCoverage | |
self.ContextData = ChainContextData | |
self.SetContextData = SetChainContextData | |
self.RuleData = ChainRuleData | |
self.SetRuleData = ChainSetRuleData | |
else: | |
self.Coverage = Coverage | |
self.ContextData = ContextData | |
self.SetContextData = SetContextData | |
self.RuleData = RuleData | |
self.SetRuleData = SetRuleData | |
if Format == 1: | |
self.Rule = ChainTyp + "Rule" | |
self.RuleCount = ChainTyp + "RuleCount" | |
self.RuleSet = ChainTyp + "RuleSet" | |
self.RuleSetCount = ChainTyp + "RuleSetCount" | |
self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] | |
elif Format == 2: | |
self.Rule = ChainTyp + "ClassRule" | |
self.RuleCount = ChainTyp + "ClassRuleCount" | |
self.RuleSet = ChainTyp + "ClassSet" | |
self.RuleSetCount = ChainTyp + "ClassSetCount" | |
self.Intersect = lambda glyphs, c, r: ( | |
c.intersect_class(glyphs, r) | |
if c | |
else (set(glyphs) if r == 0 else set()) | |
) | |
self.ClassDef = "InputClassDef" if Chain else "ClassDef" | |
self.ClassDefIndex = 1 if Chain else 0 | |
self.Input = "Input" if Chain else "Class" | |
def parseLookupRecords(items, klassName, lookupMap=None): | |
klass = getattr(ot, klassName) | |
lst = [] | |
for item in items: | |
rec = klass() | |
item = stripSplitComma(item) | |
assert len(item) == 2, item | |
idx = int(item[0]) | |
assert idx > 0, idx | |
rec.SequenceIndex = idx - 1 | |
setReference(mapLookup, lookupMap, item[1], setattr, rec, "LookupListIndex") | |
lst.append(rec) | |
return lst | |
def makeClassDef(classDefs, font, klass=ot.Coverage): | |
if not classDefs: | |
return None | |
self = klass() | |
self.classDefs = dict(classDefs) | |
return self | |
def parseClassDef(lines, font, klass=ot.ClassDef): | |
classDefs = {} | |
with lines.between("class definition"): | |
for line in lines: | |
glyph = makeGlyph(line[0]) | |
assert glyph not in classDefs, glyph | |
classDefs[glyph] = int(line[1]) | |
return makeClassDef(classDefs, font, klass) | |
def makeCoverage(glyphs, font, klass=ot.Coverage): | |
if not glyphs: | |
return None | |
if isinstance(glyphs, set): | |
glyphs = sorted(glyphs) | |
coverage = klass() | |
coverage.glyphs = sorted(set(glyphs), key=font.getGlyphID) | |
return coverage | |
def parseCoverage(lines, font, klass=ot.Coverage): | |
glyphs = [] | |
with lines.between("coverage definition"): | |
for line in lines: | |
glyphs.append(makeGlyph(line[0])) | |
return makeCoverage(glyphs, font, klass) | |
def bucketizeRules(self, c, rules, bucketKeys): | |
buckets = {} | |
for seq, recs in rules: | |
buckets.setdefault(seq[c.InputIdx][0], []).append( | |
(tuple(s[1 if i == c.InputIdx else 0 :] for i, s in enumerate(seq)), recs) | |
) | |
rulesets = [] | |
for firstGlyph in bucketKeys: | |
if firstGlyph not in buckets: | |
rulesets.append(None) | |
continue | |
thisRules = [] | |
for seq, recs in buckets[firstGlyph]: | |
rule = getattr(ot, c.Rule)() | |
c.SetRuleData(rule, seq) | |
setattr(rule, c.Type + "Count", len(recs)) | |
setattr(rule, c.LookupRecord, recs) | |
thisRules.append(rule) | |
ruleset = getattr(ot, c.RuleSet)() | |
setattr(ruleset, c.Rule, thisRules) | |
setattr(ruleset, c.RuleCount, len(thisRules)) | |
rulesets.append(ruleset) | |
setattr(self, c.RuleSet, rulesets) | |
setattr(self, c.RuleSetCount, len(rulesets)) | |
def parseContext(lines, font, Type, lookupMap=None): | |
self = getattr(ot, Type)() | |
typ = lines.peeks()[0].split()[0].lower() | |
if typ == "glyph": | |
self.Format = 1 | |
log.debug("Parsing %s format %s", Type, self.Format) | |
c = ContextHelper(Type, self.Format) | |
rules = [] | |
for line in lines: | |
assert line[0].lower() == "glyph", line[0] | |
while len(line) < 1 + c.DataLen: | |
line.append("") | |
seq = tuple(makeGlyphs(stripSplitComma(i)) for i in line[1 : 1 + c.DataLen]) | |
recs = parseLookupRecords(line[1 + c.DataLen :], c.LookupRecord, lookupMap) | |
rules.append((seq, recs)) | |
firstGlyphs = set(seq[c.InputIdx][0] for seq, recs in rules) | |
self.Coverage = makeCoverage(firstGlyphs, font) | |
bucketizeRules(self, c, rules, self.Coverage.glyphs) | |
elif typ.endswith("class"): | |
self.Format = 2 | |
log.debug("Parsing %s format %s", Type, self.Format) | |
c = ContextHelper(Type, self.Format) | |
classDefs = [None] * c.DataLen | |
while lines.peeks()[0].endswith("class definition begin"): | |
typ = lines.peek()[0][: -len("class definition begin")].lower() | |
idx, klass = { | |
1: { | |
"": (0, ot.ClassDef), | |
}, | |
3: { | |
"backtrack": (0, ot.BacktrackClassDef), | |
"": (1, ot.InputClassDef), | |
"lookahead": (2, ot.LookAheadClassDef), | |
}, | |
}[c.DataLen][typ] | |
assert classDefs[idx] is None, idx | |
classDefs[idx] = parseClassDef(lines, font, klass=klass) | |
c.SetContextData(self, classDefs) | |
rules = [] | |
for line in lines: | |
assert line[0].lower().startswith("class"), line[0] | |
while len(line) < 1 + c.DataLen: | |
line.append("") | |
seq = tuple(intSplitComma(i) for i in line[1 : 1 + c.DataLen]) | |
recs = parseLookupRecords(line[1 + c.DataLen :], c.LookupRecord, lookupMap) | |
rules.append((seq, recs)) | |
firstClasses = set(seq[c.InputIdx][0] for seq, recs in rules) | |
firstGlyphs = set( | |
g for g, c in classDefs[c.InputIdx].classDefs.items() if c in firstClasses | |
) | |
self.Coverage = makeCoverage(firstGlyphs, font) | |
bucketizeRules(self, c, rules, range(max(firstClasses) + 1)) | |
elif typ.endswith("coverage"): | |
self.Format = 3 | |
log.debug("Parsing %s format %s", Type, self.Format) | |
c = ContextHelper(Type, self.Format) | |
coverages = tuple([] for i in range(c.DataLen)) | |
while lines.peeks()[0].endswith("coverage definition begin"): | |
typ = lines.peek()[0][: -len("coverage definition begin")].lower() | |
idx, klass = { | |
1: { | |
"": (0, ot.Coverage), | |
}, | |
3: { | |
"backtrack": (0, ot.BacktrackCoverage), | |
"input": (1, ot.InputCoverage), | |
"lookahead": (2, ot.LookAheadCoverage), | |
}, | |
}[c.DataLen][typ] | |
coverages[idx].append(parseCoverage(lines, font, klass=klass)) | |
c.SetRuleData(self, coverages) | |
lines = list(lines) | |
assert len(lines) == 1 | |
line = lines[0] | |
assert line[0].lower() == "coverage", line[0] | |
recs = parseLookupRecords(line[1:], c.LookupRecord, lookupMap) | |
setattr(self, c.Type + "Count", len(recs)) | |
setattr(self, c.LookupRecord, recs) | |
else: | |
assert 0, typ | |
return self | |
def parseContextSubst(lines, font, lookupMap=None): | |
return parseContext(lines, font, "ContextSubst", lookupMap=lookupMap) | |
def parseContextPos(lines, font, lookupMap=None): | |
return parseContext(lines, font, "ContextPos", lookupMap=lookupMap) | |
def parseChainedSubst(lines, font, lookupMap=None): | |
return parseContext(lines, font, "ChainContextSubst", lookupMap=lookupMap) | |
def parseChainedPos(lines, font, lookupMap=None): | |
return parseContext(lines, font, "ChainContextPos", lookupMap=lookupMap) | |
def parseReverseChainedSubst(lines, font, _lookupMap=None): | |
self = ot.ReverseChainSingleSubst() | |
self.Format = 1 | |
coverages = ([], []) | |
while lines.peeks()[0].endswith("coverage definition begin"): | |
typ = lines.peek()[0][: -len("coverage definition begin")].lower() | |
idx, klass = { | |
"backtrack": (0, ot.BacktrackCoverage), | |
"lookahead": (1, ot.LookAheadCoverage), | |
}[typ] | |
coverages[idx].append(parseCoverage(lines, font, klass=klass)) | |
self.BacktrackCoverage = coverages[0] | |
self.BacktrackGlyphCount = len(self.BacktrackCoverage) | |
self.LookAheadCoverage = coverages[1] | |
self.LookAheadGlyphCount = len(self.LookAheadCoverage) | |
mapping = {} | |
for line in lines: | |
assert len(line) == 2, line | |
line = makeGlyphs(line) | |
mapping[line[0]] = line[1] | |
self.Coverage = makeCoverage(set(mapping.keys()), font) | |
self.Substitute = [mapping[k] for k in self.Coverage.glyphs] | |
self.GlyphCount = len(self.Substitute) | |
return self | |
def parseLookup(lines, tableTag, font, lookupMap=None): | |
line = lines.expect("lookup") | |
_, name, typ = line | |
log.debug("Parsing lookup type %s %s", typ, name) | |
lookup = ot.Lookup() | |
lookup.LookupFlag, filterset = parseLookupFlags(lines) | |
if filterset is not None: | |
lookup.MarkFilteringSet = filterset | |
lookup.LookupType, parseLookupSubTable = { | |
"GSUB": { | |
"single": (1, parseSingleSubst), | |
"multiple": (2, parseMultiple), | |
"alternate": (3, parseAlternate), | |
"ligature": (4, parseLigature), | |
"context": (5, parseContextSubst), | |
"chained": (6, parseChainedSubst), | |
"reversechained": (8, parseReverseChainedSubst), | |
}, | |
"GPOS": { | |
"single": (1, parseSinglePos), | |
"pair": (2, parsePair), | |
"kernset": (2, parseKernset), | |
"cursive": (3, parseCursive), | |
"mark to base": (4, parseMarkToBase), | |
"mark to ligature": (5, parseMarkToLigature), | |
"mark to mark": (6, parseMarkToMark), | |
"context": (7, parseContextPos), | |
"chained": (8, parseChainedPos), | |
}, | |
}[tableTag][typ] | |
with lines.until("lookup end"): | |
subtables = [] | |
while lines.peek(): | |
with lines.until(("% subtable", "subtable end")): | |
while lines.peek(): | |
subtable = parseLookupSubTable(lines, font, lookupMap) | |
assert lookup.LookupType == subtable.LookupType | |
subtables.append(subtable) | |
if lines.peeks()[0] in ("% subtable", "subtable end"): | |
next(lines) | |
lines.expect("lookup end") | |
lookup.SubTable = subtables | |
lookup.SubTableCount = len(lookup.SubTable) | |
if lookup.SubTableCount == 0: | |
# Remove this return when following is fixed: | |
# https://github.com/fonttools/fonttools/issues/789 | |
return None | |
return lookup | |
def parseGSUBGPOS(lines, font, tableTag): | |
container = ttLib.getTableClass(tableTag)() | |
lookupMap = DeferredMapping() | |
featureMap = DeferredMapping() | |
assert tableTag in ("GSUB", "GPOS") | |
log.debug("Parsing %s", tableTag) | |
self = getattr(ot, tableTag)() | |
self.Version = 0x00010000 | |
fields = { | |
"script table begin": ( | |
"ScriptList", | |
lambda lines: parseScriptList(lines, featureMap), | |
), | |
"feature table begin": ( | |
"FeatureList", | |
lambda lines: parseFeatureList(lines, lookupMap, featureMap), | |
), | |
"lookup": ("LookupList", None), | |
} | |
for attr, parser in fields.values(): | |
setattr(self, attr, None) | |
while lines.peek() is not None: | |
typ = lines.peek()[0].lower() | |
if typ not in fields: | |
log.debug("Skipping %s", lines.peek()) | |
next(lines) | |
continue | |
attr, parser = fields[typ] | |
if typ == "lookup": | |
if self.LookupList is None: | |
self.LookupList = ot.LookupList() | |
self.LookupList.Lookup = [] | |
_, name, _ = lines.peek() | |
lookup = parseLookup(lines, tableTag, font, lookupMap) | |
if lookupMap is not None: | |
assert name not in lookupMap, "Duplicate lookup name: %s" % name | |
lookupMap[name] = len(self.LookupList.Lookup) | |
else: | |
assert int(name) == len(self.LookupList.Lookup), "%d %d" % ( | |
name, | |
len(self.Lookup), | |
) | |
self.LookupList.Lookup.append(lookup) | |
else: | |
assert getattr(self, attr) is None, attr | |
setattr(self, attr, parser(lines)) | |
if self.LookupList: | |
self.LookupList.LookupCount = len(self.LookupList.Lookup) | |
if lookupMap is not None: | |
lookupMap.applyDeferredMappings() | |
if os.environ.get(LOOKUP_DEBUG_ENV_VAR): | |
if "Debg" not in font: | |
font["Debg"] = newTable("Debg") | |
font["Debg"].data = {} | |
debug = ( | |
font["Debg"] | |
.data.setdefault(LOOKUP_DEBUG_INFO_KEY, {}) | |
.setdefault(tableTag, {}) | |
) | |
for name, lookup in lookupMap.items(): | |
debug[str(lookup)] = ["", name, ""] | |
featureMap.applyDeferredMappings() | |
container.table = self | |
return container | |
def parseGSUB(lines, font): | |
return parseGSUBGPOS(lines, font, "GSUB") | |
def parseGPOS(lines, font): | |
return parseGSUBGPOS(lines, font, "GPOS") | |
def parseAttachList(lines, font): | |
points = {} | |
with lines.between("attachment list"): | |
for line in lines: | |
glyph = makeGlyph(line[0]) | |
assert glyph not in points, glyph | |
points[glyph] = [int(i) for i in line[1:]] | |
return otl.buildAttachList(points, font.getReverseGlyphMap()) | |
def parseCaretList(lines, font): | |
carets = {} | |
with lines.between("carets"): | |
for line in lines: | |
glyph = makeGlyph(line[0]) | |
assert glyph not in carets, glyph | |
num = int(line[1]) | |
thisCarets = [int(i) for i in line[2:]] | |
assert num == len(thisCarets), line | |
carets[glyph] = thisCarets | |
return otl.buildLigCaretList(carets, {}, font.getReverseGlyphMap()) | |
def makeMarkFilteringSets(sets, font): | |
self = ot.MarkGlyphSetsDef() | |
self.MarkSetTableFormat = 1 | |
self.MarkSetCount = 1 + max(sets.keys()) | |
self.Coverage = [None] * self.MarkSetCount | |
for k, v in sorted(sets.items()): | |
self.Coverage[k] = makeCoverage(set(v), font) | |
return self | |
def parseMarkFilteringSets(lines, font): | |
sets = {} | |
with lines.between("set definition"): | |
for line in lines: | |
assert len(line) == 2, line | |
glyph = makeGlyph(line[0]) | |
# TODO accept set names | |
st = int(line[1]) | |
if st not in sets: | |
sets[st] = [] | |
sets[st].append(glyph) | |
return makeMarkFilteringSets(sets, font) | |
def parseGDEF(lines, font): | |
container = ttLib.getTableClass("GDEF")() | |
log.debug("Parsing GDEF") | |
self = ot.GDEF() | |
fields = { | |
"class definition begin": ( | |
"GlyphClassDef", | |
lambda lines, font: parseClassDef(lines, font, klass=ot.GlyphClassDef), | |
), | |
"attachment list begin": ("AttachList", parseAttachList), | |
"carets begin": ("LigCaretList", parseCaretList), | |
"mark attachment class definition begin": ( | |
"MarkAttachClassDef", | |
lambda lines, font: parseClassDef(lines, font, klass=ot.MarkAttachClassDef), | |
), | |
"markfilter set definition begin": ("MarkGlyphSetsDef", parseMarkFilteringSets), | |
} | |
for attr, parser in fields.values(): | |
setattr(self, attr, None) | |
while lines.peek() is not None: | |
typ = lines.peek()[0].lower() | |
if typ not in fields: | |
log.debug("Skipping %s", typ) | |
next(lines) | |
continue | |
attr, parser = fields[typ] | |
assert getattr(self, attr) is None, attr | |
setattr(self, attr, parser(lines, font)) | |
self.Version = 0x00010000 if self.MarkGlyphSetsDef is None else 0x00010002 | |
container.table = self | |
return container | |
def parseCmap(lines, font): | |
container = ttLib.getTableClass("cmap")() | |
log.debug("Parsing cmap") | |
tables = [] | |
while lines.peek() is not None: | |
lines.expect("cmap subtable %d" % len(tables)) | |
platId, encId, fmt, lang = [ | |
parseCmapId(lines, field) | |
for field in ("platformID", "encodingID", "format", "language") | |
] | |
table = cmap_classes[fmt](fmt) | |
table.platformID = platId | |
table.platEncID = encId | |
table.language = lang | |
table.cmap = {} | |
line = next(lines) | |
while line[0] != "end subtable": | |
table.cmap[int(line[0], 16)] = line[1] | |
line = next(lines) | |
tables.append(table) | |
container.tableVersion = 0 | |
container.tables = tables | |
return container | |
def parseCmapId(lines, field): | |
line = next(lines) | |
assert field == line[0] | |
return int(line[1]) | |
def parseTable(lines, font, tableTag=None): | |
log.debug("Parsing table") | |
line = lines.peeks() | |
tag = None | |
if line[0].split()[0] == "FontDame": | |
tag = line[0].split()[1] | |
elif " ".join(line[0].split()[:3]) == "Font Chef Table": | |
tag = line[0].split()[3] | |
if tag is not None: | |
next(lines) | |
tag = tag.ljust(4) | |
if tableTag is None: | |
tableTag = tag | |
else: | |
assert tableTag == tag, (tableTag, tag) | |
assert ( | |
tableTag is not None | |
), "Don't know what table to parse and data doesn't specify" | |
return { | |
"GSUB": parseGSUB, | |
"GPOS": parseGPOS, | |
"GDEF": parseGDEF, | |
"cmap": parseCmap, | |
}[tableTag](lines, font) | |
class Tokenizer(object): | |
def __init__(self, f): | |
# TODO BytesIO / StringIO as needed? also, figure out whether we work on bytes or unicode | |
lines = iter(f) | |
try: | |
self.filename = f.name | |
except: | |
self.filename = None | |
self.lines = iter(lines) | |
self.line = "" | |
self.lineno = 0 | |
self.stoppers = [] | |
self.buffer = None | |
def __iter__(self): | |
return self | |
def _next_line(self): | |
self.lineno += 1 | |
line = self.line = next(self.lines) | |
line = [s.strip() for s in line.split("\t")] | |
if len(line) == 1 and not line[0]: | |
del line[0] | |
if line and not line[-1]: | |
log.warning("trailing tab found on line %d: %s" % (self.lineno, self.line)) | |
while line and not line[-1]: | |
del line[-1] | |
return line | |
def _next_nonempty(self): | |
while True: | |
line = self._next_line() | |
# Skip comments and empty lines | |
if line and line[0] and (line[0][0] != "%" or line[0] == "% subtable"): | |
return line | |
def _next_buffered(self): | |
if self.buffer: | |
ret = self.buffer | |
self.buffer = None | |
return ret | |
else: | |
return self._next_nonempty() | |
def __next__(self): | |
line = self._next_buffered() | |
if line[0].lower() in self.stoppers: | |
self.buffer = line | |
raise StopIteration | |
return line | |
def next(self): | |
return self.__next__() | |
def peek(self): | |
if not self.buffer: | |
try: | |
self.buffer = self._next_nonempty() | |
except StopIteration: | |
return None | |
if self.buffer[0].lower() in self.stoppers: | |
return None | |
return self.buffer | |
def peeks(self): | |
ret = self.peek() | |
return ret if ret is not None else ("",) | |
def between(self, tag): | |
start = tag + " begin" | |
end = tag + " end" | |
self.expectendswith(start) | |
self.stoppers.append(end) | |
yield | |
del self.stoppers[-1] | |
self.expect(tag + " end") | |
def until(self, tags): | |
if type(tags) is not tuple: | |
tags = (tags,) | |
self.stoppers.extend(tags) | |
yield | |
del self.stoppers[-len(tags) :] | |
def expect(self, s): | |
line = next(self) | |
tag = line[0].lower() | |
assert tag == s, "Expected '%s', got '%s'" % (s, tag) | |
return line | |
def expectendswith(self, s): | |
line = next(self) | |
tag = line[0].lower() | |
assert tag.endswith(s), "Expected '*%s', got '%s'" % (s, tag) | |
return line | |
def build(f, font, tableTag=None): | |
"""Convert a Monotype font layout file to an OpenType layout object | |
A font object must be passed, but this may be a "dummy" font; it is only | |
used for sorting glyph sets when making coverage tables and to hold the | |
OpenType layout table while it is being built. | |
Args: | |
f: A file object. | |
font (TTFont): A font object. | |
tableTag (string): If provided, asserts that the file contains data for the | |
given OpenType table. | |
Returns: | |
An object representing the table. (e.g. ``table_G_S_U_B_``) | |
""" | |
lines = Tokenizer(f) | |
return parseTable(lines, font, tableTag=tableTag) | |
def main(args=None, font=None): | |
"""Convert a FontDame OTL file to TTX XML | |
Writes XML output to stdout. | |
Args: | |
args: Command line arguments (``--font``, ``--table``, input files). | |
""" | |
import sys | |
from fontTools import configLogger | |
from fontTools.misc.testTools import MockFont | |
if args is None: | |
args = sys.argv[1:] | |
# configure the library logger (for >= WARNING) | |
configLogger() | |
# comment this out to enable debug messages from mtiLib's logger | |
# log.setLevel(logging.DEBUG) | |
import argparse | |
parser = argparse.ArgumentParser( | |
"fonttools mtiLib", | |
description=main.__doc__, | |
) | |
parser.add_argument( | |
"--font", | |
"-f", | |
metavar="FILE", | |
dest="font", | |
help="Input TTF files (used for glyph classes and sorting coverage tables)", | |
) | |
parser.add_argument( | |
"--table", | |
"-t", | |
metavar="TABLE", | |
dest="tableTag", | |
help="Table to fill (sniffed from input file if not provided)", | |
) | |
parser.add_argument( | |
"inputs", metavar="FILE", type=str, nargs="+", help="Input FontDame .txt files" | |
) | |
args = parser.parse_args(args) | |
if font is None: | |
if args.font: | |
font = ttLib.TTFont(args.font) | |
else: | |
font = MockFont() | |
for f in args.inputs: | |
log.debug("Processing %s", f) | |
with open(f, "rt", encoding="utf-8") as f: | |
table = build(f, font, tableTag=args.tableTag) | |
blob = table.compile(font) # Make sure it compiles | |
decompiled = table.__class__() | |
decompiled.decompile(blob, font) # Make sure it decompiles! | |
# continue | |
from fontTools.misc import xmlWriter | |
tag = table.tableTag | |
writer = xmlWriter.XMLWriter(sys.stdout) | |
writer.begintag(tag) | |
writer.newline() | |
# table.toXML(writer, font) | |
decompiled.toXML(writer, font) | |
writer.endtag(tag) | |
writer.newline() | |
if __name__ == "__main__": | |
import sys | |
sys.exit(main()) | |