Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/ttFont.py: 18%

582 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1from fontTools.config import Config 

2from fontTools.misc import xmlWriter 

3from fontTools.misc.configTools import AbstractConfig 

4from fontTools.misc.textTools import Tag, byteord, tostr 

5from fontTools.misc.loggingTools import deprecateArgument 

6from fontTools.ttLib import TTLibError 

7from fontTools.ttLib.ttGlyphSet import _TTGlyph, _TTGlyphSetCFF, _TTGlyphSetGlyf 

8from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 

9from io import BytesIO, StringIO, UnsupportedOperation 

10import os 

11import logging 

12import traceback 

13 

14log = logging.getLogger(__name__) 

15 

16 

17class TTFont(object): 

18 

19 """Represents a TrueType font. 

20 

21 The object manages file input and output, and offers a convenient way of 

22 accessing tables. Tables will be only decompiled when necessary, ie. when 

23 they're actually accessed. This means that simple operations can be extremely fast. 

24 

25 Example usage:: 

26 

27 >> from fontTools import ttLib 

28 >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file 

29 >> tt['maxp'].numGlyphs 

30 242 

31 >> tt['OS/2'].achVendID 

32 'B&H\000' 

33 >> tt['head'].unitsPerEm 

34 2048 

35 

36 For details of the objects returned when accessing each table, see :ref:`tables`. 

37 To add a table to the font, use the :py:func:`newTable` function:: 

38 

39 >> os2 = newTable("OS/2") 

40 >> os2.version = 4 

41 >> # set other attributes 

42 >> font["OS/2"] = os2 

43 

44 TrueType fonts can also be serialized to and from XML format (see also the 

45 :ref:`ttx` binary):: 

46 

47 >> tt.saveXML("afont.ttx") 

48 Dumping 'LTSH' table... 

49 Dumping 'OS/2' table... 

50 [...] 

51 

52 >> tt2 = ttLib.TTFont() # Create a new font object 

53 >> tt2.importXML("afont.ttx") 

54 >> tt2['maxp'].numGlyphs 

55 242 

56 

57 The TTFont object may be used as a context manager; this will cause the file 

58 reader to be closed after the context ``with`` block is exited:: 

59 

60 with TTFont(filename) as f: 

61 # Do stuff 

62 

63 Args: 

64 file: When reading a font from disk, either a pathname pointing to a file, 

65 or a readable file object. 

66 res_name_or_index: If running on a Macintosh, either a sfnt resource name or 

67 an sfnt resource index number. If the index number is zero, TTLib will 

68 autodetect whether the file is a flat file or a suitcase. (If it is a suitcase, 

69 only the first 'sfnt' resource will be read.) 

70 sfntVersion (str): When constructing a font object from scratch, sets the four-byte 

71 sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create 

72 an OpenType file, use ``OTTO``. 

73 flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2 

74 file. 

75 checkChecksums (int): How checksum data should be treated. Default is 0 

76 (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to 

77 raise an exception if any wrong checksums are found. 

78 recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``, 

79 ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save. 

80 Also compiles the glyphs on importing, which saves memory consumption and 

81 time. 

82 ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation 

83 will be ignored, and the binary data will be returned for those tables instead. 

84 recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in 

85 the ``head`` table on save. 

86 fontNumber (int): The index of the font in a TrueType Collection file. 

87 lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon 

88 access only. If it is set to False, many data structures are loaded immediately. 

89 The default is ``lazy=None`` which is somewhere in between. 

90 """ 

91 

92 def __init__( 

93 self, 

94 file=None, 

95 res_name_or_index=None, 

96 sfntVersion="\000\001\000\000", 

97 flavor=None, 

98 checkChecksums=0, 

99 verbose=None, 

100 recalcBBoxes=True, 

101 allowVID=NotImplemented, 

102 ignoreDecompileErrors=False, 

103 recalcTimestamp=True, 

104 fontNumber=-1, 

105 lazy=None, 

106 quiet=None, 

107 _tableCache=None, 

108 cfg={}, 

109 ): 

110 for name in ("verbose", "quiet"): 

111 val = locals().get(name) 

112 if val is not None: 

113 deprecateArgument(name, "configure logging instead") 

114 setattr(self, name, val) 

115 

116 self.lazy = lazy 

117 self.recalcBBoxes = recalcBBoxes 

118 self.recalcTimestamp = recalcTimestamp 

119 self.tables = {} 

120 self.reader = None 

121 self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg) 

122 self.ignoreDecompileErrors = ignoreDecompileErrors 

123 

124 if not file: 

125 self.sfntVersion = sfntVersion 

126 self.flavor = flavor 

127 self.flavorData = None 

128 return 

129 seekable = True 

130 if not hasattr(file, "read"): 

131 closeStream = True 

132 # assume file is a string 

133 if res_name_or_index is not None: 

134 # see if it contains 'sfnt' resources in the resource or data fork 

135 from . import macUtils 

136 

137 if res_name_or_index == 0: 

138 if macUtils.getSFNTResIndices(file): 

139 # get the first available sfnt font. 

140 file = macUtils.SFNTResourceReader(file, 1) 

141 else: 

142 file = open(file, "rb") 

143 else: 

