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