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