Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fontTools/ttLib/ttFont.py: 24%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

579 statements  

1from fontTools.config import Config 

2from fontTools.misc import xmlWriter 

3from fontTools.misc.configTools import AbstractConfig 

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

5from fontTools.misc.loggingTools import deprecateArgument 

6from fontTools.ttLib import TTLibError 

7from fontTools.ttLib.ttGlyphSet import ( 

8 _TTGlyph, 

9 _TTGlyphSetCFF, 

10 _TTGlyphSetGlyf, 

11 _TTGlyphSetVARC, 

12) 

13from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 

14from io import BytesIO, StringIO, UnsupportedOperation 

15import os 

16import logging 

17import traceback 

18 

19log = logging.getLogger(__name__) 

20 

21 

22class TTFont(object): 

23 """Represents a TrueType font. 

24 

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

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

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

28 

29 Example usage: 

30 

31 .. code-block:: pycon 

32 

33 >>> 

34 >> from fontTools import ttLib 

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

36 >> tt['maxp'].numGlyphs 

37 242 

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

39 'B&H\000' 

40 >> tt['head'].unitsPerEm 

41 2048 

42 

43 For details of the objects returned when accessing each table, see the 

44 :doc:`tables </ttLib/tables>` documentation. 

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

46 

47 .. code-block:: pycon 

48 

49 >>> 

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

51 >> os2.version = 4 

52 >> # set other attributes 

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

54 

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

56 :doc:`ttx </ttx>` binary): 

57 

58 .. code-block:: pycon 

59 

60 >> 

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

62 Dumping 'LTSH' table... 

63 Dumping 'OS/2' table... 

64 [...] 

65 

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

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

68 >> tt2['maxp'].numGlyphs 

69 242 

70 

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

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

73 

74 with TTFont(filename) as f: 

75 # Do stuff 

76 

77 Args: 

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

79 or a readable file object. 

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

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

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

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

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

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

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

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

88 file. 

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

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

91 raise an exception if any wrong checksums are found. 

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

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

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

95 time. 

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

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

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

99 the ``head`` table on save. 

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

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

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

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

104 """ 

105 

106 def __init__( 

107 self, 

108 file=None, 

109 res_name_or_index=None, 

110 sfntVersion="\000\001\000\000", 

111 flavor=None, 

112 checkChecksums=0, 

113 verbose=None, 

114 recalcBBoxes=True, 

115 allowVID=NotImplemented, 

116 ignoreDecompileErrors=False, 

117 recalcTimestamp=True, 

118 fontNumber=-1, 

119 lazy=None, 

120 quiet=None, 

121 _tableCache=None, 

122 cfg={}, 

123 ): 

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

125 val = locals().get(name) 

126 if val is not None: 

127 deprecateArgument(name, "configure logging instead") 

128 setattr(self, name, val) 

129 

130 self.lazy = lazy 

131 self.recalcBBoxes = recalcBBoxes 

132 self.recalcTimestamp = recalcTimestamp 

133 self.tables = {} 

134 self.reader = None 

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

136 self.ignoreDecompileErrors = ignoreDecompileErrors 

137 

138 if not file: 

139 self.sfntVersion = sfntVersion 

140 self.flavor = flavor 

141 self.flavorData = None 

142 return 

143 seekable = True 

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

145 closeStream = True 

146 # assume file is a string 

147 if res_name_or_index is not None: 

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

149 from . import macUtils 

150 

151 if res_name_or_index == 0: 

152 if macUtils.getSFNTResIndices(file): 

153 # get the first available sfnt font. 

154 file = macUtils.SFNTResourceReader(file, 1) 

155 else: 

156 file = open(file, "rb") 

157 else: 

158 file = macUtils.SFNTResourceReader(file, res_name_or_index) 

159 else: 

160 file = open(file, "rb") 

161 else: 

162 # assume "file" is a readable file object 

163 closeStream = False 