144 file = macUtils.SFNTResourceReader(file, res_name_or_index) 

145 else: 

146 file = open(file, "rb") 

147 else: 

148 # assume "file" is a readable file object 

149 closeStream = False 

150 # SFNTReader wants the input file to be seekable. 

151 # SpooledTemporaryFile has no seekable() on < 3.11, but still can seek: 

152 # https://github.com/fonttools/fonttools/issues/3052 

153 if hasattr(file, "seekable"): 

154 seekable = file.seekable() 

155 elif hasattr(file, "seek"): 

156 try: 

157 file.seek(0) 

158 except UnsupportedOperation: 

159 seekable = False 

160 

161 if not self.lazy: 

162 # read input file in memory and wrap a stream around it to allow overwriting 

163 if seekable: 

164 file.seek(0) 

165 tmp = BytesIO(file.read()) 

166 if hasattr(file, "name"): 

167 # save reference to input file name 

168 tmp.name = file.name 

169 if closeStream: 

170 file.close() 

171 file = tmp 

172 elif not seekable: 

173 raise TTLibError("Input file must be seekable when lazy=True") 

174 self._tableCache = _tableCache 

175 self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber) 

176 self.sfntVersion = self.reader.sfntVersion 

177 self.flavor = self.reader.flavor 

178 self.flavorData = self.reader.flavorData 

179 

180 def __enter__(self): 

181 return self 

182 

183 def __exit__(self, type, value, traceback): 

184 self.close() 

185 

186 def close(self): 

187 """If we still have a reader object, close it.""" 

188 if self.reader is not None: 

189 self.reader.close() 

190 

191 def save(self, file, reorderTables=True): 

192 """Save the font to disk. 

193 

194 Args: 

195 file: Similarly to the constructor, can be either a pathname or a writable 

196 file object. 

197 reorderTables (Option[bool]): If true (the default), reorder the tables, 

198 sorting them by tag (recommended by the OpenType specification). If 

199 false, retain the original font order. If None, reorder by table 

200 dependency (fastest). 

201 """ 

202 if not hasattr(file, "write"): 

203 if self.lazy and self.reader.file.name == file: 

204 raise TTLibError("Can't overwrite TTFont when 'lazy' attribute is True") 

205 createStream = True 

206 else: 

207 # assume "file" is a writable file object 

208 createStream = False 

209 

210 tmp = BytesIO() 

211 

212 writer_reordersTables = self._save(tmp) 

213 

214 if not ( 

215 reorderTables is None 

216 or writer_reordersTables 

217 or (reorderTables is False and self.reader is None) 

218 ): 

219 if reorderTables is False: 

220 # sort tables using the original font's order 

221 tableOrder = list(self.reader.keys()) 

222 else: 

223 # use the recommended order from the OpenType specification 

224 tableOrder = None 

225 tmp.flush() 

226 tmp2 = BytesIO() 

227 reorderFontTables(tmp, tmp2, tableOrder) 

228 tmp.close() 

229 tmp = tmp2 

230 

231 if createStream: 

232 # "file" is a path 

233 with open(file, "wb") as file: 

234 file.write(tmp.getvalue()) 

235 else: 

236 file.write(tmp.getvalue()) 

237 

238 tmp.close() 

239 

240 def _save(self, file, tableCache=None): 

241 """Internal function, to be shared by save() and TTCollection.save()""" 

242 

243 if self.recalcTimestamp and "head" in self: 

244 self[ 

245 "head" 

246 ] # make sure 'head' is loaded so the recalculation is actually done 

247 

248 tags = list(self.keys()) 

249 if "GlyphOrder" in tags: 

250 tags.remove("GlyphOrder") 

251 numTables = len(tags) 

252 # write to a temporary stream to allow saving to unseekable streams 

253 writer = SFNTWriter( 

254 file, numTables, self.sfntVersion, self.flavor, self.flavorData 

255 ) 

256 

257 done = [] 

258 for tag in tags: 

259 self._writeTable(tag, writer, done, tableCache) 

260 

261 writer.close() 

262 

263 return writer.reordersTables() 

264 

265 def saveXML(self, fileOrPath, newlinestr="\n", **kwargs): 

266 """Export the font as TTX (an XML-based text file), or as a series of text 

267 files when splitTables is true. In the latter case, the 'fileOrPath' 

268 argument should be a path to a directory. 

269 The 'tables' argument must either be false (dump all tables) or a 

270 list of tables to dump. The 'skipTables' argument may be a list of tables 

271 to skip, but only when the 'tables' argument is false. 

272 """ 

273 

274 writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) 

275 self._saveXML(writer, **kwargs) 

276 writer.close() 

277 

278 def _saveXML( 

279 self, 

280 writer, 

281 writeVersion=True, 

282 quiet=None, 

283 tables=None, 

284 skipTables=None, 

285 splitTables=False, 

286 splitGlyphs=False, 

287 disassembleInstructions=True, 

288 bitmapGlyphDataFormat="raw", 

289 ): 

290 if quiet is not None: 

291 deprecateArgument("quiet", "configure logging instead") 

292 

293 self.disassembleInstructions = disassembleInstructions 

294 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat 

295 if not tables: 

