Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

581 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 >> from fontTools import ttLib 

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

33 >> tt['maxp'].numGlyphs 

34 242 

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

36 'B&H\000' 

37 >> tt['head'].unitsPerEm 

38 2048 

39 

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

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

42 

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

44 >> os2.version = 4 

45 >> # set other attributes 

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

47 

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

49 :ref:`ttx` binary):: 

50 

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

52 Dumping 'LTSH' table... 

53 Dumping 'OS/2' table... 

54 [...] 

55 

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

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

58 >> tt2['maxp'].numGlyphs 

59 242 

60 

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

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

63 

64 with TTFont(filename) as f: 

65 # Do stuff 

66 

67 Args: 

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

69 or a readable file object. 

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

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

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

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

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

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

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

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

78 file. 

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

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

81 raise an exception if any wrong checksums are found. 

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

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

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

85 time. 

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

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

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

89 the ``head`` table on save. 

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

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

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

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

94 """ 

95 

96 def __init__( 

97 self, 

98 file=None, 

99 res_name_or_index=None, 

100 sfntVersion="\000\001\000\000", 

101 flavor=None, 

102 checkChecksums=0, 

103 verbose=None, 

104 recalcBBoxes=True, 

105 allowVID=NotImplemented, 

106 ignoreDecompileErrors=False, 

107 recalcTimestamp=True, 

108 fontNumber=-1, 

109 lazy=None, 

110 quiet=None, 

111 _tableCache=None, 

112 cfg={}, 

113 ): 

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

115 val = locals().get(name) 

116 if val is not None: 

117 deprecateArgument(name, "configure logging instead") 

118 setattr(self, name, val) 

119 

120 self.lazy = lazy 

121 self.recalcBBoxes = recalcBBoxes 

122 self.recalcTimestamp = recalcTimestamp 

123 self.tables = {} 

124 self.reader = None 

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

126 self.ignoreDecompileErrors = ignoreDecompileErrors 

127 

128 if not file: 

129 self.sfntVersion = sfntVersion 

130 self.flavor = flavor 

131 self.flavorData = None 

132 return 

133 seekable = True 

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

135 closeStream = True 

136 # assume file is a string 

137 if res_name_or_index is not None: 

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

139 from . import macUtils 

140 

141 if res_name_or_index == 0: 

142 if macUtils.getSFNTResIndices(file): 

143 # get the first available sfnt font. 

144 file = macUtils.SFNTResourceReader(file, 1) 

145 else: 

146 file = open(file, "rb") 

147 else: 

148 file = macUtils.SFNTResourceReader(file, res_name_or_index) 

149 else: 

150 file = open(file, "rb") 

151 else: 

152 # assume "file" is a readable file object 

153 closeStream = False 

154 # SFNTReader wants the input file to be seekable. 

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

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

157 if hasattr(file, "seekable"): 

158 seekable = file.seekable() 

159 elif hasattr(file, "seek"): 

160 try: 

161 file.seek(0) 

162 except UnsupportedOperation: 

163 seekable = False 

164 

165 if not self.lazy: 

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

167 if seekable: 

168 file.seek(0) 

169 tmp = BytesIO(file.read()) 

170 if hasattr(file, "name"): 

171 # save reference to input file name 

172 tmp.name = file.name 

173 if closeStream: 

174 file.close() 

175 file = tmp 

176 elif not seekable: 

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

178 self._tableCache = _tableCache 

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

180 self.sfntVersion = self.reader.sfntVersion 

181 self.flavor = self.reader.flavor 

182 self.flavorData = self.reader.flavorData 

183 

184 def __enter__(self): 

185 return self 

186 

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

188 self.close() 

189 

190 def close(self): 

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

192 if self.reader is not None: 

193 self.reader.close() 

194 

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

196 """Save the font to disk. 

197 

198 Args: 

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

200 file object. 

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

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

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

204 dependency (fastest). 

