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