296 tables = list(self.keys()) 

297 if "GlyphOrder" not in tables: 

298 tables = ["GlyphOrder"] + tables 

299 if skipTables: 

300 for tag in skipTables: 

301 if tag in tables: 

302 tables.remove(tag) 

303 numTables = len(tables) 

304 

305 if writeVersion: 

306 from fontTools import version 

307 

308 version = ".".join(version.split(".")[:2]) 

309 writer.begintag( 

310 "ttFont", 

311 sfntVersion=repr(tostr(self.sfntVersion))[1:-1], 

312 ttLibVersion=version, 

313 ) 

314 else: 

315 writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1]) 

316 writer.newline() 

317 

318 # always splitTables if splitGlyphs is enabled 

319 splitTables = splitTables or splitGlyphs 

320 

321 if not splitTables: 

322 writer.newline() 

323 else: 

324 path, ext = os.path.splitext(writer.filename) 

325 

326 for i in range(numTables): 

327 tag = tables[i] 

328 if splitTables: 

329 tablePath = path + "." + tagToIdentifier(tag) + ext 

330 tableWriter = xmlWriter.XMLWriter( 

331 tablePath, newlinestr=writer.newlinestr 

332 ) 

333 tableWriter.begintag("ttFont", ttLibVersion=version) 

334 tableWriter.newline() 

335 tableWriter.newline() 

336 writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) 

337 writer.newline() 

338 else: 

339 tableWriter = writer 

340 self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs) 

341 if splitTables: 

342 tableWriter.endtag("ttFont") 

343 tableWriter.newline() 

344 tableWriter.close() 

345 writer.endtag("ttFont") 

346 writer.newline() 

347 

348 def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False): 

349 if quiet is not None: 

350 deprecateArgument("quiet", "configure logging instead") 

351 if tag in self: 

352 table = self[tag] 

353 report = "Dumping '%s' table..." % tag 

354 else: 

355 report = "No '%s' table found." % tag 

356 log.info(report) 

357 if tag not in self: 

358 return 

359 xmlTag = tagToXML(tag) 

360 attrs = dict() 

361 if hasattr(table, "ERROR"): 

362 attrs["ERROR"] = "decompilation error" 

363 from .tables.DefaultTable import DefaultTable 

364 

365 if table.__class__ == DefaultTable: 

366 attrs["raw"] = True 

367 writer.begintag(xmlTag, **attrs) 

368 writer.newline() 

369 if tag == "glyf": 

370 table.toXML(writer, self, splitGlyphs=splitGlyphs) 

371 else: 

372 table.toXML(writer, self) 

373 writer.endtag(xmlTag) 

374 writer.newline() 

375 writer.newline() 

376 

377 def importXML(self, fileOrPath, quiet=None): 

378 """Import a TTX file (an XML-based text format), so as to recreate 

379 a font object. 

380 """ 

381 if quiet is not None: 

382 deprecateArgument("quiet", "configure logging instead") 

383 

384 if "maxp" in self and "post" in self: 

385 # Make sure the glyph order is loaded, as it otherwise gets 

386 # lost if the XML doesn't contain the glyph order, yet does 

387 # contain the table which was originally used to extract the 

388 # glyph names from (ie. 'post', 'cmap' or 'CFF '). 

389 self.getGlyphOrder() 

390 

391 from fontTools.misc import xmlReader 

392 

393 reader = xmlReader.XMLReader(fileOrPath, self) 

394 reader.read() 

395 

396 def isLoaded(self, tag): 

397 """Return true if the table identified by ``tag`` has been 

398 decompiled and loaded into memory.""" 

399 return tag in self.tables 

400 

401 def has_key(self, tag): 

402 """Test if the table identified by ``tag`` is present in the font. 

403 

404 As well as this method, ``tag in font`` can also be used to determine the 

405 presence of the table.""" 

406 if self.isLoaded(tag): 

407 return True 

408 elif self.reader and tag in self.reader: 

409 return True 

410 elif tag == "GlyphOrder": 

411 return True 

412 else: 

413 return False 

414 

415 __contains__ = has_key 

416 

417 def keys(self): 

418 """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table.""" 

419 keys = list(self.tables.keys()) 

420 if self.reader: 

421 for key in list(self.reader.keys()): 

422 if key not in keys: 

423 keys.append(key) 

424 

425 if "GlyphOrder" in keys: 

426 keys.remove("GlyphOrder") 

427 keys = sortedTagList(keys) 

428 return ["GlyphOrder"] + keys 

429 

430 def ensureDecompiled(self, recurse=None): 

431 """Decompile all the tables, even if a TTFont was opened in 'lazy' mode.""" 

432 for tag in self.keys(): 

433 table = self[tag] 

434 if recurse is None: 

435 recurse = self.lazy is not False 

436 if recurse and hasattr(table, "ensureDecompiled"): 

437 table.ensureDecompiled(recurse=recurse) 

438 self.lazy = False 

439 

440 def __len__(self): 

441 return len(list(self.keys())) 

442 

443 def __getitem__(self, tag): 

444 tag = Tag(tag) 

445 table = self.tables.get(tag) 

446 if table is None: 

447 if tag == "GlyphOrder": 