205 """ 

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

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

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

209 createStream = True 

210 else: 

211 # assume "file" is a writable file object 

212 createStream = False 

213 

214 tmp = BytesIO() 

215 

216 writer_reordersTables = self._save(tmp) 

217 

218 if not ( 

219 reorderTables is None 

220 or writer_reordersTables 

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

222 ): 

223 if reorderTables is False: 

224 # sort tables using the original font's order 

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

226 else: 

227 # use the recommended order from the OpenType specification 

228 tableOrder = None 

229 tmp.flush() 

230 tmp2 = BytesIO() 

231 reorderFontTables(tmp, tmp2, tableOrder) 

232 tmp.close() 

233 tmp = tmp2 

234 

235 if createStream: 

236 # "file" is a path 

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

238 file.write(tmp.getvalue()) 

239 else: 

240 file.write(tmp.getvalue()) 

241 

242 tmp.close() 

243 

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

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

246 

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

248 self[ 

249 "head" 

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

251 

252 tags = list(self.keys()) 

253 if "GlyphOrder" in tags: 

254 tags.remove("GlyphOrder") 

255 numTables = len(tags) 

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

257 writer = SFNTWriter( 

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

259 ) 

260 

261 done = [] 

262 for tag in tags: 

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

264 

265 writer.close() 

266 

267 return writer.reordersTables() 

268 

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

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

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

272 argument should be a path to a directory. 

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

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

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

276 """ 

277 

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

279 self._saveXML(writer, **kwargs) 

280 writer.close() 

281 

282 def _saveXML( 

283 self, 

284 writer, 

285 writeVersion=True, 

286 quiet=None, 

287 tables=None, 

288 skipTables=None, 

289 splitTables=False, 

290 splitGlyphs=False, 

291 disassembleInstructions=True, 

292 bitmapGlyphDataFormat="raw", 

293 ): 

294 if quiet is not None: 

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

296 

297 self.disassembleInstructions = disassembleInstructions 

298 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat 

299 if not tables: 

300 tables = list(self.keys()) 

301 if "GlyphOrder" not in tables: 

302 tables = ["GlyphOrder"] + tables 

303 if skipTables: 

304 for tag in skipTables: 

305 if tag in tables: 

306 tables.remove(tag) 

307 numTables = len(tables) 

308 

309 if writeVersion: 

310 from fontTools import version 

311 

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

313 writer.begintag( 

314 "ttFont", 

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

316 ttLibVersion=version, 

317 ) 

318 else: 

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

320 writer.newline() 

321 

322 # always splitTables if splitGlyphs is enabled 

323 splitTables = splitTables or splitGlyphs 

324 

325 if not splitTables: 

326 writer.newline() 

327 else: 

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

329 

330 for i in range(numTables): 

331 tag = tables[i] 

332 if splitTables: 

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

334 tableWriter = xmlWriter.XMLWriter( 

335 tablePath, newlinestr=writer.newlinestr 

336 ) 

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

338 tableWriter.newline() 

339 tableWriter.newline() 

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

341 writer.newline() 

342 else: 

343 tableWriter = writer 

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

345 if splitTables: 

346 tableWriter.endtag("ttFont") 

347 tableWriter.newline() 

348 tableWriter.close() 

349 writer.endtag("ttFont") 

350 writer.newline() 

351 

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

353 if quiet is not None: 

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

355 if tag in self: 

356 table = self[tag] 

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

358 else: 

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

360 log.info(report) 

361 if tag not in self: 

362 return 

363 xmlTag = tagToXML(tag) 

364 attrs = dict() 

365 if hasattr(table, "ERROR"): 

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

367 from .tables.DefaultTable import DefaultTable 

368 

369 if table.__class__ == DefaultTable: 

370 attrs["raw"] = True 

371 writer.begintag(xmlTag, **attrs) 

372 writer.newline() 

373 if tag == "glyf": 

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

375 else: 

376 table.toXML(writer, self) 

377 writer.endtag(xmlTag) 

378 writer.newline() 

379 writer.newline() 

380 

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

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

383 a font object. 