164 # SFNTReader wants the input file to be seekable. 

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

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

167 if hasattr(file, "seekable"): 

168 seekable = file.seekable() 

169 elif hasattr(file, "seek"): 

170 try: 

171 file.seek(0) 

172 except UnsupportedOperation: 

173 seekable = False 

174 

175 if not self.lazy: 

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

177 if seekable: 

178 file.seek(0) 

179 tmp = BytesIO(file.read()) 

180 if hasattr(file, "name"): 

181 # save reference to input file name 

182 tmp.name = file.name 

183 if closeStream: 

184 file.close() 

185 file = tmp 

186 elif not seekable: 

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

188 self._tableCache = _tableCache 

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

190 self.sfntVersion = self.reader.sfntVersion 

191 self.flavor = self.reader.flavor 

192 self.flavorData = self.reader.flavorData 

193 

194 def __enter__(self): 

195 return self 

196 

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

198 self.close() 

199 

200 def close(self): 

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

202 if self.reader is not None: 

203 self.reader.close() 

204 

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

206 """Save the font to disk. 

207 

208 Args: 

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

210 file object. 

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

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

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

214 dependency (fastest). 

215 """ 

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

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

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

219 createStream = True 

220 else: 

221 # assume "file" is a writable file object 

222 createStream = False 

223 

224 tmp = BytesIO() 

225 

226 writer_reordersTables = self._save(tmp) 

227 

228 if not ( 

229 reorderTables is None 

230 or writer_reordersTables 

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

232 ): 

233 if reorderTables is False: 

234 # sort tables using the original font's order 

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

236 else: 

237 # use the recommended order from the OpenType specification 

238 tableOrder = None 

239 tmp.flush() 

240 tmp2 = BytesIO() 

241 reorderFontTables(tmp, tmp2, tableOrder) 

242 tmp.close() 

243 tmp = tmp2 

244 

245 if createStream: 

246 # "file" is a path 

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

248 file.write(tmp.getvalue()) 

249 else: 

250 file.write(tmp.getvalue()) 

251 

252 tmp.close() 

253 

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

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

256 

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