448 table = GlyphOrder(tag) 

449 self.tables[tag] = table 

450 elif self.reader is not None: 

451 table = self._readTable(tag) 

452 else: 

453 raise KeyError("'%s' table not found" % tag) 

454 return table 

455 

456 def _readTable(self, tag): 

457 log.debug("Reading '%s' table from disk", tag) 

458 data = self.reader[tag] 

459 if self._tableCache is not None: 

460 table = self._tableCache.get((tag, data)) 

461 if table is not None: 

462 return table 

463 tableClass = getTableClass(tag) 

464 table = tableClass(tag) 

465 self.tables[tag] = table 

466 log.debug("Decompiling '%s' table", tag) 

467 try: 

468 table.decompile(data, self) 

469 except Exception: 

470 if not self.ignoreDecompileErrors: 

471 raise 

472 # fall back to DefaultTable, retaining the binary table data 

473 log.exception( 

474 "An exception occurred during the decompilation of the '%s' table", tag 

475 ) 

476 from .tables.DefaultTable import DefaultTable 

477 

478 file = StringIO() 

479 traceback.print_exc(file=file) 

480 table = DefaultTable(tag) 

481 table.ERROR = file.getvalue() 

482 self.tables[tag] = table 

483 table.decompile(data, self) 

484 if self._tableCache is not None: 

485 self._tableCache[(tag, data)] = table 

486 return table 

487 

488 def __setitem__(self, tag, table): 

489 self.tables[Tag(tag)] = table 

490 

491 def __delitem__(self, tag): 

492 if tag not in self: 

493 raise KeyError("'%s' table not found" % tag) 

494 if tag in self.tables: 

495 del self.tables[tag] 

496 if self.reader and tag in self.reader: 

497 del self.reader[tag] 

498 

499 def get(self, tag, default=None): 

500 """Returns the table if it exists or (optionally) a default if it doesn't.""" 

501 try: 

502 return self[tag] 

503 except KeyError: 

504 return default 

505 

506 def setGlyphOrder(self, glyphOrder): 

507 """Set the glyph order 

508 

509 Args: 

510 glyphOrder ([str]): List of glyph names in order. 

511 """ 

512 self.glyphOrder = glyphOrder 

513 if hasattr(self, "_reverseGlyphOrderDict"): 

514 del self._reverseGlyphOrderDict 

515 if self.isLoaded("glyf"): 

516 self["glyf"].setGlyphOrder(glyphOrder) 

517 

518 def getGlyphOrder(self): 

519 """Returns a list of glyph names ordered by their position in the font.""" 

520 try: 

521 return self.glyphOrder 

522 except AttributeError: 

523 pass 

524 if "CFF " in self: 

525 cff = self["CFF "] 

526 self.glyphOrder = cff.getGlyphOrder() 

527 elif "post" in self: 

528 # TrueType font 

529 glyphOrder = self["post"].getGlyphOrder() 

530 if glyphOrder is None: 

531 # 

532 # No names found in the 'post' table. 

533 # Try to create glyph names from the unicode cmap (if available) 

534 # in combination with the Adobe Glyph List (AGL). 

535 # 

536 self._getGlyphNamesFromCmap() 

537 elif len(glyphOrder) < self["maxp"].numGlyphs: 

538 # 

539 # Not enough names found in the 'post' table. 

540 # Can happen when 'post' format 1 is improperly used on a font that 

541 # has more than 258 glyphs (the lenght of 'standardGlyphOrder'). 

542 # 

543 log.warning( 

544 "Not enough names found in the 'post' table, generating them from cmap instead" 

545 ) 

546 self._getGlyphNamesFromCmap() 

547 else: 

548 self.glyphOrder = glyphOrder 

549 else: 

550 self._getGlyphNamesFromCmap() 

551 return self.glyphOrder 

552 

553 def _getGlyphNamesFromCmap(self): 

554 # 

555 # This is rather convoluted, but then again, it's an interesting problem: 

556 # - we need to use the unicode values found in the cmap table to 

557 # build glyph names (eg. because there is only a minimal post table, 

558 # or none at all). 

559 # - but the cmap parser also needs glyph names to work with... 

560 # So here's what we do: 

561 # - make up glyph names based on glyphID 

562 # - load a temporary cmap table based on those names 

563 # - extract the unicode values, build the "real" glyph names 

564 # - unload the temporary cmap table 

565 # 

566 if self.isLoaded("cmap"): 

567 # Bootstrapping: we're getting called by the cmap parser 

568 # itself. This means self.tables['cmap'] contains a partially 

569 # loaded cmap, making it impossible to get at a unicode 

570 # subtable here. We remove the partially loaded cmap and 

571 # restore it later. 

572 # This only happens if the cmap table is loaded before any 

573 # other table that does f.getGlyphOrder() or f.getGlyphName(). 

574 cmapLoading = self.tables["cmap"] 

575 del self.tables["cmap"] 

576 else: 

577 cmapLoading = None 

578 # Make up glyph names based on glyphID, which will be used by the 

579 # temporary cmap and by the real cmap in case we don't find a unicode 

580 # cmap. 

581 numGlyphs = int(self["maxp"].numGlyphs) 

582 glyphOrder = [None] * numGlyphs 