384 """ 

385 if quiet is not None: 

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

387 

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

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

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

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

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

393 self.getGlyphOrder() 

394 

395 from fontTools.misc import xmlReader 

396 

397 reader = xmlReader.XMLReader(fileOrPath, self) 

398 reader.read() 

399 

400 def isLoaded(self, tag): 

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

402 decompiled and loaded into memory.""" 

403 return tag in self.tables 

404 

405 def has_key(self, tag): 

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

407 

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

409 presence of the table.""" 

410 if self.isLoaded(tag): 

411 return True 

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

413 return True 

414 elif tag == "GlyphOrder": 

415 return True 

416 else: 

417 return False 

418 

419 __contains__ = has_key 

420 

421 def keys(self): 

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

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

424 if self.reader: 

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

426 if key not in keys: 

427 keys.append(key) 

428 

429 if "GlyphOrder" in keys: 

430 keys.remove("GlyphOrder") 

431 keys = sortedTagList(keys) 

432 return ["GlyphOrder"] + keys 

433 

434 def ensureDecompiled(self, recurse=None): 

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

436 for tag in self.keys(): 

437 table = self[tag] 

438 if recurse is None: 

439 recurse = self.lazy is not False 

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

441 table.ensureDecompiled(recurse=recurse) 

442 self.lazy = False 

443 

444 def __len__(self): 

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

446 

447 def __getitem__(self, tag): 

448 tag = Tag(tag) 

449 table = self.tables.get(tag) 

450 if table is None: 

451 if tag == "GlyphOrder": 

452 table = GlyphOrder(tag) 

453 self.tables[tag] = table 

454 elif self.reader is not None: 

455 table = self._readTable(tag) 

456 else: 

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

458 return table 

459 

460 def _readTable(self, tag): 

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

462 data = self.reader[tag] 

463 if self._tableCache is not None: 

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

465 if table is not None: 

466 return table 

467 tableClass = getTableClass(tag) 

468 table = tableClass(tag) 

469 self.tables[tag] = table 

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

471 try: 

472 table.decompile(data, self) 

473 except Exception: 

474 if not self.ignoreDecompileErrors: 

475 raise 

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

477 log.exception( 

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

479 ) 

480 from .tables.DefaultTable import DefaultTable 

481 

482 file = StringIO() 

483 traceback.print_exc(file=file) 

484 table = DefaultTable(tag) 

485 table.ERROR = file.getvalue() 

486 self.tables[tag] = table 

487 table.decompile(data, self) 

488 if self._tableCache is not None: 

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

490 return table 

491 

492 def __setitem__(self, tag, table): 

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

494 

495 def __delitem__(self, tag): 

496 if tag not in self: 

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

498 if tag in self.tables: 

499 del self.tables[tag] 

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

501 del self.reader[tag] 

502 

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

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

505 try: 

506 return self[tag] 

507 except KeyError: 

508 return default 

509 

510 def setGlyphOrder(self, glyphOrder): 

511 """Set the glyph order 

512 

513 Args: 

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

