__init__.py revision 2e4cc02ca31c43eafb6f752e44dbca9b004a3a2f
1"""fontTools.ttLib -- a package for dealing with TrueType fonts. 2 3This package offers translators to convert TrueType fonts to Python 4objects and vice versa, and additionally from Python to TTX (an XML-based 5text format) and vice versa. 6 7Example interactive session: 8 9Python 1.5.2c1 (#43, Mar 9 1999, 13:06:43) [CW PPC w/GUSI w/MSL] 10Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam 11>>> from fontTools import ttLib 12>>> tt = ttLib.TTFont("afont.ttf") 13>>> tt['maxp'].numGlyphs 14242 15>>> tt['OS/2'].achVendID 16'B&H\000' 17>>> tt['head'].unitsPerEm 182048 19>>> tt.saveXML("afont.ttx") 20Dumping 'LTSH' table... 21Dumping 'OS/2' table... 22Dumping 'VDMX' table... 23Dumping 'cmap' table... 24Dumping 'cvt ' table... 25Dumping 'fpgm' table... 26Dumping 'glyf' table... 27Dumping 'hdmx' table... 28Dumping 'head' table... 29Dumping 'hhea' table... 30Dumping 'hmtx' table... 31Dumping 'loca' table... 32Dumping 'maxp' table... 33Dumping 'name' table... 34Dumping 'post' table... 35Dumping 'prep' table... 36>>> tt2 = ttLib.TTFont() 37>>> tt2.importXML("afont.ttx") 38>>> tt2['maxp'].numGlyphs 39242 40>>> 41 42""" 43 44# 45# $Id: __init__.py,v 1.47 2005-03-08 09:50:56 jvr Exp $ 46# 47 48import sys 49import os 50import string 51 52haveMacSupport = 0 53if sys.platform == "mac": 54 haveMacSupport = 1 55elif sys.platform == "darwin" and sys.version_info[:3] != (2, 2, 0): 56 # Python 2.2's Mac support is broken, so don't enable it there. 57 haveMacSupport = 1 58 59 60class TTLibError(Exception): pass 61 62 63class TTFont: 64 65 """The main font object. It manages file input and output, and offers 66 a convenient way of accessing tables. 67 Tables will be only decompiled when neccesary, ie. when they're actually 68 accessed. This means that simple operations can be extremely fast. 69 """ 70 71 def __init__(self, file=None, res_name_or_index=None, 72 sfntVersion="\000\001\000\000", checkChecksums=0, 73 verbose=0, recalcBBoxes=1): 74 75 """The constructor can be called with a few different arguments. 76 When reading a font from disk, 'file' should be either a pathname 77 pointing to a file, or a readable file object. 78 79 It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt 80 resource name or an sfnt resource index number or zero. The latter 81 case will cause TTLib to autodetect whether the file is a flat file 82 or a suitcase. (If it's a suitcase, only the first 'sfnt' resource 83 will be read!) 84 85 The 'checkChecksums' argument is used to specify how sfnt 86 checksums are treated upon reading a file from disk: 87 0: don't check (default) 88 1: check, print warnings if a wrong checksum is found 89 2: check, raise an exception if a wrong checksum is found. 90 91 The TTFont constructor can also be called without a 'file' 92 argument: this is the way to create a new empty font. 93 In this case you can optionally supply the 'sfntVersion' argument. 94 95 If the recalcBBoxes argument is false, a number of things will *not* 96 be recalculated upon save/compile: 97 1) glyph bounding boxes 98 2) maxp font bounding box 99 3) hhea min/max values 100 (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). 101 Additionally, upon importing an TTX file, this option cause glyphs 102 to be compiled right away. This should reduce memory consumption 103 greatly, and therefore should have some impact on the time needed 104 to parse/compile large fonts. 105 """ 106 107 import sfnt 108 self.verbose = verbose 109 self.recalcBBoxes = recalcBBoxes 110 self.tables = {} 111 self.reader = None 112 if not file: 113 self.sfntVersion = sfntVersion 114 return 115 if not hasattr(file, "read"): 116 # assume file is a string 117 if haveMacSupport and res_name_or_index is not None: 118 # on the mac, we deal with sfnt resources as well as flat files 119 import macUtils 120 if res_name_or_index == 0: 121 if macUtils.getSFNTResIndices(file): 122 # get the first available sfnt font. 123 file = macUtils.SFNTResourceReader(file, 1) 124 else: 125 file = open(file, "rb") 126 else: 127 file = macUtils.SFNTResourceReader(file, res_name_or_index) 128 else: 129 file = open(file, "rb") 130 else: 131 pass # assume "file" is a readable file object 132 self.reader = sfnt.SFNTReader(file, checkChecksums) 133 self.sfntVersion = self.reader.sfntVersion 134 135 def close(self): 136 """If we still have a reader object, close it.""" 137 if self.reader is not None: 138 self.reader.close() 139 140 def save(self, file, makeSuitcase=0, reorderTables=1): 141 """Save the font to disk. Similarly to the constructor, 142 the 'file' argument can be either a pathname or a writable 143 file object. 144 145 On the Mac, if makeSuitcase is true, a suitcase (resource fork) 146 file will we made instead of a flat .ttf file. 147 """ 148 from fontTools.ttLib import sfnt 149 if not hasattr(file, "write"): 150 closeStream = 1 151 if os.name == "mac" and makeSuitcase: 152 import macUtils 153 file = macUtils.SFNTResourceWriter(file, self) 154 else: 155 file = open(file, "wb") 156 if os.name == "mac": 157 import macfs 158 fss = macfs.FSSpec(file.name) 159 fss.SetCreatorType('mdos', 'BINA') 160 else: 161 # assume "file" is a writable file object 162 closeStream = 0 163 164 tags = self.keys() 165 if "GlyphOrder" in tags: 166 tags.remove("GlyphOrder") 167 numTables = len(tags) 168 if reorderTables: 169 import tempfile 170 tmp = tempfile.TemporaryFile(prefix="ttx-fonttools") 171 else: 172 tmp = file 173 writer = sfnt.SFNTWriter(tmp, numTables, self.sfntVersion) 174 175 done = [] 176 for tag in tags: 177 self._writeTable(tag, writer, done) 178 179 writer.close() 180 181 if reorderTables: 182 tmp.flush() 183 tmp.seek(0) 184 reorderFontTables(tmp, file) 185 tmp.close() 186 187 if closeStream: 188 file.close() 189 190 def saveXML(self, fileOrPath, progress=None, 191 tables=None, skipTables=None, splitTables=0, disassembleInstructions=1): 192 """Export the font as TTX (an XML-based text file), or as a series of text 193 files when splitTables is true. In the latter case, the 'fileOrPath' 194 argument should be a path to a directory. 195 The 'tables' argument must either be false (dump all tables) or a 196 list of tables to dump. The 'skipTables' argument may be a list of tables 197 to skip, but only when the 'tables' argument is false. 198 """ 199 from fontTools import version 200 import xmlWriter 201 202 self.disassembleInstructions = disassembleInstructions 203 if not tables: 204 tables = self.keys() 205 if "GlyphOrder" not in tables: 206 tables = ["GlyphOrder"] + tables 207 if skipTables: 208 for tag in skipTables: 209 if tag in tables: 210 tables.remove(tag) 211 numTables = len(tables) 212 if progress: 213 progress.set(0, numTables) 214 idlefunc = getattr(progress, "idle", None) 215 else: 216 idlefunc = None 217 218 writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc) 219 writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1], 220 ttLibVersion=version) 221 writer.newline() 222 223 if not splitTables: 224 writer.newline() 225 else: 226 # 'fileOrPath' must now be a path 227 path, ext = os.path.splitext(fileOrPath) 228 fileNameTemplate = path + ".%s" + ext 229 230 for i in range(numTables): 231 if progress: 232 progress.set(i) 233 tag = tables[i] 234 if splitTables: 235 tablePath = fileNameTemplate % tagToIdentifier(tag) 236 tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc) 237 tableWriter.begintag("ttFont", ttLibVersion=version) 238 tableWriter.newline() 239 tableWriter.newline() 240 writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) 241 writer.newline() 242 else: 243 tableWriter = writer 244 self._tableToXML(tableWriter, tag, progress) 245 if splitTables: 246 tableWriter.endtag("ttFont") 247 tableWriter.newline() 248 tableWriter.close() 249 if progress: 250 progress.set((i + 1)) 251 writer.endtag("ttFont") 252 writer.newline() 253 writer.close() 254 if self.verbose: 255 debugmsg("Done dumping TTX") 256 257 def _tableToXML(self, writer, tag, progress): 258 if self.has_key(tag): 259 table = self[tag] 260 report = "Dumping '%s' table..." % tag 261 else: 262 report = "No '%s' table found." % tag 263 if progress: 264 progress.setLabel(report) 265 elif self.verbose: 266 debugmsg(report) 267 else: 268 print report 269 if not self.has_key(tag): 270 return 271 xmlTag = tagToXML(tag) 272 if hasattr(table, "ERROR"): 273 writer.begintag(xmlTag, ERROR="decompilation error") 274 else: 275 writer.begintag(xmlTag) 276 writer.newline() 277 if tag in ("glyf", "CFF "): 278 table.toXML(writer, self, progress) 279 else: 280 table.toXML(writer, self) 281 writer.endtag(xmlTag) 282 writer.newline() 283 writer.newline() 284 285 def importXML(self, file, progress=None): 286 """Import a TTX file (an XML-based text format), so as to recreate 287 a font object. 288 """ 289 if self.has_key("maxp") and self.has_key("post"): 290 # Make sure the glyph order is loaded, as it otherwise gets 291 # lost if the XML doesn't contain the glyph order, yet does 292 # contain the table which was originally used to extract the 293 # glyph names from (ie. 'post', 'cmap' or 'CFF '). 294 self.getGlyphOrder() 295 import xmlImport 296 xmlImport.importXML(self, file, progress) 297 298 def isLoaded(self, tag): 299 """Return true if the table identified by 'tag' has been 300 decompiled and loaded into memory.""" 301 return self.tables.has_key(tag) 302 303 def has_key(self, tag): 304 if self.isLoaded(tag): 305 return 1 306 elif self.reader and self.reader.has_key(tag): 307 return 1 308 elif tag == "GlyphOrder": 309 return 1 310 else: 311 return 0 312 313 __contains__ = has_key 314 315 def keys(self): 316 keys = self.tables.keys() 317 if self.reader: 318 for key in self.reader.keys(): 319 if key not in keys: 320 keys.append(key) 321 322 if "GlyphOrder" in keys: 323 keys.remove("GlyphOrder") 324 keys = sortedTagList(keys) 325 return ["GlyphOrder"] + keys 326 327 def __len__(self): 328 return len(self.keys()) 329 330 def __getitem__(self, tag): 331 try: 332 return self.tables[tag] 333 except KeyError: 334 if tag == "GlyphOrder": 335 table = GlyphOrder(tag) 336 self.tables[tag] = table 337 return table 338 if self.reader is not None: 339 import traceback 340 if self.verbose: 341 debugmsg("Reading '%s' table from disk" % tag) 342 data = self.reader[tag] 343 tableClass = getTableClass(tag) 344 table = tableClass(tag) 345 self.tables[tag] = table 346 if self.verbose: 347 debugmsg("Decompiling '%s' table" % tag) 348 try: 349 table.decompile(data, self) 350 except "_ _ F O O _ _": # dummy exception to disable exception catching 351 print "An exception occurred during the decompilation of the '%s' table" % tag 352 from tables.DefaultTable import DefaultTable 353 import StringIO 354 file = StringIO.StringIO() 355 traceback.print_exc(file=file) 356 table = DefaultTable(tag) 357 table.ERROR = file.getvalue() 358 self.tables[tag] = table 359 table.decompile(data, self) 360 return table 361 else: 362 raise KeyError, "'%s' table not found" % tag 363 364 def __setitem__(self, tag, table): 365 self.tables[tag] = table 366 367 def __delitem__(self, tag): 368 if not self.has_key(tag): 369 raise KeyError, "'%s' table not found" % tag 370 if self.tables.has_key(tag): 371 del self.tables[tag] 372 if self.reader and self.reader.has_key(tag): 373 del self.reader[tag] 374 375 def setGlyphOrder(self, glyphOrder): 376 self.glyphOrder = glyphOrder 377 378 def getGlyphOrder(self): 379 try: 380 return self.glyphOrder 381 except AttributeError: 382 pass 383 if self.has_key('CFF '): 384 cff = self['CFF '] 385 self.glyphOrder = cff.getGlyphOrder() 386 elif self.has_key('post'): 387 # TrueType font 388 glyphOrder = self['post'].getGlyphOrder() 389 if glyphOrder is None: 390 # 391 # No names found in the 'post' table. 392 # Try to create glyph names from the unicode cmap (if available) 393 # in combination with the Adobe Glyph List (AGL). 394 # 395 self._getGlyphNamesFromCmap() 396 else: 397 self.glyphOrder = glyphOrder 398 else: 399 self._getGlyphNamesFromCmap() 400 return self.glyphOrder 401 402 def _getGlyphNamesFromCmap(self): 403 # 404 # This is rather convoluted, but then again, it's an interesting problem: 405 # - we need to use the unicode values found in the cmap table to 406 # build glyph names (eg. because there is only a minimal post table, 407 # or none at all). 408 # - but the cmap parser also needs glyph names to work with... 409 # So here's what we do: 410 # - make up glyph names based on glyphID 411 # - load a temporary cmap table based on those names 412 # - extract the unicode values, build the "real" glyph names 413 # - unload the temporary cmap table 414 # 415 if self.isLoaded("cmap"): 416 # Bootstrapping: we're getting called by the cmap parser 417 # itself. This means self.tables['cmap'] contains a partially 418 # loaded cmap, making it impossible to get at a unicode 419 # subtable here. We remove the partially loaded cmap and 420 # restore it later. 421 # This only happens if the cmap table is loaded before any 422 # other table that does f.getGlyphOrder() or f.getGlyphName(). 423 cmapLoading = self.tables['cmap'] 424 del self.tables['cmap'] 425 else: 426 cmapLoading = None 427 # Make up glyph names based on glyphID, which will be used by the 428 # temporary cmap and by the real cmap in case we don't find a unicode 429 # cmap. 430 numGlyphs = int(self['maxp'].numGlyphs) 431 glyphOrder = [None] * numGlyphs 432 glyphOrder[0] = ".notdef" 433 for i in range(1, numGlyphs): 434 glyphOrder[i] = "glyph%.5d" % i 435 # Set the glyph order, so the cmap parser has something 436 # to work with (so we don't get called recursively). 437 self.glyphOrder = glyphOrder 438 # Get a (new) temporary cmap (based on the just invented names) 439 tempcmap = self['cmap'].getcmap(3, 1) 440 if tempcmap is not None: 441 # we have a unicode cmap 442 from fontTools import agl 443 cmap = tempcmap.cmap 444 # create a reverse cmap dict 445 reversecmap = {} 446 for unicode, name in cmap.items(): 447 reversecmap[name] = unicode 448 allNames = {} 449 for i in range(numGlyphs): 450 tempName = glyphOrder[i] 451 if reversecmap.has_key(tempName): 452 unicode = reversecmap[tempName] 453 if agl.UV2AGL.has_key(unicode): 454 # get name from the Adobe Glyph List 455 glyphName = agl.UV2AGL[unicode] 456 else: 457 # create uni<CODE> name 458 glyphName = "uni" + string.upper(string.zfill( 459 hex(unicode)[2:], 4)) 460 tempName = glyphName 461 n = 1 462 while allNames.has_key(tempName): 463 tempName = glyphName + "#" + `n` 464 n = n + 1 465 glyphOrder[i] = tempName 466 allNames[tempName] = 1 467 # Delete the temporary cmap table from the cache, so it can 468 # be parsed again with the right names. 469 del self.tables['cmap'] 470 else: 471 pass # no unicode cmap available, stick with the invented names 472 self.glyphOrder = glyphOrder 473 if cmapLoading: 474 # restore partially loaded cmap, so it can continue loading 475 # using the proper names. 476 self.tables['cmap'] = cmapLoading 477 478 def getGlyphNames(self): 479 """Get a list of glyph names, sorted alphabetically.""" 480 glyphNames = self.getGlyphOrder()[:] 481 glyphNames.sort() 482 return glyphNames 483 484 def getGlyphNames2(self): 485 """Get a list of glyph names, sorted alphabetically, 486 but not case sensitive. 487 """ 488 from fontTools.misc import textTools 489 return textTools.caselessSort(self.getGlyphOrder()) 490 491 def getGlyphName(self, glyphID): 492 try: 493 return self.getGlyphOrder()[glyphID] 494 except IndexError: 495 # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in 496 # the cmap table than there are glyphs. I don't think it's legal... 497 return "glyph%.5d" % glyphID 498 499 def getGlyphID(self, glyphName): 500 if not hasattr(self, "_reverseGlyphOrderDict"): 501 self._buildReverseGlyphOrderDict() 502 glyphOrder = self.getGlyphOrder() 503 d = self._reverseGlyphOrderDict 504 if not d.has_key(glyphName): 505 if glyphName in glyphOrder: 506 self._buildReverseGlyphOrderDict() 507 return self.getGlyphID(glyphName) 508 else: 509 raise KeyError, glyphName 510 glyphID = d[glyphName] 511 if glyphName <> glyphOrder[glyphID]: 512 self._buildReverseGlyphOrderDict() 513 return self.getGlyphID(glyphName) 514 return glyphID 515 516 def _buildReverseGlyphOrderDict(self): 517 self._reverseGlyphOrderDict = d = {} 518 glyphOrder = self.getGlyphOrder() 519 for glyphID in range(len(glyphOrder)): 520 d[glyphOrder[glyphID]] = glyphID 521 522 def _writeTable(self, tag, writer, done): 523 """Internal helper function for self.save(). Keeps track of 524 inter-table dependencies. 525 """ 526 if tag in done: 527 return 528 tableClass = getTableClass(tag) 529 for masterTable in tableClass.dependencies: 530 if masterTable not in done: 531 if self.has_key(masterTable): 532 self._writeTable(masterTable, writer, done) 533 else: 534 done.append(masterTable) 535 tabledata = self.getTableData(tag) 536 if self.verbose: 537 debugmsg("writing '%s' table to disk" % tag) 538 writer[tag] = tabledata 539 done.append(tag) 540 541 def getTableData(self, tag): 542 """Returns raw table data, whether compiled or directly read from disk. 543 """ 544 if self.isLoaded(tag): 545 if self.verbose: 546 debugmsg("compiling '%s' table" % tag) 547 return self.tables[tag].compile(self) 548 elif self.reader and self.reader.has_key(tag): 549 if self.verbose: 550 debugmsg("Reading '%s' table from disk" % tag) 551 return self.reader[tag] 552 else: 553 raise KeyError, tag 554 555 def getGlyphSet(self, preferCFF=1): 556 """Return a generic GlyphSet, which is a dict-like object 557 mapping glyph names to glyph objects. The returned glyph objects 558 have a .draw() method that supports the Pen protocol, and will 559 have an attribute named 'width', but only *after* the .draw() method 560 has been called. 561 562 If the font is CFF-based, the outlines will be taken from the 'CFF ' 563 table. Otherwise the outlines will be taken from the 'glyf' table. 564 If the font contains both a 'CFF ' and a 'glyf' table, you can use 565 the 'preferCFF' argument to specify which one should be taken. 566 """ 567 if preferCFF and self.has_key("CFF "): 568 return self["CFF "].cff.values()[0].CharStrings 569 if self.has_key("glyf"): 570 return _TTGlyphSet(self) 571 if self.has_key("CFF "): 572 return self["CFF "].cff.values()[0].CharStrings 573 raise TTLibError, "Font contains no outlines" 574 575 576class _TTGlyphSet: 577 578 """Generic dict-like GlyphSet class, meant as a TrueType counterpart 579 to CFF's CharString dict. See TTFont.getGlyphSet(). 580 """ 581 582 # This class is distinct from the 'glyf' table itself because we need 583 # access to the 'hmtx' table, which could cause a dependency problem 584 # there when reading from XML. 585 586 def __init__(self, ttFont): 587 self._ttFont = ttFont 588 589 def keys(self): 590 return self._ttFont["glyf"].keys() 591 592 def has_key(self, glyphName): 593 return self._ttFont["glyf"].has_key(glyphName) 594 595 __contains__ = has_key 596 597 def __getitem__(self, glyphName): 598 return _TTGlyph(glyphName, self._ttFont) 599 600 def get(self, glyphName, default=None): 601 try: 602 return self[glyphName] 603 except KeyError: 604 return default 605 606 607class _TTGlyph: 608 609 """Wrapper for a TrueType glyph that supports the Pen protocol, meaning 610 that it has a .draw() method that takes a pen object as its only 611 argument. Additionally there is a 'width' attribute. 612 """ 613 614 def __init__(self, glyphName, ttFont): 615 self._glyphName = glyphName 616 self._ttFont = ttFont 617 self.width, self.lsb = self._ttFont['hmtx'][self._glyphName] 618 619 def draw(self, pen): 620 """Draw the glyph onto Pen. See fontTools.pens.basePen for details 621 how that works. 622 """ 623 glyfTable = self._ttFont['glyf'] 624 glyph = glyfTable[self._glyphName] 625 if hasattr(glyph, "xMin"): 626 offset = self.lsb - glyph.xMin 627 else: 628 offset = 0 629 if glyph.isComposite(): 630 for component in glyph: 631 glyphName, transform = component.getComponentInfo() 632 pen.addComponent(glyphName, transform) 633 else: 634 coordinates, endPts, flags = glyph.getCoordinates(glyfTable) 635 if offset: 636 coordinates = coordinates + (offset, 0) 637 start = 0 638 for end in endPts: 639 end = end + 1 640 contour = coordinates[start:end].tolist() 641 cFlags = flags[start:end].tolist() 642 start = end 643 if 1 not in cFlags: 644 # There is not a single on-curve point on the curve, 645 # use pen.qCurveTo's special case by specifying None 646 # as the on-curve point. 647 contour.append(None) 648 pen.qCurveTo(*contour) 649 else: 650 # Shuffle the points so that contour the is guaranteed 651 # to *end* in an on-curve point, which we'll use for 652 # the moveTo. 653 firstOnCurve = cFlags.index(1) + 1 654 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 655 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 656 pen.moveTo(contour[-1]) 657 while contour: 658 nextOnCurve = cFlags.index(1) + 1 659 if nextOnCurve == 1: 660 pen.lineTo(contour[0]) 661 else: 662 pen.qCurveTo(*contour[:nextOnCurve]) 663 contour = contour[nextOnCurve:] 664 cFlags = cFlags[nextOnCurve:] 665 pen.closePath() 666 667 668class GlyphOrder: 669 670 """A pseudo table. The glyph order isn't in the font as a separate 671 table, but it's nice to present it as such in the TTX format. 672 """ 673 674 def __init__(self, tag): 675 pass 676 677 def toXML(self, writer, ttFont): 678 glyphOrder = ttFont.getGlyphOrder() 679 writer.comment("The 'id' attribute is only for humans; " 680 "it is ignored when parsed.") 681 writer.newline() 682 for i in range(len(glyphOrder)): 683 glyphName = glyphOrder[i] 684 writer.simpletag("GlyphID", id=i, name=glyphName) 685 writer.newline() 686 687 def fromXML(self, (name, attrs, content), ttFont): 688 if not hasattr(self, "glyphOrder"): 689 self.glyphOrder = [] 690 ttFont.setGlyphOrder(self.glyphOrder) 691 if name == "GlyphID": 692 self.glyphOrder.append(attrs["name"]) 693 694 695def _test_endianness(): 696 """Test the endianness of the machine. This is crucial to know 697 since TrueType data is always big endian, even on little endian 698 machines. There are quite a few situations where we explicitly 699 need to swap some bytes. 700 """ 701 import struct 702 data = struct.pack("h", 0x01) 703 if data == "\000\001": 704 return "big" 705 elif data == "\001\000": 706 return "little" 707 else: 708 assert 0, "endian confusion!" 709 710endian = _test_endianness() 711 712 713def getTableModule(tag): 714 """Fetch the packer/unpacker module for a table. 715 Return None when no module is found. 716 """ 717 import tables 718 pyTag = tagToIdentifier(tag) 719 try: 720 __import__("fontTools.ttLib.tables." + pyTag) 721 except ImportError: 722 return None 723 else: 724 return getattr(tables, pyTag) 725 726 727def getTableClass(tag): 728 """Fetch the packer/unpacker class for a table. 729 Return None when no class is found. 730 """ 731 module = getTableModule(tag) 732 if module is None: 733 from tables.DefaultTable import DefaultTable 734 return DefaultTable 735 pyTag = tagToIdentifier(tag) 736 tableClass = getattr(module, "table_" + pyTag) 737 return tableClass 738 739 740def newTable(tag): 741 """Return a new instance of a table.""" 742 tableClass = getTableClass(tag) 743 return tableClass(tag) 744 745 746def _escapechar(c): 747 """Helper function for tagToIdentifier()""" 748 import re 749 if re.match("[a-z0-9]", c): 750 return "_" + c 751 elif re.match("[A-Z]", c): 752 return c + "_" 753 else: 754 return hex(ord(c))[2:] 755 756 757def tagToIdentifier(tag): 758 """Convert a table tag to a valid (but UGLY) python identifier, 759 as well as a filename that's guaranteed to be unique even on a 760 caseless file system. Each character is mapped to two characters. 761 Lowercase letters get an underscore before the letter, uppercase 762 letters get an underscore after the letter. Trailing spaces are 763 trimmed. Illegal characters are escaped as two hex bytes. If the 764 result starts with a number (as the result of a hex escape), an 765 extra underscore is prepended. Examples: 766 'glyf' -> '_g_l_y_f' 767 'cvt ' -> '_c_v_t' 768 'OS/2' -> 'O_S_2f_2' 769 """ 770 import re 771 if tag == "GlyphOrder": 772 return tag 773 assert len(tag) == 4, "tag should be 4 characters long" 774 while len(tag) > 1 and tag[-1] == ' ': 775 tag = tag[:-1] 776 ident = "" 777 for c in tag: 778 ident = ident + _escapechar(c) 779 if re.match("[0-9]", ident): 780 ident = "_" + ident 781 return ident 782 783 784def identifierToTag(ident): 785 """the opposite of tagToIdentifier()""" 786 if ident == "GlyphOrder": 787 return ident 788 if len(ident) % 2 and ident[0] == "_": 789 ident = ident[1:] 790 assert not (len(ident) % 2) 791 tag = "" 792 for i in range(0, len(ident), 2): 793 if ident[i] == "_": 794 tag = tag + ident[i+1] 795 elif ident[i+1] == "_": 796 tag = tag + ident[i] 797 else: 798 # assume hex 799 tag = tag + chr(int(ident[i:i+2], 16)) 800 # append trailing spaces 801 tag = tag + (4 - len(tag)) * ' ' 802 return tag 803 804 805def tagToXML(tag): 806 """Similarly to tagToIdentifier(), this converts a TT tag 807 to a valid XML element name. Since XML element names are 808 case sensitive, this is a fairly simple/readable translation. 809 """ 810 import re 811 if tag == "OS/2": 812 return "OS_2" 813 elif tag == "GlyphOrder": 814 return tag 815 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 816 return string.strip(tag) 817 else: 818 return tagToIdentifier(tag) 819 820 821def xmlToTag(tag): 822 """The opposite of tagToXML()""" 823 if tag == "OS_2": 824 return "OS/2" 825 if len(tag) == 8: 826 return identifierToTag(tag) 827 else: 828 return tag + " " * (4 - len(tag)) 829 return tag 830 831 832def debugmsg(msg): 833 import time 834 print msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())) 835 836 837# Table order as recommended in the OpenType specification 1.4 838TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", 839 "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", 840 "kern", "name", "post", "gasp", "PCLT"] 841 842OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", 843 "CFF "] 844 845def sortedTagList(tagList, tableOrder=None): 846 """Return a sorted copy of tagList, sorted according to the OpenType 847 specification, or according to a custom tableOrder. If given and not 848 None, tableOrder needs to be a list of tag names. 849 """ 850 tagList = list(tagList) 851 tagList.sort() 852 if tableOrder is None: 853 if "DSIG" in tagList: 854 # DSIG should be last (XXX spec reference?) 855 tagList.remove("DSIG") 856 tagList.append("DSIG") 857 if "CFF " in tagList: 858 tableOrder = OTFTableOrder 859 else: 860 tableOrder = TTFTableOrder 861 orderedTables = [] 862 for tag in tableOrder: 863 if tag in tagList: 864 orderedTables.append(tag) 865 tagList.remove(tag) 866 orderedTables.extend(tagList) 867 return orderedTables 868 869 870def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=0): 871 """Rewrite a font file, ordering the tables as recommended by the 872 OpenType specification 1.4. 873 """ 874 from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 875 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 876 writer = SFNTWriter(outFile, reader.numTables, reader.sfntVersion) 877 tables = reader.keys() 878 for tag in sortedTagList(tables, tableOrder): 879 writer[tag] = reader[tag] 880 writer.close() 881