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