__init__.py revision a556f51db59a347549c08dd6483ba5b408d0bcfd
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.17 2001-02-23 21:58:57 Just Exp $ 46# 47 48__version__ = "1.0a6" 49 50import os 51import string 52import types 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 (default) 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 type(file) == types.StringType: 110 if os.name == "mac" and res_name_or_index is not None: 111 # on the mac, we deal with sfnt resources as well as flat files 112 import macUtils 113 if res_name_or_index == 0: 114 if macUtils.getSFNTResIndices(file): 115 # get the first available sfnt font. 116 file = macUtils.SFNTResourceReader(file, 1) 117 else: 118 file = open(file, "rb") 119 else: 120 file = macUtils.SFNTResourceReader(file, res_name_or_index) 121 else: 122 file = open(file, "rb") 123 else: 124 pass # assume "file" is a readable file object 125 self.reader = sfnt.SFNTReader(file, checkchecksums) 126 self.sfntVersion = self.reader.sfntVersion 127 128 def close(self): 129 """If we still have a reader object, close it.""" 130 if self.reader is not None: 131 self.reader.close() 132 133 def save(self, file, makeSuitcase=0): 134 """Save the font to disk. Similarly to the constructor, 135 the 'file' argument can be either a pathname or a writable 136 file object. 137 138 On the Mac, if makeSuitcase is true, a suitcase (resource fork) 139 file will we made instead of a flat .ttf file. 140 """ 141 from fontTools.ttLib import sfnt 142 if type(file) == types.StringType: 143 closeStream = 1 144 if os.name == "mac" and makeSuitcase: 145 import macUtils 146 file = macUtils.SFNTResourceWriter(file, self) 147 else: 148 file = open(file, "wb") 149 if os.name == "mac": 150 import macfs 151 fss = macfs.FSSpec(file.name) 152 fss.SetCreatorType('mdos', 'BINA') 153 else: 154 # assume "file" is a writable file object 155 closeStream = 0 156 157 tags = self.keys() 158 numTables = len(tags) 159 writer = sfnt.SFNTWriter(file, numTables, self.sfntVersion) 160 161 done = [] 162 for tag in tags: 163 self._writeTable(tag, writer, done) 164 165 writer.close(closeStream) 166 167 def saveXML(self, fileOrPath, progress=None, 168 tables=None, skipTables=None, splitTables=0, disassembleInstructions=1): 169 """Export the font as TTX (an XML-based text file), or as a series of text 170 files when splitTables is true. In the latter case, the 'fileOrPath' 171 argument should be a path to a directory. 172 The 'tables' argument must either be false (dump all tables) or a 173 list of tables to dump. The 'skipTables' argument may be a list of tables 174 to skip, but only when the 'tables' argument is false. 175 """ 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 an TTX file (an XML-based text format), so as to recreate 248 a font object. 249 """ 250 import xmlImport, stat 251 from xml.parsers.xmlproc import xmlproc 252 builder = xmlImport.XMLApplication(self, progress) 253 if progress: 254 progress.set(0, os.stat(file)[stat.ST_SIZE] / 100 or 1) 255 proc = xmlImport.UnicodeProcessor() 256 proc.set_application(builder) 257 proc.set_error_handler(xmlImport.XMLErrorHandler(proc)) 258 dir, filename = os.path.split(file) 259 if dir: 260 olddir = os.getcwd() 261 os.chdir(dir) 262 try: 263 proc.parse_resource(filename) 264 root = builder.root 265 finally: 266 if dir: 267 os.chdir(olddir) 268 # remove circular references 269 proc.deref() 270 del builder.progress 271 272 def isLoaded(self, tag): 273 """Return true if the table identified by 'tag' has been 274 decompiled and loaded into memory.""" 275 return self.tables.has_key(tag) 276 277 def has_key(self, tag): 278 """Pretend we're a dictionary.""" 279 if self.isLoaded(tag): 280 return 1 281 elif self.reader and self.reader.has_key(tag): 282 return 1 283 else: 284 return 0 285 286 def keys(self): 287 """Pretend we're a dictionary.""" 288 keys = self.tables.keys() 289 if self.reader: 290 for key in self.reader.keys(): 291 if key not in keys: 292 keys.append(key) 293 keys.sort() 294 return keys 295 296 def __len__(self): 297 """Pretend we're a dictionary.""" 298 return len(self.keys()) 299 300 def __getitem__(self, tag): 301 """Pretend we're a dictionary.""" 302 try: 303 return self.tables[tag] 304 except KeyError: 305 if self.reader is not None: 306 import traceback 307 if self.verbose: 308 debugmsg("reading '%s' table from disk" % tag) 309 data = self.reader[tag] 310 tableclass = getTableClass(tag) 311 table = tableclass(tag) 312 self.tables[tag] = table 313 if self.verbose: 314 debugmsg("decompiling '%s' table" % tag) 315 try: 316 table.decompile(data, self) 317 except "_ _ F O O _ _": # dummy exception to disable exception catching 318 print "An exception occurred during the decompilation of the '%s' table" % tag 319 from tables.DefaultTable import DefaultTable 320 import StringIO 321 file = StringIO.StringIO() 322 traceback.print_exc(file=file) 323 table = DefaultTable(tag) 324 table.ERROR = file.getvalue() 325 self.tables[tag] = table 326 table.decompile(data, self) 327 return table 328 else: 329 raise KeyError, "'%s' table not found" % tag 330 331 def __setitem__(self, tag, table): 332 """Pretend we're a dictionary.""" 333 self.tables[tag] = table 334 335 def __delitem__(self, tag): 336 """Pretend we're a dictionary.""" 337 del self.tables[tag] 338 339 def setGlyphOrder(self, glyphOrder): 340 self.glyphOrder = glyphOrder 341 if self.has_key('CFF '): 342 self['CFF '].setGlyphOrder(glyphOrder) 343 if self.has_key('glyf'): 344 self['glyf'].setGlyphOrder(glyphOrder) 345 346 def getGlyphOrder(self): 347 if not hasattr(self, "glyphOrder"): 348 if self.has_key('CFF '): 349 # CFF OpenType font 350 self.glyphOrder = self['CFF '].getGlyphOrder() 351 elif self.has_key('post'): 352 # TrueType font 353 glyphOrder = self['post'].getGlyphOrder() 354 if glyphOrder is None: 355 # 356 # No names found in the 'post' table. 357 # Try to create glyph names from the unicode cmap (if available) 358 # in combination with the Adobe Glyph List (AGL). 359 # 360 self._getGlyphNamesFromCmap() 361 else: 362 self.glyphOrder = glyphOrder 363 else: 364 self._getGlyphNamesFromCmap() 365 # XXX what if a font contains 'glyf'/'post' table *and* CFF? 366 return self.glyphOrder 367 368 def _getGlyphNamesFromCmap(self): 369 # Make up glyph names based on glyphID, which will be used 370 # in case we don't find a unicode cmap. 371 numGlyphs = int(self['maxp'].numGlyphs) 372 glyphOrder = [None] * numGlyphs 373 glyphOrder[0] = ".notdef" 374 for i in range(1, numGlyphs): 375 glyphOrder[i] = "glyph%.5d" % i 376 # Set the glyph order, so the cmap parser has something 377 # to work with 378 self.glyphOrder = glyphOrder 379 # Get the temporary cmap (based on the just invented names) 380 tempcmap = self['cmap'].getcmap(3, 1) 381 if tempcmap is not None: 382 # we have a unicode cmap 383 from fontTools import agl 384 cmap = tempcmap.cmap 385 # create a reverse cmap dict 386 reversecmap = {} 387 for unicode, name in cmap.items(): 388 reversecmap[name] = unicode 389 allNames = {} 390 for i in range(numGlyphs): 391 tempName = glyphOrder[i] 392 if reversecmap.has_key(tempName): 393 unicode = reversecmap[tempName] 394 if agl.UV2AGL.has_key(unicode): 395 # get name from the Adobe Glyph List 396 glyphName = agl.UV2AGL[unicode] 397 else: 398 # create uni<CODE> name 399 glyphName = "uni" + string.upper(string.zfill( 400 hex(unicode)[2:], 4)) 401 tempName = glyphName 402 n = 1 403 while allNames.has_key(tempName): 404 tempName = glyphName + "#" + `n` 405 n = n + 1 406 glyphOrder[i] = tempName 407 allNames[tempName] = 1 408 # Delete the cmap table from the cache, so it can be 409 # parsed again with the right names. 410 del self.tables['cmap'] 411 else: 412 pass # no unicode cmap available, stick with the invented names 413 self.glyphOrder = glyphOrder 414 415 def getGlyphNames(self): 416 """Get a list of glyph names, sorted alphabetically.""" 417 glyphNames = self.getGlyphOrder()[:] 418 glyphNames.sort() 419 return glyphNames 420 421 def getGlyphNames2(self): 422 """Get a list of glyph names, sorted alphabetically, 423 but not case sensitive. 424 """ 425 from fontTools.misc import textTools 426 return textTools.caselessSort(self.getGlyphOrder()) 427 428 def getGlyphName(self, glyphID): 429 return self.getGlyphOrder()[glyphID] 430 431 def getGlyphID(self, glyphName): 432 if not hasattr(self, "_reverseGlyphOrderDict"): 433 self._buildReverseGlyphOrderDict() 434 glyphOrder = self.getGlyphOrder() 435 d = self._reverseGlyphOrderDict 436 if not d.has_key(glyphName): 437 if glyphName in glyphOrder: 438 self._buildReverseGlyphOrderDict() 439 return self.getGlyphID(glyphName) 440 else: 441 raise KeyError, glyphName 442 glyphID = d[glyphName] 443 if glyphName <> glyphOrder[glyphID]: 444 self._buildReverseGlyphOrderDict() 445 return self.getGlyphID(glyphName) 446 return glyphID 447 448 def _buildReverseGlyphOrderDict(self): 449 self._reverseGlyphOrderDict = d = {} 450 glyphOrder = self.getGlyphOrder() 451 for glyphID in range(len(glyphOrder)): 452 d[glyphOrder[glyphID]] = glyphID 453 454 def _writeTable(self, tag, writer, done): 455 """Internal helper function for self.save(). Keeps track of 456 inter-table dependencies. 457 """ 458 if tag in done: 459 return 460 tableclass = getTableClass(tag) 461 for masterTable in tableclass.dependencies: 462 if masterTable not in done: 463 if self.has_key(masterTable): 464 self._writeTable(masterTable, writer, done) 465 else: 466 done.append(masterTable) 467 tabledata = self._getTableData(tag) 468 if self.verbose: 469 debugmsg("writing '%s' table to disk" % tag) 470 writer[tag] = tabledata 471 done.append(tag) 472 473 def _getTableData(self, tag): 474 """Internal helper function. Returns raw table data, 475 whether compiled or directly read from disk. 476 """ 477 if self.isLoaded(tag): 478 if self.verbose: 479 debugmsg("compiling '%s' table" % tag) 480 return self.tables[tag].compile(self) 481 elif self.reader and self.reader.has_key(tag): 482 if self.verbose: 483 debugmsg("reading '%s' table from disk" % tag) 484 return self.reader[tag] 485 else: 486 raise KeyError, tag 487 488 489def _test_endianness(): 490 """Test the endianness of the machine. This is crucial to know 491 since TrueType data is always big endian, even on little endian 492 machines. There are quite a few situations where we explicitly 493 need to swap some bytes. 494 """ 495 import struct 496 data = struct.pack("h", 0x01) 497 if data == "\000\001": 498 return "big" 499 elif data == "\001\000": 500 return "little" 501 else: 502 assert 0, "endian confusion!" 503 504endian = _test_endianness() 505 506 507def getTableModule(tag): 508 """Fetch the packer/unpacker module for a table. 509 Return None when no module is found. 510 """ 511 import imp 512 import tables 513 py_tag = tag2identifier(tag) 514 try: 515 f, path, kind = imp.find_module(py_tag, tables.__path__) 516 if f: 517 f.close() 518 except ImportError: 519 return None 520 else: 521 module = __import__("fontTools.ttLib.tables." + py_tag) 522 return getattr(tables, py_tag) 523 524 525def getTableClass(tag): 526 """Fetch the packer/unpacker class for a table. 527 Return None when no class is found. 528 """ 529 module = getTableModule(tag) 530 if module is None: 531 from tables.DefaultTable import DefaultTable 532 return DefaultTable 533 py_tag = tag2identifier(tag) 534 tableclass = getattr(module, "table_" + py_tag) 535 return tableclass 536 537 538def getNewTable(tag): 539 """Return a new instance of a table.""" 540 tableclass = getTableClass(tag) 541 return tableclass(tag) 542 543 544def _escapechar(c): 545 """Helper function for tag2identifier()""" 546 import re 547 if re.match("[a-z0-9]", c): 548 return "_" + c 549 elif re.match("[A-Z]", c): 550 return c + "_" 551 else: 552 return hex(ord(c))[2:] 553 554 555def tag2identifier(tag): 556 """Convert a table tag to a valid (but UGLY) python identifier, 557 as well as a filename that's guaranteed to be unique even on a 558 caseless file system. Each character is mapped to two characters. 559 Lowercase letters get an underscore before the letter, uppercase 560 letters get an underscore after the letter. Trailing spaces are 561 trimmed. Illegal characters are escaped as two hex bytes. If the 562 result starts with a number (as the result of a hex escape), an 563 extra underscore is prepended. Examples: 564 'glyf' -> '_g_l_y_f' 565 'cvt ' -> '_c_v_t' 566 'OS/2' -> 'O_S_2f_2' 567 """ 568 import re 569 assert len(tag) == 4, "tag should be 4 characters long" 570 while len(tag) > 1 and tag[-1] == ' ': 571 tag = tag[:-1] 572 ident = "" 573 for c in tag: 574 ident = ident + _escapechar(c) 575 if re.match("[0-9]", ident): 576 ident = "_" + ident 577 return ident 578 579 580def identifier2tag(ident): 581 """the opposite of tag2identifier()""" 582 if len(ident) % 2 and ident[0] == "_": 583 ident = ident[1:] 584 assert not (len(ident) % 2) 585 tag = "" 586 for i in range(0, len(ident), 2): 587 if ident[i] == "_": 588 tag = tag + ident[i+1] 589 elif ident[i+1] == "_": 590 tag = tag + ident[i] 591 else: 592 # assume hex 593 tag = tag + chr(string.atoi(ident[i:i+2], 16)) 594 # append trailing spaces 595 tag = tag + (4 - len(tag)) * ' ' 596 return tag 597 598 599def tag2xmltag(tag): 600 """Similarly to tag2identifier(), this converts a TT tag 601 to a valid XML element name. Since XML element names are 602 case sensitive, this is a fairly simple/readable translation. 603 """ 604 import re 605 if tag == "OS/2": 606 return "OS_2" 607 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 608 return string.strip(tag) 609 else: 610 return tag2identifier(tag) 611 612 613def xmltag2tag(tag): 614 """The opposite of tag2xmltag()""" 615 if tag == "OS_2": 616 return "OS/2" 617 if len(tag) == 8: 618 return identifier2tag(tag) 619 else: 620 return tag + " " * (4 - len(tag)) 621 return tag 622 623 624def debugmsg(msg): 625 import time 626 print msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())) 627 628 629