__init__.py revision 53602486b491e003b8dbba0b1c86709bd0c011bf
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.13 2000-02-01 15:29:03 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 import sfnt 142 if type(file) == types.StringType: 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 pass # assume "file" is a writable file object 154 155 tags = self.keys() 156 numTables = len(tags) 157 writer = sfnt.SFNTWriter(file, numTables, self.sfntVersion) 158 159 done = [] 160 for tag in tags: 161 self._writeTable(tag, writer, done) 162 163 writer.close() 164 165 def saveXML(self, fileOrPath, progress=None, 166 tables=None, skipTables=None, splitTables=0, disassembleInstructions=1): 167 """Export the font as TTX (an XML-based text file), or as a series of text 168 files when splitTables is true. In the latter case, the 'fileOrPath' 169 argument should be a path to a directory. 170 The 'tables' argument must either be false (dump all tables) or a 171 list of tables to dump. The 'skipTables' argument may be a list of tables 172 to skip, but only when the 'tables' argument is false. 173 """ 174 import xmlWriter 175 176 self.disassembleInstructions = disassembleInstructions 177 if not tables: 178 tables = self.keys() 179 if skipTables: 180 for tag in skipTables: 181 if tag in tables: 182 tables.remove(tag) 183 numTables = len(tables) 184 numGlyphs = self['maxp'].numGlyphs 185 if progress: 186 progress.set(0, numTables * numGlyphs) 187 if not splitTables: 188 writer = xmlWriter.XMLWriter(fileOrPath) 189 writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1], 190 ttLibVersion=__version__) 191 writer.newline() 192 writer.newline() 193 else: 194 # 'fileOrPath' must now be a path (pointing to a directory) 195 if not os.path.exists(fileOrPath): 196 os.mkdir(fileOrPath) 197 fileNameTemplate = os.path.join(fileOrPath, 198 os.path.basename(fileOrPath)) + ".%s.ttx" 199 200 for i in range(numTables): 201 tag = tables[i] 202 if splitTables: 203 writer = xmlWriter.XMLWriter(fileNameTemplate % tag2identifier(tag)) 204 writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1], 205 ttLibVersion=__version__) 206 writer.newline() 207 writer.newline() 208 table = self[tag] 209 report = "Dumping '%s' table..." % tag 210 if progress: 211 progress.setlabel(report) 212 elif self.verbose: 213 debugmsg(report) 214 else: 215 print report 216 xmltag = tag2xmltag(tag) 217 if hasattr(table, "ERROR"): 218 writer.begintag(xmltag, ERROR="decompilation error") 219 else: 220 writer.begintag(xmltag) 221 writer.newline() 222 if tag == "glyf": 223 table.toXML(writer, self, progress) 224 elif tag == "CFF ": 225 table.toXML(writer, self, progress) 226 else: 227 table.toXML(writer, self) 228 writer.endtag(xmltag) 229 writer.newline() 230 writer.newline() 231 if splitTables: 232 writer.endtag("ttFont") 233 writer.newline() 234 writer.close() 235 if progress: 236 progress.set(i * numGlyphs, numTables * numGlyphs) 237 if not splitTables: 238 writer.endtag("ttFont") 239 writer.newline() 240 writer.close() 241 if self.verbose: 242 debugmsg("Done dumping TTX") 243 244 def importXML(self, file, progress=None): 245 """Import an TTX file (an XML-based text format), so as to recreate 246 a font object. 247 """ 248 import xmlImport, stat 249 from xml.parsers.xmlproc import xmlproc 250 builder = xmlImport.XMLApplication(self, progress) 251 if progress: 252 progress.set(0, os.stat(file)[stat.ST_SIZE] / 100 or 1) 253 proc = xmlImport.UnicodeProcessor() 254 proc.set_application(builder) 255 proc.set_error_handler(xmlImport.XMLErrorHandler(proc)) 256 dir, filename = os.path.split(file) 257 if dir: 258 olddir = os.getcwd() 259 os.chdir(dir) 260 try: 261 proc.parse_resource(filename) 262 root = builder.root 263 finally: 264 if dir: 265 os.chdir(olddir) 266 # remove circular references 267 proc.deref() 268 del builder.progress 269 270 def isLoaded(self, tag): 271 """Return true if the table identified by 'tag' has been 272 decompiled and loaded into memory.""" 273 return self.tables.has_key(tag) 274 275 def has_key(self, tag): 276 """Pretend we're a dictionary.""" 277 if self.isLoaded(tag): 278 return 1 279 elif self.reader and self.reader.has_key(tag): 280 return 1 281 else: 282 return 0 283 284 def keys(self): 285 """Pretend we're a dictionary.""" 286 keys = self.tables.keys() 287 if self.reader: 288 for key in self.reader.keys(): 289 if key not in keys: 290 keys.append(key) 291 keys.sort() 292 return keys 293 294 def __len__(self): 295 """Pretend we're a dictionary.""" 296 return len(self.keys()) 297 298 def __getitem__(self, tag): 299 """Pretend we're a dictionary.""" 300 try: 301 return self.tables[tag] 302 except KeyError: 303 if self.reader is not None: 304 import traceback 305 if self.verbose: 306 debugmsg("reading '%s' table from disk" % tag) 307 data = self.reader[tag] 308 tableclass = getTableClass(tag) 309 table = tableclass(tag) 310 self.tables[tag] = table 311 if self.verbose: 312 debugmsg("decompiling '%s' table" % tag) 313 try: 314 table.decompile(data, self) 315 except: 316 print "An exception occurred during the decompilation of the '%s' table" % tag 317 from tables.DefaultTable import DefaultTable 318 import StringIO 319 file = StringIO.StringIO() 320 traceback.print_exc(file=file) 321 table = DefaultTable(tag) 322 table.ERROR = file.getvalue() 323 self.tables[tag] = table 324 table.decompile(data, self) 325 return table 326 else: 327 raise KeyError, "'%s' table not found" % tag 328 329 def __setitem__(self, tag, table): 330 """Pretend we're a dictionary.""" 331 self.tables[tag] = table 332 333 def __delitem__(self, tag): 334 """Pretend we're a dictionary.""" 335 del self.tables[tag] 336 337 def setGlyphOrder(self, glyphOrder): 338 self.glyphOrder = glyphOrder 339 if self.has_key('CFF '): 340 self['CFF '].setGlyphOrder(glyphOrder) 341 if self.has_key('glyf'): 342 self['glyf'].setGlyphOrder(glyphOrder) 343 344 def getGlyphOrder(self): 345 if not hasattr(self, "glyphOrder"): 346 if self.has_key('CFF '): 347 # CFF OpenType font 348 self.glyphOrder = self['CFF '].getGlyphOrder() 349 else: 350 # TrueType font 351 glyphOrder = self['post'].getGlyphOrder() 352 if glyphOrder is None: 353 # 354 # No names found in the 'post' table. 355 # Try to create glyph names from the unicode cmap (if available) 356 # in combination with the Adobe Glyph List (AGL). 357 # 358 self._getGlyphNamesFromCmap() 359 else: 360 self.glyphOrder = glyphOrder 361 # XXX what if a font contains 'glyf'/'post' table *and* CFF? 362 return self.glyphOrder 363 364 def _getGlyphNamesFromCmap(self): 365 # Make up glyph names based on glyphID, which will be used 366 # in case we don't find a unicode cmap. 367 numGlyphs = int(self['maxp'].numGlyphs) 368 glyphOrder = [None] * numGlyphs 369 glyphOrder[0] = ".notdef" 370 for i in range(1, numGlyphs): 371 glyphOrder[i] = "glyph%.5d" % i 372 # Set the glyph order, so the cmap parser has something 373 # to work with 374 self.glyphOrder = glyphOrder 375 # Get the temporary cmap (based on the just invented names) 376 tempcmap = self['cmap'].getcmap(3, 1) 377 if tempcmap is not None: 378 # we have a unicode cmap 379 from fontTools import agl 380 cmap = tempcmap.cmap 381 # create a reverse cmap dict 382 reversecmap = {} 383 for unicode, name in cmap.items(): 384 reversecmap[name] = unicode 385 for i in range(numGlyphs): 386 tempName = glyphOrder[i] 387 if reversecmap.has_key(tempName): 388 unicode = reversecmap[tempName] 389 if agl.UV2AGL.has_key(unicode): 390 # get name from the Adobe Glyph List 391 glyphOrder[i] = agl.UV2AGL[unicode] 392 else: 393 # create uni<CODE> name 394 glyphOrder[i] = "uni" + string.upper(string.zfill(hex(unicode)[2:], 4)) 395 # Delete the cmap table from the cache, so it can be 396 # parsed again with the right names. 397 del self.tables['cmap'] 398 else: 399 pass # no unicode cmap available, stick with the invented names 400 self.glyphOrder = glyphOrder 401 402 def getGlyphNames(self): 403 """Get a list of glyph names, sorted alphabetically.""" 404 glyphNames = self.getGlyphOrder()[:] 405 glyphNames.sort() 406 return glyphNames 407 408 def getGlyphNames2(self): 409 """Get a list of glyph names, sorted alphabetically, 410 but not case sensitive. 411 """ 412 from fontTools.misc import textTools 413 return textTools.caselessSort(self.getGlyphOrder()) 414 415 def getGlyphName(self, glyphID): 416 return self.getGlyphOrder()[glyphID] 417 418 def getGlyphID(self, glyphName): 419 if not hasattr(self, "_reverseGlyphOrderDict"): 420 self._buildReverseGlyphOrderDict() 421 glyphOrder = self.getGlyphOrder() 422 d = self._reverseGlyphOrderDict 423 if not d.has_key(glyphName): 424 if glyphName in glyphOrder: 425 self._buildReverseGlyphOrderDict() 426 return self.getGlyphID(glyphName) 427 else: 428 raise KeyError, glyphName 429 glyphID = d[glyphName] 430 if glyphName <> glyphOrder[glyphID]: 431 self._buildReverseGlyphOrderDict() 432 return self.getGlyphID(glyphName) 433 return glyphID 434 435 def _buildReverseGlyphOrderDict(self): 436 self._reverseGlyphOrderDict = d = {} 437 glyphOrder = self.getGlyphOrder() 438 for glyphID in range(len(glyphOrder)): 439 d[glyphOrder[glyphID]] = glyphID 440 441 def _writeTable(self, tag, writer, done): 442 """Internal helper function for self.save(). Keeps track of 443 inter-table dependencies. 444 """ 445 if tag in done: 446 return 447 tableclass = getTableClass(tag) 448 for masterTable in tableclass.dependencies: 449 if masterTable not in done: 450 if self.has_key(masterTable): 451 self._writeTable(masterTable, writer, done) 452 else: 453 done.append(masterTable) 454 tabledata = self._getTableData(tag) 455 if self.verbose: 456 debugmsg("writing '%s' table to disk" % tag) 457 writer[tag] = tabledata 458 done.append(tag) 459 460 def _getTableData(self, tag): 461 """Internal helper function. Returns raw table data, 462 whether compiled or directly read from disk. 463 """ 464 if self.isLoaded(tag): 465 if self.verbose: 466 debugmsg("compiling '%s' table" % tag) 467 return self.tables[tag].compile(self) 468 elif self.reader and self.reader.has_key(tag): 469 if self.verbose: 470 debugmsg("reading '%s' table from disk" % tag) 471 return self.reader[tag] 472 else: 473 raise KeyError, tag 474 475 476def _test_endianness(): 477 """Test the endianness of the machine. This is crucial to know 478 since TrueType data is always big endian, even on little endian 479 machines. There are quite a few situations where we explicitly 480 need to swap some bytes. 481 """ 482 import struct 483 data = struct.pack("h", 0x01) 484 if data == "\000\001": 485 return "big" 486 elif data == "\001\000": 487 return "little" 488 else: 489 assert 0, "endian confusion!" 490 491endian = _test_endianness() 492 493 494def getTableModule(tag): 495 """Fetch the packer/unpacker module for a table. 496 Return None when no module is found. 497 """ 498 import imp 499 import tables 500 py_tag = tag2identifier(tag) 501 try: 502 f, path, kind = imp.find_module(py_tag, tables.__path__) 503 if f: 504 f.close() 505 except ImportError: 506 return None 507 else: 508 module = __import__("fontTools.ttLib.tables." + py_tag) 509 return getattr(tables, py_tag) 510 511 512def getTableClass(tag): 513 """Fetch the packer/unpacker class for a table. 514 Return None when no class is found. 515 """ 516 module = getTableModule(tag) 517 if module is None: 518 from tables.DefaultTable import DefaultTable 519 return DefaultTable 520 py_tag = tag2identifier(tag) 521 tableclass = getattr(module, "table_" + py_tag) 522 return tableclass 523 524 525def newtable(tag): 526 """Return a new instance of a table.""" 527 tableclass = getTableClass(tag) 528 return tableclass(tag) 529 530 531def _escapechar(c): 532 """Helper function for tag2identifier()""" 533 import re 534 if re.match("[a-z0-9]", c): 535 return "_" + c 536 elif re.match("[A-Z]", c): 537 return c + "_" 538 else: 539 return hex(ord(c))[2:] 540 541 542def tag2identifier(tag): 543 """Convert a table tag to a valid (but UGLY) python identifier, 544 as well as a filename that's guaranteed to be unique even on a 545 caseless file system. Each character is mapped to two characters. 546 Lowercase letters get an underscore before the letter, uppercase 547 letters get an underscore after the letter. Trailing spaces are 548 trimmed. Illegal characters are escaped as two hex bytes. If the 549 result starts with a number (as the result of a hex escape), an 550 extra underscore is prepended. Examples: 551 'glyf' -> '_g_l_y_f' 552 'cvt ' -> '_c_v_t' 553 'OS/2' -> 'O_S_2f_2' 554 """ 555 import re 556 assert len(tag) == 4, "tag should be 4 characters long" 557 while len(tag) > 1 and tag[-1] == ' ': 558 tag = tag[:-1] 559 ident = "" 560 for c in tag: 561 ident = ident + _escapechar(c) 562 if re.match("[0-9]", ident): 563 ident = "_" + ident 564 return ident 565 566 567def identifier2tag(ident): 568 """the opposite of tag2identifier()""" 569 if len(ident) % 2 and ident[0] == "_": 570 ident = ident[1:] 571 assert not (len(ident) % 2) 572 tag = "" 573 for i in range(0, len(ident), 2): 574 if ident[i] == "_": 575 tag = tag + ident[i+1] 576 elif ident[i+1] == "_": 577 tag = tag + ident[i] 578 else: 579 # assume hex 580 tag = tag + chr(string.atoi(ident[i:i+2], 16)) 581 # append trailing spaces 582 tag = tag + (4 - len(tag)) * ' ' 583 return tag 584 585 586def tag2xmltag(tag): 587 """Similarly to tag2identifier(), this converts a TT tag 588 to a valid XML element name. Since XML element names are 589 case sensitive, this is a fairly simple/readable translation. 590 """ 591 import re 592 if tag == "OS/2": 593 return "OS_2" 594 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 595 return string.strip(tag) 596 else: 597 return tag2identifier(tag) 598 599 600def xmltag2tag(tag): 601 """The opposite of tag2xmltag()""" 602 if tag == "OS_2": 603 return "OS/2" 604 if len(tag) == 8: 605 return identifier2tag(tag) 606 else: 607 return tag + " " * (4 - len(tag)) 608 return tag 609 610 611def debugmsg(msg): 612 import time 613 print msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())) 614 615 616