__init__.py revision ebefbbaa58f9e821dd535b354fcccecc2d24d914
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=True, 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 or not self.allowVID: 562 raise KeyError(glyphName) 563 else: 564 # user intends virtual GID support 565 try: 566 glyphID = self.reverseVIDDict[glyphName] 567 except KeyError: 568 # if name is in glyphXXX format, use the specified name. 569 if glyphName[:5] == "glyph": 570 try: 571 glyphID = int(glyphName[5:]) 572 except (NameError, ValueError): 573 glyphID = None 574 if glyphID is None: 575 glyphID = self.last_vid -1 576 self.last_vid = glyphID 577 self.reverseVIDDict[glyphName] = glyphID 578 self.VIDDict[glyphID] = glyphName 579 return glyphID 580 581 glyphID = d[glyphName] 582 if glyphName != glyphOrder[glyphID]: 583 self._buildReverseGlyphOrderDict() 584 return self.getGlyphID(glyphName) 585 return glyphID 586 587 def getReverseGlyphMap(self, rebuild=False): 588 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): 589 self._buildReverseGlyphOrderDict() 590 return self._reverseGlyphOrderDict 591 592 def _buildReverseGlyphOrderDict(self): 593 self._reverseGlyphOrderDict = d = {} 594 glyphOrder = self.getGlyphOrder() 595 for glyphID in range(len(glyphOrder)): 596 d[glyphOrder[glyphID]] = glyphID 597 598 def _writeTable(self, tag, writer, done): 599 """Internal helper function for self.save(). Keeps track of 600 inter-table dependencies. 601 """ 602 if tag in done: 603 return 604 tableClass = getTableClass(tag) 605 for masterTable in tableClass.dependencies: 606 if masterTable not in done: 607 if masterTable in self: 608 self._writeTable(masterTable, writer, done) 609 else: 610 done.append(masterTable) 611 tabledata = self.getTableData(tag) 612 if self.verbose: 613 debugmsg("writing '%s' table to disk" % tag) 614 writer[tag] = tabledata 615 done.append(tag) 616 617 def getTableData(self, tag): 618 """Returns raw table data, whether compiled or directly read from disk. 619 """ 620 tag = Tag(tag) 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=True): 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 list(self["CFF "].cff.values())[0].CharStrings 646 if "glyf" in self: 647 return _TTGlyphSet(self) 648 if "CFF " in self: 649 return list(self["CFF "].cff.values())[0].CharStrings 650 raise TTLibError("Font contains no outlines") 651 652 653class _TTGlyphSet(object): 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 list(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(object): 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(object): 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(byteord(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 tag = Tag(tag) 838 if tag == "GlyphOrder": 839 return tag 840 assert len(tag) == 4, "tag should be 4 characters long" 841 while len(tag) > 1 and tag[-1] == ' ': 842 tag = tag[:-1] 843 ident = "" 844 for c in tag: 845 ident = ident + _escapechar(c) 846 if re.match("[0-9]", ident): 847 ident = "_" + ident 848 return ident 849 850 851def identifierToTag(ident): 852 """the opposite of tagToIdentifier()""" 853 if ident == "GlyphOrder": 854 return ident 855 if len(ident) % 2 and ident[0] == "_": 856 ident = ident[1:] 857 assert not (len(ident) % 2) 858 tag = "" 859 for i in range(0, len(ident), 2): 860 if ident[i] == "_": 861 tag = tag + ident[i+1] 862 elif ident[i+1] == "_": 863 tag = tag + ident[i] 864 else: 865 # assume hex 866 tag = tag + bytechr(int(ident[i:i+2], 16)) 867 # append trailing spaces 868 tag = tag + (4 - len(tag)) * ' ' 869 return Tag(tag) 870 871 872def tagToXML(tag): 873 """Similarly to tagToIdentifier(), this converts a TT tag 874 to a valid XML element name. Since XML element names are 875 case sensitive, this is a fairly simple/readable translation. 876 """ 877 import re 878 tag = Tag(tag) 879 if tag == "OS/2": 880 return "OS_2" 881 elif tag == "GlyphOrder": 882 return tag 883 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 884 return tag.strip() 885 else: 886 return tagToIdentifier(tag) 887 888 889def xmlToTag(tag): 890 """The opposite of tagToXML()""" 891 if tag == "OS_2": 892 return Tag("OS/2") 893 if len(tag) == 8: 894 return identifierToTag(tag) 895 else: 896 return Tag(tag + " " * (4 - len(tag))) 897 898 899def debugmsg(msg): 900 import time 901 print(msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time()))) 902 903 904# Table order as recommended in the OpenType specification 1.4 905TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", 906 "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", 907 "kern", "name", "post", "gasp", "PCLT"] 908 909OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", 910 "CFF "] 911 912def sortedTagList(tagList, tableOrder=None): 913 """Return a sorted copy of tagList, sorted according to the OpenType 914 specification, or according to a custom tableOrder. If given and not 915 None, tableOrder needs to be a list of tag names. 916 """ 917 tagList = sorted(tagList) 918 if tableOrder is None: 919 if "DSIG" in tagList: 920 # DSIG should be last (XXX spec reference?) 921 tagList.remove("DSIG") 922 tagList.append("DSIG") 923 if "CFF " in tagList: 924 tableOrder = OTFTableOrder 925 else: 926 tableOrder = TTFTableOrder 927 orderedTables = [] 928 for tag in tableOrder: 929 if tag in tagList: 930 orderedTables.append(tag) 931 tagList.remove(tag) 932 orderedTables.extend(tagList) 933 return orderedTables 934 935 936def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): 937 """Rewrite a font file, ordering the tables as recommended by the 938 OpenType specification 1.4. 939 """ 940 from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 941 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 942 writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) 943 tables = list(reader.keys()) 944 for tag in sortedTagList(tables, tableOrder): 945 writer[tag] = reader[tag] 946 writer.close() 947