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