1from __future__ import print_function, division, absolute_import 2from fontTools.misc import xmlWriter 3from fontTools.misc.py23 import * 4from fontTools.misc.loggingTools import deprecateArgument 5from fontTools.ttLib import TTLibError 6from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 7import os 8import logging 9import itertools 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=False, 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 closeStream = True 166 file = open(file, "wb") 167 else: 168 # assume "file" is a writable file object 169 closeStream = False 170 171 tmp = BytesIO() 172 173 writer_reordersTables = self._save(tmp) 174 175 if (reorderTables is None or writer_reordersTables or 176 (reorderTables is False and self.reader is None)): 177 # don't reorder tables and save as is 178 file.write(tmp.getvalue()) 179 tmp.close() 180 else: 181 if reorderTables is False: 182 # sort tables using the original font's order 183 tableOrder = list(self.reader.keys()) 184 else: 185 # use the recommended order from the OpenType specification 186 tableOrder = None 187 tmp.flush() 188 tmp2 = BytesIO() 189 reorderFontTables(tmp, tmp2, tableOrder) 190 file.write(tmp2.getvalue()) 191 tmp.close() 192 tmp2.close() 193 194 if closeStream: 195 file.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 try: 373 return self.tables[tag] 374 except KeyError: 375 if tag == "GlyphOrder": 376 table = GlyphOrder(tag) 377 self.tables[tag] = table 378 return table 379 if self.reader is not None: 380 import traceback 381 log.debug("Reading '%s' table from disk", tag) 382 data = self.reader[tag] 383 if self._tableCache is not None: 384 table = self._tableCache.get((Tag(tag), data)) 385 if table is not None: 386 return table 387 tableClass = getTableClass(tag) 388 table = tableClass(tag) 389 self.tables[tag] = table 390 log.debug("Decompiling '%s' table", tag) 391 try: 392 table.decompile(data, self) 393 except: 394 if not self.ignoreDecompileErrors: 395 raise 396 # fall back to DefaultTable, retaining the binary table data 397 log.exception( 398 "An exception occurred during the decompilation of the '%s' table", tag) 399 from .tables.DefaultTable import DefaultTable 400 file = StringIO() 401 traceback.print_exc(file=file) 402 table = DefaultTable(tag) 403 table.ERROR = file.getvalue() 404 self.tables[tag] = table 405 table.decompile(data, self) 406 if self._tableCache is not None: 407 self._tableCache[(Tag(tag), data)] = table 408 return table 409 else: 410 raise KeyError("'%s' table not found" % tag) 411 412 def __setitem__(self, tag, table): 413 self.tables[Tag(tag)] = table 414 415 def __delitem__(self, tag): 416 if tag not in self: 417 raise KeyError("'%s' table not found" % tag) 418 if tag in self.tables: 419 del self.tables[tag] 420 if self.reader and tag in self.reader: 421 del self.reader[tag] 422 423 def get(self, tag, default=None): 424 try: 425 return self[tag] 426 except KeyError: 427 return default 428 429 def setGlyphOrder(self, glyphOrder): 430 self.glyphOrder = glyphOrder 431 432 def getGlyphOrder(self): 433 try: 434 return self.glyphOrder 435 except AttributeError: 436 pass 437 if 'CFF ' in self: 438 cff = self['CFF '] 439 self.glyphOrder = cff.getGlyphOrder() 440 elif 'post' in self: 441 # TrueType font 442 glyphOrder = self['post'].getGlyphOrder() 443 if glyphOrder is None: 444 # 445 # No names found in the 'post' table. 446 # Try to create glyph names from the unicode cmap (if available) 447 # in combination with the Adobe Glyph List (AGL). 448 # 449 self._getGlyphNamesFromCmap() 450 else: 451 self.glyphOrder = glyphOrder 452 else: 453 self._getGlyphNamesFromCmap() 454 return self.glyphOrder 455 456 def _getGlyphNamesFromCmap(self): 457 # 458 # This is rather convoluted, but then again, it's an interesting problem: 459 # - we need to use the unicode values found in the cmap table to 460 # build glyph names (eg. because there is only a minimal post table, 461 # or none at all). 462 # - but the cmap parser also needs glyph names to work with... 463 # So here's what we do: 464 # - make up glyph names based on glyphID 465 # - load a temporary cmap table based on those names 466 # - extract the unicode values, build the "real" glyph names 467 # - unload the temporary cmap table 468 # 469 if self.isLoaded("cmap"): 470 # Bootstrapping: we're getting called by the cmap parser 471 # itself. This means self.tables['cmap'] contains a partially 472 # loaded cmap, making it impossible to get at a unicode 473 # subtable here. We remove the partially loaded cmap and 474 # restore it later. 475 # This only happens if the cmap table is loaded before any 476 # other table that does f.getGlyphOrder() or f.getGlyphName(). 477 cmapLoading = self.tables['cmap'] 478 del self.tables['cmap'] 479 else: 480 cmapLoading = None 481 # Make up glyph names based on glyphID, which will be used by the 482 # temporary cmap and by the real cmap in case we don't find a unicode 483 # cmap. 484 numGlyphs = int(self['maxp'].numGlyphs) 485 glyphOrder = [None] * numGlyphs 486 glyphOrder[0] = ".notdef" 487 for i in range(1, numGlyphs): 488 glyphOrder[i] = "glyph%.5d" % i 489 # Set the glyph order, so the cmap parser has something 490 # to work with (so we don't get called recursively). 491 self.glyphOrder = glyphOrder 492 493 # Make up glyph names based on the reversed cmap table. Because some 494 # glyphs (eg. ligatures or alternates) may not be reachable via cmap, 495 # this naming table will usually not cover all glyphs in the font. 496 # If the font has no Unicode cmap table, reversecmap will be empty. 497 if 'cmap' in self: 498 reversecmap = self['cmap'].buildReversed() 499 else: 500 reversecmap = {} 501 useCount = {} 502 for i in range(numGlyphs): 503 tempName = glyphOrder[i] 504 if tempName in reversecmap: 505 # If a font maps both U+0041 LATIN CAPITAL LETTER A and 506 # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, 507 # we prefer naming the glyph as "A". 508 glyphName = self._makeGlyphName(min(reversecmap[tempName])) 509 numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 510 if numUses > 1: 511 glyphName = "%s.alt%d" % (glyphName, numUses - 1) 512 glyphOrder[i] = glyphName 513 514 if 'cmap' in self: 515 # Delete the temporary cmap table from the cache, so it can 516 # be parsed again with the right names. 517 del self.tables['cmap'] 518 self.glyphOrder = glyphOrder 519 if cmapLoading: 520 # restore partially loaded cmap, so it can continue loading 521 # using the proper names. 522 self.tables['cmap'] = cmapLoading 523 524 @staticmethod 525 def _makeGlyphName(codepoint): 526 from fontTools import agl # Adobe Glyph List 527 if codepoint in agl.UV2AGL: 528 return agl.UV2AGL[codepoint] 529 elif codepoint <= 0xFFFF: 530 return "uni%04X" % codepoint 531 else: 532 return "u%X" % codepoint 533 534 def getGlyphNames(self): 535 """Get a list of glyph names, sorted alphabetically.""" 536 glyphNames = sorted(self.getGlyphOrder()) 537 return glyphNames 538 539 def getGlyphNames2(self): 540 """Get a list of glyph names, sorted alphabetically, 541 but not case sensitive. 542 """ 543 from fontTools.misc import textTools 544 return textTools.caselessSort(self.getGlyphOrder()) 545 546 def getGlyphName(self, glyphID, requireReal=False): 547 try: 548 return self.getGlyphOrder()[glyphID] 549 except IndexError: 550 if requireReal or not self.allowVID: 551 # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in 552 # the cmap table than there are glyphs. I don't think it's legal... 553 return "glyph%.5d" % glyphID 554 else: 555 # user intends virtual GID support 556 try: 557 glyphName = self.VIDDict[glyphID] 558 except KeyError: 559 glyphName ="glyph%.5d" % glyphID 560 self.last_vid = min(glyphID, self.last_vid ) 561 self.reverseVIDDict[glyphName] = glyphID 562 self.VIDDict[glyphID] = glyphName 563 return glyphName 564 565 def getGlyphID(self, glyphName, requireReal=False): 566 if not hasattr(self, "_reverseGlyphOrderDict"): 567 self._buildReverseGlyphOrderDict() 568 glyphOrder = self.getGlyphOrder() 569 d = self._reverseGlyphOrderDict 570 if glyphName not in d: 571 if glyphName in glyphOrder: 572 self._buildReverseGlyphOrderDict() 573 return self.getGlyphID(glyphName) 574 else: 575 if requireReal: 576 raise KeyError(glyphName) 577 elif not self.allowVID: 578 # Handle glyphXXX only 579 if glyphName[:5] == "glyph": 580 try: 581 return int(glyphName[5:]) 582 except (NameError, ValueError): 583 raise KeyError(glyphName) 584 else: 585 # user intends virtual GID support 586 try: 587 glyphID = self.reverseVIDDict[glyphName] 588 except KeyError: 589 # if name is in glyphXXX format, use the specified name. 590 if glyphName[:5] == "glyph": 591 try: 592 glyphID = int(glyphName[5:]) 593 except (NameError, ValueError): 594 glyphID = None 595 if glyphID is None: 596 glyphID = self.last_vid -1 597 self.last_vid = glyphID 598 self.reverseVIDDict[glyphName] = glyphID 599 self.VIDDict[glyphID] = glyphName 600 return glyphID 601 602 glyphID = d[glyphName] 603 if glyphName != glyphOrder[glyphID]: 604 self._buildReverseGlyphOrderDict() 605 return self.getGlyphID(glyphName) 606 return glyphID 607 608 def getReverseGlyphMap(self, rebuild=False): 609 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): 610 self._buildReverseGlyphOrderDict() 611 return self._reverseGlyphOrderDict 612 613 def _buildReverseGlyphOrderDict(self): 614 self._reverseGlyphOrderDict = d = {} 615 glyphOrder = self.getGlyphOrder() 616 for glyphID in range(len(glyphOrder)): 617 d[glyphOrder[glyphID]] = glyphID 618 619 def _writeTable(self, tag, writer, done, tableCache=None): 620 """Internal helper function for self.save(). Keeps track of 621 inter-table dependencies. 622 """ 623 if tag in done: 624 return 625 tableClass = getTableClass(tag) 626 for masterTable in tableClass.dependencies: 627 if masterTable not in done: 628 if masterTable in self: 629 self._writeTable(masterTable, writer, done, tableCache) 630 else: 631 done.append(masterTable) 632 done.append(tag) 633 tabledata = self.getTableData(tag) 634 if tableCache is not None: 635 entry = tableCache.get((Tag(tag), tabledata)) 636 if entry is not None: 637 log.debug("reusing '%s' table", tag) 638 writer.setEntry(tag, entry) 639 return 640 log.debug("writing '%s' table to disk", tag) 641 writer[tag] = tabledata 642 if tableCache is not None: 643 tableCache[(Tag(tag), tabledata)] = writer[tag] 644 645 def getTableData(self, tag): 646 """Returns raw table data, whether compiled or directly read from disk. 647 """ 648 tag = Tag(tag) 649 if self.isLoaded(tag): 650 log.debug("compiling '%s' table", tag) 651 return self.tables[tag].compile(self) 652 elif self.reader and tag in self.reader: 653 log.debug("Reading '%s' table from disk", tag) 654 return self.reader[tag] 655 else: 656 raise KeyError(tag) 657 658 def getGlyphSet(self, preferCFF=True): 659 """Return a generic GlyphSet, which is a dict-like object 660 mapping glyph names to glyph objects. The returned glyph objects 661 have a .draw() method that supports the Pen protocol, and will 662 have an attribute named 'width'. 663 664 If the font is CFF-based, the outlines will be taken from the 'CFF ' or 665 'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table. 666 If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use 667 the 'preferCFF' argument to specify which one should be taken. If the 668 font contains both a 'CFF ' and a 'CFF2' table, the latter is taken. 669 """ 670 glyphs = None 671 if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or 672 ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): 673 table_tag = "CFF2" if "CFF2" in self else "CFF " 674 glyphs = _TTGlyphSet(self, 675 list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) 676 677 if glyphs is None and "glyf" in self: 678 glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) 679 680 if glyphs is None: 681 raise TTLibError("Font contains no outlines") 682 683 return glyphs 684 685 def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))): 686 """Return the 'best' unicode cmap dictionary available in the font, 687 or None, if no unicode cmap subtable is available. 688 689 By default it will search for the following (platformID, platEncID) 690 pairs: 691 (3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0) 692 This can be customized via the cmapPreferences argument. 693 """ 694 return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) 695 696 697class _TTGlyphSet(object): 698 699 """Generic dict-like GlyphSet class that pulls metrics from hmtx and 700 glyph shape from TrueType or CFF. 701 """ 702 703 def __init__(self, ttFont, glyphs, glyphType): 704 self._glyphs = glyphs 705 self._hmtx = ttFont['hmtx'] 706 self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None 707 self._glyphType = glyphType 708 709 def keys(self): 710 return list(self._glyphs.keys()) 711 712 def has_key(self, glyphName): 713 return glyphName in self._glyphs 714 715 __contains__ = has_key 716 717 def __getitem__(self, glyphName): 718 horizontalMetrics = self._hmtx[glyphName] 719 verticalMetrics = self._vmtx[glyphName] if self._vmtx else None 720 return self._glyphType( 721 self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics) 722 723 def __len__(self): 724 return len(self._glyphs) 725 726 def get(self, glyphName, default=None): 727 try: 728 return self[glyphName] 729 except KeyError: 730 return default 731 732class _TTGlyph(object): 733 734 """Wrapper for a TrueType glyph that supports the Pen protocol, meaning 735 that it has .draw() and .drawPoints() methods that take a pen object as 736 their only argument. Additionally there are 'width' and 'lsb' attributes, 737 read from the 'hmtx' table. 738 739 If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' 740 attributes. 741 """ 742 743 def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None): 744 self._glyphset = glyphset 745 self._glyph = glyph 746 self.width, self.lsb = horizontalMetrics 747 if verticalMetrics: 748 self.height, self.tsb = verticalMetrics 749 else: 750 self.height, self.tsb = None, None 751 752 def draw(self, pen): 753 """Draw the glyph onto Pen. See fontTools.pens.basePen for details 754 how that works. 755 """ 756 self._glyph.draw(pen) 757 758 def drawPoints(self, pen): 759 # drawPoints is only implemented for _TTGlyphGlyf at this time. 760 raise NotImplementedError() 761 762class _TTGlyphCFF(_TTGlyph): 763 pass 764 765class _TTGlyphGlyf(_TTGlyph): 766 767 def draw(self, pen): 768 """Draw the glyph onto Pen. See fontTools.pens.basePen for details 769 how that works. 770 """ 771 glyfTable = self._glyphset._glyphs 772 glyph = self._glyph 773 offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 774 glyph.draw(pen, glyfTable, offset) 775 776 def drawPoints(self, pen): 777 """Draw the glyph onto PointPen. See fontTools.pens.pointPen 778 for details how that works. 779 """ 780 glyfTable = self._glyphset._glyphs 781 glyph = self._glyph 782 offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 783 glyph.drawPoints(pen, glyfTable, offset) 784 785 786class GlyphOrder(object): 787 788 """A pseudo table. The glyph order isn't in the font as a separate 789 table, but it's nice to present it as such in the TTX format. 790 """ 791 792 def __init__(self, tag=None): 793 pass 794 795 def toXML(self, writer, ttFont): 796 glyphOrder = ttFont.getGlyphOrder() 797 writer.comment("The 'id' attribute is only for humans; " 798 "it is ignored when parsed.") 799 writer.newline() 800 for i in range(len(glyphOrder)): 801 glyphName = glyphOrder[i] 802 writer.simpletag("GlyphID", id=i, name=glyphName) 803 writer.newline() 804 805 def fromXML(self, name, attrs, content, ttFont): 806 if not hasattr(self, "glyphOrder"): 807 self.glyphOrder = [] 808 ttFont.setGlyphOrder(self.glyphOrder) 809 if name == "GlyphID": 810 self.glyphOrder.append(attrs["name"]) 811 812 813def getTableModule(tag): 814 """Fetch the packer/unpacker module for a table. 815 Return None when no module is found. 816 """ 817 from . import tables 818 pyTag = tagToIdentifier(tag) 819 try: 820 __import__("fontTools.ttLib.tables." + pyTag) 821 except ImportError as err: 822 # If pyTag is found in the ImportError message, 823 # means table is not implemented. If it's not 824 # there, then some other module is missing, don't 825 # suppress the error. 826 if str(err).find(pyTag) >= 0: 827 return None 828 else: 829 raise err 830 else: 831 return getattr(tables, pyTag) 832 833 834def getTableClass(tag): 835 """Fetch the packer/unpacker class for a table. 836 Return None when no class is found. 837 """ 838 module = getTableModule(tag) 839 if module is None: 840 from .tables.DefaultTable import DefaultTable 841 return DefaultTable 842 pyTag = tagToIdentifier(tag) 843 tableClass = getattr(module, "table_" + pyTag) 844 return tableClass 845 846 847def getClassTag(klass): 848 """Fetch the table tag for a class object.""" 849 name = klass.__name__ 850 assert name[:6] == 'table_' 851 name = name[6:] # Chop 'table_' 852 return identifierToTag(name) 853 854 855def newTable(tag): 856 """Return a new instance of a table.""" 857 tableClass = getTableClass(tag) 858 return tableClass(tag) 859 860 861def _escapechar(c): 862 """Helper function for tagToIdentifier()""" 863 import re 864 if re.match("[a-z0-9]", c): 865 return "_" + c 866 elif re.match("[A-Z]", c): 867 return c + "_" 868 else: 869 return hex(byteord(c))[2:] 870 871 872def tagToIdentifier(tag): 873 """Convert a table tag to a valid (but UGLY) python identifier, 874 as well as a filename that's guaranteed to be unique even on a 875 caseless file system. Each character is mapped to two characters. 876 Lowercase letters get an underscore before the letter, uppercase 877 letters get an underscore after the letter. Trailing spaces are 878 trimmed. Illegal characters are escaped as two hex bytes. If the 879 result starts with a number (as the result of a hex escape), an 880 extra underscore is prepended. Examples: 881 'glyf' -> '_g_l_y_f' 882 'cvt ' -> '_c_v_t' 883 'OS/2' -> 'O_S_2f_2' 884 """ 885 import re 886 tag = Tag(tag) 887 if tag == "GlyphOrder": 888 return tag 889 assert len(tag) == 4, "tag should be 4 characters long" 890 while len(tag) > 1 and tag[-1] == ' ': 891 tag = tag[:-1] 892 ident = "" 893 for c in tag: 894 ident = ident + _escapechar(c) 895 if re.match("[0-9]", ident): 896 ident = "_" + ident 897 return ident 898 899 900def identifierToTag(ident): 901 """the opposite of tagToIdentifier()""" 902 if ident == "GlyphOrder": 903 return ident 904 if len(ident) % 2 and ident[0] == "_": 905 ident = ident[1:] 906 assert not (len(ident) % 2) 907 tag = "" 908 for i in range(0, len(ident), 2): 909 if ident[i] == "_": 910 tag = tag + ident[i+1] 911 elif ident[i+1] == "_": 912 tag = tag + ident[i] 913 else: 914 # assume hex 915 tag = tag + chr(int(ident[i:i+2], 16)) 916 # append trailing spaces 917 tag = tag + (4 - len(tag)) * ' ' 918 return Tag(tag) 919 920 921def tagToXML(tag): 922 """Similarly to tagToIdentifier(), this converts a TT tag 923 to a valid XML element name. Since XML element names are 924 case sensitive, this is a fairly simple/readable translation. 925 """ 926 import re 927 tag = Tag(tag) 928 if tag == "OS/2": 929 return "OS_2" 930 elif tag == "GlyphOrder": 931 return tag 932 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 933 return tag.strip() 934 else: 935 return tagToIdentifier(tag) 936 937 938def xmlToTag(tag): 939 """The opposite of tagToXML()""" 940 if tag == "OS_2": 941 return Tag("OS/2") 942 if len(tag) == 8: 943 return identifierToTag(tag) 944 else: 945 return Tag(tag + " " * (4 - len(tag))) 946 947 948 949# Table order as recommended in the OpenType specification 1.4 950TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", 951 "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", 952 "kern", "name", "post", "gasp", "PCLT"] 953 954OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", 955 "CFF "] 956 957def sortedTagList(tagList, tableOrder=None): 958 """Return a sorted copy of tagList, sorted according to the OpenType 959 specification, or according to a custom tableOrder. If given and not 960 None, tableOrder needs to be a list of tag names. 961 """ 962 tagList = sorted(tagList) 963 if tableOrder is None: 964 if "DSIG" in tagList: 965 # DSIG should be last (XXX spec reference?) 966 tagList.remove("DSIG") 967 tagList.append("DSIG") 968 if "CFF " in tagList: 969 tableOrder = OTFTableOrder 970 else: 971 tableOrder = TTFTableOrder 972 orderedTables = [] 973 for tag in tableOrder: 974 if tag in tagList: 975 orderedTables.append(tag) 976 tagList.remove(tag) 977 orderedTables.extend(tagList) 978 return orderedTables 979 980 981def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): 982 """Rewrite a font file, ordering the tables as recommended by the 983 OpenType specification 1.4. 984 """ 985 inFile.seek(0) 986 outFile.seek(0) 987 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 988 writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) 989 tables = list(reader.keys()) 990 for tag in sortedTagList(tables, tableOrder): 991 writer[tag] = reader[tag] 992 writer.close() 993 994 995def maxPowerOfTwo(x): 996 """Return the highest exponent of two, so that 997 (2 ** exponent) <= x. Return 0 if x is 0. 998 """ 999 exponent = 0 1000 while x: 1001 x = x >> 1 1002 exponent = exponent + 1 1003 return max(exponent - 1, 0) 1004 1005 1006def getSearchRange(n, itemSize=16): 1007 """Calculate searchRange, entrySelector, rangeShift. 1008 """ 1009 # itemSize defaults to 16, for backward compatibility 1010 # with upstream fonttools. 1011 exponent = maxPowerOfTwo(n) 1012 searchRange = (2 ** exponent) * itemSize 1013 entrySelector = exponent 1014 rangeShift = max(0, n * itemSize - searchRange) 1015 return searchRange, entrySelector, rangeShift 1016