258 self[ 

259 "head" 

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

261 

262 tags = list(self.keys()) 

263 if "GlyphOrder" in tags: 

264 tags.remove("GlyphOrder") 

265 numTables = len(tags) 

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

267 writer = SFNTWriter( 

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

269 ) 

270 

271 done = [] 

272 for tag in tags: 

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

274 

275 writer.close() 

276 

277 return writer.reordersTables() 

278 

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

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

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

282 argument should be a path to a directory. 

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

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

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

286 """ 

287 

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

289 self._saveXML(writer, **kwargs) 

290 writer.close() 

291 

292 def _saveXML( 

293 self, 

294 writer, 

295 writeVersion=True, 

296 quiet=None, 

297 tables=None, 

298 skipTables=None, 

299 splitTables=False, 

300 splitGlyphs=False, 

301 disassembleInstructions=True, 

302 bitmapGlyphDataFormat="raw", 

303 ): 

304 if quiet is not None: 

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

306 

307 self.disassembleInstructions = disassembleInstructions 

308 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat 

309 if not tables: 

310 tables = list(self.keys()) 

311 if "GlyphOrder" not in tables: 

312 tables = ["GlyphOrder"] + tables 

313 if skipTables: 

314 for tag in skipTables: 

315 if tag in tables: 

316 tables.remove(tag) 

317 numTables = len(tables) 

318 

319 if writeVersion: 

320 from fontTools import version 

321 

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

323 writer.begintag( 

324 "ttFont", 

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

326 ttLibVersion=version, 

327 ) 

328 else: 

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

330 writer.newline() 

331 

332 # always splitTables if splitGlyphs is enabled 

333 splitTables = splitTables or splitGlyphs 

334 

335 if not splitTables: 

336 writer.newline() 

337 else: 

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

339 

340 for i in range(numTables): 

341 tag = tables[i] 

342 if splitTables: 

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

344 tableWriter = xmlWriter.XMLWriter( 

345 tablePath, newlinestr=writer.newlinestr 

346 ) 

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

348 tableWriter.newline() 

349 tableWriter.newline() 

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

351 writer.newline() 

352 else: 

353 tableWriter = writer 

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

355 if splitTables: 

356 tableWriter.endtag("ttFont") 

357 tableWriter.newline() 

358 tableWriter.close() 

359 writer.endtag("ttFont") 

360 writer.newline() 

361 

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

363 if quiet is not None: 

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

365 if tag in self: 

366 table = self[tag] 

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

368 else: 

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

370 log.info(report) 

371 if tag not in self: 

372 return 

373 xmlTag = tagToXML(tag) 

374 attrs = dict() 

375 if hasattr(table, "ERROR"): 

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

377 from .tables.DefaultTable import DefaultTable 

378 

379 if table.__class__ == DefaultTable: 

380 attrs["raw"] = True 

381 writer.begintag(xmlTag, **attrs) 

382 writer.newline() 

383 if tag == "glyf": 

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

385 else: 

386 table.toXML(writer, self) 

387 writer.endtag(xmlTag) 

388 writer.newline() 

389 writer.newline() 

390 

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

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

393 a font object. 

394 """ 

395 if quiet is not None: 

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

397 

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

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

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

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

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

403 self.getGlyphOrder() 

404 

405 from fontTools.misc import xmlReader 

406 

407 reader = xmlReader.XMLReader(fileOrPath, self) 

408 reader.read() 

409 

410 def isLoaded(self, tag): 

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

412 decompiled and loaded into memory.""" 

413 return tag in self.tables 

414 

415 def has_key(self, tag): 

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

417 

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

419 presence of the table.""" 

420 if self.isLoaded(tag): 

421 return True 

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

423 return True 

424 elif tag == "GlyphOrder": 

425 return True 

426 else: 

427 return False 

428 

429 __contains__ = has_key 

430 

431 def keys(self): 

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

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

434 if self.reader: 

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

436 if key not in keys: 

437 keys.append(key) 

438 

439 if "GlyphOrder" in keys: 

440 keys.remove("GlyphOrder") 

441 keys = sortedTagList(keys) 

442 return ["GlyphOrder"] + keys 

443 

444 def ensureDecompiled(self, recurse=None): 

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

446 for tag in self.keys(): 

447 table = self[tag] 

448 if recurse is None: 

449 recurse = self.lazy is not False 

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

451 table.ensureDecompiled(recurse=recurse) 

452 self.lazy = False 

453 

454 def __len__(self): 

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

456 

457 def __getitem__(self, tag): 

458 tag = Tag(tag) 

459 table = self.tables.get(tag) 

460 if table is None: 

461 if tag == "GlyphOrder": 

462 table = GlyphOrder(tag) 

463 self.tables[tag] = table 

464 elif self.reader is not None: 

465 table = self._readTable(tag) 

466 else: 

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

468 return table 

469 

470 def _readTable(self, tag): 

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

472 data = self.reader[tag] 

473 if self._tableCache is not None: 

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

475 if table is not None: 

476 return table 

477 tableClass = getTableClass(tag) 

478 table = tableClass(tag) 

479 self.tables[tag] = table 

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

481 try: 

482 table.decompile(data, self) 

483 except Exception: 

484 if not self.ignoreDecompileErrors: 

485 raise 

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

487 log.exception( 

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

489 ) 

490 from .tables.DefaultTable import DefaultTable 

491 

492 file = StringIO() 

493 traceback.print_exc(file=file) 

494 table = DefaultTable(tag) 

495 table.ERROR = file.getvalue() 

496 self.tables[tag] = table 

497 table.decompile(data, self) 

498 if self._tableCache is not None: 

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

500 return table 

501 

502 def __setitem__(self, tag, table): 

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

504 

505 def __delitem__(self, tag): 

506 if tag not in self: 

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

508 if tag in self.tables: 

509 del self.tables[tag] 

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

511 del self.reader[tag] 

512 

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

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

515 try: 

516 return self[tag] 

517 except KeyError: 

518 return default 

519 

520 def setGlyphOrder(self, glyphOrder): 

521 """Set the glyph order 

522 

523 Args: 

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

525 """ 

526 self.glyphOrder = glyphOrder 

527 if hasattr(self, "_reverseGlyphOrderDict"): 

528 del self._reverseGlyphOrderDict 

529 if self.isLoaded("glyf"): 

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

531 

532 def getGlyphOrder(self): 

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

534 try: 

535 return self.glyphOrder 

536 except AttributeError: 

537 pass 

538 if "CFF " in self: 

539 cff = self["CFF "] 

540 self.glyphOrder = cff.getGlyphOrder() 

541 elif "post" in self: 

542 # TrueType font 

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

544 if glyphOrder is None: 

545 # 

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

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

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

549 # 

550 self._getGlyphNamesFromCmap() 

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

552 # 

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

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

555 # has more than 258 glyphs (the length of 'standardGlyphOrder'). 

556 # 

557 log.warning( 

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

559 ) 

560 self._getGlyphNamesFromCmap() 

561 else: 

562 self.glyphOrder = glyphOrder 

563 else: 

564 self._getGlyphNamesFromCmap() 

565 return self.glyphOrder 

566 

567 def _getGlyphNamesFromCmap(self): 

568 # 

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

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

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

572 # or none at all). 

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

574 # So here's what we do: 

575 # - make up glyph names based on glyphID 

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

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

578 # - unload the temporary cmap table 

579 # 

580 if self.isLoaded("cmap"): 

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

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

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

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

585 # restore it later. 

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

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

588 cmapLoading = self.tables["cmap"] 

589 del self.tables["cmap"] 

590 else: 

591 cmapLoading = None 

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

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

594 # cmap. 

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

596 glyphOrder = ["glyph%.5d" % i for i in range(numGlyphs)] 

597 glyphOrder[0] = ".notdef" 

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

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

600 self.glyphOrder = glyphOrder 

601 

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

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

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

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

606 if "cmap" in self: 

607 reversecmap = self["cmap"].buildReversedMin() 

608 else: 

609 reversecmap = {} 

610 useCount = {} 

611 for i in range(numGlyphs): 

612 tempName = glyphOrder[i] 

613 if tempName in reversecmap: 

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

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

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

617 glyphName = self._makeGlyphName(reversecmap[tempName]) 

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

619 if numUses > 1: 

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

621 glyphOrder[i] = glyphName 

622 

623 if "cmap" in self: 

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

625 # be parsed again with the right names. 

626 del self.tables["cmap"] 

627 self.glyphOrder = glyphOrder 

628 if cmapLoading: 

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

630 # using the proper names. 

631 self.tables["cmap"] = cmapLoading 

632 

633 @staticmethod 

634 def _makeGlyphName(codepoint): 

635 from fontTools import agl # Adobe Glyph List 

636 

637 if codepoint in agl.UV2AGL: 

638 return agl.UV2AGL[codepoint] 

639 elif codepoint <= 0xFFFF: 

640 return "uni%04X" % codepoint 

641 else: 

642 return "u%X" % codepoint 

643 

644 def getGlyphNames(self): 

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

646 glyphNames = sorted(self.getGlyphOrder()) 

647 return glyphNames 

648 

649 def getGlyphNames2(self): 

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

651 but not case sensitive. 

652 """ 

653 from fontTools.misc import textTools 

654 

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

656 

657 def getGlyphName(self, glyphID): 

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

659 

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

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

662 """ 

663 try: 

664 return self.getGlyphOrder()[glyphID] 

665 except IndexError: 

666 return "glyph%.5d" % glyphID 

667 

668 def getGlyphNameMany(self, lst): 

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

670 glyphOrder = self.getGlyphOrder() 

671 cnt = len(glyphOrder) 

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

673 

674 def getGlyphID(self, glyphName): 

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

676 try: 

677 return self.getReverseGlyphMap()[glyphName] 

678 except KeyError: 

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

680 try: 

681 return int(glyphName[5:]) 

682 except (NameError, ValueError): 

683 raise KeyError(glyphName) 

684 raise 

685 

686 def getGlyphIDMany(self, lst): 

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

688 d = self.getReverseGlyphMap() 

689 try: 

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

691 except KeyError: 

692 getGlyphID = self.getGlyphID 

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

694 

695 def getReverseGlyphMap(self, rebuild=False): 

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

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

698 self._buildReverseGlyphOrderDict() 

699 return self._reverseGlyphOrderDict 

700 

701 def _buildReverseGlyphOrderDict(self): 

702 self._reverseGlyphOrderDict = d = {} 

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

704 d[glyphName] = glyphID 

705 return d 

706 

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

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

709 inter-table dependencies. 

710 """ 

711 if tag in done: 

712 return 

713 tableClass = getTableClass(tag) 

714 for masterTable in tableClass.dependencies: 

715 if masterTable not in done: 

716 if masterTable in self: 

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

718 else: 

719 done.append(masterTable) 

720 done.append(tag) 

721 tabledata = self.getTableData(tag) 

722 if tableCache is not None: 

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

724 if entry is not None: 

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

726 writer.setEntry(tag, entry) 

727 return 

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

729 writer[tag] = tabledata 

730 if tableCache is not None: 

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

732 

733 def getTableData(self, tag): 

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

735 

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

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

738 read from the font file and returned. 

739 """ 

740 tag = Tag(tag) 

741 if self.isLoaded(tag): 

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

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

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

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

746 return self.reader[tag] 

747 else: 

748 raise KeyError(tag) 

749 

750 def getGlyphSet( 

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

752 ): 

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

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

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

756 have an attribute named 'width'. 

757 

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

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

760 ``glyf`` table. 

761 

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

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

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

765 taken. 

766 

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

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

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

770 location. 

771 

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

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

774 font's defined axes space. 

775 """ 

776 if location and "fvar" not in self: 

777 location = None 

778 if location and not normalized: 

779 location = self.normalizeLocation(location) 

780 glyphSet = None 

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

782 glyphSet = _TTGlyphSetCFF(self, location) 

783 elif "glyf" in self: 

784 glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds) 

785 else: 

786 raise TTLibError("Font contains no outlines") 

787 if "VARC" in self: 

788 glyphSet = _TTGlyphSetVARC(self, location, glyphSet) 

789 return glyphSet 

790 

791 def normalizeLocation(self, location): 

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

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

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

795 

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

797 variation tags to their float values. 

798 

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

800 """ 

801 from fontTools.varLib.models import normalizeLocation 

802 

803 if "fvar" not in self: 

804 raise TTLibError("Not a variable font") 

805 

806 axes = self["fvar"].getAxes() 

807 location = normalizeLocation(location, axes) 

808 if "avar" in self: 

809 location = self["avar"].renormalizeLocation(location, self) 

810 return location 

811 

812 def getBestCmap( 

813 self, 

814 cmapPreferences=( 

815 (3, 10), 

816 (0, 6), 

817 (0, 4), 

818 (3, 1), 

819 (0, 3), 

820 (0, 2), 

821 (0, 1), 

822 (0, 0), 

823 ), 

824 ): 

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

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

827 

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

829 pairs in order:: 

830 

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

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

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

834 (3, 1), # Windows Unicode BMP 

835 (0, 3), # Unicode 2.0 BMP 

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

837 (0, 1), # Unicode 1.1 

838 (0, 0) # Unicode 1.0 

839 

840 This particular order matches what HarfBuzz uses to choose what 

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

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

843 Unicode-platform as the former has wider support. 

844 

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

846 """ 

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

848 

849 def reorderGlyphs(self, new_glyph_order): 

850 from .reorderGlyphs import reorderGlyphs 

851 

852 reorderGlyphs(self, new_glyph_order) 

853 

854 

855class GlyphOrder(object): 

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

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

858 """ 

859 

860 def __init__(self, tag=None): 

861 pass 

862 

863 def toXML(self, writer, ttFont): 

864 glyphOrder = ttFont.getGlyphOrder() 

865 writer.comment( 

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

867 ) 

868 writer.newline() 

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

870 glyphName = glyphOrder[i] 

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

872 writer.newline() 

873 

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

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

876 self.glyphOrder = [] 

877 if name == "GlyphID": 

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

879 ttFont.setGlyphOrder(self.glyphOrder) 

880 

881 

882def getTableModule(tag): 

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

884 Return None when no module is found. 

885 """ 

886 from . import tables 

887 

888 pyTag = tagToIdentifier(tag) 

889 try: 

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

891 except ImportError as err: 

892 # If pyTag is found in the ImportError message, 

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

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

895 # suppress the error. 

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

897 return None 

898 else: 

899 raise err 

900 else: 

901 return getattr(tables, pyTag) 

902 

903 

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

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

906# See registerCustomTableClass() and getCustomTableClass() 

907_customTableRegistry = {} 

908 

909 

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

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

912 

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

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

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

916 

917 The registered table class should be a subclass of 

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

919 """ 

920 if className is None: 

921 className = "table_" + tagToIdentifier(tag) 

922 _customTableRegistry[tag] = (moduleName, className) 

923 

924 

925def unregisterCustomTableClass(tag): 

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

927 del _customTableRegistry[tag] 

928 

929 

930def getCustomTableClass(tag): 

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

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

933 """ 

934 if tag not in _customTableRegistry: 

935 return None 

936 import importlib 

937 

938 moduleName, className = _customTableRegistry[tag] 

939 module = importlib.import_module(moduleName) 

940 return getattr(module, className) 

941 

942 

943def getTableClass(tag): 

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

945 tableClass = getCustomTableClass(tag) 

946 if tableClass is not None: 

947 return tableClass 

948 module = getTableModule(tag) 

949 if module is None: 

950 from .tables.DefaultTable import DefaultTable 

951 

952 return DefaultTable 

953 pyTag = tagToIdentifier(tag) 

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

955 return tableClass 

956 

957 

958def getClassTag(klass): 

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

960 name = klass.__name__ 

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

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

963 return identifierToTag(name) 

964 

965 

966def newTable(tag): 

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

968 tableClass = getTableClass(tag) 

969 return tableClass(tag) 

970 

971 

972def _escapechar(c): 

973 """Helper function for tagToIdentifier()""" 

974 import re 

975 

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

977 return "_" + c 

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

979 return c + "_" 

980 else: 

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

982 

983 

984def tagToIdentifier(tag): 

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

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

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

988 Lowercase letters get an underscore before the letter, uppercase 

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

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

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

992 extra underscore is prepended. Examples: 

993 .. code-block:: pycon 

994 

995 >>> 

996 >> tagToIdentifier('glyf') 

997 '_g_l_y_f' 

998 >> tagToIdentifier('cvt ') 

999 '_c_v_t' 

1000 >> tagToIdentifier('OS/2') 

1001 'O_S_2f_2' 

1002 """ 

1003 import re 

1004 

1005 tag = Tag(tag) 

1006 if tag == "GlyphOrder": 

1007 return tag 

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

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

1010 tag = tag[:-1] 

1011 ident = "" 

1012 for c in tag: 

1013 ident = ident + _escapechar(c) 

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

1015 ident = "_" + ident 

1016 return ident 

1017 

1018 

1019def identifierToTag(ident): 

1020 """the opposite of tagToIdentifier()""" 

1021 if ident == "GlyphOrder": 

1022 return ident 

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

1024 ident = ident[1:] 

1025 assert not (len(ident) % 2) 

1026 tag = "" 

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

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

1029 tag = tag + ident[i + 1] 

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

1031 tag = tag + ident[i] 

1032 else: 

1033 # assume hex 

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

1035 # append trailing spaces 

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

1037 return Tag(tag) 

1038 

1039 

1040def tagToXML(tag): 

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

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

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

1044 """ 

1045 import re 

1046 

1047 tag = Tag(tag) 

1048 if tag == "OS/2": 

1049 return "OS_2" 

1050 elif tag == "GlyphOrder": 

1051 return tag 

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

1053 return tag.strip() 

1054 else: 

1055 return tagToIdentifier(tag) 

1056 

1057 

1058def xmlToTag(tag): 

1059 """The opposite of tagToXML()""" 

1060 if tag == "OS_2": 

1061 return Tag("OS/2") 

1062 if len(tag) == 8: 

1063 return identifierToTag(tag) 

1064 else: 

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

1066 

1067 

1068# Table order as recommended in the OpenType specification 1.4 

1069TTFTableOrder = [ 

1070 "head", 

1071 "hhea", 

1072 "maxp", 

1073 "OS/2", 

1074 "hmtx", 

1075 "LTSH", 

1076 "VDMX", 

1077 "hdmx", 

1078 "cmap", 

1079 "fpgm", 

1080 "prep", 

1081 "cvt ", 

1082 "loca", 

1083 "glyf", 

1084 "kern", 

1085 "name", 

1086 "post", 

1087 "gasp", 

1088 "PCLT", 

1089] 

1090 

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

1092 

1093 

1094def sortedTagList(tagList, tableOrder=None): 

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

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

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

1098 """ 

1099 tagList = sorted(tagList) 

1100 if tableOrder is None: 

1101 if "DSIG" in tagList: 

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

1103 tagList.remove("DSIG") 

1104 tagList.append("DSIG") 

1105 if "CFF " in tagList: 

1106 tableOrder = OTFTableOrder 

1107 else: 

1108 tableOrder = TTFTableOrder 

1109 orderedTables = [] 

1110 for tag in tableOrder: 

1111 if tag in tagList: 

1112 orderedTables.append(tag) 

1113 tagList.remove(tag) 

1114 orderedTables.extend(tagList) 

1115 return orderedTables 

1116 

1117 

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

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

1120 OpenType specification 1.4. 

1121 """ 

1122 inFile.seek(0) 

1123 outFile.seek(0) 

1124 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 

1125 writer = SFNTWriter( 

1126 outFile, 

1127 len(reader.tables), 

1128 reader.sfntVersion, 

1129 reader.flavor, 

1130 reader.flavorData, 

1131 ) 

1132 tables = list(reader.keys()) 

1133 for tag in sortedTagList(tables, tableOrder): 

1134 writer[tag] = reader[tag] 

1135 writer.close() 

1136 

1137 

1138def maxPowerOfTwo(x): 

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

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

1141 """ 

1142 exponent = 0 

1143 while x: 

1144 x = x >> 1 

1145 exponent = exponent + 1 

1146 return max(exponent - 1, 0) 

1147 

1148 

1149def getSearchRange(n, itemSize=16): 

1150 """Calculate searchRange, entrySelector, rangeShift.""" 

1151 # itemSize defaults to 16, for backward compatibility 

1152 # with upstream fonttools. 

1153 exponent = maxPowerOfTwo(n) 

1154 searchRange = (2**exponent) * itemSize 

1155 entrySelector = exponent 

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

1157 return searchRange, entrySelector, rangeShift