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

582 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:33 +0000

1from fontTools.config import Config 

2from fontTools.misc import xmlWriter 

3from fontTools.misc.configTools import AbstractConfig 

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

5from fontTools.misc.loggingTools import deprecateArgument 

6from fontTools.ttLib import TTLibError 

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

8from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter 

9from io import BytesIO, StringIO, UnsupportedOperation 

10import os 

11import logging 

12import traceback 

13 

14log = logging.getLogger(__name__) 

15 

16 

17class TTFont(object): 

18 

19 """Represents a TrueType font. 

20 

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

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

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

24 

25 Example usage:: 

26 

27 >> from fontTools import ttLib 

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

29 >> tt['maxp'].numGlyphs 

30 242 

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

32 'B&H\000' 

33 >> tt['head'].unitsPerEm 

34 2048 

35 

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

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

38 

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

40 >> os2.version = 4 

41 >> # set other attributes 

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

43 

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

45 :ref:`ttx` binary):: 

46 

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

48 Dumping 'LTSH' table... 

49 Dumping 'OS/2' table... 

50 [...] 

51 

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

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

54 >> tt2['maxp'].numGlyphs 

55 242 

56 

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

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

59 

60 with TTFont(filename) as f: 

61 # Do stuff 

62 

63 Args: 

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

65 or a readable file object. 

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

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

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

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

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

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

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

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

74 file. 

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

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

77 raise an exception if any wrong checksums are found. 

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

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

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

81 time. 

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

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

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

85 the ``head`` table on save. 

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

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

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

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

90 """ 

91 

92 def __init__( 

93 self, 

94 file=None, 

95 res_name_or_index=None, 

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

97 flavor=None, 

98 checkChecksums=0, 

99 verbose=None, 

100 recalcBBoxes=True, 

101 allowVID=NotImplemented, 

102 ignoreDecompileErrors=False, 

103 recalcTimestamp=True, 

104 fontNumber=-1, 

105 lazy=None, 

106 quiet=None, 

107 _tableCache=None, 

108 cfg={}, 

109 ): 

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

111 val = locals().get(name) 

112 if val is not None: 

113 deprecateArgument(name, "configure logging instead") 

114 setattr(self, name, val) 

115 

116 self.lazy = lazy 

117 self.recalcBBoxes = recalcBBoxes 

118 self.recalcTimestamp = recalcTimestamp 

119 self.tables = {} 

120 self.reader = None 

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

122 self.ignoreDecompileErrors = ignoreDecompileErrors 

123 

124 if not file: 

125 self.sfntVersion = sfntVersion 

126 self.flavor = flavor 

127 self.flavorData = None 

128 return 

129 seekable = True 

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

131 closeStream = True 

132 # assume file is a string 

133 if res_name_or_index is not None: 

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

135 from . import macUtils 

136 

137 if res_name_or_index == 0: 

138 if macUtils.getSFNTResIndices(file): 

139 # get the first available sfnt font. 

140 file = macUtils.SFNTResourceReader(file, 1) 

141 else: 

142 file = open(file, "rb") 

143 else: 

144 file = macUtils.SFNTResourceReader(file, res_name_or_index) 

145 else: 

146 file = open(file, "rb") 

147 else: 

148 # assume "file" is a readable file object 

149 closeStream = False 

150 # SFNTReader wants the input file to be seekable. 

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

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

153 if hasattr(file, "seekable"): 

154 seekable = file.seekable() 

155 elif hasattr(file, "seek"): 

156 try: 

157 file.seek(0) 

158 except UnsupportedOperation: 

159 seekable = False 

160 

161 if not self.lazy: 

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

163 if seekable: 

164 file.seek(0) 

165 tmp = BytesIO(file.read()) 

166 if hasattr(file, "name"): 

167 # save reference to input file name 

168 tmp.name = file.name 

169 if closeStream: 

170 file.close() 

171 file = tmp 

172 elif not seekable: 

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

174 self._tableCache = _tableCache 

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

176 self.sfntVersion = self.reader.sfntVersion 

177 self.flavor = self.reader.flavor 

178 self.flavorData = self.reader.flavorData 

179 

180 def __enter__(self): 

181 return self 

182 

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

184 self.close() 

185 

186 def close(self): 

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

188 if self.reader is not None: 

189 self.reader.close() 

190 

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

