__init__.py revision d8b32bf6e208a18421cfd4705801d6237e281f93
1"""fontTools.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 TTX (an XML-based 5text format) and vice versa. 6 7Example interactive session: 8 9Python 1.5.2c1 (#43, Mar 9 1999, 13:06:43) [CW PPC w/GUSI w/MSL] 10Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam 11>>> from fontTools import ttLib 12>>> tt = ttLib.TTFont("afont.ttf") 13>>> tt['maxp'].numGlyphs 14242 15>>> tt['OS/2'].achVendID 16'B&H\000' 17>>> tt['head'].unitsPerEm 182048 19>>> tt.saveXML("afont.ttx") 20Dumping 'LTSH' table... 21Dumping 'OS/2' table... 22Dumping 'VDMX' table... 23Dumping 'cmap' table... 24Dumping 'cvt ' table... 25Dumping 'fpgm' table... 26Dumping 'glyf' table... 27Dumping 'hdmx' table... 28Dumping 'head' table... 29Dumping 'hhea' table... 30Dumping 'hmtx' table... 31Dumping 'loca' table... 32Dumping 'maxp' table... 33Dumping 'name' table... 34Dumping 'post' table... 35Dumping 'prep' table... 36>>> tt2 = ttLib.TTFont() 37>>> tt2.importXML("afont.ttx") 38>>> tt2['maxp'].numGlyphs 39242 40>>> 41 42""" 43 44# 45# $Id: __init__.py,v 1.51 2009-02-22 08:55:00 pabs3 Exp $ 46# 47 48import sys 49import os 50import string 51 52haveMacSupport = 0 53if sys.platform == "mac": 54 haveMacSupport = 1 55elif sys.platform == "darwin" and sys.version_info[:3] != (2, 2, 0): 56 # Python 2.2's Mac support is broken, so don't enable it there. 57 haveMacSupport = 1 58 59 60class TTLibError(Exception): pass 61 62 63class TTFont: 64 65 """The main font object. It manages file input and output, and offers 66 a convenient way of accessing tables. 67 Tables will be only decompiled when necessary, ie. when they're actually 68 accessed. This means that simple operations can be extremely fast. 69 """ 70 71 def __init__(self, file=None, res_name_or_index=None, 72 sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0, 73 verbose=0, recalcBBoxes=1, allowVID=0, ignoreDecompileErrors=False, 74 fontNumber=-1): 75 76 """The constructor can be called with a few different arguments. 77 When reading a font from disk, 'file' should be either a pathname 78 pointing to a file, or a readable file object. 79 80 It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt 81 resource name or an sfnt resource index number or zero. The latter 82 case will cause TTLib to autodetect whether the file is a flat file 83 or a suitcase. (If it's a suitcase, only the first 'sfnt' resource 84 will be read!) 85 86 The 'checkChecksums' argument is used to specify how sfnt 87 checksums are treated upon reading a file from disk: 88 0: don't check (default) 89 1: check, print warnings if a wrong checksum is found 90 2: check, raise an exception if a wrong checksum is found. 91 92 The TTFont constructor can also be called without a 'file' 93 argument: this is the way to create a new empty font. 94 In this case you can optionally supply the 'sfntVersion' argument, 95 and a 'flavor' which can be None, or 'woff'. 96 97 If the recalcBBoxes argument is false, a number of things will *not* 98 be recalculated upon save/compile: 99 1) glyph bounding boxes 100 2) maxp font bounding box 101 3) hhea min/max values 102 (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). 103 Additionally, upon importing an TTX file, this option cause glyphs 104 to be compiled right away. This should reduce memory consumption 105 greatly, and therefore should have some impact on the time needed 106 to parse/compile large fonts. 107 108 If the allowVID argument is set to true, then virtual GID's are 109 supported. Asking for a glyph ID with a glyph name or GID that is not in 110 the font will return a virtual GID. This is valid for GSUB and cmap 111 tables. For SING glyphlets, the cmap table is used to specify Unicode 112 values for virtual GI's used in GSUB/GPOS rules. If the gid Nis requested 113 and does not exist in the font, or the glyphname has the form glyphN 114 and does not exist in the font, then N is used as the virtual GID. 115 Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new 116 virtual GIDs, the next is one less than the previous. 117 118 If ignoreDecompileErrors is set to True, exceptions raised in 119 individual tables during decompilation will be ignored, falling 120 back to the DefaultTable implementation, which simply keeps the 121 binary data. 122 """ 123 124 from fontTools.ttLib import sfnt 125 self.verbose = verbose 126 self.recalcBBoxes = recalcBBoxes 127 self.tables = {} 128 self.reader = None 129 130 # Permit the user to reference glyphs that are not int the font. 131 self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value. 132 self.reverseVIDDict = {} 133 self.VIDDict = {} 134 self.allowVID = allowVID 135 self.ignoreDecompileErrors = ignoreDecompileErrors 136 137 if not file: 138 self.sfntVersion = sfntVersion 139 self.flavor = flavor 140 self.flavorData = None 141 return 142 if not hasattr(file, "read"): 143 # assume file is a string 144 if haveMacSupport and res_name_or_index is not None: 145 # on the mac, we deal with sfnt resources as well as flat files 146 import macUtils 147 if res_name_or_index == 0: 148 if macUtils.getSFNTResIndices(file): 149 # get the first available sfnt font. 150 file = macUtils.SFNTResourceReader(file, 1) 151 else: 152 file = open(file, "rb") 153 else: 154 file = macUtils.SFNTResourceReader(file, res_name_or_index) 155 else: 156 file = open(file, "rb") 157 else: 158 pass # assume "file" is a readable file object 159 self.reader = sfnt.SFNTReader(file, checkChecksums, fontNumber=fontNumber) 160 self.sfntVersion = self.reader.sfntVersion 161 self.flavor = self.reader.flavor 162 self.flavorData = self.reader.flavorData 163 164 def close(self): 165 """If we still have a reader object, close it.""" 166 if self.reader is not None: 167 self.reader.close() 168 169 def save(self, file, makeSuitcase=0, reorderTables=1): 170 """Save the font to disk. Similarly to the constructor, 171 the 'file' argument can be either a pathname or a writable 172 file object. 173 174 On the Mac, if makeSuitcase is true, a suitcase (resource fork) 175 file will we made instead of a flat .ttf file. 176 """ 177 from fontTools.ttLib import sfnt 178 if not hasattr(file, "write"): 179 closeStream = 1 180 if os.name == "mac" and makeSuitcase: 181 import macUtils 182 file = macUtils.SFNTResourceWriter(file, self) 183 else: 184 file = open(file, "wb") 185 if os.name == "mac": 186 from fontTools.misc.macCreator import setMacCreatorAndType 187 setMacCreatorAndType(file.name, 'mdos', 'BINA') 188 else: 189 # assume "file" is a writable file object 190 closeStream = 0 191 192 tags = self.keys() 193 if "GlyphOrder" in tags: 194 tags.remove("GlyphOrder") 195 numTables = len(tags) 196 if reorderTables: 197 import tempfile 198 tmp = tempfile.TemporaryFile(prefix="ttx-fonttools") 199 else: 200 tmp = file 201 writer = sfnt.SFNTWriter(tmp, numTables, self.sfntVersion, self.flavor, self.flavorData) 202 203 done = [] 204 for tag in tags: 205 self._writeTable(tag, writer, done) 206 207 writer.close() 208 209 if reorderTables: 210 tmp.flush() 211 tmp.seek(0) 212 reorderFontTables(tmp, file) 213 tmp.close() 214 215 if closeStream: 216 file.close() 217 218 def saveXML(self, fileOrPath, progress=None, 219 tables=None, skipTables=None, splitTables=0, disassembleInstructions=1, 220 bitmapGlyphDataFormat='raw'): 221 """Export the font as TTX (an XML-based text file), or as a series of text 222 files when splitTables is true. In the latter case, the 'fileOrPath' 223 argument should be a path to a directory. 224 The 'tables' argument must either be false (dump all tables) or a 225 list of tables to dump. The 'skipTables' argument may be a list of tables 226 to skip, but only when the 'tables' argument is false. 227 """ 228 from fontTools import version 229 import xmlWriter 230 231 self.disassembleInstructions = disassembleInstructions 232 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat 233 if not tables: 234 tables = self.keys() 235 if "GlyphOrder" not in tables: 236 tables = ["GlyphOrder"] + tables 237 if skipTables: 238 for tag in skipTables: 239 if tag in tables: 240 tables.remove(tag) 241 numTables = len(tables) 242 if progress: 243 progress.set(0, numTables) 244 idlefunc = getattr(progress, "idle", None) 245 else: 246 idlefunc = None 247 248 writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc) 249 writer.begintag("ttFont", sfntVersion=`self.sfntVersion`[1:-1], 250 ttLibVersion=version) 251 writer.newline() 252 253 if not splitTables: 254 writer.newline() 255 else: 256 # 'fileOrPath' must now be a path 257 path, ext = os.path.splitext(fileOrPath) 258 fileNameTemplate = path + ".%s" + ext 259 260 for i in range(numTables): 261 if progress: 262 progress.set(i) 263 tag = tables[i] 264 if splitTables: 265 tablePath = fileNameTemplate % tagToIdentifier(tag) 266 tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc) 267 tableWriter.begintag("ttFont", ttLibVersion=version) 268 tableWriter.newline() 269 tableWriter.newline() 270 writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) 271 writer.newline() 272 else: 273 tableWriter = writer 274 self._tableToXML(tableWriter, tag, progress) 275 if splitTables: 276 tableWriter.endtag("ttFont") 277 tableWriter.newline() 278 tableWriter.close() 279 if progress: 280 progress.set((i + 1)) 281 writer.endtag("ttFont") 282 writer.newline() 283 writer.close() 284 if self.verbose: 285 debugmsg("Done dumping TTX") 286 287 def _tableToXML(self, writer, tag, progress): 288 if self.has_key(tag): 289 table = self[tag] 290 report = "Dumping '%s' table..." % tag 291 else: 292 report = "No '%s' table found." % tag 293 if progress: 294 progress.setLabel(report) 295 elif self.verbose: 296 debugmsg(report) 297 else: 298 print report 299 if not self.has_key(tag): 300 return 301 xmlTag = tagToXML(tag) 302 if hasattr(table, "ERROR"): 303 writer.begintag(xmlTag, ERROR="decompilation error") 304 else: 305 writer.begintag(xmlTag) 306 writer.newline() 307 if tag in ("glyf", "CFF "): 308 table.toXML(writer, self, progress) 309 else: 310 table.toXML(writer, self) 311 writer.endtag(xmlTag) 312 writer.newline() 313 writer.newline() 314 315 def importXML(self, file, progress=None): 316 """Import a TTX file (an XML-based text format), so as to recreate 317 a font object. 318 """ 319 if self.has_key("maxp") and self.has_key("post"): 320 # Make sure the glyph order is loaded, as it otherwise gets 321 # lost if the XML doesn't contain the glyph order, yet does 322 # contain the table which was originally used to extract the 323 # glyph names from (ie. 'post', 'cmap' or 'CFF '). 324 self.getGlyphOrder() 325 import xmlImport 326 xmlImport.importXML(self, file, progress) 327 328 def isLoaded(self, tag): 329 """Return true if the table identified by 'tag' has been 330 decompiled and loaded into memory.""" 331 return self.tables.has_key(tag) 332 333 def has_key(self, tag): 334 if self.isLoaded(tag): 335 return 1 336 elif self.reader and self.reader.has_key(tag): 337 return 1 338 elif tag == "GlyphOrder": 339 return 1 340 else: 341 return 0 342 343 __contains__ = has_key 344 345 def keys(self): 346 keys = self.tables.keys() 347 if self.reader: 348 for key in self.reader.keys(): 349 if key not in keys: 350 keys.append(key) 351 352 if "GlyphOrder" in keys: 353 keys.remove("GlyphOrder") 354 keys = sortedTagList(keys) 355 return ["GlyphOrder"] + keys 356 357 def __len__(self): 358 return len(self.keys()) 359 360 def __getitem__(self, tag): 361 try: 362 return self.tables[tag] 363 except KeyError: 364 if tag == "GlyphOrder": 365 table = GlyphOrder(self) 366 self.tables[tag] = table 367 return table 368 if self.reader is not None: 369 import traceback 370 if self.verbose: 371 debugmsg("Reading '%s' table from disk" % tag) 372 data = self.reader[tag] 373 tableClass = getTableClass(tag) 374 table = tableClass(tag) 375 self.tables[tag] = table 376 if self.verbose: 377 debugmsg("Decompiling '%s' table" % tag) 378 try: 379 table.decompile(data, self) 380 except: 381 if not self.ignoreDecompileErrors: 382 raise 383 # fall back to DefaultTable, retaining the binary table data 384 print "An exception occurred during the decompilation of the '%s' table" % tag 385 from tables.DefaultTable import DefaultTable 386 import StringIO 387 file = StringIO.StringIO() 388 traceback.print_exc(file=file) 389 table = DefaultTable(tag) 390 table.ERROR = file.getvalue() 391 self.tables[tag] = table 392 table.decompile(data, self) 393 return table 394 else: 395 raise KeyError, "'%s' table not found" % tag 396 397 def __setitem__(self, tag, table): 398 self.tables[tag] = table 399 400 def __delitem__(self, tag): 401 if not self.has_key(tag): 402 raise KeyError, "'%s' table not found" % tag 403 if self.tables.has_key(tag): 404 del self.tables[tag] 405 if self.reader and self.reader.has_key(tag): 406 del self.reader[tag] 407 408 def setGlyphOrder(self, glyphOrder): 409 self.glyphOrder = glyphOrder 410 411 def getGlyphOrder(self): 412 try: 413 return self.glyphOrder 414 except AttributeError: 415 pass 416 if self.has_key('CFF '): 417 cff = self['CFF '] 418 self.glyphOrder = cff.getGlyphOrder() 419 elif self.has_key('post'): 420 # TrueType font 421 glyphOrder = self['post'].getGlyphOrder() 422 if glyphOrder is None: 423 # 424 # No names found in the 'post' table. 425 # Try to create glyph names from the unicode cmap (if available) 426 # in combination with the Adobe Glyph List (AGL). 427 # 428 self._getGlyphNamesFromCmap() 429 else: 430 self.glyphOrder = glyphOrder 431 else: 432 self._getGlyphNamesFromCmap() 433 return self.glyphOrder 434 435 def _getGlyphNamesFromCmap(self): 436 # 437 # This is rather convoluted, but then again, it's an interesting problem: 438 # - we need to use the unicode values found in the cmap table to 439 # build glyph names (eg. because there is only a minimal post table, 440 # or none at all). 441 # - but the cmap parser also needs glyph names to work with... 442 # So here's what we do: 443 # - make up glyph names based on glyphID 444 # - load a temporary cmap table based on those names 445 # - extract the unicode values, build the "real" glyph names 446 # - unload the temporary cmap table 447 # 448 if self.isLoaded("cmap"): 449 # Bootstrapping: we're getting called by the cmap parser 450 # itself. This means self.tables['cmap'] contains a partially 451 # loaded cmap, making it impossible to get at a unicode 452 # subtable here. We remove the partially loaded cmap and 453 # restore it later. 454 # This only happens if the cmap table is loaded before any 455 # other table that does f.getGlyphOrder() or f.getGlyphName(). 456 cmapLoading = self.tables['cmap'] 457 del self.tables['cmap'] 458 else: 459 cmapLoading = None 460 # Make up glyph names based on glyphID, which will be used by the 461 # temporary cmap and by the real cmap in case we don't find a unicode 462 # cmap. 463 numGlyphs = int(self['maxp'].numGlyphs) 464 glyphOrder = [None] * numGlyphs 465 glyphOrder[0] = ".notdef" 466 for i in range(1, numGlyphs): 467 glyphOrder[i] = "glyph%.5d" % i 468 # Set the glyph order, so the cmap parser has something 469 # to work with (so we don't get called recursively). 470 self.glyphOrder = glyphOrder 471 # Get a (new) temporary cmap (based on the just invented names) 472 tempcmap = self['cmap'].getcmap(3, 1) 473 if tempcmap is not None: 474 # we have a unicode cmap 475 from fontTools import agl 476 cmap = tempcmap.cmap 477 # create a reverse cmap dict 478 reversecmap = {} 479 for unicode, name in cmap.items(): 480 reversecmap[name] = unicode 481 allNames = {} 482 for i in range(numGlyphs): 483 tempName = glyphOrder[i] 484 if reversecmap.has_key(tempName): 485 unicode = reversecmap[tempName] 486 if agl.UV2AGL.has_key(unicode): 487 # get name from the Adobe Glyph List 488 glyphName = agl.UV2AGL[unicode] 489 else: 490 # create uni<CODE> name 491 glyphName = "uni" + string.upper(string.zfill( 492 hex(unicode)[2:], 4)) 493 tempName = glyphName 494 n = 1 495 while allNames.has_key(tempName): 496 tempName = glyphName + "#" + `n` 497 n = n + 1 498 glyphOrder[i] = tempName 499 allNames[tempName] = 1 500 # Delete the temporary cmap table from the cache, so it can 501 # be parsed again with the right names. 502 del self.tables['cmap'] 503 else: 504 pass # no unicode cmap available, stick with the invented names 505 self.glyphOrder = glyphOrder 506 if cmapLoading: 507 # restore partially loaded cmap, so it can continue loading 508 # using the proper names. 509 self.tables['cmap'] = cmapLoading 510 511 def getGlyphNames(self): 512 """Get a list of glyph names, sorted alphabetically.""" 513 glyphNames = self.getGlyphOrder()[:] 514 glyphNames.sort() 515 return glyphNames 516 517 def getGlyphNames2(self): 518 """Get a list of glyph names, sorted alphabetically, 519 but not case sensitive. 520 """ 521 from fontTools.misc import textTools 522 return textTools.caselessSort(self.getGlyphOrder()) 523 524 def getGlyphName(self, glyphID, requireReal=0): 525 try: 526 return self.getGlyphOrder()[glyphID] 527 except IndexError: 528 if requireReal or not self.allowVID: 529 # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in 530 # the cmap table than there are glyphs. I don't think it's legal... 531 return "glyph%.5d" % glyphID 532 else: 533 # user intends virtual GID support 534 try: 535 glyphName = self.VIDDict[glyphID] 536 except KeyError: 537 glyphName ="glyph%.5d" % glyphID 538 self.last_vid = min(glyphID, self.last_vid ) 539 self.reverseVIDDict[glyphName] = glyphID 540 self.VIDDict[glyphID] = glyphName 541 return glyphName 542 543 def getGlyphID(self, glyphName, requireReal = 0): 544 if not hasattr(self, "_reverseGlyphOrderDict"): 545 self._buildReverseGlyphOrderDict() 546 glyphOrder = self.getGlyphOrder() 547 d = self._reverseGlyphOrderDict 548 if not d.has_key(glyphName): 549 if glyphName in glyphOrder: 550 self._buildReverseGlyphOrderDict() 551 return self.getGlyphID(glyphName) 552 else: 553 if requireReal or not self.allowVID: 554 raise KeyError, glyphName 555 else: 556 # user intends virtual GID support 557 try: 558 glyphID = self.reverseVIDDict[glyphName] 559 except KeyError: 560 # if name is in glyphXXX format, use the specified name. 561 if glyphName[:5] == "glyph": 562 try: 563 glyphID = int(glyphName[5:]) 564 except (NameError, ValueError): 565 glyphID = None 566 if glyphID == None: 567 glyphID = self.last_vid -1 568 self.last_vid = glyphID 569 self.reverseVIDDict[glyphName] = glyphID 570 self.VIDDict[glyphID] = glyphName 571 return glyphID 572 573 glyphID = d[glyphName] 574 if glyphName <> glyphOrder[glyphID]: 575 self._buildReverseGlyphOrderDict() 576 return self.getGlyphID(glyphName) 577 return glyphID 578 579 def getReverseGlyphMap(self, rebuild=0): 580 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): 581 self._buildReverseGlyphOrderDict() 582 return self._reverseGlyphOrderDict 583 584 def _buildReverseGlyphOrderDict(self): 585 self._reverseGlyphOrderDict = d = {} 586 glyphOrder = self.getGlyphOrder() 587 for glyphID in range(len(glyphOrder)): 588 d[glyphOrder[glyphID]] = glyphID 589 590 def _writeTable(self, tag, writer, done): 591 """Internal helper function for self.save(). Keeps track of 592 inter-table dependencies. 593 """ 594 if tag in done: 595 return 596 tableClass = getTableClass(tag) 597 for masterTable in tableClass.dependencies: 598 if masterTable not in done: 599 if self.has_key(masterTable): 600 self._writeTable(masterTable, writer, done) 601 else: 602 done.append(masterTable) 603 tabledata = self.getTableData(tag) 604 if self.verbose: 605 debugmsg("writing '%s' table to disk" % tag) 606 writer[tag] = tabledata 607 done.append(tag) 608 609 def getTableData(self, tag): 610 """Returns raw table data, whether compiled or directly read from disk. 611 """ 612 if self.isLoaded(tag): 613 if self.verbose: 614 debugmsg("compiling '%s' table" % tag) 615 return self.tables[tag].compile(self) 616 elif self.reader and self.reader.has_key(tag): 617 if self.verbose: 618 debugmsg("Reading '%s' table from disk" % tag) 619 return self.reader[tag] 620 else: 621 raise KeyError, tag 622 623 def getGlyphSet(self, preferCFF=1): 624 """Return a generic GlyphSet, which is a dict-like object 625 mapping glyph names to glyph objects. The returned glyph objects 626 have a .draw() method that supports the Pen protocol, and will 627 have an attribute named 'width', but only *after* the .draw() method 628 has been called. 629 630 If the font is CFF-based, the outlines will be taken from the 'CFF ' 631 table. Otherwise the outlines will be taken from the 'glyf' table. 632 If the font contains both a 'CFF ' and a 'glyf' table, you can use 633 the 'preferCFF' argument to specify which one should be taken. 634 """ 635 if preferCFF and self.has_key("CFF "): 636 return self["CFF "].cff.values()[0].CharStrings 637 if self.has_key("glyf"): 638 return _TTGlyphSet(self) 639 if self.has_key("CFF "): 640 return self["CFF "].cff.values()[0].CharStrings 641 raise TTLibError, "Font contains no outlines" 642 643 644class _TTGlyphSet: 645 646 """Generic dict-like GlyphSet class, meant as a TrueType counterpart 647 to CFF's CharString dict. See TTFont.getGlyphSet(). 648 """ 649 650 # This class is distinct from the 'glyf' table itself because we need 651 # access to the 'hmtx' table, which could cause a dependency problem 652 # there when reading from XML. 653 654 def __init__(self, ttFont): 655 self._ttFont = ttFont 656 657 def keys(self): 658 return self._ttFont["glyf"].keys() 659 660 def has_key(self, glyphName): 661 return self._ttFont["glyf"].has_key(glyphName) 662 663 __contains__ = has_key 664 665 def __getitem__(self, glyphName): 666 return _TTGlyph(glyphName, self._ttFont) 667 668 def get(self, glyphName, default=None): 669 try: 670 return self[glyphName] 671 except KeyError: 672 return default 673 674 675class _TTGlyph: 676 677 """Wrapper for a TrueType glyph that supports the Pen protocol, meaning 678 that it has a .draw() method that takes a pen object as its only 679 argument. Additionally there is a 'width' attribute. 680 """ 681 682 def __init__(self, glyphName, ttFont): 683 self._glyphName = glyphName 684 self._ttFont = ttFont 685 self.width, self.lsb = self._ttFont['hmtx'][self._glyphName] 686 687 def draw(self, pen): 688 """Draw the glyph onto Pen. See fontTools.pens.basePen for details 689 how that works. 690 """ 691 glyfTable = self._ttFont['glyf'] 692 glyph = glyfTable[self._glyphName] 693 if hasattr(glyph, "xMin"): 694 offset = self.lsb - glyph.xMin 695 else: 696 offset = 0 697 if glyph.isComposite(): 698 for component in glyph: 699 glyphName, transform = component.getComponentInfo() 700 pen.addComponent(glyphName, transform) 701 else: 702 coordinates, endPts, flags = glyph.getCoordinates(glyfTable) 703 if offset: 704 coordinates = coordinates + (offset, 0) 705 start = 0 706 for end in endPts: 707 end = end + 1 708 contour = coordinates[start:end].tolist() 709 cFlags = flags[start:end].tolist() 710 start = end 711 if 1 not in cFlags: 712 # There is not a single on-curve point on the curve, 713 # use pen.qCurveTo's special case by specifying None 714 # as the on-curve point. 715 contour.append(None) 716 pen.qCurveTo(*contour) 717 else: 718 # Shuffle the points so that contour the is guaranteed 719 # to *end* in an on-curve point, which we'll use for 720 # the moveTo. 721 firstOnCurve = cFlags.index(1) + 1 722 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 723 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 724 pen.moveTo(contour[-1]) 725 while contour: 726 nextOnCurve = cFlags.index(1) + 1 727 if nextOnCurve == 1: 728 pen.lineTo(contour[0]) 729 else: 730 pen.qCurveTo(*contour[:nextOnCurve]) 731 contour = contour[nextOnCurve:] 732 cFlags = cFlags[nextOnCurve:] 733 pen.closePath() 734 735 736class GlyphOrder: 737 738 """A pseudo table. The glyph order isn't in the font as a separate 739 table, but it's nice to present it as such in the TTX format. 740 """ 741 742 def __init__(self, ttFont): 743 self.glyphOrder = ttFont.getGlyphOrder() 744 745 def __len__(self): 746 return len(self.glyphOrder) 747 748 def __getitem__(self, i): 749 return self.glyphOrder[i] 750 751 def toXML(self, writer, ttFont): 752 writer.comment("The 'id' attribute is only for humans; " 753 "it is ignored when parsed.") 754 writer.newline() 755 for i in range(len(self.glyphOrder)): 756 glyphName = self.glyphOrder[i] 757 writer.simpletag("GlyphID", id=i, name=glyphName) 758 writer.newline() 759 760 def fromXML(self, (name, attrs, content), ttFont): 761 if not hasattr(self, "glyphOrder"): 762 self.glyphOrder = [] 763 ttFont.setGlyphOrder(self.glyphOrder) 764 if name == "GlyphID": 765 self.glyphOrder.append(attrs["name"]) 766 767 768def getTableModule(tag): 769 """Fetch the packer/unpacker module for a table. 770 Return None when no module is found. 771 """ 772 import tables 773 pyTag = tagToIdentifier(tag) 774 try: 775 __import__("fontTools.ttLib.tables." + pyTag) 776 except ImportError, err: 777 # If pyTag is found in the ImportError message, 778 # means table is not implemented. If it's not 779 # there, then some other module is missing, don't 780 # suppress the error. 781 if str(err).find(pyTag) >= 0: 782 return None 783 else: 784 raise err 785 else: 786 return getattr(tables, pyTag) 787 788 789def getTableClass(tag): 790 """Fetch the packer/unpacker class for a table. 791 Return None when no class is found. 792 """ 793 module = getTableModule(tag) 794 if module is None: 795 from tables.DefaultTable import DefaultTable 796 return DefaultTable 797 pyTag = tagToIdentifier(tag) 798 tableClass = getattr(module, "table_" + pyTag) 799 return tableClass 800 801 802def newTable(tag): 803 """Return a new instance of a table.""" 804 tableClass = getTableClass(tag) 805 return tableClass(tag) 806 807 808def _escapechar(c): 809 """Helper function for tagToIdentifier()""" 810 import re 811 if re.match("[a-z0-9]", c): 812 return "_" + c 813 elif re.match("[A-Z]", c): 814 return c + "_" 815 else: 816 return hex(ord(c))[2:] 817 818 819def tagToIdentifier(tag): 820 """Convert a table tag to a valid (but UGLY) python identifier, 821 as well as a filename that's guaranteed to be unique even on a 822 caseless file system. Each character is mapped to two characters. 823 Lowercase letters get an underscore before the letter, uppercase 824 letters get an underscore after the letter. Trailing spaces are 825 trimmed. Illegal characters are escaped as two hex bytes. If the 826 result starts with a number (as the result of a hex escape), an 827 extra underscore is prepended. Examples: 828 'glyf' -> '_g_l_y_f' 829 'cvt ' -> '_c_v_t' 830 'OS/2' -> 'O_S_2f_2' 831 """ 832 import re 833 if tag == "GlyphOrder": 834 return tag 835 assert len(tag) == 4, "tag should be 4 characters long" 836 while len(tag) > 1 and tag[-1] == ' ': 837 tag = tag[:-1] 838 ident = "" 839 for c in tag: 840 ident = ident + _escapechar(c) 841 if re.match("[0-9]", ident): 842 ident = "_" + ident 843 return ident 844 845 846def identifierToTag(ident): 847 """the opposite of tagToIdentifier()""" 848 if ident == "GlyphOrder": 849 return ident 850 if len(ident) % 2 and ident[0] == "_": 851 ident = ident[1:] 852 assert not (len(ident) % 2) 853 tag = "" 854 for i in range(0, len(ident), 2): 855 if ident[i] == "_": 856 tag = tag + ident[i+1] 857 elif ident[i+1] == "_": 858 tag = tag + ident[i] 859 else: 860 # assume hex 861 tag = tag + chr(int(ident[i:i+2], 16)) 862 # append trailing spaces 863 tag = tag + (4 - len(tag)) * ' ' 864 return tag 865 866 867def tagToXML(tag): 868 """Similarly to tagToIdentifier(), this converts a TT tag 869 to a valid XML element name. Since XML element names are 870 case sensitive, this is a fairly simple/readable translation. 871 """ 872 import re 873 if tag == "OS/2": 874 return "OS_2" 875 elif tag == "GlyphOrder": 876 return tag 877 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 878 return string.strip(tag) 879 else: 880 return tagToIdentifier(tag) 881 882 883def xmlToTag(tag): 884 """The opposite of tagToXML()""" 885 if tag == "OS_2": 886 return "OS/2" 887 if len(tag) == 8: 888 return identifierToTag(tag) 889 else: 890 return tag + " " * (4 - len(tag)) 891 return tag 892 893 894def debugmsg(msg): 895 import time 896 print msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())) 897 898 899# Table order as recommended in the OpenType specification 1.4 900TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", 901 "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", 902 "kern", "name", "post", "gasp", "PCLT"] 903 904OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", 905 "CFF "] 906 907def sortedTagList(tagList, tableOrder=None): 908 """Return a sorted copy of tagList, sorted according to the OpenType 909 specification, or according to a custom tableOrder. If given and not 910 None, tableOrder needs to be a list of tag names. 911 """ 912 tagList = list(tagList) 913 tagList.sort() 914 if tableOrder is None: 915 if "DSIG" in tagList: 916 # DSIG should be last (XXX spec reference?) 917 tagList.remove("DSIG") 918 tagList.append("DSIG") 919 if "CFF " in tagList: 920 tableOrder = OTFTableOrder 921 else: 922 tableOrder = TTFTableOrder 923 orderedTables = [] 924 for tag in tableOrder: 925 if tag in tagList: 926 orderedTables.append(tag) 927 tagList.remove(tag) 928 orderedTables.extend(tagList) 929 return orderedTables 930 931 932def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=0): 933 """Rewrite a font file, ordering the tables as recommended by the 934 OpenType specification 1.4. 935 """ 936 from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 937 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 938 writer = SFNTWriter(outFile, reader.numTables, reader.sfntVersion, reader.flavor, reader.flavorData) 939 tables = reader.keys() 940 for tag in sortedTagList(tables, tableOrder): 941 writer[tag] = reader[tag] 942 writer.close() 943