583 glyphOrder[0] = ".notdef" 

584 for i in range(1, numGlyphs): 

585 glyphOrder[i] = "glyph%.5d" % i 

586 # Set the glyph order, so the cmap parser has something 

587 # to work with (so we don't get called recursively). 

588 self.glyphOrder = glyphOrder 

589 

590 # Make up glyph names based on the reversed cmap table. Because some 

591 # glyphs (eg. ligatures or alternates) may not be reachable via cmap, 

592 # this naming table will usually not cover all glyphs in the font. 

593 # If the font has no Unicode cmap table, reversecmap will be empty. 

594 if "cmap" in self: 

595 reversecmap = self["cmap"].buildReversed() 

596 else: 

597 reversecmap = {} 

598 useCount = {} 

599 for i in range(numGlyphs): 

600 tempName = glyphOrder[i] 

601 if tempName in reversecmap: 

602 # If a font maps both U+0041 LATIN CAPITAL LETTER A and 

603 # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, 

604 # we prefer naming the glyph as "A". 

605 glyphName = self._makeGlyphName(min(reversecmap[tempName])) 

606 numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 

607 if numUses > 1: 

608 glyphName = "%s.alt%d" % (glyphName, numUses - 1) 

609 glyphOrder[i] = glyphName 

610 

611 if "cmap" in self: 

612 # Delete the temporary cmap table from the cache, so it can 

613 # be parsed again with the right names. 

614 del self.tables["cmap"] 

615 self.glyphOrder = glyphOrder 

616 if cmapLoading: 

617 # restore partially loaded cmap, so it can continue loading 

618 # using the proper names. 

619 self.tables["cmap"] = cmapLoading 

620 

621 @staticmethod 

622 def _makeGlyphName(codepoint): 

623 from fontTools import agl # Adobe Glyph List 

624 

625 if codepoint in agl.UV2AGL: 

626 return agl.UV2AGL[codepoint] 

627 elif codepoint <= 0xFFFF: 

628 return "uni%04X" % codepoint 

629 else: 

630 return "u%X" % codepoint 

631 

632 def getGlyphNames(self): 

633 """Get a list of glyph names, sorted alphabetically.""" 

634 glyphNames = sorted(self.getGlyphOrder()) 

635 return glyphNames 

636 

637 def getGlyphNames2(self): 

638 """Get a list of glyph names, sorted alphabetically, 

639 but not case sensitive. 

640 """ 

641 from fontTools.misc import textTools 

642 

643 return textTools.caselessSort(self.getGlyphOrder()) 

644 

645 def getGlyphName(self, glyphID): 

646 """Returns the name for the glyph with the given ID. 

647 

648 If no name is available, synthesises one with the form ``glyphXXXXX``` where 

649 ```XXXXX`` is the zero-padded glyph ID. 

650 """ 

651 try: 

652 return self.getGlyphOrder()[glyphID] 

653 except IndexError: 

654 return "glyph%.5d" % glyphID 

655 

656 def getGlyphNameMany(self, lst): 

657 """Converts a list of glyph IDs into a list of glyph names.""" 

658 glyphOrder = self.getGlyphOrder() 

659 cnt = len(glyphOrder) 

660 return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid for gid in lst] 

661 

662 def getGlyphID(self, glyphName): 

663 """Returns the ID of the glyph with the given name.""" 

664 try: 

665 return self.getReverseGlyphMap()[glyphName] 

666 except KeyError: 

667 if glyphName[:5] == "glyph": 

668 try: 

669 return int(glyphName[5:]) 

670 except (NameError, ValueError): 

671 raise KeyError(glyphName) 

672 raise 

673 

674 def getGlyphIDMany(self, lst): 

675 """Converts a list of glyph names into a list of glyph IDs.""" 

676 d = self.getReverseGlyphMap() 

677 try: 

678 return [d[glyphName] for glyphName in lst] 

679 except KeyError: 

680 getGlyphID = self.getGlyphID 

681 return [getGlyphID(glyphName) for glyphName in lst] 

682 

683 def getReverseGlyphMap(self, rebuild=False): 

684 """Returns a mapping of glyph names to glyph IDs.""" 

685 if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): 

686 self._buildReverseGlyphOrderDict() 

687 return self._reverseGlyphOrderDict 

688 

689 def _buildReverseGlyphOrderDict(self): 

690 self._reverseGlyphOrderDict = d = {} 

691 for glyphID, glyphName in enumerate(self.getGlyphOrder()): 

692 d[glyphName] = glyphID 

693 return d 

694 

695 def _writeTable(self, tag, writer, done, tableCache=None): 

696 """Internal helper function for self.save(). Keeps track of 

697 inter-table dependencies. 

698 """ 

699 if tag in done: 

700 return 

701 tableClass = getTableClass(tag) 

702 for masterTable in tableClass.dependencies: 

703 if masterTable not in done: 

704 if masterTable in self: 

705 self._writeTable(masterTable, writer, done, tableCache) 

706 else: 

707 done.append(masterTable) 

708 done.append(tag) 

709 tabledata = self.getTableData(tag) 

710 if tableCache is not None: 

711 entry = tableCache.get((Tag(tag), tabledata)) 