192 """Save the font to disk. 

193 

194 Args: 

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

196 file object. 

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

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

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

200 dependency (fastest). 

201 """ 

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

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

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

205 createStream = True 

206 else: 

207 # assume "file" is a writable file object 

208 createStream = False 

209 

210 tmp = BytesIO() 

211 

212 writer_reordersTables = self._save(tmp) 

213 

214 if not ( 

215 reorderTables is None 

216 or writer_reordersTables 

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

218 ): 

219 if reorderTables is False: 

220 # sort tables using the original font's order 

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

222 else: 

223 # use the recommended order from the OpenType specification 

224 tableOrder = None 

225 tmp.flush() 

226 tmp2 = BytesIO() 

227 reorderFontTables(tmp, tmp2, tableOrder) 

228 tmp.close() 

229 tmp = tmp2 

230 

231 if createStream: 

232 # "file" is a path 

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

234 file.write(tmp.getvalue()) 

235 else: 

236 file.write(tmp.getvalue()) 

237 

238 tmp.close() 

239 

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

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

242 

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

244 self[ 

245 "head" 

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

247 

248 tags = list(self.keys()) 

249 if "GlyphOrder" in tags: 

250 tags.remove("GlyphOrder") 

251 numTables = len(tags) 

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

253 writer = SFNTWriter( 

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

255 ) 

256 

257 done = [] 

258 for tag in tags: 

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

260 

261 writer.close() 

262 

263 return writer.reordersTables() 

264 

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

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

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

268 argument should be a path to a directory. 

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

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

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

272 """ 

273 

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

275 self._saveXML(writer, **kwargs) 

276 writer.close() 

277 

278 def _saveXML( 

279 self, 

280 writer, 

281 writeVersion=True, 

282 quiet=None, 

283 tables=None, 

284 skipTables=None, 

285 splitTables=False, 

286 splitGlyphs=False, 

287 disassembleInstructions=True, 

288 bitmapGlyphDataFormat="raw", 

289 ): 

290 

291 if quiet is not None: 

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

293 

294 self.disassembleInstructions = disassembleInstructions 

295 self.bitmapGlyphDataFormat = bitmapGlyphDataFormat 

296 if not tables: 

297 tables = list(self.keys()) 

298 if "GlyphOrder" not in tables: 

299 tables = ["GlyphOrder"] + tables 

300 if skipTables: 

301 for tag in skipTables: 

302 if tag in tables: 

303 tables.remove(tag) 

304 numTables = len(tables) 

305 

306 if writeVersion: 

307 from fontTools import version 

308 

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

310 writer.begintag( 

311 "ttFont", 

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

313 ttLibVersion=version, 

314 ) 

315 else: 

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

317 writer.newline() 

318 

319 # always splitTables if splitGlyphs is enabled 

320 splitTables = splitTables or splitGlyphs 

321 

322 if not splitTables: 

323 writer.newline() 

324 else: 

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

326 

327 for i in range(numTables): 

328 tag = tables[i] 

329 if splitTables: 

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

331 tableWriter = xmlWriter.XMLWriter( 

332 tablePath, newlinestr=writer.newlinestr 

333 ) 

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

335 tableWriter.newline() 

336 tableWriter.newline() 

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

338 writer.newline() 

339 else: 

340 tableWriter = writer 

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

342 if splitTables: 

343 tableWriter.endtag("ttFont") 

344 tableWriter.newline() 

345 tableWriter.close() 

346 writer.endtag("ttFont") 

347 writer.newline() 

348 

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

350 if quiet is not None: 

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

352 if tag in self: 

353 table = self[tag] 

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

355 else: 

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

357 log.info(report) 

358 if tag not in self: 

359 return 

360 xmlTag = tagToXML(tag) 

361 attrs = dict() 

362 if hasattr(table, "ERROR"): 

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

364 from .tables.DefaultTable import DefaultTable 

365 

366 if table.__class__ == DefaultTable: 

367 attrs["raw"] = True 

368 writer.begintag(xmlTag, **attrs) 

369 writer.newline() 

370 if tag == "glyf": 

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

372 else: 

373 table.toXML(writer, self) 

374 writer.endtag(xmlTag) 

375 writer.newline() 

376 writer.newline() 

377 

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

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

380 a font object. 

381 """ 

382 if quiet is not None: 

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

384 

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

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

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

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

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

390 self.getGlyphOrder() 

391 

392 from fontTools.misc import xmlReader 

393 

394 reader = xmlReader.XMLReader(fileOrPath, self) 

395 reader.read() 

396 

397 def isLoaded(self, tag): 

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

399 decompiled and loaded into memory.""" 

400 return tag in self.tables 

401 

402 def has_key(self, tag): 

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

404 

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

406 presence of the table.""" 

407 if self.isLoaded(tag): 

408 return True 

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

410 return True 

411 elif tag == "GlyphOrder": 

412 return True 

413 else: 

414 return False 

415 

416 __contains__ = has_key 

417 

418 def keys(self): 

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

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

421 if self.reader: 

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

423 if key not in keys: 

424 keys.append(key) 

425 

426 if "GlyphOrder" in keys: 

427 keys.remove("GlyphOrder") 

428 keys = sortedTagList(keys) 

429 return ["GlyphOrder"] + keys 

430 

431 def ensureDecompiled(self, recurse=None): 

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

433 for tag in self.keys(): 

434 table = self[tag] 

435 if recurse is None: 

436 recurse = self.lazy is not False 

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

438 table.ensureDecompiled(recurse=recurse) 

439 self.lazy = False 

440 

441 def __len__(self): 

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

443 

444 def __getitem__(self, tag): 

445 tag = Tag(tag) 

446 table = self.tables.get(tag) 

447 if table is None: 

448 if tag == "GlyphOrder": 

449 table = GlyphOrder(tag) 

450 self.tables[tag] = table 

451 elif self.reader is not None: 

452 table = self._readTable(tag) 

453 else: 

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

455 return table 

456 

457 def _readTable(self, tag): 

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

459 data = self.reader[tag] 

460 if self._tableCache is not None: 

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

462 if table is not None: 

463 return table 

464 tableClass = getTableClass(tag) 

465 table = tableClass(tag) 

466 self.tables[tag] = table 

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

468 try: 

469 table.decompile(data, self) 

470 except Exception: 

471 if not self.ignoreDecompileErrors: 

472 raise 

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

474 log.exception( 

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

476 ) 

477 from .tables.DefaultTable import DefaultTable 

478 

479 file = StringIO() 

480 traceback.print_exc(file=file) 

481 table = DefaultTable(tag) 

482 table.ERROR = file.getvalue() 

483 self.tables[tag] = table 

484 table.decompile(data, self) 

485 if self._tableCache is not None: 

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

487 return table 

488 

489 def __setitem__(self, tag, table): 

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

491 

492 def __delitem__(self, tag): 

493 if tag not in self: 

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

495 if tag in self.tables: 

496 del self.tables[tag] 

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

498 del self.reader[tag] 

499 

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

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

502 try: 

503 return self[tag] 

504 except KeyError: 

505 return default 

506 

507 def setGlyphOrder(self, glyphOrder): 

508 """Set the glyph order 

509 

510 Args: 

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

512 """ 

513 self.glyphOrder = glyphOrder 

514 if hasattr(self, "_reverseGlyphOrderDict"): 

515 del self._reverseGlyphOrderDict 

516 if self.isLoaded("glyf"): 

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

518 

519 def getGlyphOrder(self): 

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

521 try: 

522 return self.glyphOrder 

523 except AttributeError: 

524 pass 

525 if "CFF " in self: 

526 cff = self["CFF "] 

527 self.glyphOrder = cff.getGlyphOrder() 

528 elif "post" in self: 

529 # TrueType font 

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

531 if glyphOrder is None: 

532 # 

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

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

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

536 # 

537 self._getGlyphNamesFromCmap() 

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

539 # 

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

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

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

543 # 

544 log.warning( 

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

546 ) 

547 self._getGlyphNamesFromCmap() 

548 else: 

549 self.glyphOrder = glyphOrder 

550 else: 

551 self._getGlyphNamesFromCmap() 

552 return self.glyphOrder 

553 

554 def _getGlyphNamesFromCmap(self): 

555 # 

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

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

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

559 # or none at all). 

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

561 # So here's what we do: 

562 # - make up glyph names based on glyphID 

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

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

565 # - unload the temporary cmap table 

566 # 

567 if self.isLoaded("cmap"): 

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

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

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

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

572 # restore it later. 

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

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

575 cmapLoading = self.tables["cmap"] 

576 del self.tables["cmap"] 

577 else: 

578 cmapLoading = None 

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

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

581 # cmap. 

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

583 glyphOrder = [None] * numGlyphs 

584 glyphOrder[0] = ".notdef" 

585 for i in range(1, numGlyphs): 

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

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

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

589 self.glyphOrder = glyphOrder 

590 

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

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

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

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

595 if "cmap" in self: 

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

597 else: 

598 reversecmap = {} 

599 useCount = {} 

600 for i in range(numGlyphs): 

601 tempName = glyphOrder[i] 

602 if tempName in reversecmap: 

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

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

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

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

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

608 if numUses > 1: 

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

610 glyphOrder[i] = glyphName 

611 

612 if "cmap" in self: 

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

614 # be parsed again with the right names. 

615 del self.tables["cmap"] 

616 self.glyphOrder = glyphOrder 

617 if cmapLoading: 

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

619 # using the proper names. 

620 self.tables["cmap"] = cmapLoading 

621 

622 @staticmethod 

623 def _makeGlyphName(codepoint): 

624 from fontTools import agl # Adobe Glyph List 

625 

626 if codepoint in agl.UV2AGL: 

627 return agl.UV2AGL[codepoint] 

628 elif codepoint <= 0xFFFF: 

629 return "uni%04X" % codepoint 

630 else: 

631 return "u%X" % codepoint 

632 

633 def getGlyphNames(self): 

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

635 glyphNames = sorted(self.getGlyphOrder()) 

636 return glyphNames 

637 

638 def getGlyphNames2(self): 

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

640 but not case sensitive. 

641 """ 

642 from fontTools.misc import textTools 

643 

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

645 

646 def getGlyphName(self, glyphID): 

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

648 

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

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

651 """ 

652 try: 

653 return self.getGlyphOrder()[glyphID] 

654 except IndexError: 

655 return "glyph%.5d" % glyphID 

656 

657 def getGlyphNameMany(self, lst): 

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

659 glyphOrder = self.getGlyphOrder() 

660 cnt = len(glyphOrder) 

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

662 

663 def getGlyphID(self, glyphName): 

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

665 try: 

666 return self.getReverseGlyphMap()[glyphName] 

667 except KeyError: 

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

669 try: 

670 return int(glyphName[5:]) 

671 except (NameError, ValueError): 

672 raise KeyError(glyphName) 

673 raise 

674 

675 def getGlyphIDMany(self, lst): 

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

677 d = self.getReverseGlyphMap() 

678 try: 

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

680 except KeyError: 

681 getGlyphID = self.getGlyphID 

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

683 

684 def getReverseGlyphMap(self, rebuild=False): 

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

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

687 self._buildReverseGlyphOrderDict() 

688 return self._reverseGlyphOrderDict 

689 

690 def _buildReverseGlyphOrderDict(self): 

691 self._reverseGlyphOrderDict = d = {} 

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

693 d[glyphName] = glyphID 

694 return d 

695 

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

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

698 inter-table dependencies. 

699 """ 

700 if tag in done: 

701 return 

702 tableClass = getTableClass(tag) 

703 for masterTable in tableClass.dependencies: 

704 if masterTable not in done: 

705 if masterTable in self: 

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

707 else: 

708 done.append(masterTable) 

709 done.append(tag) 

710 tabledata = self.getTableData(tag) 

711 if tableCache is not None: 

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

713 if entry is not None: 

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

715 writer.setEntry(tag, entry) 

716 return 

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

718 writer[tag] = tabledata 

719 if tableCache is not None: 

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

721 

722 def getTableData(self, tag): 

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

724 

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

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

727 read from the font file and returned. 

728 """ 

729 tag = Tag(tag) 

730 if self.isLoaded(tag): 

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

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

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

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

735 return self.reader[tag] 

736 else: 

737 raise KeyError(tag) 

738 

739 def getGlyphSet(self, preferCFF=True, location=None, normalized=False): 

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

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

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

743 have an attribute named 'width'. 

744 

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

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

747 ``glyf`` table. 

748 

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

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

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

752 taken. 

753 

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

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

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

757 location. 

758 

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

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

761 font's defined axes space. 

762 """ 

763 if location and "fvar" not in self: 

764 location = None 

765 if location and not normalized: 

766 location = self.normalizeLocation(location) 

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

768 return _TTGlyphSetCFF(self, location) 

769 elif "glyf" in self: 

770 return _TTGlyphSetGlyf(self, location) 

771 else: 

772 raise TTLibError("Font contains no outlines") 

773 

774 def normalizeLocation(self, location): 

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

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

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

778 

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

780 variation tags to their float values. 

781 

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

783 """ 

784 from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap 

785 

786 if "fvar" not in self: 

787 raise TTLibError("Not a variable font") 

788 

789 axes = { 

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

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

792 } 

793 location = normalizeLocation(location, axes) 

794 if "avar" in self: 

795 avar = self["avar"] 

796 avarSegments = avar.segments 

797 mappedLocation = {} 

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

799 avarMapping = avarSegments.get(axisTag, None) 

800 if avarMapping is not None: 

801 value = piecewiseLinearMap(value, avarMapping) 

802 mappedLocation[axisTag] = value 

803 location = mappedLocation 

804 return location 

805 

806 def getBestCmap( 

807 self, 

808 cmapPreferences=( 

809 (3, 10), 

810 (0, 6), 

811 (0, 4), 

812 (3, 1), 

813 (0, 3), 

814 (0, 2), 

815 (0, 1), 

816 (0, 0), 

817 ), 

818 ): 

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

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

821 

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

823 pairs in order:: 

824 

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

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

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

828 (3, 1), # Windows Unicode BMP 

829 (0, 3), # Unicode 2.0 BMP 

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

831 (0, 1), # Unicode 1.1 

832 (0, 0) # Unicode 1.0 

833 

834 This particular order matches what HarfBuzz uses to choose what 

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

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

837 Unicode-platform as the former has wider support. 

838 

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

840 """ 

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

842 

843 

844class GlyphOrder(object): 

845 

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

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

848 """ 

849 

850 def __init__(self, tag=None): 

851 pass 

852 

853 def toXML(self, writer, ttFont): 

854 glyphOrder = ttFont.getGlyphOrder() 

855 writer.comment( 

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

857 ) 

858 writer.newline() 

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

860 glyphName = glyphOrder[i] 

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

862 writer.newline() 

863 

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

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

866 self.glyphOrder = [] 

867 if name == "GlyphID": 

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

869 ttFont.setGlyphOrder(self.glyphOrder) 

870 

871 

872def getTableModule(tag): 

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

874 Return None when no module is found. 

875 """ 

876 from . import tables 

877 

878 pyTag = tagToIdentifier(tag) 

879 try: 

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

881 except ImportError as err: 

882 # If pyTag is found in the ImportError message, 

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

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

885 # suppress the error. 

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

887 return None 

888 else: 

889 raise err 

890 else: 

891 return getattr(tables, pyTag) 

892 

893 

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

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

896# See registerCustomTableClass() and getCustomTableClass() 

897_customTableRegistry = {} 

898 

899 

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

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

902 

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

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

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

906 

907 The registered table class should be a subclass of 

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

909 """ 

910 if className is None: 

911 className = "table_" + tagToIdentifier(tag) 

912 _customTableRegistry[tag] = (moduleName, className) 

913 

914 

915def unregisterCustomTableClass(tag): 

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

917 del _customTableRegistry[tag] 

918 

919 

920def getCustomTableClass(tag): 

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

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

923 """ 

924 if tag not in _customTableRegistry: 

925 return None 

926 import importlib 

927 

928 moduleName, className = _customTableRegistry[tag] 

929 module = importlib.import_module(moduleName) 

930 return getattr(module, className) 

931 

932 

933def getTableClass(tag): 

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

935 tableClass = getCustomTableClass(tag) 

936 if tableClass is not None: 

937 return tableClass 

938 module = getTableModule(tag) 

939 if module is None: 

940 from .tables.DefaultTable import DefaultTable 

941 

942 return DefaultTable 

943 pyTag = tagToIdentifier(tag) 

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

945 return tableClass 

946 

947 

948def getClassTag(klass): 

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

950 name = klass.__name__ 

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

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

953 return identifierToTag(name) 

954 

955 

956def newTable(tag): 

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

958 tableClass = getTableClass(tag) 

959 return tableClass(tag) 

960 

961 

962def _escapechar(c): 

963 """Helper function for tagToIdentifier()""" 

964 import re 

965 

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

967 return "_" + c 

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

969 return c + "_" 

970 else: 

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

972 

973 

974def tagToIdentifier(tag): 

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

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

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

978 Lowercase letters get an underscore before the letter, uppercase 

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

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

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

982 extra underscore is prepended. Examples:: 

983 

984 >>> tagToIdentifier('glyf') 

985 '_g_l_y_f' 

986 >>> tagToIdentifier('cvt ') 

987 '_c_v_t' 

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

989 'O_S_2f_2' 

990 """ 

991 import re 

992 

993 tag = Tag(tag) 

994 if tag == "GlyphOrder": 

995 return tag 

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

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

998 tag = tag[:-1] 

999 ident = "" 

1000 for c in tag: 

1001 ident = ident + _escapechar(c) 

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

1003 ident = "_" + ident 

1004 return ident 

1005 

1006 

1007def identifierToTag(ident): 

1008 """the opposite of tagToIdentifier()""" 

1009 if ident == "GlyphOrder": 

1010 return ident 

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

1012 ident = ident[1:] 

1013 assert not (len(ident) % 2) 

1014 tag = "" 

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

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

1017 tag = tag + ident[i + 1] 

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

1019 tag = tag + ident[i] 

1020 else: 

1021 # assume hex 

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

1023 # append trailing spaces 

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

1025 return Tag(tag) 

1026 

1027 

1028def tagToXML(tag): 

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

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

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

1032 """ 

1033 import re 

1034 

1035 tag = Tag(tag) 

1036 if tag == "OS/2": 

1037 return "OS_2" 

1038 elif tag == "GlyphOrder": 

1039 return tag 

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

1041 return tag.strip() 

1042 else: 

1043 return tagToIdentifier(tag) 

1044 

1045 

1046def xmlToTag(tag): 

1047 """The opposite of tagToXML()""" 

1048 if tag == "OS_2": 

1049 return Tag("OS/2") 

1050 if len(tag) == 8: 

1051 return identifierToTag(tag) 

1052 else: 

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

1054 

1055 

1056# Table order as recommended in the OpenType specification 1.4 

1057TTFTableOrder = [ 

1058 "head", 

1059 "hhea", 

1060 "maxp", 

1061 "OS/2", 

1062 "hmtx", 

1063 "LTSH", 

1064 "VDMX", 

1065 "hdmx", 

1066 "cmap", 

1067 "fpgm", 

1068 "prep", 

1069 "cvt ", 

1070 "loca", 

1071 "glyf", 

1072 "kern", 

1073 "name", 

1074 "post", 

1075 "gasp", 

1076 "PCLT", 

1077] 

1078 

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

1080 

1081 

1082def sortedTagList(tagList, tableOrder=None): 

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

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

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

1086 """ 

1087 tagList = sorted(tagList) 

1088 if tableOrder is None: 

1089 if "DSIG" in tagList: 

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

1091 tagList.remove("DSIG") 

1092 tagList.append("DSIG") 

1093 if "CFF " in tagList: 

1094 tableOrder = OTFTableOrder 

1095 else: 

1096 tableOrder = TTFTableOrder 

1097 orderedTables = [] 

1098 for tag in tableOrder: 

1099 if tag in tagList: 

1100 orderedTables.append(tag) 

1101 tagList.remove(tag) 

1102 orderedTables.extend(tagList) 

1103 return orderedTables 

1104 

1105 

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

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

1108 OpenType specification 1.4. 

1109 """ 

1110 inFile.seek(0) 

1111 outFile.seek(0) 

1112 reader = SFNTReader(inFile, checkChecksums=checkChecksums) 

1113 writer = SFNTWriter( 

1114 outFile, 

1115 len(reader.tables), 

1116 reader.sfntVersion, 

1117 reader.flavor, 

1118 reader.flavorData, 

1119 ) 

1120 tables = list(reader.keys()) 

1121 for tag in sortedTagList(tables, tableOrder): 

1122 writer[tag] = reader[tag] 

1123 writer.close() 

1124 

1125 

1126def maxPowerOfTwo(x): 

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

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

1129 """ 

1130 exponent = 0 

1131 while x: 

1132 x = x >> 1 

1133 exponent = exponent + 1 

1134 return max(exponent - 1, 0) 

1135 

1136 

1137def getSearchRange(n, itemSize=16): 

1138 """Calculate searchRange, entrySelector, rangeShift.""" 

1139 # itemSize defaults to 16, for backward compatibility 

1140 # with upstream fonttools. 

1141 exponent = maxPowerOfTwo(n) 

1142 searchRange = (2**exponent) * itemSize 

1143 entrySelector = exponent 

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

1145 return searchRange, entrySelector, rangeShift