1 """fontTools.ttLib -- a package for dealing with TrueType fonts. 2 3 This package offers translators to convert TrueType fonts to Python 4 objects and vice versa, and additionally from Python to TTX (an XML-based 5 text format) and vice versa. 6 7 Example interactive session: 8 9 Python 1.5.2c1 (#43, Mar 9 1999, 13:06:43) [CW PPC w/GUSI w/MSL] 10 Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam 11 >>> from fontTools import ttLib 12 >>> tt = ttLib.TTFont("afont.ttf") 13 >>> tt['maxp'].numGlyphs 14 242 15 >>> tt['OS/2'].achVendID 16 'B&H\000' 17 >>> tt['head'].unitsPerEm 18 2048 19 >>> tt.saveXML("afont.ttx") 20 Dumping 'LTSH' table... 21 Dumping 'OS/2' table... 22 Dumping 'VDMX' table... 23 Dumping 'cmap' table... 24 Dumping 'cvt ' table... 25 Dumping 'fpgm' table... 26 Dumping 'glyf' table... 27 Dumping 'hdmx' table... 28 Dumping 'head' table... 29 Dumping 'hhea' table... 30 Dumping 'hmtx' table... 31 Dumping 'loca' table... 32 Dumping 'maxp' table... 33 Dumping 'name' table... 34 Dumping 'post' table... 35 Dumping 'prep' table... 36 >>> tt2 = ttLib.TTFont() 37 >>> tt2.importXML("afont.ttx") 38 >>> tt2['maxp'].numGlyphs 39 242 40 >>> 41 42 """ 43 44 from __future__ import print_function, division, absolute_import 45 from fontTools.misc.py23 import * 46 import os 47 import sys 48 49 haveMacSupport = 0 50 if sys.platform == "mac": 51 haveMacSupport = 1 52 elif sys.platform == "darwin" and sys.version_info[:3] != (2, 2, 0): 53 # Python 2.2's Mac support is broken, so don't enable it there. 54 haveMacSupport = 1 55 56 57 class TTLibError(Exception): pass 58 59 60 class TTFont(object): 61 62 """The main font object. It manages file input and output, and offers 63 a convenient way of accessing tables. 64 Tables will be only decompiled when necessary, ie. when they're actually 65 accessed. This means that simple operations can be extremely fast. 66 """ 67 68 def __init__(self, file=None, res_name_or_index=None, 69 sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, 70 verbose=False, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, 71 recalcTimestamp=True, fontNumber=-1, lazy=False, quiet=False): 72 73 """The constructor can be called with a few different arguments. 74 When reading a font from disk, 'file' should be either a pathname 75 pointing to a file, or a readable file object. 76 77 It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt 78 resource name or an sfnt resource index number or zero. The latter 79 case will cause TTLib to autodetect whether the file is a flat file 80 or a suitcase. (If it's a suitcase, only the first 'sfnt' resource 81 will be read!) 82 83 The 'checkChecksums' argument is used to specify how sfnt 84 checksums are treated upon reading a file from disk: 85 0: don't check (default) 86 1: check, print warnings if a wrong checksum is found 87 2: check, raise an exception if a wrong checksum is found. 88 89 The TTFont constructor can also be called without a 'file' 90 argument: this is the way to create a new empty font. 91 In this case you can optionally supply the 'sfntVersion' argument, 92 and a 'flavor' which can be None, or 'woff'. 93 94 If the recalcBBoxes argument is false, a number of things will *not* 95 be recalculated upon save/compile: 96 1) glyph bounding boxes 97 2) maxp font bounding box 98 3) hhea min/max values 99 (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). 100 Additionally, upon importing an TTX file, this option cause glyphs 101 to be compiled right away. This should reduce memory consumption 102 greatly, and therefore should have some impact on the time needed 103 to parse/compile large fonts. 104 105 If the recalcTimestamp argument is false, the modified timestamp in the 106 'head' table will *not* be recalculated upon save/compile. 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 N is 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.recalcTimestamp = recalcTimestamp 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 get(self, tag, default=None): 419 try: 420 return self[tag] 421 except KeyError: 422 return default 423 424 def setGlyphOrder(self, glyphOrder): 425 self.glyphOrder = glyphOrder 426 427 def getGlyphOrder(self): 428 try: 429 return self.glyphOrder 430 except AttributeError: 431 pass 432 if 'CFF ' in self: 433 cff = self['CFF '] 434 self.glyphOrder = cff.getGlyphOrder() 435 elif 'post' in self: 436 # TrueType font 437 glyphOrder = self['post'].getGlyphOrder() 438 if glyphOrder is None: 439 # 440 # No names found in the 'post' table. 441 # Try to create glyph names from the unicode cmap (if available) 442 # in combination with the Adobe Glyph List (AGL). 443 # 444 self._getGlyphNamesFromCmap() 445 else: 446 self.glyphOrder = glyphOrder 447 else: 448 self._getGlyphNamesFromCmap() 449 return self.glyphOrder 450 451 def _getGlyphNamesFromCmap(self): 452 # 453 # This is rather convoluted, but then again, it's an interesting problem: 454 # - we need to use the unicode values found in the cmap table to 455 # build glyph names (eg. because there is only a minimal post table, 456 # or none at all). 457 # - but the cmap parser also needs glyph names to work with... 458 # So here's what we do: 459 # - make up glyph names based on glyphID 460 # - load a temporary cmap table based on those names 461 # - extract the unicode values, build the "real" glyph names 462 # - unload the temporary cmap table 463 # 464 if self.isLoaded("cmap"): 465 # Bootstrapping: we're getting called by the cmap parser 466 # itself. This means self.tables['cmap'] contains a partially 467 # loaded cmap, making it impossible to get at a unicode 468 # subtable here. We remove the partially loaded cmap and 469 # restore it later. 470 # This only happens if the cmap table is loaded before any 471 # other table that does f.getGlyphOrder() or f.getGlyphName(). 472 cmapLoading = self.tables['cmap'] 473 del self.tables['cmap'] 474 else: 475 cmapLoading = None 476 # Make up glyph names based on glyphID, which will be used by the 477 # temporary cmap and by the real cmap in case we don't find a unicode 478 # cmap. 479 numGlyphs = int(self['maxp'].numGlyphs) 480 glyphOrder = [None] * numGlyphs 481 glyphOrder[0] = ".notdef" 482 for i in range(1, numGlyphs): 483 glyphOrder[i] = "glyph%.5d" % i 484 # Set the glyph order, so the cmap parser has something 485 # to work with (so we don't get called recursively). 486 self.glyphOrder = glyphOrder 487 # Get a (new) temporary cmap (based on the just invented names) 488 tempcmap = self['cmap'].getcmap(3, 1) 489 if tempcmap is not None: 490 # we have a unicode cmap 491 from fontTools import agl 492 cmap = tempcmap.cmap 493 # create a reverse cmap dict 494 reversecmap = {} 495 for unicode, name in list(cmap.items()): 496 reversecmap[name] = unicode 497 allNames = {} 498 for i in range(numGlyphs): 499 tempName = glyphOrder[i] 500 if tempName in reversecmap: 501 unicode = reversecmap[tempName] 502 if unicode in agl.UV2AGL: 503 # get name from the Adobe Glyph List 504 glyphName = agl.UV2AGL[unicode] 505 else: 506 # create uni<CODE> name 507 glyphName = "uni%04X" % unicode 508 tempName = glyphName 509 n = 1 510 while tempName in allNames: 511 tempName = glyphName + "#" + repr(n) 512 n = n + 1 513 glyphOrder[i] = tempName 514 allNames[tempName] = 1 515 # Delete the temporary cmap table from the cache, so it can 516 # be parsed again with the right names. 517 del self.tables['cmap'] 518 else: 519 pass # no unicode cmap available, stick with the invented names 520 self.glyphOrder = glyphOrder 521 if cmapLoading: 522 # restore partially loaded cmap, so it can continue loading 523 # using the proper names. 524 self.tables['cmap'] = cmapLoading 525 526 def getGlyphNames(self): 527 """Get a list of glyph names, sorted alphabetically.""" 528 glyphNames = sorted(self.getGlyphOrder()[:]) 529 return glyphNames 530 531 def getGlyphNames2(self): 532 """Get a list of glyph names, sorted alphabetically, 533 but not case sensitive. 534 """ 535 from fontTools.misc import textTools 536 return textTools.caselessSort(self.getGlyphOrder()) 537 538 def getGlyphName(self, glyphID, requireReal=False): 539 try: 540 return self.getGlyphOrder()[glyphID] 541 except IndexError: 542 if requireReal or not self.allowVID: 543 # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in 544 # the cmap table than there are glyphs. I don't think it's legal... 545 return "glyph%.5d" % glyphID 546 else: 547 # user intends virtual GID support 548 try: 549 glyphName = self.VIDDict[glyphID] 550 except KeyError: 551 glyphName ="glyph%.5d" % glyphID 552 self.last_vid = min(glyphID, self.last_vid ) 553 self.reverseVIDDict[glyphName] = glyphID 554 self.VIDDict[glyphID] = glyphName 555 return glyphName 556 557 def getGlyphID(self, glyphName, requireReal=False): 558 if not hasattr(self, "_reverseGlyphOrderDict"): 559 self._buildReverseGlyphOrderDict() 560 glyphOrder = self.getGlyphOrder() 561 d = self._reverseGlyphOrderDict 562 if glyphName not in d: 563 if glyphName in glyphOrder: 564 self._buildReverseGlyphOrderDict() 565 return self.getGlyphID(glyphName) 566 else: 567 if requireReal: 568 raise KeyError(glyphName) 569 elif not self.allowVID: 570 # Handle glyphXXX only 571 if glyphName[:5] == "glyph": 572 try: 573 return int(glyphName[5:]) 574 except (NameError, ValueError): 575 raise KeyError(glyphName) 576 else: 577 # user intends virtual GID support 578 try: 579 glyphID = self.reverseVIDDict[glyphName] 580 except KeyError: 581 # if name is in glyphXXX format, use the specified name. 582 if glyphName[:5] == "glyph": 583 try: 584 glyphID = int(glyphName[5:]) 585 except (NameError, ValueError): 586 glyphID = None 587 if glyphID is None: 588 glyphID = self.last_vid -1 589 self.last_vid = glyphID 590 self.reverseVIDDict[glyphName] = glyphID 591 self.VIDDict[glyphID] = glyphName 592 return glyphID 593 594 glyphID = d[glyphName] 595 if glyphName != glyphOrder[glyphID]: 596 self._buildReverseGlyphOrderDict() 597 return self.getGlyphID(glyphName) 598 return glyphID 599 600 def getReverseGlyphMap(self, rebuild=False): 601 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): 602 self._buildReverseGlyphOrderDict() 603 return self._reverseGlyphOrderDict 604 605 def _buildReverseGlyphOrderDict(self): 606 self._reverseGlyphOrderDict = d = {} 607 glyphOrder = self.getGlyphOrder() 608 for glyphID in range(len(glyphOrder)): 609 d[glyphOrder[glyphID]] = glyphID 610 611 def _writeTable(self, tag, writer, done): 612 """Internal helper function for self.save(). Keeps track of 613 inter-table dependencies. 614 """ 615 if tag in done: 616 return 617 tableClass = getTableClass(tag) 618 for masterTable in tableClass.dependencies: 619 if masterTable not in done: 620 if masterTable in self: 621 self._writeTable(masterTable, writer, done) 622 else: 623 done.append(masterTable) 624 tabledata = self.getTableData(tag) 625 if self.verbose: 626 debugmsg("writing '%s' table to disk" % tag) 627 writer[tag] = tabledata 628 done.append(tag) 629 630 def getTableData(self, tag): 631 """Returns raw table data, whether compiled or directly read from disk. 632 """ 633 tag = Tag(tag) 634 if self.isLoaded(tag): 635 if self.verbose: 636 debugmsg("compiling '%s' table" % tag) 637 return self.tables[tag].compile(self) 638 elif self.reader and tag in self.reader: 639 if self.verbose: 640 debugmsg("Reading '%s' table from disk" % tag) 641 return self.reader[tag] 642 else: 643 raise KeyError(tag) 644 645 def getGlyphSet(self, preferCFF=True): 646 """Return a generic GlyphSet, which is a dict-like object 647 mapping glyph names to glyph objects. The returned glyph objects 648 have a .draw() method that supports the Pen protocol, and will 649 have an attribute named 'width', but only *after* the .draw() method 650 has been called. 651 652 If the font is CFF-based, the outlines will be taken from the 'CFF ' 653 table. Otherwise the outlines will be taken from the 'glyf' table. 654 If the font contains both a 'CFF ' and a 'glyf' table, you can use 655 the 'preferCFF' argument to specify which one should be taken. 656 """ 657 if preferCFF and "CFF " in self: 658 return list(self["CFF "].cff.values())[0].CharStrings 659 if "glyf" in self: 660 return _TTGlyphSet(self) 661 if "CFF " in self: 662 return list(self["CFF "].cff.values())[0].CharStrings 663 raise TTLibError("Font contains no outlines") 664 665 666 class _TTGlyphSet(object): 667 668 """Generic dict-like GlyphSet class, meant as a TrueType counterpart 669 to CFF's CharString dict. See TTFont.getGlyphSet(). 670 """ 671 672 # This class is distinct from the 'glyf' table itself because we need 673 # access to the 'hmtx' table, which could cause a dependency problem 674 # there when reading from XML. 675 676 def __init__(self, ttFont): 677 self._ttFont = ttFont 678 679 def keys(self): 680 return list(self._ttFont["glyf"].keys()) 681 682 def has_key(self, glyphName): 683 return glyphName in self._ttFont["glyf"] 684 685 __contains__ = has_key 686 687 def __getitem__(self, glyphName): 688 return _TTGlyph(glyphName, self._ttFont) 689 690 def get(self, glyphName, default=None): 691 try: 692 return self[glyphName] 693 except KeyError: 694 return default 695 696 697 class _TTGlyph(object): 698 699 """Wrapper for a TrueType glyph that supports the Pen protocol, meaning 700 that it has a .draw() method that takes a pen object as its only 701 argument. Additionally there is a 'width' attribute. 702 """ 703 704 def __init__(self, glyphName, ttFont): 705 self._glyphName = glyphName 706 self._ttFont = ttFont 707 self.width, self.lsb = self._ttFont['hmtx'][self._glyphName] 708 709 def draw(self, pen): 710 """Draw the glyph onto Pen. See fontTools.pens.basePen for details 711 how that works. 712 """ 713 glyfTable = self._ttFont['glyf'] 714 glyph = glyfTable[self._glyphName] 715 if hasattr(glyph, "xMin"): 716 offset = self.lsb - glyph.xMin 717 else: 718 offset = 0 719 if glyph.isComposite(): 720 for component in glyph: 721 glyphName, transform = component.getComponentInfo() 722 pen.addComponent(glyphName, transform) 723 else: 724 coordinates, endPts, flags = glyph.getCoordinates(glyfTable) 725 if offset: 726 coordinates = coordinates + (offset, 0) 727 start = 0 728 for end in endPts: 729 end = end + 1 730 contour = coordinates[start:end].tolist() 731 cFlags = flags[start:end].tolist() 732 start = end 733 if 1 not in cFlags: 734 # There is not a single on-curve point on the curve, 735 # use pen.qCurveTo's special case by specifying None 736 # as the on-curve point. 737 contour.append(None) 738 pen.qCurveTo(*contour) 739 else: 740 # Shuffle the points so that contour the is guaranteed 741 # to *end* in an on-curve point, which we'll use for 742 # the moveTo. 743 firstOnCurve = cFlags.index(1) + 1 744 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 745 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 746 pen.moveTo(contour[-1]) 747 while contour: 748 nextOnCurve = cFlags.index(1) + 1 749 if nextOnCurve == 1: 750 pen.lineTo(contour[0]) 751 else: 752 pen.qCurveTo(*contour[:nextOnCurve]) 753 contour = contour[nextOnCurve:] 754 cFlags = cFlags[nextOnCurve:] 755 pen.closePath() 756 757 758 class GlyphOrder(object): 759 760 """A pseudo table. The glyph order isn't in the font as a separate 761 table, but it's nice to present it as such in the TTX format. 762 """ 763 764 def __init__(self, tag=None): 765 pass 766 767 def toXML(self, writer, ttFont): 768 glyphOrder = ttFont.getGlyphOrder() 769 writer.comment("The 'id' attribute is only for humans; " 770 "it is ignored when parsed.") 771 writer.newline() 772 for i in range(len(glyphOrder)): 773 glyphName = glyphOrder[i] 774 writer.simpletag("GlyphID", id=i, name=glyphName) 775 writer.newline() 776 777 def fromXML(self, name, attrs, content, ttFont): 778 if not hasattr(self, "glyphOrder"): 779 self.glyphOrder = [] 780 ttFont.setGlyphOrder(self.glyphOrder) 781 if name == "GlyphID": 782 self.glyphOrder.append(attrs["name"]) 783 784 785 def getTableModule(tag): 786 """Fetch the packer/unpacker module for a table. 787 Return None when no module is found. 788 """ 789 from . import tables 790 pyTag = tagToIdentifier(tag) 791 try: 792 __import__("fontTools.ttLib.tables." + pyTag) 793 except ImportError as err: 794 # If pyTag is found in the ImportError message, 795 # means table is not implemented. If it's not 796 # there, then some other module is missing, don't 797 # suppress the error. 798 if str(err).find(pyTag) >= 0: 799 return None 800 else: 801 raise err 802 else: 803 return getattr(tables, pyTag) 804 805 806 def getTableClass(tag): 807 """Fetch the packer/unpacker class for a table. 808 Return None when no class is found. 809 """ 810 module = getTableModule(tag) 811 if module is None: 812 from .tables.DefaultTable import DefaultTable 813 return DefaultTable 814 pyTag = tagToIdentifier(tag) 815 tableClass = getattr(module, "table_" + pyTag) 816 return tableClass 817 818 819 def getClassTag(klass): 820 """Fetch the table tag for a class object.""" 821 name = klass.__name__ 822 assert name[:6] == 'table_' 823 name = name[6:] # Chop 'table_' 824 return identifierToTag(name) 825 826 827 828 def newTable(tag): 829 """Return a new instance of a table.""" 830 tableClass = getTableClass(tag) 831 return tableClass(tag) 832 833 834 def _escapechar(c): 835 """Helper function for tagToIdentifier()""" 836 import re 837 if re.match("[a-z0-9]", c): 838 return "_" + c 839 elif re.match("[A-Z]", c): 840 return c + "_" 841 else: 842 return hex(byteord(c))[2:] 843 844 845 def tagToIdentifier(tag): 846 """Convert a table tag to a valid (but UGLY) python identifier, 847 as well as a filename that's guaranteed to be unique even on a 848 caseless file system. Each character is mapped to two characters. 849 Lowercase letters get an underscore before the letter, uppercase 850 letters get an underscore after the letter. Trailing spaces are 851 trimmed. Illegal characters are escaped as two hex bytes. If the 852 result starts with a number (as the result of a hex escape), an 853 extra underscore is prepended. Examples: 854 'glyf' -> '_g_l_y_f' 855 'cvt ' -> '_c_v_t' 856 'OS/2' -> 'O_S_2f_2' 857 """ 858 import re 859 tag = Tag(tag) 860 if tag == "GlyphOrder": 861 return tag 862 assert len(tag) == 4, "tag should be 4 characters long" 863 while len(tag) > 1 and tag[-1] == ' ': 864 tag = tag[:-1] 865 ident = "" 866 for c in tag: 867 ident = ident + _escapechar(c) 868 if re.match("[0-9]", ident): 869 ident = "_" + ident 870 return ident 871 872 873 def identifierToTag(ident): 874 """the opposite of tagToIdentifier()""" 875 if ident == "GlyphOrder": 876 return ident 877 if len(ident) % 2 and ident[0] == "_": 878 ident = ident[1:] 879 assert not (len(ident) % 2) 880 tag = "" 881 for i in range(0, len(ident), 2): 882 if ident[i] == "_": 883 tag = tag + ident[i+1] 884 elif ident[i+1] == "_": 885 tag = tag + ident[i] 886 else: 887 # assume hex 888 tag = tag + chr(int(ident[i:i+2], 16)) 889 # append trailing spaces 890 tag = tag + (4 - len(tag)) * ' ' 891 return Tag(tag) 892 893 894 def tagToXML(tag): 895 """Similarly to tagToIdentifier(), this converts a TT tag 896 to a valid XML element name. Since XML element names are 897 case sensitive, this is a fairly simple/readable translation. 898 """ 899 import re 900 tag = Tag(tag) 901 if tag == "OS/2": 902 return "OS_2" 903 elif tag == "GlyphOrder": 904 return tag 905 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 906 return tag.strip() 907 else: 908 return tagToIdentifier(tag) 909 910 911 def xmlToTag(tag): 912 """The opposite of tagToXML()""" 913 if tag == "OS_2": 914 return Tag("OS/2") 915 if len(tag) == 8: 916 return identifierToTag(tag) 917 else: 918 return Tag(tag + " " * (4 - len(tag))) 919 920 921 def debugmsg(msg): 922 import time 923 print(msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time()))) 924 925 926 # Table order as recommended in the OpenType specification 1.4 927 TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", 928 "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", 929 "kern", "name", "post", "gasp", "PCLT"] 930 931 OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", 932 "CFF "] 933 934 def sortedTagList(tagList, tableOrder=None): 935 """Return a sorted copy of tagList, sorted according to the OpenType 936 specification, or according to a custom tableOrder. If given and not 937 None, tableOrder needs to be a list of tag names. 938 """ 939 tagList = sorted(tagList) 940 if tableOrder is None: 941 if "DSIG" in tagList: 942 # DSIG should be last (XXX spec reference?) 943 tagList.remove("DSIG") 944 tagList.append("DSIG") 945 if "CFF " in tagList: 946 tableOrder = OTFTableOrder 947 else: 948 tableOrder = TTFTableOrder 949 orderedTables = [] 950 for tag in tableOrder: 951 if tag in tagList: 952 orderedTables.append(tag) 953 tagList.remove(tag) 954 orderedTables.extend(tagList) 955 return orderedTables 956 957 958 def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): 959 """Rewrite a font file, ordering the tables as recommended by the 960 OpenType specification 1.4. 961 """ 962 from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 963 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 964 writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) 965 tables = list(reader.keys()) 966 for tag in sortedTagList(tables, tableOrder): 967 writer[tag] = reader[tag] 968 writer.close() 969 970 971 def maxPowerOfTwo(x): 972 """Return the highest exponent of two, so that 973 (2 ** exponent) <= x. Return 0 if x is 0. 974 """ 975 exponent = 0 976 while x: 977 x = x >> 1 978 exponent = exponent + 1 979 return max(exponent - 1, 0) 980 981 982 def getSearchRange(n, itemSize): 983 """Calculate searchRange, entrySelector, rangeShift. 984 """ 985 # This stuff needs to be stored in the file, because? 986 exponent = maxPowerOfTwo(n) 987 searchRange = (2 ** exponent) * itemSize 988 entrySelector = exponent 989 rangeShift = max(0, n * itemSize - searchRange) 990 return searchRange, entrySelector, rangeShift 991