515 """ 

516 self.glyphOrder = glyphOrder 

517 if hasattr(self, "_reverseGlyphOrderDict"): 

518 del self._reverseGlyphOrderDict 

519 if self.isLoaded("glyf"): 

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

521 

522 def getGlyphOrder(self): 

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

524 try: 

525 return self.glyphOrder 

526 except AttributeError: 

527 pass 

528 if "CFF " in self: 

529 cff = self["CFF "] 

530 self.glyphOrder = cff.getGlyphOrder() 

531 elif "post" in self: 

532 # TrueType font 

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

534 if glyphOrder is None: 

535 # 

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

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

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

539 # 

540 self._getGlyphNamesFromCmap() 

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

542 # 

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

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

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

546 # 

547 log.warning( 

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

549 ) 

550 self._getGlyphNamesFromCmap() 

551 else: 

552 self.glyphOrder = glyphOrder 

553 else: 

554 self._getGlyphNamesFromCmap() 

555 return self.glyphOrder 

556 

557 def _getGlyphNamesFromCmap(self): 

558 # 

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

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

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

562 # or none at all). 

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

564 # So here's what we do: 

565 # - make up glyph names based on glyphID 

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

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

568 # - unload the temporary cmap table 

569 # 

570 if self.isLoaded("cmap"): 

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

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

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

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

575 # restore it later. 

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

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

578 cmapLoading = self.tables["cmap"] 

579 del self.tables["cmap"] 

580 else: 

581 cmapLoading = None 

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

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

584 # cmap. 

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

586 glyphOrder = [None] * numGlyphs 

587 glyphOrder[0] = ".notdef" 

588 for i in range(1, numGlyphs): 

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

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

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

592 self.glyphOrder = glyphOrder 

593 

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

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

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

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

598 if "cmap" in self: 

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

600 else: 

601 reversecmap = {} 

602 useCount = {} 

603 for i in range(numGlyphs): 

604 tempName = glyphOrder[i] 

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(min(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 in range(len(glyphOrder)): 

862 glyphName = glyphOrder[i] 

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

864 writer.newline() 

865 

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

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

868 self.glyphOrder = [] 

869 if name == "GlyphID": 

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

871 ttFont.setGlyphOrder(self.glyphOrder) 

872 

873 

874def getTableModule(tag): 

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

876 Return None when no module is found. 

877 """ 

878 from . import tables 

879 

880 pyTag = tagToIdentifier(tag) 

881 try: 

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

883 except ImportError as err: 

884 # If pyTag is found in the ImportError message, 

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

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

887 # suppress the error. 

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

889 return None 

890 else: 

891 raise err 

892 else: 

893 return getattr(tables, pyTag) 

894 

895 

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

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

898# See registerCustomTableClass() and getCustomTableClass() 

899_customTableRegistry = {} 

900 

901 

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

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

904 

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

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

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

908 

909 The registered table class should be a subclass of 

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

911 """ 

912 if className is None: 

913 className = "table_" + tagToIdentifier(tag) 

914 _customTableRegistry[tag] = (moduleName, className) 

915 

916 

917def unregisterCustomTableClass(tag): 

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

919 del _customTableRegistry[tag] 

920 

921 

922def getCustomTableClass(tag): 

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

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

925 """ 

926 if tag not in _customTableRegistry: 

927 return None 

928 import importlib 

929 

930 moduleName, className = _customTableRegistry[tag] 

931 module = importlib.import_module(moduleName) 

932 return getattr(module, className) 

933 

934 

935def getTableClass(tag): 

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

937 tableClass = getCustomTableClass(tag) 

938 if tableClass is not None: 

939 return tableClass 

940 module = getTableModule(tag) 

941 if module is None: 

942 from .tables.DefaultTable import DefaultTable 

943 

944 return DefaultTable 

945 pyTag = tagToIdentifier(tag) 

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

947 return tableClass 

948 

949 

950def getClassTag(klass): 

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

952 name = klass.__name__ 

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

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

955 return identifierToTag(name) 

956 

957 

958def newTable(tag): 

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

960 tableClass = getTableClass(tag) 

961 return tableClass(tag) 

962 

963 

964def _escapechar(c): 

965 """Helper function for tagToIdentifier()""" 

966 import re 

967 

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

969 return "_" + c 

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

971 return c + "_" 

972 else: 

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

974 

975 

976def tagToIdentifier(tag): 

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

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

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

980 Lowercase letters get an underscore before the letter, uppercase 

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

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

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

984 extra underscore is prepended. Examples:: 

985 

986 >>> tagToIdentifier('glyf') 

987 '_g_l_y_f' 

988 >>> tagToIdentifier('cvt ') 

989 '_c_v_t' 

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

991 'O_S_2f_2' 

