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

570 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 = self.keys() 

263 tags.pop(0) # skip GlyphOrder tag 

264 numTables = len(tags) 

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

266 writer = SFNTWriter( 

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

268 ) 

269 

270 done = [] 

271 for tag in tags: 

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

273 

274 writer.close() 

275 

276 return writer.reordersTables() 

277 

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

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

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

281 argument should be a path to a directory. 

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

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

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

285 """ 

286 

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

288 self._saveXML(writer, **kwargs) 

289 writer.close() 

290 

291 def _saveXML( 

292 self, 

293 writer, 

294 writeVersion=True, 

295 quiet=None, 

296 tables=None, 

297 skipTables=None, 

298 splitTables=False, 

299 splitGlyphs=False, 

300 disassembleInstructions=True, 

301 bitmapGlyphDataFormat="raw", 

302 ): 

303 if quiet is not None: 

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

305 

306 self.disassembleInstructions = disassembleInstructions 

307 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat 

308 if not tables: 

309 tables = self.keys() 

310 if skipTables: 

311 tables = [tag for tag in tables if tag not in skipTables] 

312 

313 if writeVersion: 

314 from fontTools import version 

315 

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

317 writer.begintag( 

318 "ttFont", 

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

320 ttLibVersion=version, 

321 ) 

322 else: 

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

324 writer.newline() 

325 

326 # always splitTables if splitGlyphs is enabled 

327 splitTables = splitTables or splitGlyphs 

328 

329 if not splitTables: 

330 writer.newline() 

331 else: 

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

333 

334 for tag in tables: 

335 if splitTables: 

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

337 tableWriter = xmlWriter.XMLWriter( 

338 tablePath, newlinestr=writer.newlinestr 

339 ) 

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

341 tableWriter.newline() 

342 tableWriter.newline() 

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

344 writer.newline() 

345 else: 

346 tableWriter = writer 

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

348 if splitTables: 

349 tableWriter.endtag("ttFont") 

350 tableWriter.newline() 

351 tableWriter.close() 

352 writer.endtag("ttFont") 

353 writer.newline() 

354 

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

356 if quiet is not None: 

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

358 if tag in self: 

359 table = self[tag] 

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

361 else: 

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

363 log.info(report) 

364 if tag not in self: 

365 return 

366 xmlTag = tagToXML(tag) 

367 attrs = dict() 

368 if hasattr(table, "ERROR"): 

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

370 from .tables.DefaultTable import DefaultTable 

371 

372 if table.__class__ == DefaultTable: 

373 attrs["raw"] = True 

374 writer.begintag(xmlTag, **attrs) 

375 writer.newline() 

376 if tag == "glyf": 

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

378 else: 

379 table.toXML(writer, self) 

380 writer.endtag(xmlTag) 

381 writer.newline() 

382 writer.newline() 

383 

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

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

386 a font object. 

387 """ 

388 if quiet is not None: 

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

390 

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

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

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

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

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

396 self.getGlyphOrder() 

397 

398 from fontTools.misc import xmlReader 

399 

400 reader = xmlReader.XMLReader(fileOrPath, self) 

401 reader.read() 

402 

403 def isLoaded(self, tag): 

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

405 decompiled and loaded into memory.""" 

406 return tag in self.tables 

407 

408 def has_key(self, tag): 

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

410 

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

412 presence of the table.""" 

413 if self.isLoaded(tag): 

414 return True 

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

416 return True 

417 elif tag == "GlyphOrder": 

418 return True 

419 else: 

420 return False 

421 

422 __contains__ = has_key 

423 

424 def keys(self): 

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

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

427 if self.reader: 

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

429 if key not in keys: 

430 keys.append(key) 

431 

432 if "GlyphOrder" in keys: 

433 keys.remove("GlyphOrder") 

434 keys = sortedTagList(keys) 

435 return ["GlyphOrder"] + keys 

436 

437 def ensureDecompiled(self, recurse=None): 

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

439 for tag in self.keys(): 

440 table = self[tag] 

441 if recurse is None: 

442 recurse = self.lazy is not False 

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

444 table.ensureDecompiled(recurse=recurse) 

445 self.lazy = False 

446 

447 def __len__(self): 

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

449 

450 def __getitem__(self, tag): 

451 tag = Tag(tag) 

452 table = self.tables.get(tag) 

453 if table is None: 

454 if tag == "GlyphOrder": 

455 table = GlyphOrder(tag) 

456 self.tables[tag] = table 

457 elif self.reader is not None: 

