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