712 if entry is not None: 

713 log.debug("reusing '%s' table", tag) 

714 writer.setEntry(tag, entry) 

715 return 

716 log.debug("Writing '%s' table to disk", tag) 

717 writer[tag] = tabledata 

718 if tableCache is not None: 

719 tableCache[(Tag(tag), tabledata)] = writer[tag] 

720 

721 def getTableData(self, tag): 

722 """Returns the binary representation of a table. 

723 

724 If the table is currently loaded and in memory, the data is compiled to 

725 binary and returned; if it is not currently loaded, the binary data is 

726 read from the font file and returned. 

727 """ 

728 tag = Tag(tag) 

729 if self.isLoaded(tag): 

730 log.debug("Compiling '%s' table", tag) 

731 return self.tables[tag].compile(self) 

732 elif self.reader and tag in self.reader: 

733 log.debug("Reading '%s' table from disk", tag) 

734 return self.reader[tag] 

735 else: 

736 raise KeyError(tag) 

737 

738 def getGlyphSet( 

739 self, preferCFF=True, location=None, normalized=False, recalcBounds=True 

740 ): 

741 """Return a generic GlyphSet, which is a dict-like object 

742 mapping glyph names to glyph objects. The returned glyph objects 

743 have a ``.draw()`` method that supports the Pen protocol, and will 

744 have an attribute named 'width'. 

745 

746 If the font is CFF-based, the outlines will be taken from the ``CFF `` 

747 or ``CFF2`` tables. Otherwise the outlines will be taken from the 

748 ``glyf`` table. 

749 

750 If the font contains both a ``CFF ``/``CFF2`` and a ``glyf`` table, you 

751 can use the ``preferCFF`` argument to specify which one should be taken. 

752 If the font contains both a ``CFF `` and a ``CFF2`` table, the latter is 

753 taken. 

754 

755 If the ``location`` parameter is set, it should be a dictionary mapping 

756 four-letter variation tags to their float values, and the returned 

757 glyph-set will represent an instance of a variable font at that 

758 location. 

759 

760 If the ``normalized`` variable is set to True, that location is 

761 interpreted as in the normalized (-1..+1) space, otherwise it is in the 

762 font's defined axes space. 

763 """ 

764 if location and "fvar" not in self: 

765 location = None 

766 if location and not normalized: 

767 location = self.normalizeLocation(location) 

768 if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self): 

769 return _TTGlyphSetCFF(self, location) 

770 elif "glyf" in self: 

771 return _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds) 

772 else: 

773 raise TTLibError("Font contains no outlines") 

774 

775 def normalizeLocation(self, location): 

776 """Normalize a ``location`` from the font's defined axes space (also 

777 known as user space) into the normalized (-1..+1) space. It applies 

778 ``avar`` mapping if the font contains an ``avar`` table. 

779 

780 The ``location`` parameter should be a dictionary mapping four-letter 

781 variation tags to their float values. 

782 

783 Raises ``TTLibError`` if the font is not a variable font. 

784 """ 

785 from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap 

786 

787 if "fvar" not in self: 

788 raise TTLibError("Not a variable font") 

789 

790 axes = { 

791 a.axisTag: (a.minValue, a.defaultValue, a.maxValue) 

792 for a in self["fvar"].axes 

793 } 

794 location = normalizeLocation(location, axes) 

795 if "avar" in self: 

796 avar = self["avar"] 

797 avarSegments = avar.segments 

798 mappedLocation = {} 

799 for axisTag, value in location.items(): 

800 avarMapping = avarSegments.get(axisTag, None) 

801 if avarMapping is not None: 

802 value = piecewiseLinearMap(value, avarMapping) 

803 mappedLocation[axisTag] = value 

804 location = mappedLocation 

805 return location 

806 

807 def getBestCmap( 

808 self, 

809 cmapPreferences=( 

810 (3, 10), 

811 (0, 6), 

812 (0, 4), 

813 (3, 1), 

814 (0, 3), 

815 (0, 2), 

816 (0, 1), 

817 (0, 0), 

818 ), 

819 ): 

820 """Returns the 'best' Unicode cmap dictionary available in the font 

821 or ``None``, if no Unicode cmap subtable is available. 

822 

823 By default it will search for the following (platformID, platEncID) 

824 pairs in order:: 

825 

826 (3, 10), # Windows Unicode full repertoire 

827 (0, 6), # Unicode full repertoire (format 13 subtable) 

828 (0, 4), # Unicode 2.0 full repertoire 

829 (3, 1), # Windows Unicode BMP 

830 (0, 3), # Unicode 2.0 BMP 

831 (0, 2), # Unicode ISO/IEC 10646 

832 (0, 1), # Unicode 1.1 

833 (0, 0) # Unicode 1.0 

834 

835 This particular order matches what HarfBuzz uses to choose what 

836 subtable to use by default. This order prefers the largest-repertoire 

837 subtable, and among those, prefers the Windows-platform over the 

838 Unicode-platform as the former has wider support. 

839 

840 This order can be customized via the ``cmapPreferences`` argument. 

841 """ 

842 return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) 

843 

844 

845class GlyphOrder(object): 

846 

