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