458 table = self._readTable(tag) 

459 else: 

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

461 return table 

462 

463 def _readTable(self, tag): 

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

465 data = self.reader[tag] 

466 if self._tableCache is not None: 

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

468 if table is not None: 

469 return table 

470 tableClass = getTableClass(tag) 

471 table = tableClass(tag) 

472 self.tables[tag] = table 

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

474 try: 

475 table.decompile(data, self) 

476 except Exception: 

477 if not self.ignoreDecompileErrors: 

478 raise 

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

480 log.exception( 

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

482 ) 

483 from .tables.DefaultTable import DefaultTable 

484 

485 file = StringIO() 

486 traceback.print_exc(file=file) 

487 table = DefaultTable(tag) 

488 table.ERROR = file.getvalue() 

489 self.tables[tag] = table 

490 table.decompile(data, self) 

491 if self._tableCache is not None: 

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

493 return table 

494 

495 def __setitem__(self, tag, table): 

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

497 

498 def __delitem__(self, tag): 

499 if tag not in self: 

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

501 if tag in self.tables: 

502 del self.tables[tag] 

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

504 del self.reader[tag] 

505 

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

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

508 try: 

509 return self[tag] 

510 except KeyError: 

511 return default 

512 

513 def setGlyphOrder(self, glyphOrder): 

514 """Set the glyph order 

515 

516 Args: 

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

518 """ 

519 self.glyphOrder = glyphOrder 

520 if hasattr(self, "_reverseGlyphOrderDict"): 

521 del self._reverseGlyphOrderDict 

522 if self.isLoaded("glyf"): 

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

524 

525 def getGlyphOrder(self): 

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

527 try: 

528 return self.glyphOrder 

529 except AttributeError: 

530 pass 

531 if "CFF " in self: 

532 cff = self["CFF "] 

533 self.glyphOrder = cff.getGlyphOrder() 

534 elif "post" in self: 

535 # TrueType font 

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

537 if glyphOrder is None: 

538 # 

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

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

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

542 # 

543 self._getGlyphNamesFromCmap() 

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

545 # 

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

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

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

549 # 

550 log.warning( 

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

552 ) 

553 self._getGlyphNamesFromCmap() 

554 else: 

555 self.glyphOrder = glyphOrder 

556 else: 

557 self._getGlyphNamesFromCmap() 

558 return self.glyphOrder 

559 

560 def _getGlyphNamesFromCmap(self): 

561 # 

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

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

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

565 # or none at all). 

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

567 # So here's what we do: 

568 # - make up glyph names based on glyphID 

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

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

571 # - unload the temporary cmap table 

572 # 

573 if self.isLoaded("cmap"): 

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

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

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

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

578 # restore it later. 

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

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

581 cmapLoading = self.tables["cmap"] 

582 del self.tables["cmap"] 

583 else: 

584 cmapLoading = None 

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

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

587 # cmap. 

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

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

590 glyphOrder[0] = ".notdef" 

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

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

593 self.glyphOrder = glyphOrder 

594 

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

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

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

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

599 if "cmap" in self: 

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

601 else: 

602 reversecmap = {} 

603 useCount = {} 

604 for i, tempName in enumerate(glyphOrder): 

605 if tempName in reversecmap: 

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

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

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

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

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

611 if numUses > 1: 

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

613 glyphOrder[i] = glyphName 

614 

615 if "cmap" in self: 

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

617 # be parsed again with the right names. 

618 del self.tables["cmap"] 

619 self.glyphOrder = glyphOrder 

620 if cmapLoading: 

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

622 # using the proper names. 

623 self.tables["cmap"] = cmapLoading 

624 

625 @staticmethod 

626 def _makeGlyphName(codepoint): 

627 from fontTools import agl # Adobe Glyph List 

628 

629 if codepoint in agl.UV2AGL: 

630 return agl.UV2AGL[codepoint] 

631 elif codepoint <= 0xFFFF: 

632 return "uni%04X" % codepoint 

633 else: 

634 return "u%X" % codepoint 

635 

636 def getGlyphNames(self): 

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

638 glyphNames = sorted(self.getGlyphOrder()) 

639 return glyphNames 

640 

641 def getGlyphNames2(self): 

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

643 but not case sensitive. 

644 """ 

645 from fontTools.misc import textTools 

646 

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

648 

649 def getGlyphName(self, glyphID): 

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

651 

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

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

654 """ 

655 try: 

656 return self.getGlyphOrder()[glyphID] 

657 except IndexError: 