847 """A pseudo table. The glyph order isn't in the font as a separate 

848 table, but it's nice to present it as such in the TTX format. 

849 """ 

850 

851 def __init__(self, tag=None): 

852 pass 

853 

854 def toXML(self, writer, ttFont): 

855 glyphOrder = ttFont.getGlyphOrder() 

856 writer.comment( 

857 "The 'id' attribute is only for humans; " "it is ignored when parsed." 

858 ) 

859 writer.newline() 

860 for i in range(len(glyphOrder)): 

861 glyphName = glyphOrder[i] 

862 writer.simpletag("GlyphID", id=i, name=glyphName) 

863 writer.newline() 

864 

865 def fromXML(self, name, attrs, content, ttFont): 

866 if not hasattr(self, "glyphOrder"): 

867 self.glyphOrder = [] 

868 if name == "GlyphID": 

869 self.glyphOrder.append(attrs["name"]) 

870 ttFont.setGlyphOrder(self.glyphOrder) 

871 

872 

873def getTableModule(tag): 

874 """Fetch the packer/unpacker module for a table. 

875 Return None when no module is found. 

876 """ 

877 from . import tables 

878 

879 pyTag = tagToIdentifier(tag) 

880 try: 

881 __import__("fontTools.ttLib.tables." + pyTag) 

882 except ImportError as err: 

883 # If pyTag is found in the ImportError message, 

884 # means table is not implemented. If it's not 

885 # there, then some other module is missing, don't 

886 # suppress the error. 

887 if str(err).find(pyTag) >= 0: 

888 return None 

889 else: 

890 raise err 

891 else: 

892 return getattr(tables, pyTag) 

893 

894 

895# Registry for custom table packer/unpacker classes. Keys are table 

896# tags, values are (moduleName, className) tuples. 

897# See registerCustomTableClass() and getCustomTableClass() 

898_customTableRegistry = {} 

899 

900 

901def registerCustomTableClass(tag, moduleName, className=None): 

902 """Register a custom packer/unpacker class for a table. 

903 

904 The 'moduleName' must be an importable module. If no 'className' 

905 is given, it is derived from the tag, for example it will be 

906 ``table_C_U_S_T_`` for a 'CUST' tag. 

907 

908 The registered table class should be a subclass of 

909 :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable` 

910 """ 

911 if className is None: 

912 className = "table_" + tagToIdentifier(tag) 

913 _customTableRegistry[tag] = (moduleName, className) 

914 

915 

916def unregisterCustomTableClass(tag): 

917 """Unregister the custom packer/unpacker class for a table.""" 

918 del _customTableRegistry[tag] 

919 

920 

921def getCustomTableClass(tag): 

922 """Return the custom table class for tag, if one has been registered 

923 with 'registerCustomTableClass()'. Else return None. 

924 """ 

925 if tag not in _customTableRegistry: 

926 return None 

927 import importlib 

928 

929 moduleName, className = _customTableRegistry[tag] 

930 module = importlib.import_module(moduleName) 

931 return getattr(module, className) 

932 

933 

934def getTableClass(tag): 

935 """Fetch the packer/unpacker class for a table.""" 

936 tableClass = getCustomTableClass(tag) 

937 if tableClass is not None: 

938 return tableClass 

939 module = getTableModule(tag) 

940 if module is None: 

941 from .tables.DefaultTable import DefaultTable 

942 

943 return DefaultTable 

944 pyTag = tagToIdentifier(tag) 

945 tableClass = getattr(module, "table_" + pyTag) 

946 return tableClass 

947 

948 

949def getClassTag(klass): 

950 """Fetch the table tag for a class object.""" 

951 name = klass.__name__ 

952 assert name[:6] == "table_" 

953 name = name[6:] # Chop 'table_' 

954 return identifierToTag(name) 

955 

956 

957def newTable(tag): 

958 """Return a new instance of a table.""" 

959 tableClass = getTableClass(tag) 

960 return tableClass(tag) 

961 

962 

963def _escapechar(c): 

964 """Helper function for tagToIdentifier()""" 

965 import re 

966 

967 if re.match("[a-z0-9]", c): 

968 return "_" + c 

969 elif re.match("[A-Z]", c): 

970 return c + "_" 

971 else: 

972 return hex(byteord(c))[2:] 

973 

974 

975def tagToIdentifier(tag): 

976 """Convert a table tag to a valid (but UGLY) python identifier, 

977 as well as a filename that's guaranteed to be unique even on a 

978 caseless file system. Each character is mapped to two characters. 

979 Lowercase letters get an underscore before the letter, uppercase 

980 letters get an underscore after the letter. Trailing spaces are 

981 trimmed. Illegal characters are escaped as two hex bytes. If the 

982 result starts with a number (as the result of a hex escape), an 

983 extra underscore is prepended. Examples:: 

984 

985 >>> tagToIdentifier('glyf') 

986 '_g_l_y_f' 

987 >>> tagToIdentifier('cvt ') 

988 '_c_v_t' 

989 >>> tagToIdentifier('OS/2') 

990 'O_S_2f_2' 

991 """ 

992 import re 

993 

994 tag = Tag(tag) 

995 if tag == "GlyphOrder": 

996 return tag 

