__init__.py revision 79c1316d6561f3ae3196425008883c1872f1f814
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.46 2005-01-24 10:06:45 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 601class _TTGlyph: 602 603 """Wrapper for a TrueType glyph that supports the Pen protocol, meaning 604 that it has a .draw() method that takes a pen object as its only 605 argument. Additionally there is a 'width' attribute. 606 """ 607 608 def __init__(self, glyphName, ttFont): 609 self._glyphName = glyphName 610 self._ttFont = ttFont 611 self.width, self.lsb = self._ttFont['hmtx'][self._glyphName] 612 613 def draw(self, pen): 614 """Draw the glyph onto Pen. See fontTools.pens.basePen for details 615 how that works. 616 """ 617 glyfTable = self._ttFont['glyf'] 618 glyph = glyfTable[self._glyphName] 619 if hasattr(glyph, "xMin"): 620 offset = self.lsb - glyph.xMin 621 else: 622 offset = 0 623 if glyph.isComposite(): 624 for component in glyph: 625 glyphName, transform = component.getComponentInfo() 626 pen.addComponent(glyphName, transform) 627 else: 628 coordinates, endPts, flags = glyph.getCoordinates(glyfTable) 629 if offset: 630 coordinates = coordinates + (offset, 0) 631 start = 0 632 for end in endPts: 633 end = end + 1 634 contour = coordinates[start:end].tolist() 635 cFlags = flags[start:end].tolist() 636 start = end 637 if 1 not in cFlags: 638 # There is not a single on-curve point on the curve, 639 # use pen.qCurveTo's special case by specifying None 640 # as the on-curve point. 641 contour.append(None) 642 pen.qCurveTo(*contour) 643 else: 644 # Shuffle the points so that contour the is guaranteed 645 # to *end* in an on-curve point, which we'll use for 646 # the moveTo. 647 firstOnCurve = cFlags.index(1) + 1 648 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 649 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 650 pen.moveTo(contour[-1]) 651 while contour: 652 nextOnCurve = cFlags.index(1) + 1 653 if nextOnCurve == 1: 654 pen.lineTo(contour[0]) 655 else: 656 pen.qCurveTo(*contour[:nextOnCurve]) 657 contour = contour[nextOnCurve:] 658 cFlags = cFlags[nextOnCurve:] 659 pen.closePath() 660 661 662class GlyphOrder: 663 664 """A pseudo table. The glyph order isn't in the font as a separate 665 table, but it's nice to present it as such in the TTX format. 666 """ 667 668 def __init__(self, tag): 669 pass 670 671 def toXML(self, writer, ttFont): 672 glyphOrder = ttFont.getGlyphOrder() 673 writer.comment("The 'id' attribute is only for humans; " 674 "it is ignored when parsed.") 675 writer.newline() 676 for i in range(len(glyphOrder)): 677 glyphName = glyphOrder[i] 678 writer.simpletag("GlyphID", id=i, name=glyphName) 679 writer.newline() 680 681 def fromXML(self, (name, attrs, content), ttFont): 682 if not hasattr(self, "glyphOrder"): 683 self.glyphOrder = [] 684 ttFont.setGlyphOrder(self.glyphOrder) 685 if name == "GlyphID": 686 self.glyphOrder.append(attrs["name"]) 687 688 689def _test_endianness(): 690 """Test the endianness of the machine. This is crucial to know 691 since TrueType data is always big endian, even on little endian 692 machines. There are quite a few situations where we explicitly 693 need to swap some bytes. 694 """ 695 import struct 696 data = struct.pack("h", 0x01) 697 if data == "\000\001": 698 return "big" 699 elif data == "\001\000": 700 return "little" 701 else: 702 assert 0, "endian confusion!" 703 704endian = _test_endianness() 705 706 707def getTableModule(tag): 708 """Fetch the packer/unpacker module for a table. 709 Return None when no module is found. 710 """ 711 import tables 712 pyTag = tagToIdentifier(tag) 713 try: 714 __import__("fontTools.ttLib.tables." + pyTag) 715 except ImportError: 716 return None 717 else: 718 return getattr(tables, pyTag) 719 720 721def getTableClass(tag): 722 """Fetch the packer/unpacker class for a table. 723 Return None when no class is found. 724 """ 725 module = getTableModule(tag) 726 if module is None: 727 from tables.DefaultTable import DefaultTable 728 return DefaultTable 729 pyTag = tagToIdentifier(tag) 730 tableClass = getattr(module, "table_" + pyTag) 731 return tableClass 732 733 734def newTable(tag): 735 """Return a new instance of a table.""" 736 tableClass = getTableClass(tag) 737 return tableClass(tag) 738 739 740def _escapechar(c): 741 """Helper function for tagToIdentifier()""" 742 import re 743 if re.match("[a-z0-9]", c): 744 return "_" + c 745 elif re.match("[A-Z]", c): 746 return c + "_" 747 else: 748 return hex(ord(c))[2:] 749 750 751def tagToIdentifier(tag): 752 """Convert a table tag to a valid (but UGLY) python identifier, 753 as well as a filename that's guaranteed to be unique even on a 754 caseless file system. Each character is mapped to two characters. 755 Lowercase letters get an underscore before the letter, uppercase 756 letters get an underscore after the letter. Trailing spaces are 757 trimmed. Illegal characters are escaped as two hex bytes. If the 758 result starts with a number (as the result of a hex escape), an 759 extra underscore is prepended. Examples: 760 'glyf' -> '_g_l_y_f' 761 'cvt ' -> '_c_v_t' 762 'OS/2' -> 'O_S_2f_2' 763 """ 764 import re 765 if tag == "GlyphOrder": 766 return tag 767 assert len(tag) == 4, "tag should be 4 characters long" 768 while len(tag) > 1 and tag[-1] == ' ': 769 tag = tag[:-1] 770 ident = "" 771 for c in tag: 772 ident = ident + _escapechar(c) 773 if re.match("[0-9]", ident): 774 ident = "_" + ident 775 return ident 776 777 778def identifierToTag(ident): 779 """the opposite of tagToIdentifier()""" 780 if ident == "GlyphOrder": 781 return ident 782 if len(ident) % 2 and ident[0] == "_": 783 ident = ident[1:] 784 assert not (len(ident) % 2) 785 tag = "" 786 for i in range(0, len(ident), 2): 787 if ident[i] == "_": 788 tag = tag + ident[i+1] 789 elif ident[i+1] == "_": 790 tag = tag + ident[i] 791 else: 792 # assume hex 793 tag = tag + chr(int(ident[i:i+2], 16)) 794 # append trailing spaces 795 tag = tag + (4 - len(tag)) * ' ' 796 return tag 797 798 799def tagToXML(tag): 800 """Similarly to tagToIdentifier(), this converts a TT tag 801 to a valid XML element name. Since XML element names are 802 case sensitive, this is a fairly simple/readable translation. 803 """ 804 import re 805 if tag == "OS/2": 806 return "OS_2" 807 elif tag == "GlyphOrder": 808 return tag 809 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 810 return string.strip(tag) 811 else: 812 return tagToIdentifier(tag) 813 814 815def xmlToTag(tag): 816 """The opposite of tagToXML()""" 817 if tag == "OS_2": 818 return "OS/2" 819 if len(tag) == 8: 820 return identifierToTag(tag) 821 else: 822 return tag + " " * (4 - len(tag)) 823 return tag 824 825 826def debugmsg(msg): 827 import time 828 print msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())) 829 830 831# Table order as recommended in the OpenType specification 1.4 832TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", 833 "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", 834 "kern", "name", "post", "gasp", "PCLT"] 835 836OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", 837 "CFF "] 838 839def sortedTagList(tagList, tableOrder=None): 840 """Return a sorted copy of tagList, sorted according to the OpenType 841 specification, or according to a custom tableOrder. If given and not 842 None, tableOrder needs to be a list of tag names. 843 """ 844 tagList = list(tagList) 845 tagList.sort() 846 if tableOrder is None: 847 if "DSIG" in tagList: 848 # DSIG should be last (XXX spec reference?) 849 tagList.remove("DSIG") 850 tagList.append("DSIG") 851 if "CFF " in tagList: 852 tableOrder = OTFTableOrder 853 else: 854 tableOrder = TTFTableOrder 855 orderedTables = [] 856 for tag in tableOrder: 857 if tag in tagList: 858 orderedTables.append(tag) 859 tagList.remove(tag) 860 orderedTables.extend(tagList) 861 return orderedTables 862 863 864def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=0): 865 """Rewrite a font file, ordering the tables as recommended by the 866 OpenType specification 1.4. 867 """ 868 from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 869 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 870 writer = SFNTWriter(outFile, reader.numTables, reader.sfntVersion) 871 tables = reader.keys() 872 for tag in sortedTagList(tables, tableOrder): 873 writer[tag] = reader[tag] 874 writer.close() 875