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