#!/usr/bin/python # FontDame-to-FontTools for OpenType Layout tables # # Source language spec is available at: # https://rawgit.com/Monotype/OpenType_Table_Source/master/otl_source.html # https://github.com/Monotype/OpenType_Table_Source/ from __future__ import print_function, division, absolute_import from fontTools import ttLib 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 class MtiLibError(Exception): pass class ReferenceNotFoundError(MtiLibError): pass class FeatureNotFoundError(ReferenceNotFoundError): pass class LookupNotFoundError(ReferenceNotFoundError): pass def debug(*args): #print(*args) pass def makeGlyph(s): if s[:2] == '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 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): debug("Adding deferred mapping for symbol '%s'" % sym, type(e).__name__) self._deferredMappings.append((setter,sym, e)) def applyDeferredMappings(self): for setter,sym,e in self._deferredMappings: debug("Applying deferred mapping for symbol '%s'" % sym, type(e).__name__) try: mapped = self[sym] except KeyError: raise e setter(mapped) 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: scriptTag, langSysTag, defaultFeature, features = line debug("Adding script", scriptTag, "language-system", 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.peek()[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.buildSingleSubst(mapping) def parseMultiple(lines, font, _lookupMap=None): mapping = {} for line in lines: line = makeGlyphs(line) mapping[line[0]] = line[1:] return otl.buildMultipleSubst(mapping) def parseAlternate(lines, font, _lookupMap=None): mapping = {} for line in lines: line = makeGlyphs(line) mapping[line[0]] = line[1:] return otl.buildAlternateSubst(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.buildLigatureSubst(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(self, lines, font, _lookupMap=None): self.ValueFormat1 = self.ValueFormat2 = 0 typ = lines.peek()[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(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,pair.Value2 = values2 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.peek()[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, 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) self.Coverage = makeCoverage(self.ClassDef1.classDefs.keys(), font) else: assert 0, typ def parseKernset(self, lines, font, _lookupMap=None): typ = lines.peek()[0].split()[0].lower() if typ in ('left', 'right'): with lines.until(("firstclass definition begin", "secondclass definition begin")): return parsePair(self, lines, font) return parsePair(self, 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.buildCursivePos(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(self, lines, font, c): self.Format = 1 markData = {} baseData = {} Data = { 'mark': (markData, c.MarkAnchorClass), 'base': (baseData, c.BaseAnchorClass), 'ligature': (baseData, c.BaseAnchorClass), } 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 # Mark markCoverage = makeCoverage(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) # Base self.classCount = 0 if not baseData else 1+max(k[1] for k,v in baseData.items()) baseCoverage = makeCoverage([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) 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' class MarkToMarkHelper(MarkHelper): Mark = 'Mark1' Base = 'Mark2' class MarkToLigatureHelper(MarkHelper): Mark = 'Mark' Base = 'Ligature' def parseMarkToBase(self, lines, font, _lookupMap=None): return parseMarkToSomething(self, lines, font, MarkToBaseHelper()) def parseMarkToMark(self, lines, font, _lookupMap=None): return parseMarkToSomething(self, lines, font, MarkToMarkHelper()) def parseMarkToLigature(self, lines, font, _lookupMap=None): return parseMarkToSomething(self, 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, klass=ot.Coverage): if not classDefs: return None self = klass() self.classDefs = dict(classDefs) return self def parseClassDef(lines, 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, klass) def makeCoverage(glyphs, font, klass=ot.Coverage): if not glyphs: return None 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(self, lines, font, Type, lookupMap=None): typ = lines.peek()[0].split()[0].lower() if typ == 'glyph': self.Format = 1 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] 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 debug("Parsing %s format %s" % (Type, self.Format)) c = ContextHelper(Type, self.Format) classDefs = [None] * c.DataLen while lines.peek()[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, klass=klass) c.SetContextData(self, classDefs) rules = [] for line in lines: assert line[0].lower().startswith('class'), line[0] 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 debug("Parsing %s format %s" % (Type, self.Format)) c = ContextHelper(Type, self.Format) coverages = tuple([] for i in range(c.DataLen)) while lines.peek()[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 def parseContextSubst(self, lines, font, lookupMap=None): return parseContext(self, lines, font, "ContextSubst", lookupMap=lookupMap) def parseContextPos(self, lines, font, lookupMap=None): return parseContext(self, lines, font, "ContextPos", lookupMap=lookupMap) def parseChainedSubst(self, lines, font, lookupMap=None): return parseContext(self, lines, font, "ChainContextSubst", lookupMap=lookupMap) def parseChainedPos(self, lines, font, lookupMap=None): return parseContext(self, lines, font, "ChainContextPos", lookupMap=lookupMap) def parseReverseChainedSubst(self, lines, font, _lookupMap=None): self.Format = 1 coverages = ([], []) while lines.peek()[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(mapping.keys(), font) self.Substitute = [mapping[k] for k in self.Coverage.glyphs] self.GlyphCount = len(self.Substitute) def parseLookup(lines, tableTag, font, lookupMap=None): line = lines.expect('lookup') _, name, typ = line debug("Parsing lookup type %s %s" % (typ, name)) lookup = ot.Lookup() with lines.until('lookup end'): lookup.LookupFlag,filterset = parseLookupFlags(lines) if filterset is not None: lookup.MarkFilteringSet = filterset lookup.LookupType, parseLookupSubTable = { 'GSUB': { 'single': (0, parseSingleSubst), 'multiple': (0, parseMultiple), 'alternate': (0, parseAlternate), 'ligature': (0, parseLigature), 'context': (5, parseContextSubst), 'chained': (6, parseChainedSubst), 'reversechained':(8, parseReverseChainedSubst), }, 'GPOS': { 'single': (0, parseSinglePos), 'pair': (2, parsePair), 'kernset': (2, parseKernset), 'cursive': (0, parseCursive), 'mark to base': (4, parseMarkToBase), 'mark to ligature':(5, parseMarkToLigature), 'mark to mark': (6, parseMarkToMark), 'context': (7, parseContextPos), 'chained': (8, parseChainedPos), }, }[tableTag][typ] subtables = [] while lines.peek(): with lines.until(('% subtable', 'subtable end')): while lines.peek(): if lookup.LookupType is 0: subtable = parseLookupSubTable(lines, font, lookupMap) lookup.LookupType = subtable.LookupType else: subtable = ot.lookupTypes[tableTag][lookup.LookupType]() parseLookupSubTable(subtable, lines, font, lookupMap) subtables.append(subtable) if lines.peek() and lines.peek()[0] in ('% subtable', 'subtable end'): next(lines) lines.expect('lookup end') lookup.SubTable = subtables lookup.SubTableCount = len(lookup.SubTable) if lookup.SubTableCount is 0: return None return lookup def parseGSUBGPOS(lines, font, tableTag): lookupMap = DeferredMapping() featureMap = DeferredMapping() assert tableTag in ('GSUB', 'GPOS') debug("Parsing", tableTag) self = getattr(ot, tableTag)() self.Version = 1.0 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: debug ('Skipping', 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() return self def parseGSUB(lines, font): return parseGSUBGPOS(lines, font, 'GSUB') def parseGPOS(lines, font): return parseGSUBGPOS(lines, font, 'GPOS') def makeAttachList(points, font): self = ot.AttachList() self.Coverage = makeCoverage(points.keys(), font) records = [] for glyph in self.Coverage.glyphs: record = ot.AttachPoint() record.PointIndex = sorted(set(points[glyph])) record.PointCount = len(record.PointIndex) records.append(record) self.AttachPoint = records self.GlyphCount = len(records) return self 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 makeAttachList(points, font) 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 sets.items(): self.Coverage[k] = makeCoverage(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): debug("Parsing GDEF") self = ot.GDEF() fields = { 'class definition begin': ('GlyphClassDef', lambda lines, font: parseClassDef(lines, klass=ot.GlyphClassDef)), 'attachment list begin': ('AttachList', parseAttachList), 'carets begin': ('LigCaretList', parseCaretList), 'mark attachment class definition begin': ('MarkAttachClassDef', lambda lines, font: parseClassDef(lines, 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: debug ('Skipping', line) next(lines) continue attr,parser = fields[typ] assert getattr(self, attr) is None, attr setattr(self, attr, parser(lines, font)) self.Version = 1.0 if self.MarkGlyphSetsDef is None else 0x00010002 return self def parseTable(lines, font, tableTag=None): debug("Parsing table") line = lines.peek() if line[0].split()[0] == 'FontDame': next(lines) tag = line[0].split()[1].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" container = ttLib.getTableClass(tableTag)() table = {'GSUB': parseGSUB, 'GPOS': parseGPOS, 'GDEF': parseGDEF, }[tableTag](lines, font) container.table = table return container 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) lines = ([s.strip() for s in line.split('\t')] for line in lines) try: self.filename = f.name except: self.filename = None self.lines = lines self.lineno = 0 self.stoppers = [] self.buffer = None def __iter__(self): return self def _next_line(self): self.lineno += 1 return next(self.lines) def _next_nonempty(self): while True: line = self._next_line() # Skip comments and empty lines if 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 @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): lines = Tokenizer(f) return parseTable(lines, font, tableTag=tableTag) class MockFont(object): def __init__(self): self._glyphOrder = ['.notdef'] class AllocatingDict(dict): def __missing__(reverseDict, key): self._glyphOrder.append(key) gid = len(reverseDict) reverseDict[key] = gid return gid self._reverseGlyphOrder = AllocatingDict({'.notdef': 0}) self.lazy = False def getGlyphID(self, glyph, requireReal=None): gid = self._reverseGlyphOrder[glyph] return gid def getReverseGlyphMap(self): return self._reverseGlyphOrder def getGlyphName(self, gid): return self._glyphOrder[gid] def getGlyphOrder(self): return self._glyphOrder def main(args): font = MockFont() tableTag = None if args[0].startswith('-t'): tableTag = args[0][2:] del args[0] for f in args: debug("Processing", f) table = build(open(f, 'rt'), font, tableTag=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 main (sys.argv[1:])