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