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