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