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