__init__.py revision 9f1e14bec9dad6d59300bbff6a5a9d5a8d0828f9
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.22 2002-05-05 11:29:33 jvr Exp $ 46# 47 48import os 49import string 50import types 51 52 53class TTLibError(Exception): pass 54 55 56class TTFont: 57 58 """The main font object. It manages file input and output, and offers 59 a convenient way of accessing tables. 60 Tables will be only decompiled when neccesary, ie. when they're actually 61 accessed. This means that simple operations can be extremely fast. 62 """ 63 64 def __init__(self, file=None, res_name_or_index=None, 65 sfntVersion="\000\001\000\000", checkchecksums=0, 66 verbose=0, recalcBBoxes=1): 67 68 """The constructor can be called with a few different arguments. 69 When reading a font from disk, 'file' should be either a pathname 70 pointing to a file, or a readable file object. 71 72 It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt 73 resource name or an sfnt resource index number or zero. The latter 74 case will cause TTLib to autodetect whether the file is a flat file 75 or a suitcase. (If it's a suitcase, only the first 'sfnt' resource 76 will be read!) 77 78 The 'checkchecksums' argument is used to specify how sfnt 79 checksums are treated upon reading a file from disk: 80 0: don't check (default) 81 1: check, print warnings if a wrong checksum is found (default) 82 2: check, raise an exception if a wrong checksum is found. 83 84 The TTFont constructor can also be called without a 'file' 85 argument: this is the way to create a new empty font. 86 In this case you can optionally supply the 'sfntVersion' argument. 87 88 If the recalcBBoxes argument is false, a number of things will *not* 89 be recalculated upon save/compile: 90 1) glyph bounding boxes 91 2) maxp font bounding box 92 3) hhea min/max values 93 (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). 94 Additionally, upon importing an TTX file, this option cause glyphs 95 to be compiled right away. This should reduce memory consumption 96 greatly, and therefore should have some impact on the time needed 97 to parse/compile large fonts. 98 """ 99 100 import sfnt 101 self.verbose = verbose 102 self.recalcBBoxes = recalcBBoxes 103 self.tables = {} 104 self.reader = None 105 if not file: 106 self.sfntVersion = sfntVersion 107 return 108 if type(file) == types.StringType: 109 if os.name == "mac" and res_name_or_index is not None: 110 # on the mac, we deal with sfnt resources as well as flat files 111 import macUtils 112 if res_name_or_index == 0: 113 if macUtils.getSFNTResIndices(file): 114 # get the first available sfnt font. 115 file = macUtils.SFNTResourceReader(file, 1) 116 else: 117 file = open(file, "rb") 118 else: 119 file = macUtils.SFNTResourceReader(file, res_name_or_index) 120 else: 121 file = open(file, "rb") 122 else: 123 pass # assume "file" is a readable file object 124 self.reader = sfnt.SFNTReader(file, checkchecksums) 125 self.sfntVersion = self.reader.sfntVersion 126 127 def close(self): 128 """If we still have a reader object, close it.""" 129 if self.reader is not None: 130 self.reader.close() 131 132 def save(self, file, makeSuitcase=0): 133 """Save the font to disk. Similarly to the constructor, 134 the 'file' argument can be either a pathname or a writable 135 file object. 136 137 On the Mac, if makeSuitcase is true, a suitcase (resource fork) 138 file will we made instead of a flat .ttf file. 139 """ 140 from fontTools.ttLib import sfnt 141 if type(file) == types.StringType: 142 closeStream = 1 143 if os.name == "mac" and makeSuitcase: 144 import macUtils 145 file = macUtils.SFNTResourceWriter(file, self) 146 else: 147 file = open(file, "wb") 148 if os.name == "mac": 149 import macfs 150 fss = macfs.FSSpec(file.name) 151 fss.SetCreatorType('mdos', 'BINA') 152 else: 153 # assume "file" is a writable file object 154 closeStream = 0 155 156 tags = self.keys() 157 numTables = len(tags) 158 writer = sfnt.SFNTWriter(file, numTables, self.sfntVersion) 159 160 done = [] 161 for tag in tags: 162 self._writeTable(tag, writer, done) 163 164 writer.close(closeStream) 165 166 def saveXML(self, fileOrPath, progress=None, 167 tables=None, skipTables=None, splitTables=0, disassembleInstructions=1): 168 """Export the font as TTX (an XML-based text file), or as a series of text 169 files when splitTables is true. In the latter case, the 'fileOrPath' 170 argument should be a path to a directory. 171 The 'tables' argument must either be false (dump all tables) or a 172 list of tables to dump. The 'skipTables' argument may be a list of tables 173 to skip, but only when the 'tables' argument is false. 174 """ 175 from fontTools import version 176 import xmlWriter 177 178 self.disassembleInstructions = disassembleInstructions 179 if not tables: 180 tables = self.keys() 181 if skipTables: 182 for tag in skipTables: 183 if tag in tables: 184 tables.remove(tag) 185 numTables = len(tables) 186 numGlyphs = self['maxp'].numGlyphs 187 if progress: 188 progress.set(0, numTables * numGlyphs) 189 if not splitTables: 190 writer = xmlWriter.XMLWriter(fileOrPath) 191 writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1], 192 ttLibVersion=version) 193 writer.newline() 194 writer.newline() 195 else: 196 # 'fileOrPath' must now be a path (pointing to a directory) 197 if not os.path.exists(fileOrPath): 198 os.mkdir(fileOrPath) 199 fileNameTemplate = os.path.join(fileOrPath, 200 os.path.basename(fileOrPath)) + ".%s.ttx" 201 202 for i in range(numTables): 203 tag = tables[i] 204 if splitTables: 205 writer = xmlWriter.XMLWriter(fileNameTemplate % tag2identifier(tag)) 206 writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1], 207 ttLibVersion=version) 208 writer.newline() 209 writer.newline() 210 table = self[tag] 211 report = "Dumping '%s' table..." % tag 212 if progress: 213 progress.setlabel(report) 214 elif self.verbose: 215 debugmsg(report) 216 else: 217 print report 218 xmltag = tag2xmltag(tag) 219 if hasattr(table, "ERROR"): 220 writer.begintag(xmltag, ERROR="decompilation error") 221 else: 222 writer.begintag(xmltag) 223 writer.newline() 224 if tag == "glyf": 225 table.toXML(writer, self, progress) 226 elif tag == "CFF ": 227 table.toXML(writer, self, progress) 228 else: 229 table.toXML(writer, self) 230 writer.endtag(xmltag) 231 writer.newline() 232 writer.newline() 233 if splitTables: 234 writer.endtag("ttFont") 235 writer.newline() 236 writer.close() 237 if progress: 238 progress.set(i * numGlyphs, numTables * numGlyphs) 239 if not splitTables: 240 writer.endtag("ttFont") 241 writer.newline() 242 writer.close() 243 if self.verbose: 244 debugmsg("Done dumping TTX") 245 246 def importXML(self, file, progress=None): 247 """Import a TTX file (an XML-based text format), so as to recreate 248 a font object. 249 """ 250 import xmlImport 251 xmlImport.importXML(self, file, progress) 252 253 def isLoaded(self, tag): 254 """Return true if the table identified by 'tag' has been 255 decompiled and loaded into memory.""" 256 return self.tables.has_key(tag) 257 258 def has_key(self, tag): 259 if self.isLoaded(tag): 260 return 1 261 elif self.reader and self.reader.has_key(tag): 262 return 1 263 else: 264 return 0 265 266 def keys(self): 267 keys = self.tables.keys() 268 if self.reader: 269 for key in self.reader.keys(): 270 if key not in keys: 271 keys.append(key) 272 keys.sort() 273 return keys 274 275 def __len__(self): 276 return len(self.keys()) 277 278 def __getitem__(self, tag): 279 try: 280 return self.tables[tag] 281 except KeyError: 282 if self.reader is not None: 283 import traceback 284 if self.verbose: 285 debugmsg("reading '%s' table from disk" % tag) 286 data = self.reader[tag] 287 tableclass = getTableClass(tag) 288 table = tableclass(tag) 289 self.tables[tag] = table 290 if self.verbose: 291 debugmsg("decompiling '%s' table" % tag) 292 try: 293 table.decompile(data, self) 294 except "_ _ F O O _ _": # dummy exception to disable exception catching 295 print "An exception occurred during the decompilation of the '%s' table" % tag 296 from tables.DefaultTable import DefaultTable 297 import StringIO 298 file = StringIO.StringIO() 299 traceback.print_exc(file=file) 300 table = DefaultTable(tag) 301 table.ERROR = file.getvalue() 302 self.tables[tag] = table 303 table.decompile(data, self) 304 return table 305 else: 306 raise KeyError, "'%s' table not found" % tag 307 308 def __setitem__(self, tag, table): 309 self.tables[tag] = table 310 311 def __delitem__(self, tag): 312 if not self.has_key(tag): 313 raise KeyError, "'%s' table not found" % tag 314 if self.tables.has_key(tag): 315 del self.tables[tag] 316 if self.reader and self.reader.has_key(tag): 317 del self.reader[tag] 318 319 def setGlyphOrder(self, glyphOrder): 320 self.glyphOrder = glyphOrder 321 if self.has_key('CFF '): 322 self['CFF '].setGlyphOrder(glyphOrder) 323 if self.has_key('glyf'): 324 self['glyf'].setGlyphOrder(glyphOrder) 325 326 def getGlyphOrder(self): 327 try: 328 return self.glyphOrder 329 except AttributeError: 330 pass 331 if self.has_key('CFF '): 332 # CFF OpenType font 333 self.glyphOrder = self['CFF '].getGlyphOrder() 334 elif self.has_key('post'): 335 # TrueType font 336 glyphOrder = self['post'].getGlyphOrder() 337 if glyphOrder is None: 338 # 339 # No names found in the 'post' table. 340 # Try to create glyph names from the unicode cmap (if available) 341 # in combination with the Adobe Glyph List (AGL). 342 # 343 self._getGlyphNamesFromCmap() 344 else: 345 self.glyphOrder = glyphOrder 346 else: 347 self._getGlyphNamesFromCmap() 348 return self.glyphOrder 349 350 def _getGlyphNamesFromCmap(self): 351 # 352 # This is rather convoluted, but then again, it's an interesting problem: 353 # - we need to use the unicode values found in the cmap table to 354 # build glyph names (eg. because there is only a minimal post table, 355 # or none at all). 356 # - but the cmap parser also needs glyph names to work with... 357 # So here's what we do: 358 # - make up glyph names based on glyphID 359 # - load a temporary cmap table based on those names 360 # - extract the unicode values, build the "real" glyph names 361 # - unload the temporary cmap table 362 # 363 if self.isLoaded("cmap"): 364 # Bootstrapping: we're getting called by the cmap parser 365 # itself. This means self.tables['cmap'] contains a partially 366 # loaded cmap, making it impossible to get at a unicode 367 # subtable here. We remove the partially loaded cmap and 368 # restore it later. 369 # This only happens if the cmap table is loaded before any 370 # other table that does f.getGlyphOrder() or f.getGlyphName(). 371 cmapLoading = self.tables['cmap'] 372 del self.tables['cmap'] 373 else: 374 cmapLoading = None 375 # Make up glyph names based on glyphID, which will be used by the 376 # temporary cmap and by the real cmap in case we don't find a unicode 377 # cmap. 378 numGlyphs = int(self['maxp'].numGlyphs) 379 glyphOrder = [None] * numGlyphs 380 glyphOrder[0] = ".notdef" 381 for i in range(1, numGlyphs): 382 glyphOrder[i] = "glyph%.5d" % i 383 # Set the glyph order, so the cmap parser has something 384 # to work with (so we don't get called recursively). 385 self.glyphOrder = glyphOrder 386 # Get a (new) temporary cmap (based on the just invented names) 387 tempcmap = self['cmap'].getcmap(3, 1) 388 if tempcmap is not None: 389 # we have a unicode cmap 390 from fontTools import agl 391 cmap = tempcmap.cmap 392 # create a reverse cmap dict 393 reversecmap = {} 394 for unicode, name in cmap.items(): 395 reversecmap[name] = unicode 396 allNames = {} 397 for i in range(numGlyphs): 398 tempName = glyphOrder[i] 399 if reversecmap.has_key(tempName): 400 unicode = reversecmap[tempName] 401 if agl.UV2AGL.has_key(unicode): 402 # get name from the Adobe Glyph List 403 glyphName = agl.UV2AGL[unicode] 404 else: 405 # create uni<CODE> name 406 glyphName = "uni" + string.upper(string.zfill( 407 hex(unicode)[2:], 4)) 408 tempName = glyphName 409 n = 1 410 while allNames.has_key(tempName): 411 tempName = glyphName + "#" + `n` 412 n = n + 1 413 glyphOrder[i] = tempName 414 allNames[tempName] = 1 415 # Delete the temporary cmap table from the cache, so it can 416 # be parsed again with the right names. 417 del self.tables['cmap'] 418 else: 419 pass # no unicode cmap available, stick with the invented names 420 self.glyphOrder = glyphOrder 421 if cmapLoading: 422 # restore partially loaded cmap, so it can continue loading 423 # using the proper names. 424 self.tables['cmap'] = cmapLoading 425 426 def getGlyphNames(self): 427 """Get a list of glyph names, sorted alphabetically.""" 428 glyphNames = self.getGlyphOrder()[:] 429 glyphNames.sort() 430 return glyphNames 431 432 def getGlyphNames2(self): 433 """Get a list of glyph names, sorted alphabetically, 434 but not case sensitive. 435 """ 436 from fontTools.misc import textTools 437 return textTools.caselessSort(self.getGlyphOrder()) 438 439 def getGlyphName(self, glyphID): 440 return self.getGlyphOrder()[glyphID] 441 442 def getGlyphID(self, glyphName): 443 if not hasattr(self, "_reverseGlyphOrderDict"): 444 self._buildReverseGlyphOrderDict() 445 glyphOrder = self.getGlyphOrder() 446 d = self._reverseGlyphOrderDict 447 if not d.has_key(glyphName): 448 if glyphName in glyphOrder: 449 self._buildReverseGlyphOrderDict() 450 return self.getGlyphID(glyphName) 451 else: 452 raise KeyError, glyphName 453 glyphID = d[glyphName] 454 if glyphName <> glyphOrder[glyphID]: 455 self._buildReverseGlyphOrderDict() 456 return self.getGlyphID(glyphName) 457 return glyphID 458 459 def _buildReverseGlyphOrderDict(self): 460 self._reverseGlyphOrderDict = d = {} 461 glyphOrder = self.getGlyphOrder() 462 for glyphID in range(len(glyphOrder)): 463 d[glyphOrder[glyphID]] = glyphID 464 465 def _writeTable(self, tag, writer, done): 466 """Internal helper function for self.save(). Keeps track of 467 inter-table dependencies. 468 """ 469 if tag in done: 470 return 471 tableclass = getTableClass(tag) 472 for masterTable in tableclass.dependencies: 473 if masterTable not in done: 474 if self.has_key(masterTable): 475 self._writeTable(masterTable, writer, done) 476 else: 477 done.append(masterTable) 478 tabledata = self.getTableData(tag) 479 if self.verbose: 480 debugmsg("writing '%s' table to disk" % tag) 481 writer[tag] = tabledata 482 done.append(tag) 483 484 def getTableData(self, tag): 485 """Returns raw table data, whether compiled or directly read from disk. 486 """ 487 if self.isLoaded(tag): 488 if self.verbose: 489 debugmsg("compiling '%s' table" % tag) 490 return self.tables[tag].compile(self) 491 elif self.reader and self.reader.has_key(tag): 492 if self.verbose: 493 debugmsg("reading '%s' table from disk" % tag) 494 return self.reader[tag] 495 else: 496 raise KeyError, tag 497 498 499def _test_endianness(): 500 """Test the endianness of the machine. This is crucial to know 501 since TrueType data is always big endian, even on little endian 502 machines. There are quite a few situations where we explicitly 503 need to swap some bytes. 504 """ 505 import struct 506 data = struct.pack("h", 0x01) 507 if data == "\000\001": 508 return "big" 509 elif data == "\001\000": 510 return "little" 511 else: 512 assert 0, "endian confusion!" 513 514endian = _test_endianness() 515 516 517def getTableModule(tag): 518 """Fetch the packer/unpacker module for a table. 519 Return None when no module is found. 520 """ 521 import imp 522 import tables 523 py_tag = tag2identifier(tag) 524 try: 525 f, path, kind = imp.find_module(py_tag, tables.__path__) 526 if f: 527 f.close() 528 except ImportError: 529 return None 530 else: 531 module = __import__("fontTools.ttLib.tables." + py_tag) 532 return getattr(tables, py_tag) 533 534 535def getTableClass(tag): 536 """Fetch the packer/unpacker class for a table. 537 Return None when no class is found. 538 """ 539 module = getTableModule(tag) 540 if module is None: 541 from tables.DefaultTable import DefaultTable 542 return DefaultTable 543 py_tag = tag2identifier(tag) 544 tableclass = getattr(module, "table_" + py_tag) 545 return tableclass 546 547 548def getNewTable(tag): 549 """Return a new instance of a table.""" 550 tableclass = getTableClass(tag) 551 return tableclass(tag) 552 553 554def _escapechar(c): 555 """Helper function for tag2identifier()""" 556 import re 557 if re.match("[a-z0-9]", c): 558 return "_" + c 559 elif re.match("[A-Z]", c): 560 return c + "_" 561 else: 562 return hex(ord(c))[2:] 563 564 565def tag2identifier(tag): 566 """Convert a table tag to a valid (but UGLY) python identifier, 567 as well as a filename that's guaranteed to be unique even on a 568 caseless file system. Each character is mapped to two characters. 569 Lowercase letters get an underscore before the letter, uppercase 570 letters get an underscore after the letter. Trailing spaces are 571 trimmed. Illegal characters are escaped as two hex bytes. If the 572 result starts with a number (as the result of a hex escape), an 573 extra underscore is prepended. Examples: 574 'glyf' -> '_g_l_y_f' 575 'cvt ' -> '_c_v_t' 576 'OS/2' -> 'O_S_2f_2' 577 """ 578 import re 579 assert len(tag) == 4, "tag should be 4 characters long" 580 while len(tag) > 1 and tag[-1] == ' ': 581 tag = tag[:-1] 582 ident = "" 583 for c in tag: 584 ident = ident + _escapechar(c) 585 if re.match("[0-9]", ident): 586 ident = "_" + ident 587 return ident 588 589 590def identifier2tag(ident): 591 """the opposite of tag2identifier()""" 592 if len(ident) % 2 and ident[0] == "_": 593 ident = ident[1:] 594 assert not (len(ident) % 2) 595 tag = "" 596 for i in range(0, len(ident), 2): 597 if ident[i] == "_": 598 tag = tag + ident[i+1] 599 elif ident[i+1] == "_": 600 tag = tag + ident[i] 601 else: 602 # assume hex 603 tag = tag + chr(string.atoi(ident[i:i+2], 16)) 604 # append trailing spaces 605 tag = tag + (4 - len(tag)) * ' ' 606 return tag 607 608 609def tag2xmltag(tag): 610 """Similarly to tag2identifier(), this converts a TT tag 611 to a valid XML element name. Since XML element names are 612 case sensitive, this is a fairly simple/readable translation. 613 """ 614 import re 615 if tag == "OS/2": 616 return "OS_2" 617 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 618 return string.strip(tag) 619 else: 620 return tag2identifier(tag) 621 622 623def xmltag2tag(tag): 624 """The opposite of tag2xmltag()""" 625 if tag == "OS_2": 626 return "OS/2" 627 if len(tag) == 8: 628 return identifier2tag(tag) 629 else: 630 return tag + " " * (4 - len(tag)) 631 return tag 632 633 634def debugmsg(msg): 635 import time 636 print msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())) 637 638 639