__init__.py revision b0dc6dfc8baf01db94782fccc2e734a281b9ba12
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 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(tag) 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, tag): 743 pass 744 745 def toXML(self, writer, ttFont): 746 glyphOrder = ttFont.getGlyphOrder() 747 writer.comment("The 'id' attribute is only for humans; " 748 "it is ignored when parsed.") 749 writer.newline() 750 for i in range(len(glyphOrder)): 751 glyphName = glyphOrder[i] 752 writer.simpletag("GlyphID", id=i, name=glyphName) 753 writer.newline() 754 755 def fromXML(self, (name, attrs, content), ttFont): 756 if not hasattr(self, "glyphOrder"): 757 self.glyphOrder = [] 758 ttFont.setGlyphOrder(self.glyphOrder) 759 if name == "GlyphID": 760 self.glyphOrder.append(attrs["name"]) 761 762 763def getTableModule(tag): 764 """Fetch the packer/unpacker module for a table. 765 Return None when no module is found. 766 """ 767 import tables 768 pyTag = tagToIdentifier(tag) 769 try: 770 __import__("fontTools.ttLib.tables." + pyTag) 771 except ImportError: 772 return None 773 else: 774 return getattr(tables, pyTag) 775 776 777def getTableClass(tag): 778 """Fetch the packer/unpacker class for a table. 779 Return None when no class is found. 780 """ 781 module = getTableModule(tag) 782 if module is None: 783 from tables.DefaultTable import DefaultTable 784 return DefaultTable 785 pyTag = tagToIdentifier(tag) 786 tableClass = getattr(module, "table_" + pyTag) 787 return tableClass 788 789 790def newTable(tag): 791 """Return a new instance of a table.""" 792 tableClass = getTableClass(tag) 793 return tableClass(tag) 794 795 796def _escapechar(c): 797 """Helper function for tagToIdentifier()""" 798 import re 799 if re.match("[a-z0-9]", c): 800 return "_" + c 801 elif re.match("[A-Z]", c): 802 return c + "_" 803 else: 804 return hex(ord(c))[2:] 805 806 807def tagToIdentifier(tag): 808 """Convert a table tag to a valid (but UGLY) python identifier, 809 as well as a filename that's guaranteed to be unique even on a 810 caseless file system. Each character is mapped to two characters. 811 Lowercase letters get an underscore before the letter, uppercase 812 letters get an underscore after the letter. Trailing spaces are 813 trimmed. Illegal characters are escaped as two hex bytes. If the 814 result starts with a number (as the result of a hex escape), an 815 extra underscore is prepended. Examples: 816 'glyf' -> '_g_l_y_f' 817 'cvt ' -> '_c_v_t' 818 'OS/2' -> 'O_S_2f_2' 819 """ 820 import re 821 if tag == "GlyphOrder": 822 return tag 823 assert len(tag) == 4, "tag should be 4 characters long" 824 while len(tag) > 1 and tag[-1] == ' ': 825 tag = tag[:-1] 826 ident = "" 827 for c in tag: 828 ident = ident + _escapechar(c) 829 if re.match("[0-9]", ident): 830 ident = "_" + ident 831 return ident 832 833 834def identifierToTag(ident): 835 """the opposite of tagToIdentifier()""" 836 if ident == "GlyphOrder": 837 return ident 838 if len(ident) % 2 and ident[0] == "_": 839 ident = ident[1:] 840 assert not (len(ident) % 2) 841 tag = "" 842 for i in range(0, len(ident), 2): 843 if ident[i] == "_": 844 tag = tag + ident[i+1] 845 elif ident[i+1] == "_": 846 tag = tag + ident[i] 847 else: 848 # assume hex 849 tag = tag + chr(int(ident[i:i+2], 16)) 850 # append trailing spaces 851 tag = tag + (4 - len(tag)) * ' ' 852 return tag 853 854 855def tagToXML(tag): 856 """Similarly to tagToIdentifier(), this converts a TT tag 857 to a valid XML element name. Since XML element names are 858 case sensitive, this is a fairly simple/readable translation. 859 """ 860 import re 861 if tag == "OS/2": 862 return "OS_2" 863 elif tag == "GlyphOrder": 864 return tag 865 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 866 return string.strip(tag) 867 else: 868 return tagToIdentifier(tag) 869 870 871def xmlToTag(tag): 872 """The opposite of tagToXML()""" 873 if tag == "OS_2": 874 return "OS/2" 875 if len(tag) == 8: 876 return identifierToTag(tag) 877 else: 878 return tag + " " * (4 - len(tag)) 879 return tag 880 881 882def debugmsg(msg): 883 import time 884 print msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())) 885 886 887# Table order as recommended in the OpenType specification 1.4 888TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", 889 "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", 890 "kern", "name", "post", "gasp", "PCLT"] 891 892OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", 893 "CFF "] 894 895def sortedTagList(tagList, tableOrder=None): 896 """Return a sorted copy of tagList, sorted according to the OpenType 897 specification, or according to a custom tableOrder. If given and not 898 None, tableOrder needs to be a list of tag names. 899 """ 900 tagList = list(tagList) 901 tagList.sort() 902 if tableOrder is None: 903 if "DSIG" in tagList: 904 # DSIG should be last (XXX spec reference?) 905 tagList.remove("DSIG") 906 tagList.append("DSIG") 907 if "CFF " in tagList: 908 tableOrder = OTFTableOrder 909 else: 910 tableOrder = TTFTableOrder 911 orderedTables = [] 912 for tag in tableOrder: 913 if tag in tagList: 914 orderedTables.append(tag) 915 tagList.remove(tag) 916 orderedTables.extend(tagList) 917 return orderedTables 918 919 920def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=0): 921 """Rewrite a font file, ordering the tables as recommended by the 922 OpenType specification 1.4. 923 """ 924 from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 925 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 926 writer = SFNTWriter(outFile, reader.numTables, reader.sfntVersion, reader.flavor, reader.flavorData) 927 tables = reader.keys() 928 for tag in sortedTagList(tables, tableOrder): 929 writer[tag] = reader[tag] 930 writer.close() 931