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