#!/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 operator import setitem 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 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 featureMap is not None: 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 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())