xref: /aosp_15_r20/external/fonttools/Lib/fontTools/mtiLib/__init__.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1#!/usr/bin/python
2
3# FontDame-to-FontTools for OpenType Layout tables
4#
5# Source language spec is available at:
6# http://monotype.github.io/OpenType_Table_Source/otl_source.html
7# https://github.com/Monotype/OpenType_Table_Source/
8
9from fontTools import ttLib
10from fontTools.ttLib.tables._c_m_a_p import cmap_classes
11from fontTools.ttLib.tables import otTables as ot
12from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict
13from fontTools.otlLib import builder as otl
14from contextlib import contextmanager
15from fontTools.ttLib import newTable
16from fontTools.feaLib.lookupDebugInfo import LOOKUP_DEBUG_ENV_VAR, LOOKUP_DEBUG_INFO_KEY
17from operator import setitem
18import os
19import logging
20
21
22class MtiLibError(Exception):
23    pass
24
25
26class ReferenceNotFoundError(MtiLibError):
27    pass
28
29
30class FeatureNotFoundError(ReferenceNotFoundError):
31    pass
32
33
34class LookupNotFoundError(ReferenceNotFoundError):
35    pass
36
37
38log = logging.getLogger("fontTools.mtiLib")
39
40
41def makeGlyph(s):
42    if s[:2] in ["U ", "u "]:
43        return ttLib.TTFont._makeGlyphName(int(s[2:], 16))
44    elif s[:2] == "# ":
45        return "glyph%.5d" % int(s[2:])
46    assert s.find(" ") < 0, "Space found in glyph name: %s" % s
47    assert s, "Glyph name is empty"
48    return s
49
50
51def makeGlyphs(l):
52    return [makeGlyph(g) for g in l]
53
54
55def mapLookup(sym, mapping):
56    # Lookups are addressed by name.  So resolved them using a map if available.
57    # Fallback to parsing as lookup index if a map isn't provided.
58    if mapping is not None:
59        try:
60            idx = mapping[sym]
61        except KeyError:
62            raise LookupNotFoundError(sym)
63    else:
64        idx = int(sym)
65    return idx
66
67
68def mapFeature(sym, mapping):
69    # Features are referenced by index according the spec.  So, if symbol is an
70    # integer, use it directly.  Otherwise look up in the map if provided.
71    try:
72        idx = int(sym)
73    except ValueError:
74        try:
75            idx = mapping[sym]
76        except KeyError:
77            raise FeatureNotFoundError(sym)
78    return idx
79
80
81def setReference(mapper, mapping, sym, setter, collection, key):
82    try:
83        mapped = mapper(sym, mapping)
84    except ReferenceNotFoundError as e:
85        try:
86            if mapping is not None:
87                mapping.addDeferredMapping(
88                    lambda ref: setter(collection, key, ref), sym, e
89                )
90                return
91        except AttributeError:
92            pass
93        raise
94    setter(collection, key, mapped)
95
96
97class DeferredMapping(dict):
98    def __init__(self):
99        self._deferredMappings = []
100
101    def addDeferredMapping(self, setter, sym, e):
102        log.debug("Adding deferred mapping for symbol '%s' %s", sym, type(e).__name__)
103        self._deferredMappings.append((setter, sym, e))
104
105    def applyDeferredMappings(self):
106        for setter, sym, e in self._deferredMappings:
107            log.debug(
108                "Applying deferred mapping for symbol '%s' %s", sym, type(e).__name__
109            )
110            try:
111                mapped = self[sym]
112            except KeyError:
113                raise e
114            setter(mapped)
115            log.debug("Set to %s", mapped)
116        self._deferredMappings = []
117
118
119def parseScriptList(lines, featureMap=None):
120    self = ot.ScriptList()
121    records = []
122    with lines.between("script table"):
123        for line in lines:
124            while len(line) < 4:
125                line.append("")
126            scriptTag, langSysTag, defaultFeature, features = line
127            log.debug("Adding script %s language-system %s", scriptTag, langSysTag)
128
129            langSys = ot.LangSys()
130            langSys.LookupOrder = None
131            if defaultFeature:
132                setReference(
133                    mapFeature,
134                    featureMap,
135                    defaultFeature,
136                    setattr,
137                    langSys,
138                    "ReqFeatureIndex",
139                )
140            else:
141                langSys.ReqFeatureIndex = 0xFFFF
142            syms = stripSplitComma(features)
143            langSys.FeatureIndex = theList = [3] * len(syms)
144            for i, sym in enumerate(syms):
145                setReference(mapFeature, featureMap, sym, setitem, theList, i)
146            langSys.FeatureCount = len(langSys.FeatureIndex)
147
148            script = [s for s in records if s.ScriptTag == scriptTag]
149            if script:
150                script = script[0].Script
151            else:
152                scriptRec = ot.ScriptRecord()
153                scriptRec.ScriptTag = scriptTag + " " * (4 - len(scriptTag))
154                scriptRec.Script = ot.Script()
155                records.append(scriptRec)
156                script = scriptRec.Script
157                script.DefaultLangSys = None
158                script.LangSysRecord = []
159                script.LangSysCount = 0
160
161            if langSysTag == "default":
162                script.DefaultLangSys = langSys
163            else:
164                langSysRec = ot.LangSysRecord()
165                langSysRec.LangSysTag = langSysTag + " " * (4 - len(langSysTag))
166                langSysRec.LangSys = langSys
167                script.LangSysRecord.append(langSysRec)
168                script.LangSysCount = len(script.LangSysRecord)
169
170    for script in records:
171        script.Script.LangSysRecord = sorted(
172            script.Script.LangSysRecord, key=lambda rec: rec.LangSysTag
173        )
174    self.ScriptRecord = sorted(records, key=lambda rec: rec.ScriptTag)
175    self.ScriptCount = len(self.ScriptRecord)
176    return self
177
178
179def parseFeatureList(lines, lookupMap=None, featureMap=None):
180    self = ot.FeatureList()
181    self.FeatureRecord = []
182    with lines.between("feature table"):
183        for line in lines:
184            name, featureTag, lookups = line
185            if featureMap is not None:
186                assert name not in featureMap, "Duplicate feature name: %s" % name
187                featureMap[name] = len(self.FeatureRecord)
188            # If feature name is integer, make sure it matches its index.
189            try:
190                assert int(name) == len(self.FeatureRecord), "%d %d" % (
191                    name,
192                    len(self.FeatureRecord),
193                )
194            except ValueError:
195                pass
196            featureRec = ot.FeatureRecord()
197            featureRec.FeatureTag = featureTag
198            featureRec.Feature = ot.Feature()
199            self.FeatureRecord.append(featureRec)
200            feature = featureRec.Feature
201            feature.FeatureParams = None
202            syms = stripSplitComma(lookups)
203            feature.LookupListIndex = theList = [None] * len(syms)
204            for i, sym in enumerate(syms):
205                setReference(mapLookup, lookupMap, sym, setitem, theList, i)
206            feature.LookupCount = len(feature.LookupListIndex)
207
208    self.FeatureCount = len(self.FeatureRecord)
209    return self
210
211
212def parseLookupFlags(lines):
213    flags = 0
214    filterset = None
215    allFlags = [
216        "righttoleft",
217        "ignorebaseglyphs",
218        "ignoreligatures",
219        "ignoremarks",
220        "markattachmenttype",
221        "markfiltertype",
222    ]
223    while lines.peeks()[0].lower() in allFlags:
224        line = next(lines)
225        flag = {
226            "righttoleft": 0x0001,
227            "ignorebaseglyphs": 0x0002,
228            "ignoreligatures": 0x0004,
229            "ignoremarks": 0x0008,
230        }.get(line[0].lower())
231        if flag:
232            assert line[1].lower() in ["yes", "no"], line[1]
233            if line[1].lower() == "yes":
234                flags |= flag
235            continue
236        if line[0].lower() == "markattachmenttype":
237            flags |= int(line[1]) << 8
238            continue
239        if line[0].lower() == "markfiltertype":
240            flags |= 0x10
241            filterset = int(line[1])
242    return flags, filterset
243
244
245def parseSingleSubst(lines, font, _lookupMap=None):
246    mapping = {}
247    for line in lines:
248        assert len(line) == 2, line
249        line = makeGlyphs(line)
250        mapping[line[0]] = line[1]
251    return otl.buildSingleSubstSubtable(mapping)
252
253
254def parseMultiple(lines, font, _lookupMap=None):
255    mapping = {}
256    for line in lines:
257        line = makeGlyphs(line)
258        mapping[line[0]] = line[1:]
259    return otl.buildMultipleSubstSubtable(mapping)
260
261
262def parseAlternate(lines, font, _lookupMap=None):
263    mapping = {}
264    for line in lines:
265        line = makeGlyphs(line)
266        mapping[line[0]] = line[1:]
267    return otl.buildAlternateSubstSubtable(mapping)
268
269
270def parseLigature(lines, font, _lookupMap=None):
271    mapping = {}
272    for line in lines:
273        assert len(line) >= 2, line
274        line = makeGlyphs(line)
275        mapping[tuple(line[1:])] = line[0]
276    return otl.buildLigatureSubstSubtable(mapping)
277
278
279def parseSinglePos(lines, font, _lookupMap=None):
280    values = {}
281    for line in lines:
282        assert len(line) == 3, line
283        w = line[0].title().replace(" ", "")
284        assert w in valueRecordFormatDict
285        g = makeGlyph(line[1])
286        v = int(line[2])
287        if g not in values:
288            values[g] = ValueRecord()
289        assert not hasattr(values[g], w), (g, w)
290        setattr(values[g], w, v)
291    return otl.buildSinglePosSubtable(values, font.getReverseGlyphMap())
292
293
294def parsePair(lines, font, _lookupMap=None):
295    self = ot.PairPos()
296    self.ValueFormat1 = self.ValueFormat2 = 0
297    typ = lines.peeks()[0].split()[0].lower()
298    if typ in ("left", "right"):
299        self.Format = 1
300        values = {}
301        for line in lines:
302            assert len(line) == 4, line
303            side = line[0].split()[0].lower()
304            assert side in ("left", "right"), side
305            what = line[0][len(side) :].title().replace(" ", "")
306            mask = valueRecordFormatDict[what][0]
307            glyph1, glyph2 = makeGlyphs(line[1:3])
308            value = int(line[3])
309            if not glyph1 in values:
310                values[glyph1] = {}
311            if not glyph2 in values[glyph1]:
312                values[glyph1][glyph2] = (ValueRecord(), ValueRecord())
313            rec2 = values[glyph1][glyph2]
314            if side == "left":
315                self.ValueFormat1 |= mask
316                vr = rec2[0]
317            else:
318                self.ValueFormat2 |= mask
319                vr = rec2[1]
320            assert not hasattr(vr, what), (vr, what)
321            setattr(vr, what, value)
322        self.Coverage = makeCoverage(set(values.keys()), font)
323        self.PairSet = []
324        for glyph1 in self.Coverage.glyphs:
325            values1 = values[glyph1]
326            pairset = ot.PairSet()
327            records = pairset.PairValueRecord = []
328            for glyph2 in sorted(values1.keys(), key=font.getGlyphID):
329                values2 = values1[glyph2]
330                pair = ot.PairValueRecord()
331                pair.SecondGlyph = glyph2
332                pair.Value1 = values2[0]
333                pair.Value2 = values2[1] if self.ValueFormat2 else None
334                records.append(pair)
335            pairset.PairValueCount = len(pairset.PairValueRecord)
336            self.PairSet.append(pairset)
337        self.PairSetCount = len(self.PairSet)
338    elif typ.endswith("class"):
339        self.Format = 2
340        classDefs = [None, None]
341        while lines.peeks()[0].endswith("class definition begin"):
342            typ = lines.peek()[0][: -len("class definition begin")].lower()
343            idx, klass = {
344                "first": (0, ot.ClassDef1),
345                "second": (1, ot.ClassDef2),
346            }[typ]
347            assert classDefs[idx] is None
348            classDefs[idx] = parseClassDef(lines, font, klass=klass)
349        self.ClassDef1, self.ClassDef2 = classDefs
350        self.Class1Count, self.Class2Count = (
351            1 + max(c.classDefs.values()) for c in classDefs
352        )
353        self.Class1Record = [ot.Class1Record() for i in range(self.Class1Count)]
354        for rec1 in self.Class1Record:
355            rec1.Class2Record = [ot.Class2Record() for j in range(self.Class2Count)]
356            for rec2 in rec1.Class2Record:
357                rec2.Value1 = ValueRecord()
358                rec2.Value2 = ValueRecord()
359        for line in lines:
360            assert len(line) == 4, line
361            side = line[0].split()[0].lower()
362            assert side in ("left", "right"), side
363            what = line[0][len(side) :].title().replace(" ", "")
364            mask = valueRecordFormatDict[what][0]
365            class1, class2, value = (int(x) for x in line[1:4])
366            rec2 = self.Class1Record[class1].Class2Record[class2]
367            if side == "left":
368                self.ValueFormat1 |= mask
369                vr = rec2.Value1
370            else:
371                self.ValueFormat2 |= mask
372                vr = rec2.Value2
373            assert not hasattr(vr, what), (vr, what)
374            setattr(vr, what, value)
375        for rec1 in self.Class1Record:
376            for rec2 in rec1.Class2Record:
377                rec2.Value1 = ValueRecord(self.ValueFormat1, rec2.Value1)
378                rec2.Value2 = (
379                    ValueRecord(self.ValueFormat2, rec2.Value2)
380                    if self.ValueFormat2
381                    else None
382                )
383
384        self.Coverage = makeCoverage(set(self.ClassDef1.classDefs.keys()), font)
385    else:
386        assert 0, typ
387    return self
388
389
390def parseKernset(lines, font, _lookupMap=None):
391    typ = lines.peeks()[0].split()[0].lower()
392    if typ in ("left", "right"):
393        with lines.until(
394            ("firstclass definition begin", "secondclass definition begin")
395        ):
396            return parsePair(lines, font)
397    return parsePair(lines, font)
398
399
400def makeAnchor(data, klass=ot.Anchor):
401    assert len(data) <= 2
402    anchor = klass()
403    anchor.Format = 1
404    anchor.XCoordinate, anchor.YCoordinate = intSplitComma(data[0])
405    if len(data) > 1 and data[1] != "":
406        anchor.Format = 2
407        anchor.AnchorPoint = int(data[1])
408    return anchor
409
410
411def parseCursive(lines, font, _lookupMap=None):
412    records = {}
413    for line in lines:
414        assert len(line) in [3, 4], line
415        idx, klass = {
416            "entry": (0, ot.EntryAnchor),
417            "exit": (1, ot.ExitAnchor),
418        }[line[0]]
419        glyph = makeGlyph(line[1])
420        if glyph not in records:
421            records[glyph] = [None, None]
422        assert records[glyph][idx] is None, (glyph, idx)
423        records[glyph][idx] = makeAnchor(line[2:], klass)
424    return otl.buildCursivePosSubtable(records, font.getReverseGlyphMap())
425
426
427def makeMarkRecords(data, coverage, c):
428    records = []
429    for glyph in coverage.glyphs:
430        klass, anchor = data[glyph]
431        record = c.MarkRecordClass()
432        record.Class = klass
433        setattr(record, c.MarkAnchor, anchor)
434        records.append(record)
435    return records
436
437
438def makeBaseRecords(data, coverage, c, classCount):
439    records = []
440    idx = {}
441    for glyph in coverage.glyphs:
442        idx[glyph] = len(records)
443        record = c.BaseRecordClass()
444        anchors = [None] * classCount
445        setattr(record, c.BaseAnchor, anchors)
446        records.append(record)
447    for (glyph, klass), anchor in data.items():
448        record = records[idx[glyph]]
449        anchors = getattr(record, c.BaseAnchor)
450        assert anchors[klass] is None, (glyph, klass)
451        anchors[klass] = anchor
452    return records
453
454
455def makeLigatureRecords(data, coverage, c, classCount):
456    records = [None] * len(coverage.glyphs)
457    idx = {g: i for i, g in enumerate(coverage.glyphs)}
458
459    for (glyph, klass, compIdx, compCount), anchor in data.items():
460        record = records[idx[glyph]]
461        if record is None:
462            record = records[idx[glyph]] = ot.LigatureAttach()
463            record.ComponentCount = compCount
464            record.ComponentRecord = [ot.ComponentRecord() for i in range(compCount)]
465            for compRec in record.ComponentRecord:
466                compRec.LigatureAnchor = [None] * classCount
467        assert record.ComponentCount == compCount, (
468            glyph,
469            record.ComponentCount,
470            compCount,
471        )
472
473        anchors = record.ComponentRecord[compIdx - 1].LigatureAnchor
474        assert anchors[klass] is None, (glyph, compIdx, klass)
475        anchors[klass] = anchor
476    return records
477
478
479def parseMarkToSomething(lines, font, c):
480    self = c.Type()
481    self.Format = 1
482    markData = {}
483    baseData = {}
484    Data = {
485        "mark": (markData, c.MarkAnchorClass),
486        "base": (baseData, c.BaseAnchorClass),
487        "ligature": (baseData, c.BaseAnchorClass),
488    }
489    maxKlass = 0
490    for line in lines:
491        typ = line[0]
492        assert typ in ("mark", "base", "ligature")
493        glyph = makeGlyph(line[1])
494        data, anchorClass = Data[typ]
495        extraItems = 2 if typ == "ligature" else 0
496        extras = tuple(int(i) for i in line[2 : 2 + extraItems])
497        klass = int(line[2 + extraItems])
498        anchor = makeAnchor(line[3 + extraItems :], anchorClass)
499        if typ == "mark":
500            key, value = glyph, (klass, anchor)
501        else:
502            key, value = ((glyph, klass) + extras), anchor
503        assert key not in data, key
504        data[key] = value
505        maxKlass = max(maxKlass, klass)
506
507    # Mark
508    markCoverage = makeCoverage(set(markData.keys()), font, c.MarkCoverageClass)
509    markArray = c.MarkArrayClass()
510    markRecords = makeMarkRecords(markData, markCoverage, c)
511    setattr(markArray, c.MarkRecord, markRecords)
512    setattr(markArray, c.MarkCount, len(markRecords))
513    setattr(self, c.MarkCoverage, markCoverage)
514    setattr(self, c.MarkArray, markArray)
515    self.ClassCount = maxKlass + 1
516
517    # Base
518    self.classCount = 0 if not baseData else 1 + max(k[1] for k, v in baseData.items())
519    baseCoverage = makeCoverage(
520        set([k[0] for k in baseData.keys()]), font, c.BaseCoverageClass
521    )
522    baseArray = c.BaseArrayClass()
523    if c.Base == "Ligature":
524        baseRecords = makeLigatureRecords(baseData, baseCoverage, c, self.classCount)
525    else:
526        baseRecords = makeBaseRecords(baseData, baseCoverage, c, self.classCount)
527    setattr(baseArray, c.BaseRecord, baseRecords)
528    setattr(baseArray, c.BaseCount, len(baseRecords))
529    setattr(self, c.BaseCoverage, baseCoverage)
530    setattr(self, c.BaseArray, baseArray)
531
532    return self
533
534
535class MarkHelper(object):
536    def __init__(self):
537        for Which in ("Mark", "Base"):
538            for What in ("Coverage", "Array", "Count", "Record", "Anchor"):
539                key = Which + What
540                if Which == "Mark" and What in ("Count", "Record", "Anchor"):
541                    value = key
542                else:
543                    value = getattr(self, Which) + What
544                if value == "LigatureRecord":
545                    value = "LigatureAttach"
546                setattr(self, key, value)
547                if What != "Count":
548                    klass = getattr(ot, value)
549                    setattr(self, key + "Class", klass)
550
551
552class MarkToBaseHelper(MarkHelper):
553    Mark = "Mark"
554    Base = "Base"
555    Type = ot.MarkBasePos
556
557
558class MarkToMarkHelper(MarkHelper):
559    Mark = "Mark1"
560    Base = "Mark2"
561    Type = ot.MarkMarkPos
562
563
564class MarkToLigatureHelper(MarkHelper):
565    Mark = "Mark"
566    Base = "Ligature"
567    Type = ot.MarkLigPos
568
569
570def parseMarkToBase(lines, font, _lookupMap=None):
571    return parseMarkToSomething(lines, font, MarkToBaseHelper())
572
573
574def parseMarkToMark(lines, font, _lookupMap=None):
575    return parseMarkToSomething(lines, font, MarkToMarkHelper())
576
577
578def parseMarkToLigature(lines, font, _lookupMap=None):
579    return parseMarkToSomething(lines, font, MarkToLigatureHelper())
580
581
582def stripSplitComma(line):
583    return [s.strip() for s in line.split(",")] if line else []
584
585
586def intSplitComma(line):
587    return [int(i) for i in line.split(",")] if line else []
588
589
590# Copied from fontTools.subset
591class ContextHelper(object):
592    def __init__(self, klassName, Format):
593        if klassName.endswith("Subst"):
594            Typ = "Sub"
595            Type = "Subst"
596        else:
597            Typ = "Pos"
598            Type = "Pos"
599        if klassName.startswith("Chain"):
600            Chain = "Chain"
601            InputIdx = 1
602            DataLen = 3
603        else:
604            Chain = ""
605            InputIdx = 0
606            DataLen = 1
607        ChainTyp = Chain + Typ
608
609        self.Typ = Typ
610        self.Type = Type
611        self.Chain = Chain
612        self.ChainTyp = ChainTyp
613        self.InputIdx = InputIdx
614        self.DataLen = DataLen
615
616        self.LookupRecord = Type + "LookupRecord"
617
618        if Format == 1:
619            Coverage = lambda r: r.Coverage
620            ChainCoverage = lambda r: r.Coverage
621            ContextData = lambda r: (None,)
622            ChainContextData = lambda r: (None, None, None)
623            SetContextData = None
624            SetChainContextData = None
625            RuleData = lambda r: (r.Input,)
626            ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
627
628            def SetRuleData(r, d):
629                (r.Input,) = d
630                (r.GlyphCount,) = (len(x) + 1 for x in d)
631
632            def ChainSetRuleData(r, d):
633                (r.Backtrack, r.Input, r.LookAhead) = d
634                (
635                    r.BacktrackGlyphCount,
636                    r.InputGlyphCount,
637                    r.LookAheadGlyphCount,
638                ) = (len(d[0]), len(d[1]) + 1, len(d[2]))
639
640        elif Format == 2:
641            Coverage = lambda r: r.Coverage
642            ChainCoverage = lambda r: r.Coverage
643            ContextData = lambda r: (r.ClassDef,)
644            ChainContextData = lambda r: (
645                r.BacktrackClassDef,
646                r.InputClassDef,
647                r.LookAheadClassDef,
648            )
649
650            def SetContextData(r, d):
651                (r.ClassDef,) = d
652
653            def SetChainContextData(r, d):
654                (r.BacktrackClassDef, r.InputClassDef, r.LookAheadClassDef) = d
655
656            RuleData = lambda r: (r.Class,)
657            ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
658
659            def SetRuleData(r, d):
660                (r.Class,) = d
661                (r.GlyphCount,) = (len(x) + 1 for x in d)
662
663            def ChainSetRuleData(r, d):
664                (r.Backtrack, r.Input, r.LookAhead) = d
665                (
666                    r.BacktrackGlyphCount,
667                    r.InputGlyphCount,
668                    r.LookAheadGlyphCount,
669                ) = (len(d[0]), len(d[1]) + 1, len(d[2]))
670
671        elif Format == 3:
672            Coverage = lambda r: r.Coverage[0]
673            ChainCoverage = lambda r: r.InputCoverage[0]
674            ContextData = None
675            ChainContextData = None
676            SetContextData = None
677            SetChainContextData = None
678            RuleData = lambda r: r.Coverage
679            ChainRuleData = lambda r: (
680                r.BacktrackCoverage + r.InputCoverage + r.LookAheadCoverage
681            )
682
683            def SetRuleData(r, d):
684                (r.Coverage,) = d
685                (r.GlyphCount,) = (len(x) for x in d)
686
687            def ChainSetRuleData(r, d):
688                (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d
689                (
690                    r.BacktrackGlyphCount,
691                    r.InputGlyphCount,
692                    r.LookAheadGlyphCount,
693                ) = (len(x) for x in d)
694
695        else:
696            assert 0, "unknown format: %s" % Format
697
698        if Chain:
699            self.Coverage = ChainCoverage
700            self.ContextData = ChainContextData
701            self.SetContextData = SetChainContextData
702            self.RuleData = ChainRuleData
703            self.SetRuleData = ChainSetRuleData
704        else:
705            self.Coverage = Coverage
706            self.ContextData = ContextData
707            self.SetContextData = SetContextData
708            self.RuleData = RuleData
709            self.SetRuleData = SetRuleData
710
711        if Format == 1:
712            self.Rule = ChainTyp + "Rule"
713            self.RuleCount = ChainTyp + "RuleCount"
714            self.RuleSet = ChainTyp + "RuleSet"
715            self.RuleSetCount = ChainTyp + "RuleSetCount"
716            self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
717        elif Format == 2:
718            self.Rule = ChainTyp + "ClassRule"
719            self.RuleCount = ChainTyp + "ClassRuleCount"
720            self.RuleSet = ChainTyp + "ClassSet"
721            self.RuleSetCount = ChainTyp + "ClassSetCount"
722            self.Intersect = lambda glyphs, c, r: (
723                c.intersect_class(glyphs, r)
724                if c
725                else (set(glyphs) if r == 0 else set())
726            )
727
728            self.ClassDef = "InputClassDef" if Chain else "ClassDef"
729            self.ClassDefIndex = 1 if Chain else 0
730            self.Input = "Input" if Chain else "Class"
731
732
733def parseLookupRecords(items, klassName, lookupMap=None):
734    klass = getattr(ot, klassName)
735    lst = []
736    for item in items:
737        rec = klass()
738        item = stripSplitComma(item)
739        assert len(item) == 2, item
740        idx = int(item[0])
741        assert idx > 0, idx
742        rec.SequenceIndex = idx - 1
743        setReference(mapLookup, lookupMap, item[1], setattr, rec, "LookupListIndex")
744        lst.append(rec)
745    return lst
746
747
748def makeClassDef(classDefs, font, klass=ot.Coverage):
749    if not classDefs:
750        return None
751    self = klass()
752    self.classDefs = dict(classDefs)
753    return self
754
755
756def parseClassDef(lines, font, klass=ot.ClassDef):
757    classDefs = {}
758    with lines.between("class definition"):
759        for line in lines:
760            glyph = makeGlyph(line[0])
761            assert glyph not in classDefs, glyph
762            classDefs[glyph] = int(line[1])
763    return makeClassDef(classDefs, font, klass)
764
765
766def makeCoverage(glyphs, font, klass=ot.Coverage):
767    if not glyphs:
768        return None
769    if isinstance(glyphs, set):
770        glyphs = sorted(glyphs)
771    coverage = klass()
772    coverage.glyphs = sorted(set(glyphs), key=font.getGlyphID)
773    return coverage
774
775
776def parseCoverage(lines, font, klass=ot.Coverage):
777    glyphs = []
778    with lines.between("coverage definition"):
779        for line in lines:
780            glyphs.append(makeGlyph(line[0]))
781    return makeCoverage(glyphs, font, klass)
782
783
784def bucketizeRules(self, c, rules, bucketKeys):
785    buckets = {}
786    for seq, recs in rules:
787        buckets.setdefault(seq[c.InputIdx][0], []).append(
788            (tuple(s[1 if i == c.InputIdx else 0 :] for i, s in enumerate(seq)), recs)
789        )
790
791    rulesets = []
792    for firstGlyph in bucketKeys:
793        if firstGlyph not in buckets:
794            rulesets.append(None)
795            continue
796        thisRules = []
797        for seq, recs in buckets[firstGlyph]:
798            rule = getattr(ot, c.Rule)()
799            c.SetRuleData(rule, seq)
800            setattr(rule, c.Type + "Count", len(recs))
801            setattr(rule, c.LookupRecord, recs)
802            thisRules.append(rule)
803
804        ruleset = getattr(ot, c.RuleSet)()
805        setattr(ruleset, c.Rule, thisRules)
806        setattr(ruleset, c.RuleCount, len(thisRules))
807        rulesets.append(ruleset)
808
809    setattr(self, c.RuleSet, rulesets)
810    setattr(self, c.RuleSetCount, len(rulesets))
811
812
813def parseContext(lines, font, Type, lookupMap=None):
814    self = getattr(ot, Type)()
815    typ = lines.peeks()[0].split()[0].lower()
816    if typ == "glyph":
817        self.Format = 1
818        log.debug("Parsing %s format %s", Type, self.Format)
819        c = ContextHelper(Type, self.Format)
820        rules = []
821        for line in lines:
822            assert line[0].lower() == "glyph", line[0]
823            while len(line) < 1 + c.DataLen:
824                line.append("")
825            seq = tuple(makeGlyphs(stripSplitComma(i)) for i in line[1 : 1 + c.DataLen])
826            recs = parseLookupRecords(line[1 + c.DataLen :], c.LookupRecord, lookupMap)
827            rules.append((seq, recs))
828
829        firstGlyphs = set(seq[c.InputIdx][0] for seq, recs in rules)
830        self.Coverage = makeCoverage(firstGlyphs, font)
831        bucketizeRules(self, c, rules, self.Coverage.glyphs)
832    elif typ.endswith("class"):
833        self.Format = 2
834        log.debug("Parsing %s format %s", Type, self.Format)
835        c = ContextHelper(Type, self.Format)
836        classDefs = [None] * c.DataLen
837        while lines.peeks()[0].endswith("class definition begin"):
838            typ = lines.peek()[0][: -len("class definition begin")].lower()
839            idx, klass = {
840                1: {
841                    "": (0, ot.ClassDef),
842                },
843                3: {
844                    "backtrack": (0, ot.BacktrackClassDef),
845                    "": (1, ot.InputClassDef),
846                    "lookahead": (2, ot.LookAheadClassDef),
847                },
848            }[c.DataLen][typ]
849            assert classDefs[idx] is None, idx
850            classDefs[idx] = parseClassDef(lines, font, klass=klass)
851        c.SetContextData(self, classDefs)
852        rules = []
853        for line in lines:
854            assert line[0].lower().startswith("class"), line[0]
855            while len(line) < 1 + c.DataLen:
856                line.append("")
857            seq = tuple(intSplitComma(i) for i in line[1 : 1 + c.DataLen])
858            recs = parseLookupRecords(line[1 + c.DataLen :], c.LookupRecord, lookupMap)
859            rules.append((seq, recs))
860        firstClasses = set(seq[c.InputIdx][0] for seq, recs in rules)
861        firstGlyphs = set(
862            g for g, c in classDefs[c.InputIdx].classDefs.items() if c in firstClasses
863        )
864        self.Coverage = makeCoverage(firstGlyphs, font)
865        bucketizeRules(self, c, rules, range(max(firstClasses) + 1))
866    elif typ.endswith("coverage"):
867        self.Format = 3
868        log.debug("Parsing %s format %s", Type, self.Format)
869        c = ContextHelper(Type, self.Format)
870        coverages = tuple([] for i in range(c.DataLen))
871        while lines.peeks()[0].endswith("coverage definition begin"):
872            typ = lines.peek()[0][: -len("coverage definition begin")].lower()
873            idx, klass = {
874                1: {
875                    "": (0, ot.Coverage),
876                },
877                3: {
878                    "backtrack": (0, ot.BacktrackCoverage),
879                    "input": (1, ot.InputCoverage),
880                    "lookahead": (2, ot.LookAheadCoverage),
881                },
882            }[c.DataLen][typ]
883            coverages[idx].append(parseCoverage(lines, font, klass=klass))
884        c.SetRuleData(self, coverages)
885        lines = list(lines)
886        assert len(lines) == 1
887        line = lines[0]
888        assert line[0].lower() == "coverage", line[0]
889        recs = parseLookupRecords(line[1:], c.LookupRecord, lookupMap)
890        setattr(self, c.Type + "Count", len(recs))
891        setattr(self, c.LookupRecord, recs)
892    else:
893        assert 0, typ
894    return self
895
896
897def parseContextSubst(lines, font, lookupMap=None):
898    return parseContext(lines, font, "ContextSubst", lookupMap=lookupMap)
899
900
901def parseContextPos(lines, font, lookupMap=None):
902    return parseContext(lines, font, "ContextPos", lookupMap=lookupMap)
903
904
905def parseChainedSubst(lines, font, lookupMap=None):
906    return parseContext(lines, font, "ChainContextSubst", lookupMap=lookupMap)
907
908
909def parseChainedPos(lines, font, lookupMap=None):
910    return parseContext(lines, font, "ChainContextPos", lookupMap=lookupMap)
911
912
913def parseReverseChainedSubst(lines, font, _lookupMap=None):
914    self = ot.ReverseChainSingleSubst()
915    self.Format = 1
916    coverages = ([], [])
917    while lines.peeks()[0].endswith("coverage definition begin"):
918        typ = lines.peek()[0][: -len("coverage definition begin")].lower()
919        idx, klass = {
920            "backtrack": (0, ot.BacktrackCoverage),
921            "lookahead": (1, ot.LookAheadCoverage),
922        }[typ]
923        coverages[idx].append(parseCoverage(lines, font, klass=klass))
924    self.BacktrackCoverage = coverages[0]
925    self.BacktrackGlyphCount = len(self.BacktrackCoverage)
926    self.LookAheadCoverage = coverages[1]
927    self.LookAheadGlyphCount = len(self.LookAheadCoverage)
928    mapping = {}
929    for line in lines:
930        assert len(line) == 2, line
931        line = makeGlyphs(line)
932        mapping[line[0]] = line[1]
933    self.Coverage = makeCoverage(set(mapping.keys()), font)
934    self.Substitute = [mapping[k] for k in self.Coverage.glyphs]
935    self.GlyphCount = len(self.Substitute)
936    return self
937
938
939def parseLookup(lines, tableTag, font, lookupMap=None):
940    line = lines.expect("lookup")
941    _, name, typ = line
942    log.debug("Parsing lookup type %s %s", typ, name)
943    lookup = ot.Lookup()
944    lookup.LookupFlag, filterset = parseLookupFlags(lines)
945    if filterset is not None:
946        lookup.MarkFilteringSet = filterset
947    lookup.LookupType, parseLookupSubTable = {
948        "GSUB": {
949            "single": (1, parseSingleSubst),
950            "multiple": (2, parseMultiple),
951            "alternate": (3, parseAlternate),
952            "ligature": (4, parseLigature),
953            "context": (5, parseContextSubst),
954            "chained": (6, parseChainedSubst),
955            "reversechained": (8, parseReverseChainedSubst),
956        },
957        "GPOS": {
958            "single": (1, parseSinglePos),
959            "pair": (2, parsePair),
960            "kernset": (2, parseKernset),
961            "cursive": (3, parseCursive),
962            "mark to base": (4, parseMarkToBase),
963            "mark to ligature": (5, parseMarkToLigature),
964            "mark to mark": (6, parseMarkToMark),
965            "context": (7, parseContextPos),
966            "chained": (8, parseChainedPos),
967        },
968    }[tableTag][typ]
969
970    with lines.until("lookup end"):
971        subtables = []
972
973        while lines.peek():
974            with lines.until(("% subtable", "subtable end")):
975                while lines.peek():
976                    subtable = parseLookupSubTable(lines, font, lookupMap)
977                    assert lookup.LookupType == subtable.LookupType
978                    subtables.append(subtable)
979            if lines.peeks()[0] in ("% subtable", "subtable end"):
980                next(lines)
981    lines.expect("lookup end")
982
983    lookup.SubTable = subtables
984    lookup.SubTableCount = len(lookup.SubTable)
985    if lookup.SubTableCount == 0:
986        # Remove this return when following is fixed:
987        # https://github.com/fonttools/fonttools/issues/789
988        return None
989    return lookup
990
991
992def parseGSUBGPOS(lines, font, tableTag):
993    container = ttLib.getTableClass(tableTag)()
994    lookupMap = DeferredMapping()
995    featureMap = DeferredMapping()
996    assert tableTag in ("GSUB", "GPOS")
997    log.debug("Parsing %s", tableTag)
998    self = getattr(ot, tableTag)()
999    self.Version = 0x00010000
1000    fields = {
1001        "script table begin": (
1002            "ScriptList",
1003            lambda lines: parseScriptList(lines, featureMap),
1004        ),
1005        "feature table begin": (
1006            "FeatureList",
1007            lambda lines: parseFeatureList(lines, lookupMap, featureMap),
1008        ),
1009        "lookup": ("LookupList", None),
1010    }
1011    for attr, parser in fields.values():
1012        setattr(self, attr, None)
1013    while lines.peek() is not None:
1014        typ = lines.peek()[0].lower()
1015        if typ not in fields:
1016            log.debug("Skipping %s", lines.peek())
1017            next(lines)
1018            continue
1019        attr, parser = fields[typ]
1020        if typ == "lookup":
1021            if self.LookupList is None:
1022                self.LookupList = ot.LookupList()
1023                self.LookupList.Lookup = []
1024            _, name, _ = lines.peek()
1025            lookup = parseLookup(lines, tableTag, font, lookupMap)
1026            if lookupMap is not None:
1027                assert name not in lookupMap, "Duplicate lookup name: %s" % name
1028                lookupMap[name] = len(self.LookupList.Lookup)
1029            else:
1030                assert int(name) == len(self.LookupList.Lookup), "%d %d" % (
1031                    name,
1032                    len(self.Lookup),
1033                )
1034            self.LookupList.Lookup.append(lookup)
1035        else:
1036            assert getattr(self, attr) is None, attr
1037            setattr(self, attr, parser(lines))
1038    if self.LookupList:
1039        self.LookupList.LookupCount = len(self.LookupList.Lookup)
1040    if lookupMap is not None:
1041        lookupMap.applyDeferredMappings()
1042        if os.environ.get(LOOKUP_DEBUG_ENV_VAR):
1043            if "Debg" not in font:
1044                font["Debg"] = newTable("Debg")
1045                font["Debg"].data = {}
1046            debug = (
1047                font["Debg"]
1048                .data.setdefault(LOOKUP_DEBUG_INFO_KEY, {})
1049                .setdefault(tableTag, {})
1050            )
1051            for name, lookup in lookupMap.items():
1052                debug[str(lookup)] = ["", name, ""]
1053
1054        featureMap.applyDeferredMappings()
1055    container.table = self
1056    return container
1057
1058
1059def parseGSUB(lines, font):
1060    return parseGSUBGPOS(lines, font, "GSUB")
1061
1062
1063def parseGPOS(lines, font):
1064    return parseGSUBGPOS(lines, font, "GPOS")
1065
1066
1067def parseAttachList(lines, font):
1068    points = {}
1069    with lines.between("attachment list"):
1070        for line in lines:
1071            glyph = makeGlyph(line[0])
1072            assert glyph not in points, glyph
1073            points[glyph] = [int(i) for i in line[1:]]
1074    return otl.buildAttachList(points, font.getReverseGlyphMap())
1075
1076
1077def parseCaretList(lines, font):
1078    carets = {}
1079    with lines.between("carets"):
1080        for line in lines:
1081            glyph = makeGlyph(line[0])
1082            assert glyph not in carets, glyph
1083            num = int(line[1])
1084            thisCarets = [int(i) for i in line[2:]]
1085            assert num == len(thisCarets), line
1086            carets[glyph] = thisCarets
1087    return otl.buildLigCaretList(carets, {}, font.getReverseGlyphMap())
1088
1089
1090def makeMarkFilteringSets(sets, font):
1091    self = ot.MarkGlyphSetsDef()
1092    self.MarkSetTableFormat = 1
1093    self.MarkSetCount = 1 + max(sets.keys())
1094    self.Coverage = [None] * self.MarkSetCount
1095    for k, v in sorted(sets.items()):
1096        self.Coverage[k] = makeCoverage(set(v), font)
1097    return self
1098
1099
1100def parseMarkFilteringSets(lines, font):
1101    sets = {}
1102    with lines.between("set definition"):
1103        for line in lines:
1104            assert len(line) == 2, line
1105            glyph = makeGlyph(line[0])
1106            # TODO accept set names
1107            st = int(line[1])
1108            if st not in sets:
1109                sets[st] = []
1110            sets[st].append(glyph)
1111    return makeMarkFilteringSets(sets, font)
1112
1113
1114def parseGDEF(lines, font):
1115    container = ttLib.getTableClass("GDEF")()
1116    log.debug("Parsing GDEF")
1117    self = ot.GDEF()
1118    fields = {
1119        "class definition begin": (
1120            "GlyphClassDef",
1121            lambda lines, font: parseClassDef(lines, font, klass=ot.GlyphClassDef),
1122        ),
1123        "attachment list begin": ("AttachList", parseAttachList),
1124        "carets begin": ("LigCaretList", parseCaretList),
1125        "mark attachment class definition begin": (
1126            "MarkAttachClassDef",
1127            lambda lines, font: parseClassDef(lines, font, klass=ot.MarkAttachClassDef),
1128        ),
1129        "markfilter set definition begin": ("MarkGlyphSetsDef", parseMarkFilteringSets),
1130    }
1131    for attr, parser in fields.values():
1132        setattr(self, attr, None)
1133    while lines.peek() is not None:
1134        typ = lines.peek()[0].lower()
1135        if typ not in fields:
1136            log.debug("Skipping %s", typ)
1137            next(lines)
1138            continue
1139        attr, parser = fields[typ]
1140        assert getattr(self, attr) is None, attr
1141        setattr(self, attr, parser(lines, font))
1142    self.Version = 0x00010000 if self.MarkGlyphSetsDef is None else 0x00010002
1143    container.table = self
1144    return container
1145
1146
1147def parseCmap(lines, font):
1148    container = ttLib.getTableClass("cmap")()
1149    log.debug("Parsing cmap")
1150    tables = []
1151    while lines.peek() is not None:
1152        lines.expect("cmap subtable %d" % len(tables))
1153        platId, encId, fmt, lang = [
1154            parseCmapId(lines, field)
1155            for field in ("platformID", "encodingID", "format", "language")
1156        ]
1157        table = cmap_classes[fmt](fmt)
1158        table.platformID = platId
1159        table.platEncID = encId
1160        table.language = lang
1161        table.cmap = {}
1162        line = next(lines)
1163        while line[0] != "end subtable":
1164            table.cmap[int(line[0], 16)] = line[1]
1165            line = next(lines)
1166        tables.append(table)
1167    container.tableVersion = 0
1168    container.tables = tables
1169    return container
1170
1171
1172def parseCmapId(lines, field):
1173    line = next(lines)
1174    assert field == line[0]
1175    return int(line[1])
1176
1177
1178def parseTable(lines, font, tableTag=None):
1179    log.debug("Parsing table")
1180    line = lines.peeks()
1181    tag = None
1182    if line[0].split()[0] == "FontDame":
1183        tag = line[0].split()[1]
1184    elif " ".join(line[0].split()[:3]) == "Font Chef Table":
1185        tag = line[0].split()[3]
1186    if tag is not None:
1187        next(lines)
1188        tag = tag.ljust(4)
1189        if tableTag is None:
1190            tableTag = tag
1191        else:
1192            assert tableTag == tag, (tableTag, tag)
1193
1194    assert (
1195        tableTag is not None
1196    ), "Don't know what table to parse and data doesn't specify"
1197
1198    return {
1199        "GSUB": parseGSUB,
1200        "GPOS": parseGPOS,
1201        "GDEF": parseGDEF,
1202        "cmap": parseCmap,
1203    }[tableTag](lines, font)
1204
1205
1206class Tokenizer(object):
1207    def __init__(self, f):
1208        # TODO BytesIO / StringIO as needed?  also, figure out whether we work on bytes or unicode
1209        lines = iter(f)
1210        try:
1211            self.filename = f.name
1212        except:
1213            self.filename = None
1214        self.lines = iter(lines)
1215        self.line = ""
1216        self.lineno = 0
1217        self.stoppers = []
1218        self.buffer = None
1219
1220    def __iter__(self):
1221        return self
1222
1223    def _next_line(self):
1224        self.lineno += 1
1225        line = self.line = next(self.lines)
1226        line = [s.strip() for s in line.split("\t")]
1227        if len(line) == 1 and not line[0]:
1228            del line[0]
1229        if line and not line[-1]:
1230            log.warning("trailing tab found on line %d: %s" % (self.lineno, self.line))
1231            while line and not line[-1]:
1232                del line[-1]
1233        return line
1234
1235    def _next_nonempty(self):
1236        while True:
1237            line = self._next_line()
1238            # Skip comments and empty lines
1239            if line and line[0] and (line[0][0] != "%" or line[0] == "% subtable"):
1240                return line
1241
1242    def _next_buffered(self):
1243        if self.buffer:
1244            ret = self.buffer
1245            self.buffer = None
1246            return ret
1247        else:
1248            return self._next_nonempty()
1249
1250    def __next__(self):
1251        line = self._next_buffered()
1252        if line[0].lower() in self.stoppers:
1253            self.buffer = line
1254            raise StopIteration
1255        return line
1256
1257    def next(self):
1258        return self.__next__()
1259
1260    def peek(self):
1261        if not self.buffer:
1262            try:
1263                self.buffer = self._next_nonempty()
1264            except StopIteration:
1265                return None
1266        if self.buffer[0].lower() in self.stoppers:
1267            return None
1268        return self.buffer
1269
1270    def peeks(self):
1271        ret = self.peek()
1272        return ret if ret is not None else ("",)
1273
1274    @contextmanager
1275    def between(self, tag):
1276        start = tag + " begin"
1277        end = tag + " end"
1278        self.expectendswith(start)
1279        self.stoppers.append(end)
1280        yield
1281        del self.stoppers[-1]
1282        self.expect(tag + " end")
1283
1284    @contextmanager
1285    def until(self, tags):
1286        if type(tags) is not tuple:
1287            tags = (tags,)
1288        self.stoppers.extend(tags)
1289        yield
1290        del self.stoppers[-len(tags) :]
1291
1292    def expect(self, s):
1293        line = next(self)
1294        tag = line[0].lower()
1295        assert tag == s, "Expected '%s', got '%s'" % (s, tag)
1296        return line
1297
1298    def expectendswith(self, s):
1299        line = next(self)
1300        tag = line[0].lower()
1301        assert tag.endswith(s), "Expected '*%s', got '%s'" % (s, tag)
1302        return line
1303
1304
1305def build(f, font, tableTag=None):
1306    """Convert a Monotype font layout file to an OpenType layout object
1307
1308    A font object must be passed, but this may be a "dummy" font; it is only
1309    used for sorting glyph sets when making coverage tables and to hold the
1310    OpenType layout table while it is being built.
1311
1312    Args:
1313            f: A file object.
1314            font (TTFont): A font object.
1315            tableTag (string): If provided, asserts that the file contains data for the
1316                    given OpenType table.
1317
1318    Returns:
1319            An object representing the table. (e.g. ``table_G_S_U_B_``)
1320    """
1321    lines = Tokenizer(f)
1322    return parseTable(lines, font, tableTag=tableTag)
1323
1324
1325def main(args=None, font=None):
1326    """Convert a FontDame OTL file to TTX XML
1327
1328    Writes XML output to stdout.
1329
1330    Args:
1331            args: Command line arguments (``--font``, ``--table``, input files).
1332    """
1333    import sys
1334    from fontTools import configLogger
1335    from fontTools.misc.testTools import MockFont
1336
1337    if args is None:
1338        args = sys.argv[1:]
1339
1340    # configure the library logger (for >= WARNING)
1341    configLogger()
1342    # comment this out to enable debug messages from mtiLib's logger
1343    # log.setLevel(logging.DEBUG)
1344
1345    import argparse
1346
1347    parser = argparse.ArgumentParser(
1348        "fonttools mtiLib",
1349        description=main.__doc__,
1350    )
1351
1352    parser.add_argument(
1353        "--font",
1354        "-f",
1355        metavar="FILE",
1356        dest="font",
1357        help="Input TTF files (used for glyph classes and sorting coverage tables)",
1358    )
1359    parser.add_argument(
1360        "--table",
1361        "-t",
1362        metavar="TABLE",
1363        dest="tableTag",
1364        help="Table to fill (sniffed from input file if not provided)",
1365    )
1366    parser.add_argument(
1367        "inputs", metavar="FILE", type=str, nargs="+", help="Input FontDame .txt files"
1368    )
1369
1370    args = parser.parse_args(args)
1371
1372    if font is None:
1373        if args.font:
1374            font = ttLib.TTFont(args.font)
1375        else:
1376            font = MockFont()
1377
1378    for f in args.inputs:
1379        log.debug("Processing %s", f)
1380        with open(f, "rt", encoding="utf-8") as f:
1381            table = build(f, font, tableTag=args.tableTag)
1382        blob = table.compile(font)  # Make sure it compiles
1383        decompiled = table.__class__()
1384        decompiled.decompile(blob, font)  # Make sure it decompiles!
1385
1386        # continue
1387        from fontTools.misc import xmlWriter
1388
1389        tag = table.tableTag
1390        writer = xmlWriter.XMLWriter(sys.stdout)
1391        writer.begintag(tag)
1392        writer.newline()
1393        # table.toXML(writer, font)
1394        decompiled.toXML(writer, font)
1395        writer.endtag(tag)
1396        writer.newline()
1397
1398
1399if __name__ == "__main__":
1400    import sys
1401
1402    sys.exit(main())
1403