658 return "glyph%.5d" % glyphID 

659 

660 def getGlyphNameMany(self, lst): 

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

662 glyphOrder = self.getGlyphOrder() 

663 cnt = len(glyphOrder) 

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

665 

666 def getGlyphID(self, glyphName): 

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

668 try: 

669 return self.getReverseGlyphMap()[glyphName] 

670 except KeyError: 

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

672 try: 

673 return int(glyphName[5:]) 

674 except (NameError, ValueError): 

675 raise KeyError(glyphName) 

676 raise 

677 

678 def getGlyphIDMany(self, lst): 

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

680 d = self.getReverseGlyphMap() 

681 try: 

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

683 except KeyError: 

684 getGlyphID = self.getGlyphID 

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

686 

687 def getReverseGlyphMap(self, rebuild=False): 

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

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

690 self._buildReverseGlyphOrderDict() 

691 return self._reverseGlyphOrderDict 

692 

693 def _buildReverseGlyphOrderDict(self): 

694 self._reverseGlyphOrderDict = d = {} 

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

696 d[glyphName] = glyphID 

697 return d 

698 

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

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

701 inter-table dependencies. 

702 """ 

703 if tag in done: 

704 return 

705 tableClass = getTableClass(tag) 

706 for masterTable in tableClass.dependencies: 

707 if masterTable not in done: 

708 if masterTable in self: 

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

710 else: 

711 done.append(masterTable) 

712 done.append(tag) 

713 tabledata = self.getTableData(tag) 

714 if tableCache is not None: 

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

716 if entry is not None: 

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

718 writer.setEntry(tag, entry) 

719 return 

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

721 writer[tag] = tabledata 

722 if tableCache is not None: 

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

724 

725 def getTableData(self, tag): 

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

727 

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

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

730 read from the font file and returned. 

731 """ 

732 tag = Tag(tag) 

733 if self.isLoaded(tag): 

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

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

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

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

738 return self.reader[tag] 

739 else: 

740 raise KeyError(tag) 

741 

742 def getGlyphSet( 

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

744 ): 

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

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

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

748 have an attribute named 'width'. 

749 

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

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

752 ``glyf`` table. 

753 

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

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

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

757 taken. 

758 

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

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

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

762 location. 

763 

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

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

766 font's defined axes space. 

767 """ 

768 if location and "fvar" not in self: 

769 location = None 

770 if location and not normalized: 

771 location = self.normalizeLocation(location) 

772 glyphSet = None 

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

774 glyphSet = _TTGlyphSetCFF(self, location) 

775 elif "glyf" in self: 

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

777 else: 

778 raise TTLibError("Font contains no outlines") 

779 if "VARC" in self: 

780 glyphSet = _TTGlyphSetVARC(self, location, glyphSet) 

781 return glyphSet 

782 

783 def normalizeLocation(self, location): 

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

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

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

787 

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

789 variation tags to their float values. 

790 

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

792 """ 

793 from fontTools.varLib.models import normalizeLocation 

794 

795 if "fvar" not in self: 

796 raise TTLibError("Not a variable font") 

797 

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

799 location = normalizeLocation(location, axes) 

800 if "avar" in self: 

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

802 return location 

803 

804 def getBestCmap( 

805 self, 

806 cmapPreferences=( 

807 (3, 10), 

808 (0, 6), 

809 (0, 4), 

810 (3, 1), 

811 (0, 3), 

812 (0, 2), 

813 (0, 1), 

814 (0, 0), 

815 ), 

816 ): 

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

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

819 

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

821 pairs in order:: 

822 

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

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

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

826 (3, 1), # Windows Unicode BMP 

827 (0, 3), # Unicode 2.0 BMP 

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

829 (0, 1), # Unicode 1.1 

830 (0, 0) # Unicode 1.0 

831 

832 This particular order matches what HarfBuzz uses to choose what 

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

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

835 Unicode-platform as the former has wider support. 

836 

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

838 """ 

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

840 

841 def reorderGlyphs(self, new_glyph_order): 

842 from .reorderGlyphs import reorderGlyphs 

843 

844 reorderGlyphs(self, new_glyph_order) 

845 

846 

847class GlyphOrder(object): 

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

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

850 """ 

851 

852 def __init__(self, tag=None): 

853 pass 

854 

855 def toXML(self, writer, ttFont): 

856 glyphOrder = ttFont.getGlyphOrder() 

857 writer.comment( 

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

859 ) 

860 writer.newline() 