992 """ 

993 import re 

994 

995 tag = Tag(tag) 

996 if tag == "GlyphOrder": 

997 return tag 

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

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

1000 tag = tag[:-1] 

1001 ident = "" 

1002 for c in tag: 

1003 ident = ident + _escapechar(c) 

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

1005 ident = "_" + ident 

1006 return ident 

1007 

1008 

1009def identifierToTag(ident): 

1010 """the opposite of tagToIdentifier()""" 

1011 if ident == "GlyphOrder": 

1012 return ident 

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

1014 ident = ident[1:] 

1015 assert not (len(ident) % 2) 

1016 tag = "" 

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

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

1019 tag = tag + ident[i + 1] 

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

1021 tag = tag + ident[i] 

1022 else: 

1023 # assume hex 

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

1025 # append trailing spaces 

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

1027 return Tag(tag) 

1028 

1029 

1030def tagToXML(tag): 

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

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

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

1034 """ 

1035 import re 

1036 

1037 tag = Tag(tag) 

1038 if tag == "OS/2": 

1039 return "OS_2" 

1040 elif tag == "GlyphOrder": 

1041 return tag 

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

1043 return tag.strip() 

1044 else: 

1045 return tagToIdentifier(tag) 

1046 

1047 

1048def xmlToTag(tag): 

1049 """The opposite of tagToXML()""" 

1050 if tag == "OS_2": 

1051 return Tag("OS/2") 

1052 if len(tag) == 8: 

1053 return identifierToTag(tag) 

1054 else: 

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

1056 

1057 

1058# Table order as recommended in the OpenType specification 1.4 

1059TTFTableOrder = [ 

1060 "head", 

1061 "hhea", 

1062 "maxp", 

1063 "OS/2", 

1064 "hmtx", 

1065 "LTSH", 

1066 "VDMX", 

1067 "hdmx", 

1068 "cmap", 

1069 "fpgm", 

1070 "prep", 

1071 "cvt ", 

1072 "loca", 

1073 "glyf", 

1074 "kern", 

1075 "name", 

1076 "post", 

1077 "gasp", 

1078 "PCLT", 

1079] 

1080 

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

1082 

1083 

1084def sortedTagList(tagList, tableOrder=None): 

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

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

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

1088 """ 

1089 tagList = sorted(tagList) 

1090 if tableOrder is None: 

1091 if "DSIG" in tagList: 

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

1093 tagList.remove("DSIG") 

1094 tagList.append("DSIG") 

1095 if "CFF " in tagList: 

1096 tableOrder = OTFTableOrder 

1097 else: 

1098 tableOrder = TTFTableOrder 

1099 orderedTables = [] 

1100 for tag in tableOrder: 

1101 if tag in tagList: 

1102 orderedTables.append(tag) 

1103 tagList.remove(tag) 

1104 orderedTables.extend(tagList) 

1105 return orderedTables 

1106 

1107 

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

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

1110 OpenType specification 1.4. 

1111 """ 

1112 inFile.seek(0) 

1113 outFile.seek(0) 

1114 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 

1115 writer = SFNTWriter( 

1116 outFile, 

1117 len(reader.tables), 

1118 reader.sfntVersion, 

1119 reader.flavor, 

1120 reader.flavorData, 

1121 ) 

1122 tables = list(reader.keys()) 

1123 for tag in sortedTagList(tables, tableOrder): 

1124 writer[tag] = reader[tag] 

1125 writer.close() 

1126 

1127 

1128def maxPowerOfTwo(x): 

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

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

1131 """ 

1132 exponent = 0 

1133 while x: 

1134 x = x >> 1 

1135 exponent = exponent + 1 

1136 return max(exponent - 1, 0) 

1137 

1138 

1139def getSearchRange(n, itemSize=16): 

1140 """Calculate searchRange, entrySelector, rangeShift.""" 

1141 # itemSize defaults to 16, for backward compatibility 

1142 # with upstream fonttools. 

1143 exponent = maxPowerOfTwo(n) 

1144 searchRange = (2**exponent) * itemSize 

1145 entrySelector = exponent 

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

1147 return searchRange, entrySelector, rangeShift