__init__.py revision 0f293034749578d29494c2560c042c01ced50601
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.24 2002-05-11 21:18:12 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 197 path, ext = os.path.splitext(fileOrPath) 198 fileNameTemplate = path + ".%s" + ext 199 collection = xmlWriter.XMLWriter(fileOrPath) 200 collection.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1], 201 ttLibVersion=version) 202 collection.newline() 203 204 for i in range(numTables): 205 tag = tables[i] 206 xmltag = tag2xmltag(tag) 207 if splitTables: 208 tablePath = fileNameTemplate % tag2identifier(tag) 209 writer = xmlWriter.XMLWriter(tablePath) 210 writer.begintag("ttFont", ttLibVersion=version) 211 writer.newline() 212 writer.newline() 213 collection.simpletag(xmltag, src=os.path.basename(tablePath)) 214 collection.newline() 215 table = self[tag] 216 report = "Dumping '%s' table..." % tag 217 if progress: 218 progress.setlabel(report) 219 elif self.verbose: 220 debugmsg(report) 221 else: 222 print report 223 if hasattr(table, "ERROR"): 224 writer.begintag(xmltag, ERROR="decompilation error") 225 else: 226 writer.begintag(xmltag) 227 writer.newline() 228 if tag in ("glyf", "CFF "): 229 table.toXML(writer, self, progress) 230 else: 231 table.toXML(writer, self) 232 writer.endtag(xmltag) 233 writer.newline() 234 writer.newline() 235 if splitTables: 236 writer.endtag("ttFont") 237 writer.newline() 238 writer.close() 239 if progress: 240 progress.set(i * numGlyphs, numTables * numGlyphs) 241 if not splitTables: 242 writer.endtag("ttFont") 243 writer.newline() 244 writer.close() 245 else: 246 collection.endtag("ttFont") 247 collection.newline() 248 collection.close() 249 if self.verbose: 250 debugmsg("Done dumping TTX") 251 252 def importXML(self, file, progress=None): 253 """Import a TTX file (an XML-based text format), so as to recreate 254 a font object. 255 """ 256 import xmlImport 257 xmlImport.importXML(self, file, progress) 258 259 def isLoaded(self, tag): 260 """Return true if the table identified by 'tag' has been 261 decompiled and loaded into memory.""" 262 return self.tables.has_key(tag) 263 264 def has_key(self, tag): 265 if self.isLoaded(tag): 266 return 1 267 elif self.reader and self.reader.has_key(tag): 268 return 1 269 else: 270 return 0 271 272 def keys(self): 273 keys = self.tables.keys() 274 if self.reader: 275 for key in self.reader.keys(): 276 if key not in keys: 277 keys.append(key) 278 keys.sort() 279 return keys 280 281 def __len__(self): 282 return len(self.keys()) 283 284 def __getitem__(self, tag): 285 try: 286 return self.tables[tag] 287 except KeyError: 288 if self.reader is not None: 289 import traceback 290 if self.verbose: 291 debugmsg("reading '%s' table from disk" % tag) 292 data = self.reader[tag] 293 tableclass = getTableClass(tag) 294 table = tableclass(tag) 295 self.tables[tag] = table 296 if self.verbose: 297 debugmsg("decompiling '%s' table" % tag) 298 try: 299 table.decompile(data, self) 300 except "_ _ F O O _ _": # dummy exception to disable exception catching 301 print "An exception occurred during the decompilation of the '%s' table" % tag 302 from tables.DefaultTable import DefaultTable 303 import StringIO 304 file = StringIO.StringIO() 305 traceback.print_exc(file=file) 306 table = DefaultTable(tag) 307 table.ERROR = file.getvalue() 308 self.tables[tag] = table 309 table.decompile(data, self) 310 return table 311 else: 312 raise KeyError, "'%s' table not found" % tag 313 314 def __setitem__(self, tag, table): 315 self.tables[tag] = table 316 317 def __delitem__(self, tag): 318 if not self.has_key(tag): 319 raise KeyError, "'%s' table not found" % tag 320 if self.tables.has_key(tag): 321 del self.tables[tag] 322 if self.reader and self.reader.has_key(tag): 323 del self.reader[tag] 324 325 def setGlyphOrder(self, glyphOrder): 326 self.glyphOrder = glyphOrder 327 if self.has_key('CFF '): 328 self['CFF '].setGlyphOrder(glyphOrder) 329 if self.has_key('glyf'): 330 self['glyf'].setGlyphOrder(glyphOrder) 331 332 def getGlyphOrder(self): 333 try: 334 return self.glyphOrder 335 except AttributeError: 336 pass 337 if self.has_key('CFF '): 338 # CFF OpenType font 339 self.glyphOrder = self['CFF '].getGlyphOrder() 340 elif self.has_key('post'): 341 # TrueType font 342 glyphOrder = self['post'].getGlyphOrder() 343 if glyphOrder is None: 344 # 345 # No names found in the 'post' table. 346 # Try to create glyph names from the unicode cmap (if available) 347 # in combination with the Adobe Glyph List (AGL). 348 # 349 self._getGlyphNamesFromCmap() 350 else: 351 self.glyphOrder = glyphOrder 352 else: 353 self._getGlyphNamesFromCmap() 354 return self.glyphOrder 355 356 def _getGlyphNamesFromCmap(self): 357 # 358 # This is rather convoluted, but then again, it's an interesting problem: 359 # - we need to use the unicode values found in the cmap table to 360 # build glyph names (eg. because there is only a minimal post table, 361 # or none at all). 362 # - but the cmap parser also needs glyph names to work with... 363 # So here's what we do: 364 # - make up glyph names based on glyphID 365 # - load a temporary cmap table based on those names 366 # - extract the unicode values, build the "real" glyph names 367 # - unload the temporary cmap table 368 # 369 if self.isLoaded("cmap"): 370 # Bootstrapping: we're getting called by the cmap parser 371 # itself. This means self.tables['cmap'] contains a partially 372 # loaded cmap, making it impossible to get at a unicode 373 # subtable here. We remove the partially loaded cmap and 374 # restore it later. 375 # This only happens if the cmap table is loaded before any 376 # other table that does f.getGlyphOrder() or f.getGlyphName(). 377 cmapLoading = self.tables['cmap'] 378 del self.tables['cmap'] 379 else: 380 cmapLoading = None 381 # Make up glyph names based on glyphID, which will be used by the 382 # temporary cmap and by the real cmap in case we don't find a unicode 383 # cmap. 384 numGlyphs = int(self['maxp'].numGlyphs) 385 glyphOrder = [None] * numGlyphs 386 glyphOrder[0] = ".notdef" 387 for i in range(1, numGlyphs): 388 glyphOrder[i] = "glyph%.5d" % i 389 # Set the glyph order, so the cmap parser has something 390 # to work with (so we don't get called recursively). 391 self.glyphOrder = glyphOrder 392 # Get a (new) temporary cmap (based on the just invented names) 393 tempcmap = self['cmap'].getcmap(3, 1) 394 if tempcmap is not None: 395 # we have a unicode cmap 396 from fontTools import agl 397 cmap = tempcmap.cmap 398 # create a reverse cmap dict 399 reversecmap = {} 400 for unicode, name in cmap.items(): 401 reversecmap[name] = unicode 402 allNames = {} 403 for i in range(numGlyphs): 404 tempName = glyphOrder[i] 405 if reversecmap.has_key(tempName): 406 unicode = reversecmap[tempName] 407 if agl.UV2AGL.has_key(unicode): 408 # get name from the Adobe Glyph List 409 glyphName = agl.UV2AGL[unicode] 410 else: 411 # create uni<CODE> name 412 glyphName = "uni" + string.upper(string.zfill( 413 hex(unicode)[2:], 4)) 414 tempName = glyphName 415 n = 1 416 while allNames.has_key(tempName): 417 tempName = glyphName + "#" + `n` 418 n = n + 1 419 glyphOrder[i] = tempName 420 allNames[tempName] = 1 421 # Delete the temporary cmap table from the cache, so it can 422 # be parsed again with the right names. 423 del self.tables['cmap'] 424 else: 425 pass # no unicode cmap available, stick with the invented names 426 self.glyphOrder = glyphOrder 427 if cmapLoading: 428 # restore partially loaded cmap, so it can continue loading 429 # using the proper names. 430 self.tables['cmap'] = cmapLoading 431 432 def getGlyphNames(self): 433 """Get a list of glyph names, sorted alphabetically.""" 434 glyphNames = self.getGlyphOrder()[:] 435 glyphNames.sort() 436 return glyphNames 437 438 def getGlyphNames2(self): 439 """Get a list of glyph names, sorted alphabetically, 440 but not case sensitive. 441 """ 442 from fontTools.misc import textTools 443 return textTools.caselessSort(self.getGlyphOrder()) 444 445 def getGlyphName(self, glyphID): 446 return self.getGlyphOrder()[glyphID] 447 448 def getGlyphID(self, glyphName): 449 if not hasattr(self, "_reverseGlyphOrderDict"): 450 self._buildReverseGlyphOrderDict() 451 glyphOrder = self.getGlyphOrder() 452 d = self._reverseGlyphOrderDict 453 if not d.has_key(glyphName): 454 if glyphName in glyphOrder: 455 self._buildReverseGlyphOrderDict() 456 return self.getGlyphID(glyphName) 457 else: 458 raise KeyError, glyphName 459 glyphID = d[glyphName] 460 if glyphName <> glyphOrder[glyphID]: 461 self._buildReverseGlyphOrderDict() 462 return self.getGlyphID(glyphName) 463 return glyphID 464 465 def _buildReverseGlyphOrderDict(self): 466 self._reverseGlyphOrderDict = d = {} 467 glyphOrder = self.getGlyphOrder() 468 for glyphID in range(len(glyphOrder)): 469 d[glyphOrder[glyphID]] = glyphID 470 471 def _writeTable(self, tag, writer, done): 472 """Internal helper function for self.save(). Keeps track of 473 inter-table dependencies. 474 """ 475 if tag in done: 476 return 477 tableclass = getTableClass(tag) 478 for masterTable in tableclass.dependencies: 479 if masterTable not in done: 480 if self.has_key(masterTable): 481 self._writeTable(masterTable, writer, done) 482 else: 483 done.append(masterTable) 484 tabledata = self.getTableData(tag) 485 if self.verbose: 486 debugmsg("writing '%s' table to disk" % tag) 487 writer[tag] = tabledata 488 done.append(tag) 489 490 def getTableData(self, tag): 491 """Returns raw table data, whether compiled or directly read from disk. 492 """ 493 if self.isLoaded(tag): 494 if self.verbose: 495 debugmsg("compiling '%s' table" % tag) 496 return self.tables[tag].compile(self) 497 elif self.reader and self.reader.has_key(tag): 498 if self.verbose: 499 debugmsg("reading '%s' table from disk" % tag) 500 return self.reader[tag] 501 else: 502 raise KeyError, tag 503 504 505def _test_endianness(): 506 """Test the endianness of the machine. This is crucial to know 507 since TrueType data is always big endian, even on little endian 508 machines. There are quite a few situations where we explicitly 509 need to swap some bytes. 510 """ 511 import struct 512 data = struct.pack("h", 0x01) 513 if data == "\000\001": 514 return "big" 515 elif data == "\001\000": 516 return "little" 517 else: 518 assert 0, "endian confusion!" 519 520endian = _test_endianness() 521 522 523def getTableModule(tag): 524 """Fetch the packer/unpacker module for a table. 525 Return None when no module is found. 526 """ 527 import imp 528 import tables 529 py_tag = tag2identifier(tag) 530 try: 531 f, path, kind = imp.find_module(py_tag, tables.__path__) 532 if f: 533 f.close() 534 except ImportError: 535 return None 536 else: 537 module = __import__("fontTools.ttLib.tables." + py_tag) 538 return getattr(tables, py_tag) 539 540 541def getTableClass(tag): 542 """Fetch the packer/unpacker class for a table. 543 Return None when no class is found. 544 """ 545 module = getTableModule(tag) 546 if module is None: 547 from tables.DefaultTable import DefaultTable 548 return DefaultTable 549 py_tag = tag2identifier(tag) 550 tableclass = getattr(module, "table_" + py_tag) 551 return tableclass 552 553 554def getNewTable(tag): 555 """Return a new instance of a table.""" 556 tableclass = getTableClass(tag) 557 return tableclass(tag) 558 559 560def _escapechar(c): 561 """Helper function for tag2identifier()""" 562 import re 563 if re.match("[a-z0-9]", c): 564 return "_" + c 565 elif re.match("[A-Z]", c): 566 return c + "_" 567 else: 568 return hex(ord(c))[2:] 569 570 571def tag2identifier(tag): 572 """Convert a table tag to a valid (but UGLY) python identifier, 573 as well as a filename that's guaranteed to be unique even on a 574 caseless file system. Each character is mapped to two characters. 575 Lowercase letters get an underscore before the letter, uppercase 576 letters get an underscore after the letter. Trailing spaces are 577 trimmed. Illegal characters are escaped as two hex bytes. If the 578 result starts with a number (as the result of a hex escape), an 579 extra underscore is prepended. Examples: 580 'glyf' -> '_g_l_y_f' 581 'cvt ' -> '_c_v_t' 582 'OS/2' -> 'O_S_2f_2' 583 """ 584 import re 585 assert len(tag) == 4, "tag should be 4 characters long" 586 while len(tag) > 1 and tag[-1] == ' ': 587 tag = tag[:-1] 588 ident = "" 589 for c in tag: 590 ident = ident + _escapechar(c) 591 if re.match("[0-9]", ident): 592 ident = "_" + ident 593 return ident 594 595 596def identifier2tag(ident): 597 """the opposite of tag2identifier()""" 598 if len(ident) % 2 and ident[0] == "_": 599 ident = ident[1:] 600 assert not (len(ident) % 2) 601 tag = "" 602 for i in range(0, len(ident), 2): 603 if ident[i] == "_": 604 tag = tag + ident[i+1] 605 elif ident[i+1] == "_": 606 tag = tag + ident[i] 607 else: 608 # assume hex 609 tag = tag + chr(string.atoi(ident[i:i+2], 16)) 610 # append trailing spaces 611 tag = tag + (4 - len(tag)) * ' ' 612 return tag 613 614 615def tag2xmltag(tag): 616 """Similarly to tag2identifier(), this converts a TT tag 617 to a valid XML element name. Since XML element names are 618 case sensitive, this is a fairly simple/readable translation. 619 """ 620 import re 621 if tag == "OS/2": 622 return "OS_2" 623 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 624 return string.strip(tag) 625 else: 626 return tag2identifier(tag) 627 628 629def xmltag2tag(tag): 630 """The opposite of tag2xmltag()""" 631 if tag == "OS_2": 632 return "OS/2" 633 if len(tag) == 8: 634 return identifier2tag(tag) 635 else: 636 return tag + " " * (4 - len(tag)) 637 return tag 638 639 640def debugmsg(msg): 641 import time 642 print msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())) 643 644 645