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