1from fontTools.misc import xmlWriter 2from fontTools.misc.textTools import Tag, byteord, tostr 3from fontTools.misc.loggingTools import deprecateArgument 4from fontTools.ttLib import TTLibError 5from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 6from io import BytesIO, StringIO 7import os 8import logging 9import traceback 10 11log = logging.getLogger(__name__) 12 13class TTFont(object): 14 15 """Represents a TrueType font. 16 17 The object manages file input and output, and offers a convenient way of 18 accessing tables. Tables will be only decompiled when necessary, ie. when 19 they're actually accessed. This means that simple operations can be extremely fast. 20 21 Example usage:: 22 23 >> from fontTools import ttLib 24 >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file 25 >> tt['maxp'].numGlyphs 26 242 27 >> tt['OS/2'].achVendID 28 'B&H\000' 29 >> tt['head'].unitsPerEm 30 2048 31 32 For details of the objects returned when accessing each table, see :ref:`tables`. 33 To add a table to the font, use the :py:func:`newTable` function:: 34 35 >> os2 = newTable("OS/2") 36 >> os2.version = 4 37 >> # set other attributes 38 >> font["OS/2"] = os2 39 40 TrueType fonts can also be serialized to and from XML format (see also the 41 :ref:`ttx` binary):: 42 43 >> tt.saveXML("afont.ttx") 44 Dumping 'LTSH' table... 45 Dumping 'OS/2' table... 46 [...] 47 48 >> tt2 = ttLib.TTFont() # Create a new font object 49 >> tt2.importXML("afont.ttx") 50 >> tt2['maxp'].numGlyphs 51 242 52 53 The TTFont object may be used as a context manager; this will cause the file 54 reader to be closed after the context ``with`` block is exited:: 55 56 with TTFont(filename) as f: 57 # Do stuff 58 59 Args: 60 file: When reading a font from disk, either a pathname pointing to a file, 61 or a readable file object. 62 res_name_or_index: If running on a Macintosh, either a sfnt resource name or 63 an sfnt resource index number. If the index number is zero, TTLib will 64 autodetect whether the file is a flat file or a suitcase. (If it is a suitcase, 65 only the first 'sfnt' resource will be read.) 66 sfntVersion (str): When constructing a font object from scratch, sets the four-byte 67 sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create 68 an OpenType file, use ``OTTO``. 69 flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2 70 file. 71 checkChecksums (int): How checksum data should be treated. Default is 0 72 (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to 73 raise an exception if any wrong checksums are found. 74 recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``, 75 ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save. 76 Also compiles the glyphs on importing, which saves memory consumption and 77 time. 78 ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation 79 will be ignored, and the binary data will be returned for those tables instead. 80 recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in 81 the ``head`` table on save. 82 fontNumber (int): The index of the font in a TrueType Collection file. 83 lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon 84 access only. If it is set to False, many data structures are loaded immediately. 85 The default is ``lazy=None`` which is somewhere in between. 86 """ 87 88 def __init__(self, file=None, res_name_or_index=None, 89 sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0, 90 verbose=None, recalcBBoxes=True, allowVID=NotImplemented, ignoreDecompileErrors=False, 91 recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None, 92 _tableCache=None): 93 for name in ("verbose", "quiet"): 94 val = locals().get(name) 95 if val is not None: 96 deprecateArgument(name, "configure logging instead") 97 setattr(self, name, val) 98 99 self.lazy = lazy 100 self.recalcBBoxes = recalcBBoxes 101 self.recalcTimestamp = recalcTimestamp 102 self.tables = {} 103 self.reader = None 104 self.ignoreDecompileErrors = ignoreDecompileErrors 105 106 if not file: 107 self.sfntVersion = sfntVersion 108 self.flavor = flavor 109 self.flavorData = None 110 return 111 if not hasattr(file, "read"): 112 closeStream = True 113 # assume file is a string 114 if res_name_or_index is not None: 115 # see if it contains 'sfnt' resources in the resource or data fork 116 from . import macUtils 117 if res_name_or_index == 0: 118 if macUtils.getSFNTResIndices(file): 119 # get the first available sfnt font. 120 file = macUtils.SFNTResourceReader(file, 1) 121 else: 122 file = open(file, "rb") 123 else: 124 file = macUtils.SFNTResourceReader(file, res_name_or_index) 125 else: 126 file = open(file, "rb") 127 else: 128 # assume "file" is a readable file object 129 closeStream = False 130 file.seek(0) 131 132 if not self.lazy: 133 # read input file in memory and wrap a stream around it to allow overwriting 134 file.seek(0) 135 tmp = BytesIO(file.read()) 136 if hasattr(file, 'name'): 137 # save reference to input file name 138 tmp.name = file.name 139 if closeStream: 140 file.close() 141 file = tmp 142 self._tableCache = _tableCache 143 self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber) 144 self.sfntVersion = self.reader.sfntVersion 145 self.flavor = self.reader.flavor 146 self.flavorData = self.reader.flavorData 147 148 def __enter__(self): 149 return self 150 151 def __exit__(self, type, value, traceback): 152 self.close() 153 154 def close(self): 155 """If we still have a reader object, close it.""" 156 if self.reader is not None: 157 self.reader.close() 158 159 def save(self, file, reorderTables=True): 160 """Save the font to disk. 161 162 Args: 163 file: Similarly to the constructor, can be either a pathname or a writable 164 file object. 165 reorderTables (Option[bool]): If true (the default), reorder the tables, 166 sorting them by tag (recommended by the OpenType specification). If 167 false, retain the original font order. If None, reorder by table 168 dependency (fastest). 169 """ 170 if not hasattr(file, "write"): 171 if self.lazy and self.reader.file.name == file: 172 raise TTLibError( 173 "Can't overwrite TTFont when 'lazy' attribute is True") 174 createStream = True 175 else: 176 # assume "file" is a writable file object 177 createStream = False 178 179 tmp = BytesIO() 180 181 writer_reordersTables = self._save(tmp) 182 183 if not (reorderTables is None or writer_reordersTables or 184 (reorderTables is False and self.reader is None)): 185 if reorderTables is False: 186 # sort tables using the original font's order 187 tableOrder = list(self.reader.keys()) 188 else: 189 # use the recommended order from the OpenType specification 190 tableOrder = None 191 tmp.flush() 192 tmp2 = BytesIO() 193 reorderFontTables(tmp, tmp2, tableOrder) 194 tmp.close() 195 tmp = tmp2 196 197 if createStream: 198 # "file" is a path 199 with open(file, "wb") as file: 200 file.write(tmp.getvalue()) 201 else: 202 file.write(tmp.getvalue()) 203 204 tmp.close() 205 206 def _save(self, file, tableCache=None): 207 """Internal function, to be shared by save() and TTCollection.save()""" 208 209 if self.recalcTimestamp and 'head' in self: 210 self['head'] # make sure 'head' is loaded so the recalculation is actually done 211 212 tags = list(self.keys()) 213 if "GlyphOrder" in tags: 214 tags.remove("GlyphOrder") 215 numTables = len(tags) 216 # write to a temporary stream to allow saving to unseekable streams 217 writer = SFNTWriter(file, numTables, self.sfntVersion, self.flavor, self.flavorData) 218 219 done = [] 220 for tag in tags: 221 self._writeTable(tag, writer, done, tableCache) 222 223 writer.close() 224 225 return writer.reordersTables() 226 227 def saveXML(self, fileOrPath, newlinestr="\n", **kwargs): 228 """Export the font as TTX (an XML-based text file), or as a series of text 229 files when splitTables is true. In the latter case, the 'fileOrPath' 230 argument should be a path to a directory. 231 The 'tables' argument must either be false (dump all tables) or a 232 list of tables to dump. The 'skipTables' argument may be a list of tables 233 to skip, but only when the 'tables' argument is false. 234 """ 235 236 writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) 237 self._saveXML(writer, **kwargs) 238 writer.close() 239 240 def _saveXML(self, writer, 241 writeVersion=True, 242 quiet=None, tables=None, skipTables=None, splitTables=False, 243 splitGlyphs=False, disassembleInstructions=True, 244 bitmapGlyphDataFormat='raw'): 245 246 if quiet is not None: 247 deprecateArgument("quiet", "configure logging instead") 248 249 self.disassembleInstructions = disassembleInstructions 250 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat 251 if not tables: 252 tables = list(self.keys()) 253 if "GlyphOrder" not in tables: 254 tables = ["GlyphOrder"] + tables 255 if skipTables: 256 for tag in skipTables: 257 if tag in tables: 258 tables.remove(tag) 259 numTables = len(tables) 260 261 if writeVersion: 262 from fontTools import version 263 version = ".".join(version.split('.')[:2]) 264 writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1], 265 ttLibVersion=version) 266 else: 267 writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1]) 268 writer.newline() 269 270 # always splitTables if splitGlyphs is enabled 271 splitTables = splitTables or splitGlyphs 272 273 if not splitTables: 274 writer.newline() 275 else: 276 path, ext = os.path.splitext(writer.filename) 277 fileNameTemplate = path + ".%s" + ext 278 279 for i in range(numTables): 280 tag = tables[i] 281 if splitTables: 282 tablePath = fileNameTemplate % tagToIdentifier(tag) 283 tableWriter = xmlWriter.XMLWriter(tablePath, 284 newlinestr=writer.newlinestr) 285 tableWriter.begintag("ttFont", ttLibVersion=version) 286 tableWriter.newline() 287 tableWriter.newline() 288 writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) 289 writer.newline() 290 else: 291 tableWriter = writer 292 self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs) 293 if splitTables: 294 tableWriter.endtag("ttFont") 295 tableWriter.newline() 296 tableWriter.close() 297 writer.endtag("ttFont") 298 writer.newline() 299 300 def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False): 301 if quiet is not None: 302 deprecateArgument("quiet", "configure logging instead") 303 if tag in self: 304 table = self[tag] 305 report = "Dumping '%s' table..." % tag 306 else: 307 report = "No '%s' table found." % tag 308 log.info(report) 309 if tag not in self: 310 return 311 xmlTag = tagToXML(tag) 312 attrs = dict() 313 if hasattr(table, "ERROR"): 314 attrs['ERROR'] = "decompilation error" 315 from .tables.DefaultTable import DefaultTable 316 if table.__class__ == DefaultTable: 317 attrs['raw'] = True 318 writer.begintag(xmlTag, **attrs) 319 writer.newline() 320 if tag == "glyf": 321 table.toXML(writer, self, splitGlyphs=splitGlyphs) 322 else: 323 table.toXML(writer, self) 324 writer.endtag(xmlTag) 325 writer.newline() 326 writer.newline() 327 328 def importXML(self, fileOrPath, quiet=None): 329 """Import a TTX file (an XML-based text format), so as to recreate 330 a font object. 331 """ 332 if quiet is not None: 333 deprecateArgument("quiet", "configure logging instead") 334 335 if "maxp" in self and "post" in self: 336 # Make sure the glyph order is loaded, as it otherwise gets 337 # lost if the XML doesn't contain the glyph order, yet does 338 # contain the table which was originally used to extract the 339 # glyph names from (ie. 'post', 'cmap' or 'CFF '). 340 self.getGlyphOrder() 341 342 from fontTools.misc import xmlReader 343 344 reader = xmlReader.XMLReader(fileOrPath, self) 345 reader.read() 346 347 def isLoaded(self, tag): 348 """Return true if the table identified by ``tag`` has been 349 decompiled and loaded into memory.""" 350 return tag in self.tables 351 352 def has_key(self, tag): 353 """Test if the table identified by ``tag`` is present in the font. 354 355 As well as this method, ``tag in font`` can also be used to determine the 356 presence of the table.""" 357 if self.isLoaded(tag): 358 return True 359 elif self.reader and tag in self.reader: 360 return True 361 elif tag == "GlyphOrder": 362 return True 363 else: 364 return False 365 366 __contains__ = has_key 367 368 def keys(self): 369 """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table.""" 370 keys = list(self.tables.keys()) 371 if self.reader: 372 for key in list(self.reader.keys()): 373 if key not in keys: 374 keys.append(key) 375 376 if "GlyphOrder" in keys: 377 keys.remove("GlyphOrder") 378 keys = sortedTagList(keys) 379 return ["GlyphOrder"] + keys 380 381 def ensureDecompiled(self): 382 """Decompile all the tables, even if a TTFont was opened in 'lazy' mode.""" 383 for tag in self.keys(): 384 table = self[tag] 385 if self.lazy is not False and hasattr(table, "ensureDecompiled"): 386 table.ensureDecompiled() 387 self.lazy = False 388 389 def __len__(self): 390 return len(list(self.keys())) 391 392 def __getitem__(self, tag): 393 tag = Tag(tag) 394 table = self.tables.get(tag) 395 if table is None: 396 if tag == "GlyphOrder": 397 table = GlyphOrder(tag) 398 self.tables[tag] = table 399 elif self.reader is not None: 400 table = self._readTable(tag) 401 else: 402 raise KeyError("'%s' table not found" % tag) 403 return table 404 405 def _readTable(self, tag): 406 log.debug("Reading '%s' table from disk", tag) 407 data = self.reader[tag] 408 if self._tableCache is not None: 409 table = self._tableCache.get((tag, data)) 410 if table is not None: 411 return table 412 tableClass = getTableClass(tag) 413 table = tableClass(tag) 414 self.tables[tag] = table 415 log.debug("Decompiling '%s' table", tag) 416 try: 417 table.decompile(data, self) 418 except Exception: 419 if not self.ignoreDecompileErrors: 420 raise 421 # fall back to DefaultTable, retaining the binary table data 422 log.exception( 423 "An exception occurred during the decompilation of the '%s' table", tag) 424 from .tables.DefaultTable import DefaultTable 425 file = StringIO() 426 traceback.print_exc(file=file) 427 table = DefaultTable(tag) 428 table.ERROR = file.getvalue() 429 self.tables[tag] = table 430 table.decompile(data, self) 431 if self._tableCache is not None: 432 self._tableCache[(tag, data)] = table 433 return table 434 435 def __setitem__(self, tag, table): 436 self.tables[Tag(tag)] = table 437 438 def __delitem__(self, tag): 439 if tag not in self: 440 raise KeyError("'%s' table not found" % tag) 441 if tag in self.tables: 442 del self.tables[tag] 443 if self.reader and tag in self.reader: 444 del self.reader[tag] 445 446 def get(self, tag, default=None): 447 """Returns the table if it exists or (optionally) a default if it doesn't.""" 448 try: 449 return self[tag] 450 except KeyError: 451 return default 452 453 def setGlyphOrder(self, glyphOrder): 454 """Set the glyph order 455 456 Args: 457 glyphOrder ([str]): List of glyph names in order. 458 """ 459 self.glyphOrder = glyphOrder 460 if hasattr(self, '_reverseGlyphOrderDict'): 461 del self._reverseGlyphOrderDict 462 if self.isLoaded("glyf"): 463 self["glyf"].setGlyphOrder(glyphOrder) 464 465 def getGlyphOrder(self): 466 """Returns a list of glyph names ordered by their position in the font.""" 467 try: 468 return self.glyphOrder 469 except AttributeError: 470 pass 471 if 'CFF ' in self: 472 cff = self['CFF '] 473 self.glyphOrder = cff.getGlyphOrder() 474 elif 'post' in self: 475 # TrueType font 476 glyphOrder = self['post'].getGlyphOrder() 477 if glyphOrder is None: 478 # 479 # No names found in the 'post' table. 480 # Try to create glyph names from the unicode cmap (if available) 481 # in combination with the Adobe Glyph List (AGL). 482 # 483 self._getGlyphNamesFromCmap() 484 else: 485 self.glyphOrder = glyphOrder 486 else: 487 self._getGlyphNamesFromCmap() 488 return self.glyphOrder 489 490 def _getGlyphNamesFromCmap(self): 491 # 492 # This is rather convoluted, but then again, it's an interesting problem: 493 # - we need to use the unicode values found in the cmap table to 494 # build glyph names (eg. because there is only a minimal post table, 495 # or none at all). 496 # - but the cmap parser also needs glyph names to work with... 497 # So here's what we do: 498 # - make up glyph names based on glyphID 499 # - load a temporary cmap table based on those names 500 # - extract the unicode values, build the "real" glyph names 501 # - unload the temporary cmap table 502 # 503 if self.isLoaded("cmap"): 504 # Bootstrapping: we're getting called by the cmap parser 505 # itself. This means self.tables['cmap'] contains a partially 506 # loaded cmap, making it impossible to get at a unicode 507 # subtable here. We remove the partially loaded cmap and 508 # restore it later. 509 # This only happens if the cmap table is loaded before any 510 # other table that does f.getGlyphOrder() or f.getGlyphName(). 511 cmapLoading = self.tables['cmap'] 512 del self.tables['cmap'] 513 else: 514 cmapLoading = None 515 # Make up glyph names based on glyphID, which will be used by the 516 # temporary cmap and by the real cmap in case we don't find a unicode 517 # cmap. 518 numGlyphs = int(self['maxp'].numGlyphs) 519 glyphOrder = [None] * numGlyphs 520 glyphOrder[0] = ".notdef" 521 for i in range(1, numGlyphs): 522 glyphOrder[i] = "glyph%.5d" % i 523 # Set the glyph order, so the cmap parser has something 524 # to work with (so we don't get called recursively). 525 self.glyphOrder = glyphOrder 526 527 # Make up glyph names based on the reversed cmap table. Because some 528 # glyphs (eg. ligatures or alternates) may not be reachable via cmap, 529 # this naming table will usually not cover all glyphs in the font. 530 # If the font has no Unicode cmap table, reversecmap will be empty. 531 if 'cmap' in self: 532 reversecmap = self['cmap'].buildReversed() 533 else: 534 reversecmap = {} 535 useCount = {} 536 for i in range(numGlyphs): 537 tempName = glyphOrder[i] 538 if tempName in reversecmap: 539 # If a font maps both U+0041 LATIN CAPITAL LETTER A and 540 # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, 541 # we prefer naming the glyph as "A". 542 glyphName = self._makeGlyphName(min(reversecmap[tempName])) 543 numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 544 if numUses > 1: 545 glyphName = "%s.alt%d" % (glyphName, numUses - 1) 546 glyphOrder[i] = glyphName 547 548 if 'cmap' in self: 549 # Delete the temporary cmap table from the cache, so it can 550 # be parsed again with the right names. 551 del self.tables['cmap'] 552 self.glyphOrder = glyphOrder 553 if cmapLoading: 554 # restore partially loaded cmap, so it can continue loading 555 # using the proper names. 556 self.tables['cmap'] = cmapLoading 557 558 @staticmethod 559 def _makeGlyphName(codepoint): 560 from fontTools import agl # Adobe Glyph List 561 if codepoint in agl.UV2AGL: 562 return agl.UV2AGL[codepoint] 563 elif codepoint <= 0xFFFF: 564 return "uni%04X" % codepoint 565 else: 566 return "u%X" % codepoint 567 568 def getGlyphNames(self): 569 """Get a list of glyph names, sorted alphabetically.""" 570 glyphNames = sorted(self.getGlyphOrder()) 571 return glyphNames 572 573 def getGlyphNames2(self): 574 """Get a list of glyph names, sorted alphabetically, 575 but not case sensitive. 576 """ 577 from fontTools.misc import textTools 578 return textTools.caselessSort(self.getGlyphOrder()) 579 580 def getGlyphName(self, glyphID): 581 """Returns the name for the glyph with the given ID. 582 583 If no name is available, synthesises one with the form ``glyphXXXXX``` where 584 ```XXXXX`` is the zero-padded glyph ID. 585 """ 586 try: 587 return self.getGlyphOrder()[glyphID] 588 except IndexError: 589 return "glyph%.5d" % glyphID 590 591 def getGlyphNameMany(self, lst): 592 """Converts a list of glyph IDs into a list of glyph names.""" 593 glyphOrder = self.getGlyphOrder(); 594 cnt = len(glyphOrder) 595 return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid 596 for gid in lst] 597 598 def getGlyphID(self, glyphName): 599 """Returns the ID of the glyph with the given name.""" 600 try: 601 return self.getReverseGlyphMap()[glyphName] 602 except KeyError: 603 if glyphName[:5] == "glyph": 604 try: 605 return int(glyphName[5:]) 606 except (NameError, ValueError): 607 raise KeyError(glyphName) 608 609 def getGlyphIDMany(self, lst): 610 """Converts a list of glyph names into a list of glyph IDs.""" 611 d = self.getReverseGlyphMap() 612 try: 613 return [d[glyphName] for glyphName in lst] 614 except KeyError: 615 getGlyphID = self.getGlyphID 616 return [getGlyphID(glyphName) for glyphName in lst] 617 618 def getReverseGlyphMap(self, rebuild=False): 619 """Returns a mapping of glyph names to glyph IDs.""" 620 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): 621 self._buildReverseGlyphOrderDict() 622 return self._reverseGlyphOrderDict 623 624 def _buildReverseGlyphOrderDict(self): 625 self._reverseGlyphOrderDict = d = {} 626 for glyphID,glyphName in enumerate(self.getGlyphOrder()): 627 d[glyphName] = glyphID 628 return d 629 630 def _writeTable(self, tag, writer, done, tableCache=None): 631 """Internal helper function for self.save(). Keeps track of 632 inter-table dependencies. 633 """ 634 if tag in done: 635 return 636 tableClass = getTableClass(tag) 637 for masterTable in tableClass.dependencies: 638 if masterTable not in done: 639 if masterTable in self: 640 self._writeTable(masterTable, writer, done, tableCache) 641 else: 642 done.append(masterTable) 643 done.append(tag) 644 tabledata = self.getTableData(tag) 645 if tableCache is not None: 646 entry = tableCache.get((Tag(tag), tabledata)) 647 if entry is not None: 648 log.debug("reusing '%s' table", tag) 649 writer.setEntry(tag, entry) 650 return 651 log.debug("Writing '%s' table to disk", tag) 652 writer[tag] = tabledata 653 if tableCache is not None: 654 tableCache[(Tag(tag), tabledata)] = writer[tag] 655 656 def getTableData(self, tag): 657 """Returns the binary representation of a table. 658 659 If the table is currently loaded and in memory, the data is compiled to 660 binary and returned; if it is not currently loaded, the binary data is 661 read from the font file and returned. 662 """ 663 tag = Tag(tag) 664 if self.isLoaded(tag): 665 log.debug("Compiling '%s' table", tag) 666 return self.tables[tag].compile(self) 667 elif self.reader and tag in self.reader: 668 log.debug("Reading '%s' table from disk", tag) 669 return self.reader[tag] 670 else: 671 raise KeyError(tag) 672 673 def getGlyphSet(self, preferCFF=True): 674 """Return a generic GlyphSet, which is a dict-like object 675 mapping glyph names to glyph objects. The returned glyph objects 676 have a .draw() method that supports the Pen protocol, and will 677 have an attribute named 'width'. 678 679 If the font is CFF-based, the outlines will be taken from the 'CFF ' or 680 'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table. 681 If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use 682 the 'preferCFF' argument to specify which one should be taken. If the 683 font contains both a 'CFF ' and a 'CFF2' table, the latter is taken. 684 """ 685 glyphs = None 686 if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or 687 ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): 688 table_tag = "CFF2" if "CFF2" in self else "CFF " 689 glyphs = _TTGlyphSet(self, 690 list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) 691 692 if glyphs is None and "glyf" in self: 693 glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) 694 695 if glyphs is None: 696 raise TTLibError("Font contains no outlines") 697 698 return glyphs 699 700 def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))): 701 """Return the 'best' unicode cmap dictionary available in the font, 702 or None, if no unicode cmap subtable is available. 703 704 By default it will search for the following (platformID, platEncID) 705 pairs:: 706 707 (3, 10), 708 (0, 6), 709 (0, 4), 710 (3, 1), 711 (0, 3), 712 (0, 2), 713 (0, 1), 714 (0, 0) 715 716 This can be customized via the ``cmapPreferences`` argument. 717 """ 718 return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) 719 720 721class _TTGlyphSet(object): 722 723 """Generic dict-like GlyphSet class that pulls metrics from hmtx and 724 glyph shape from TrueType or CFF. 725 """ 726 727 def __init__(self, ttFont, glyphs, glyphType): 728 """Construct a new glyphset. 729 730 Args: 731 font (TTFont): The font object (used to get metrics). 732 glyphs (dict): A dictionary mapping glyph names to ``_TTGlyph`` objects. 733 glyphType (class): Either ``_TTGlyphCFF`` or ``_TTGlyphGlyf``. 734 """ 735 self._glyphs = glyphs 736 self._hmtx = ttFont['hmtx'] 737 self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None 738 self._glyphType = glyphType 739 740 def keys(self): 741 return list(self._glyphs.keys()) 742 743 def has_key(self, glyphName): 744 return glyphName in self._glyphs 745 746 __contains__ = has_key 747 748 def __getitem__(self, glyphName): 749 horizontalMetrics = self._hmtx[glyphName] 750 verticalMetrics = self._vmtx[glyphName] if self._vmtx else None 751 return self._glyphType( 752 self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics) 753 754 def __len__(self): 755 return len(self._glyphs) 756 757 def get(self, glyphName, default=None): 758 try: 759 return self[glyphName] 760 except KeyError: 761 return default 762 763class _TTGlyph(object): 764 765 """Wrapper for a TrueType glyph that supports the Pen protocol, meaning 766 that it has .draw() and .drawPoints() methods that take a pen object as 767 their only argument. Additionally there are 'width' and 'lsb' attributes, 768 read from the 'hmtx' table. 769 770 If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' 771 attributes. 772 """ 773 774 def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None): 775 """Construct a new _TTGlyph. 776 777 Args: 778 glyphset (_TTGlyphSet): A glyphset object used to resolve components. 779 glyph (ttLib.tables._g_l_y_f.Glyph): The glyph object. 780 horizontalMetrics (int, int): The glyph's width and left sidebearing. 781 """ 782 self._glyphset = glyphset 783 self._glyph = glyph 784 self.width, self.lsb = horizontalMetrics 785 if verticalMetrics: 786 self.height, self.tsb = verticalMetrics 787 else: 788 self.height, self.tsb = None, None 789 790 def draw(self, pen): 791 """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details 792 how that works. 793 """ 794 self._glyph.draw(pen) 795 796 def drawPoints(self, pen): 797 # drawPoints is only implemented for _TTGlyphGlyf at this time. 798 raise NotImplementedError() 799 800class _TTGlyphCFF(_TTGlyph): 801 pass 802 803class _TTGlyphGlyf(_TTGlyph): 804 805 def draw(self, pen): 806 """Draw the glyph onto Pen. See fontTools.pens.basePen for details 807 how that works. 808 """ 809 glyfTable = self._glyphset._glyphs 810 glyph = self._glyph 811 offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 812 glyph.draw(pen, glyfTable, offset) 813 814 def drawPoints(self, pen): 815 """Draw the glyph onto PointPen. See fontTools.pens.pointPen 816 for details how that works. 817 """ 818 glyfTable = self._glyphset._glyphs 819 glyph = self._glyph 820 offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 821 glyph.drawPoints(pen, glyfTable, offset) 822 823 824class GlyphOrder(object): 825 826 """A pseudo table. The glyph order isn't in the font as a separate 827 table, but it's nice to present it as such in the TTX format. 828 """ 829 830 def __init__(self, tag=None): 831 pass 832 833 def toXML(self, writer, ttFont): 834 glyphOrder = ttFont.getGlyphOrder() 835 writer.comment("The 'id' attribute is only for humans; " 836 "it is ignored when parsed.") 837 writer.newline() 838 for i in range(len(glyphOrder)): 839 glyphName = glyphOrder[i] 840 writer.simpletag("GlyphID", id=i, name=glyphName) 841 writer.newline() 842 843 def fromXML(self, name, attrs, content, ttFont): 844 if not hasattr(self, "glyphOrder"): 845 self.glyphOrder = [] 846 if name == "GlyphID": 847 self.glyphOrder.append(attrs["name"]) 848 ttFont.setGlyphOrder(self.glyphOrder) 849 850 851def getTableModule(tag): 852 """Fetch the packer/unpacker module for a table. 853 Return None when no module is found. 854 """ 855 from . import tables 856 pyTag = tagToIdentifier(tag) 857 try: 858 __import__("fontTools.ttLib.tables." + pyTag) 859 except ImportError as err: 860 # If pyTag is found in the ImportError message, 861 # means table is not implemented. If it's not 862 # there, then some other module is missing, don't 863 # suppress the error. 864 if str(err).find(pyTag) >= 0: 865 return None 866 else: 867 raise err 868 else: 869 return getattr(tables, pyTag) 870 871 872# Registry for custom table packer/unpacker classes. Keys are table 873# tags, values are (moduleName, className) tuples. 874# See registerCustomTableClass() and getCustomTableClass() 875_customTableRegistry = {} 876 877 878def registerCustomTableClass(tag, moduleName, className=None): 879 """Register a custom packer/unpacker class for a table. 880 881 The 'moduleName' must be an importable module. If no 'className' 882 is given, it is derived from the tag, for example it will be 883 ``table_C_U_S_T_`` for a 'CUST' tag. 884 885 The registered table class should be a subclass of 886 :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable` 887 """ 888 if className is None: 889 className = "table_" + tagToIdentifier(tag) 890 _customTableRegistry[tag] = (moduleName, className) 891 892 893def unregisterCustomTableClass(tag): 894 """Unregister the custom packer/unpacker class for a table.""" 895 del _customTableRegistry[tag] 896 897 898def getCustomTableClass(tag): 899 """Return the custom table class for tag, if one has been registered 900 with 'registerCustomTableClass()'. Else return None. 901 """ 902 if tag not in _customTableRegistry: 903 return None 904 import importlib 905 moduleName, className = _customTableRegistry[tag] 906 module = importlib.import_module(moduleName) 907 return getattr(module, className) 908 909 910def getTableClass(tag): 911 """Fetch the packer/unpacker class for a table.""" 912 tableClass = getCustomTableClass(tag) 913 if tableClass is not None: 914 return tableClass 915 module = getTableModule(tag) 916 if module is None: 917 from .tables.DefaultTable import DefaultTable 918 return DefaultTable 919 pyTag = tagToIdentifier(tag) 920 tableClass = getattr(module, "table_" + pyTag) 921 return tableClass 922 923 924def getClassTag(klass): 925 """Fetch the table tag for a class object.""" 926 name = klass.__name__ 927 assert name[:6] == 'table_' 928 name = name[6:] # Chop 'table_' 929 return identifierToTag(name) 930 931 932def newTable(tag): 933 """Return a new instance of a table.""" 934 tableClass = getTableClass(tag) 935 return tableClass(tag) 936 937 938def _escapechar(c): 939 """Helper function for tagToIdentifier()""" 940 import re 941 if re.match("[a-z0-9]", c): 942 return "_" + c 943 elif re.match("[A-Z]", c): 944 return c + "_" 945 else: 946 return hex(byteord(c))[2:] 947 948 949def tagToIdentifier(tag): 950 """Convert a table tag to a valid (but UGLY) python identifier, 951 as well as a filename that's guaranteed to be unique even on a 952 caseless file system. Each character is mapped to two characters. 953 Lowercase letters get an underscore before the letter, uppercase 954 letters get an underscore after the letter. Trailing spaces are 955 trimmed. Illegal characters are escaped as two hex bytes. If the 956 result starts with a number (as the result of a hex escape), an 957 extra underscore is prepended. Examples:: 958 959 >>> tagToIdentifier('glyf') 960 '_g_l_y_f' 961 >>> tagToIdentifier('cvt ') 962 '_c_v_t' 963 >>> tagToIdentifier('OS/2') 964 'O_S_2f_2' 965 """ 966 import re 967 tag = Tag(tag) 968 if tag == "GlyphOrder": 969 return tag 970 assert len(tag) == 4, "tag should be 4 characters long" 971 while len(tag) > 1 and tag[-1] == ' ': 972 tag = tag[:-1] 973 ident = "" 974 for c in tag: 975 ident = ident + _escapechar(c) 976 if re.match("[0-9]", ident): 977 ident = "_" + ident 978 return ident 979 980 981def identifierToTag(ident): 982 """the opposite of tagToIdentifier()""" 983 if ident == "GlyphOrder": 984 return ident 985 if len(ident) % 2 and ident[0] == "_": 986 ident = ident[1:] 987 assert not (len(ident) % 2) 988 tag = "" 989 for i in range(0, len(ident), 2): 990 if ident[i] == "_": 991 tag = tag + ident[i+1] 992 elif ident[i+1] == "_": 993 tag = tag + ident[i] 994 else: 995 # assume hex 996 tag = tag + chr(int(ident[i:i+2], 16)) 997 # append trailing spaces 998 tag = tag + (4 - len(tag)) * ' ' 999 return Tag(tag) 1000 1001 1002def tagToXML(tag): 1003 """Similarly to tagToIdentifier(), this converts a TT tag 1004 to a valid XML element name. Since XML element names are 1005 case sensitive, this is a fairly simple/readable translation. 1006 """ 1007 import re 1008 tag = Tag(tag) 1009 if tag == "OS/2": 1010 return "OS_2" 1011 elif tag == "GlyphOrder": 1012 return tag 1013 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 1014 return tag.strip() 1015 else: 1016 return tagToIdentifier(tag) 1017 1018 1019def xmlToTag(tag): 1020 """The opposite of tagToXML()""" 1021 if tag == "OS_2": 1022 return Tag("OS/2") 1023 if len(tag) == 8: 1024 return identifierToTag(tag) 1025 else: 1026 return Tag(tag + " " * (4 - len(tag))) 1027 1028 1029 1030# Table order as recommended in the OpenType specification 1.4 1031TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", 1032 "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", 1033 "kern", "name", "post", "gasp", "PCLT"] 1034 1035OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", 1036 "CFF "] 1037 1038def sortedTagList(tagList, tableOrder=None): 1039 """Return a sorted copy of tagList, sorted according to the OpenType 1040 specification, or according to a custom tableOrder. If given and not 1041 None, tableOrder needs to be a list of tag names. 1042 """ 1043 tagList = sorted(tagList) 1044 if tableOrder is None: 1045 if "DSIG" in tagList: 1046 # DSIG should be last (XXX spec reference?) 1047 tagList.remove("DSIG") 1048 tagList.append("DSIG") 1049 if "CFF " in tagList: 1050 tableOrder = OTFTableOrder 1051 else: 1052 tableOrder = TTFTableOrder 1053 orderedTables = [] 1054 for tag in tableOrder: 1055 if tag in tagList: 1056 orderedTables.append(tag) 1057 tagList.remove(tag) 1058 orderedTables.extend(tagList) 1059 return orderedTables 1060 1061 1062def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): 1063 """Rewrite a font file, ordering the tables as recommended by the 1064 OpenType specification 1.4. 1065 """ 1066 inFile.seek(0) 1067 outFile.seek(0) 1068 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 1069 writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) 1070 tables = list(reader.keys()) 1071 for tag in sortedTagList(tables, tableOrder): 1072 writer[tag] = reader[tag] 1073 writer.close() 1074 1075 1076def maxPowerOfTwo(x): 1077 """Return the highest exponent of two, so that 1078 (2 ** exponent) <= x. Return 0 if x is 0. 1079 """ 1080 exponent = 0 1081 while x: 1082 x = x >> 1 1083 exponent = exponent + 1 1084 return max(exponent - 1, 0) 1085 1086 1087def getSearchRange(n, itemSize=16): 1088 """Calculate searchRange, entrySelector, rangeShift. 1089 """ 1090 # itemSize defaults to 16, for backward compatibility 1091 # with upstream fonttools. 1092 exponent = maxPowerOfTwo(n) 1093 searchRange = (2 ** exponent) * itemSize 1094 entrySelector = exponent 1095 rangeShift = max(0, n * itemSize - searchRange) 1096 return searchRange, entrySelector, rangeShift 1097