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