861 for i, glyphName in enumerate(glyphOrder): 

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 .. code-block:: pycon 

985 

986 >>> 

987 >> tagToIdentifier('glyf') 

988 '_g_l_y_f' 

989 >> tagToIdentifier('cvt ') 

990 '_c_v_t' 

991 >> tagToIdentifier('OS/2') 

992 'O_S_2f_2' 

993 """ 

994 import re 

995 

996 tag = Tag(tag) 

997 if tag == "GlyphOrder": 

998 return tag 

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

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

1001 tag = tag[:-1] 

1002 ident = "" 

1003 for c in tag: 

1004 ident = ident + _escapechar(c) 

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

1006 ident = "_" + ident 

1007 return ident 

1008 

1009 

1010def identifierToTag(ident): 

1011 """the opposite of tagToIdentifier()""" 

1012 if ident == "GlyphOrder": 

1013 return ident 

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

1015 ident = ident[1:] 

1016 assert not (len(ident) % 2) 

1017 tag = "" 

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

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

1020 tag = tag + ident[i + 1] 

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

1022 tag = tag + ident[i] 

1023 else: 

1024 # assume hex 

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

1026 # append trailing spaces 

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

1028 return Tag(tag) 

1029 

1030 

1031def tagToXML(tag): 

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

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

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

1035 """ 

1036 import re 

1037 

1038 tag = Tag(tag) 

1039 if tag == "OS/2": 

1040 return "OS_2" 

1041 elif tag == "GlyphOrder": 

1042 return tag 

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

1044 return tag.strip() 

1045 else: 

1046 return tagToIdentifier(tag) 

1047 

1048 

1049def xmlToTag(tag): 

1050 """The opposite of tagToXML()""" 

1051 if tag == "OS_2": 

1052 return Tag("OS/2") 

1053 if len(tag) == 8: 

1054 return identifierToTag(tag) 

1055 else: 

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

1057 

1058 

1059# Table order as recommended in the OpenType specification 1.4 

1060TTFTableOrder = [ 

1061 "head", 

1062 "hhea", 

1063 "maxp", 

1064 "OS/2", 

1065 "hmtx", 

1066 "LTSH", 

1067 "VDMX", 

1068 "hdmx", 

1069 "cmap", 

1070 "fpgm", 

1071 "prep", 

1072 "cvt ", 

1073 "loca", 

1074 "glyf", 

1075 "kern", 

1076 "name", 

1077 "post", 

1078 "gasp", 

1079 "PCLT", 

1080] 

1081 

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

1083 

1084 

1085def sortedTagList(tagList, tableOrder=None): 

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

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

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

1089 """ 

1090 tagList = sorted(tagList) 

1091 if tableOrder is None: 

1092 if "DSIG" in tagList: 

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

1094 tagList.remove("DSIG") 

1095 tagList.append("DSIG") 

1096 if "CFF " in tagList: 

1097 tableOrder = OTFTableOrder 

1098 else: 

1099 tableOrder = TTFTableOrder 

1100 orderedTables = [] 

1101 for tag in tableOrder: 

1102 if tag in tagList: 

1103 orderedTables.append(tag) 

1104 tagList.remove(tag) 

1105 orderedTables.extend(tagList) 

1106 return orderedTables 

1107 

1108 

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

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

1111 OpenType specification 1.4. 

1112 """ 

1113 inFile.seek(0) 

1114 outFile.seek(0) 

1115 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 

1116 writer = SFNTWriter( 

1117 outFile, 

1118 len(reader.tables), 

1119 reader.sfntVersion, 

1120 reader.flavor, 

1121 reader.flavorData, 

1122 ) 

1123 tables = list(reader.keys()) 

1124 for tag in sortedTagList(tables, tableOrder): 

1125 writer[tag] = reader[tag] 

1126 writer.close() 

1127 

1128 

1129def maxPowerOfTwo(x): 

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

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

1132 """ 

1133 exponent = 0 

1134 while x: 

1135 x = x >> 1 

1136 exponent = exponent + 1 

1137 return max(exponent - 1, 0) 

1138 

1139 

1140def getSearchRange(n, itemSize=16): 

1141 """Calculate searchRange, entrySelector, rangeShift.""" 

1142 # itemSize defaults to 16, for backward compatibility 

1143 # with upstream fonttools. 

1144 exponent = maxPowerOfTwo(n) 

1145 searchRange = (2**exponent) * itemSize 

1146 entrySelector = exponent 

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

1148 return searchRange, entrySelector, rangeShift