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