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