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