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