997 assert len(tag) == 4, "tag should be 4 characters long" 

998 while len(tag) > 1 and tag[-1] == " ": 

999 tag = tag[:-1] 

1000 ident = "" 

1001 for c in tag: 

1002 ident = ident + _escapechar(c) 

1003 if re.match("[0-9]", ident): 

1004 ident = "_" + ident 

1005 return ident 

1006 

1007 

1008def identifierToTag(ident): 

1009 """the opposite of tagToIdentifier()""" 

1010 if ident == "GlyphOrder": 

1011 return ident 

1012 if len(ident) % 2 and ident[0] == "_": 

1013 ident = ident[1:] 

1014 assert not (len(ident) % 2) 

1015 tag = "" 

1016 for i in range(0, len(ident), 2): 

1017 if ident[i] == "_": 

1018 tag = tag + ident[i + 1] 

1019 elif ident[i + 1] == "_": 

1020 tag = tag + ident[i] 

1021 else: 

1022 # assume hex 

1023 tag = tag + chr(int(ident[i : i + 2], 16)) 

1024 # append trailing spaces 

1025 tag = tag + (4 - len(tag)) * " " 

1026 return Tag(tag) 

1027 

1028 

1029def tagToXML(tag): 

1030 """Similarly to tagToIdentifier(), this converts a TT tag 

1031 to a valid XML element name. Since XML element names are 

1032 case sensitive, this is a fairly simple/readable translation. 

1033 """ 

1034 import re 

1035 

1036 tag = Tag(tag) 

1037 if tag == "OS/2": 

1038 return "OS_2" 

1039 elif tag == "GlyphOrder": 

1040 return tag 

1041 if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): 

1042 return tag.strip() 

1043 else: 

1044 return tagToIdentifier(tag) 

1045 

1046 

1047def xmlToTag(tag): 

1048 """The opposite of tagToXML()""" 

1049 if tag == "OS_2": 

1050 return Tag("OS/2") 

1051 if len(tag) == 8: 

1052 return identifierToTag(tag) 

1053 else: 

1054 return Tag(tag + " " * (4 - len(tag))) 

1055 

1056 

1057# Table order as recommended in the OpenType specification 1.4 

1058TTFTableOrder = [ 

1059 "head", 

1060 "hhea", 

1061 "maxp", 

1062 "OS/2", 

1063 "hmtx", 

1064 "LTSH", 

1065 "VDMX", 

1066 "hdmx", 

1067 "cmap", 

1068 "fpgm", 

1069 "prep", 

1070 "cvt ", 

1071 "loca", 

1072 "glyf", 

1073 "kern", 

1074 "name", 

1075 "post", 

1076 "gasp", 

1077 "PCLT", 

1078] 

1079 

1080OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF "] 

1081 

1082 

1083def sortedTagList(tagList, tableOrder=None): 

1084 """Return a sorted copy of tagList, sorted according to the OpenType 

1085 specification, or according to a custom tableOrder. If given and not 

1086 None, tableOrder needs to be a list of tag names. 

1087 """ 

1088 tagList = sorted(tagList) 

1089 if tableOrder is None: 

1090 if "DSIG" in tagList: 

1091 # DSIG should be last (XXX spec reference?) 

1092 tagList.remove("DSIG") 

1093 tagList.append("DSIG") 

1094 if "CFF " in tagList: 

1095 tableOrder = OTFTableOrder 

1096 else: 

1097 tableOrder = TTFTableOrder 

1098 orderedTables = [] 

1099 for tag in tableOrder: 

1100 if tag in tagList: 

1101 orderedTables.append(tag) 

1102 tagList.remove(tag) 

1103 orderedTables.extend(tagList) 

1104 return orderedTables 

1105 

1106 

1107def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): 

1108 """Rewrite a font file, ordering the tables as recommended by the 

1109 OpenType specification 1.4. 

1110 """ 

1111 inFile.seek(0) 

1112 outFile.seek(0) 

1113 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 

1114 writer = SFNTWriter( 

1115 outFile, 

1116 len(reader.tables), 

1117 reader.sfntVersion, 

1118 reader.flavor, 

1119 reader.flavorData, 

1120 ) 

1121 tables = list(reader.keys()) 

1122 for tag in sortedTagList(tables, tableOrder): 

1123 writer[tag] = reader[tag] 

1124 writer.close() 

1125 

1126 

1127def maxPowerOfTwo(x): 

1128 """Return the highest exponent of two, so that 

1129 (2 ** exponent) <= x. Return 0 if x is 0. 

1130 """ 

1131 exponent = 0 

1132 while x: 

1133 x = x >> 1 

1134 exponent = exponent + 1 

1135 return max(exponent - 1, 0) 

1136 

1137 

1138def getSearchRange(n, itemSize=16): 

1139 """Calculate searchRange, entrySelector, rangeShift.""" 

1140 # itemSize defaults to 16, for backward compatibility 

1141 # with upstream fonttools. 

1142 exponent = maxPowerOfTwo(n) 

1143 searchRange = (2**exponent) * itemSize 

1144 entrySelector = exponent 

1145 rangeShift = max(0, n * itemSize - searchRange) 

1146 return searchRange, entrySelector, rangeShift