Source code for fontTools.mtiLib

#!/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


[docs] class MtiLibError(Exception): pass
[docs] class ReferenceNotFoundError(MtiLibError): pass
[docs] class FeatureNotFoundError(ReferenceNotFoundError): pass
[docs] 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)
[docs] 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 ("",) @contextmanager 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") @contextmanager 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
[docs] 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)
[docs] 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())