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

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

381 statements  

1"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format. 

2 

3Defines two public classes: 

4 

5- SFNTReader 

6- SFNTWriter 

7 

8(Normally you don't have to use these classes explicitly; they are 

9used automatically by ttLib.TTFont.) 

10 

11The reading and writing of sfnt files is separated in two distinct 

12classes, since whenever the number of tables changes or whenever 

13a table's length changes you need to rewrite the whole file anyway. 

14""" 

15 

16from io import BytesIO 

17from types import SimpleNamespace 

18from fontTools.misc.textTools import Tag 

19from fontTools.misc import sstruct 

20from fontTools.ttLib import TTLibError, TTLibFileIsCollectionError 

21import struct 

22from collections import OrderedDict 

23import logging 

24 

25 

26log = logging.getLogger(__name__) 

27 

28 

29class SFNTReader(object): 

30 def __new__(cls, *args, **kwargs): 

31 """Return an instance of the SFNTReader sub-class which is compatible 

32 with the input file type. 

33 """ 

34 if args and cls is SFNTReader: 

35 infile = args[0] 

36 infile.seek(0) 

37 sfntVersion = Tag(infile.read(4)) 

38 infile.seek(0) 

39 if sfntVersion == "wOF2": 

40 # return new WOFF2Reader object 

41 from fontTools.ttLib.woff2 import WOFF2Reader 

42 

43 return object.__new__(WOFF2Reader) 

44 # return default object 

45 return object.__new__(cls) 

46 

47 def __init__(self, file, checkChecksums=0, fontNumber=-1): 

48 self.file = file 

49 self.checkChecksums = checkChecksums 

50 

51 self.flavor = None 

52 self.flavorData = None 

53 self.DirectoryEntry = SFNTDirectoryEntry 

54 self.file.seek(0) 

55 self.sfntVersion = self.file.read(4) 

56 self.file.seek(0) 

57 if self.sfntVersion == b"ttcf": 

58 header = readTTCHeader(self.file) 

59 numFonts = header.numFonts 

60 if not 0 <= fontNumber < numFonts: 

61 raise TTLibFileIsCollectionError( 

62 "specify a font number between 0 and %d (inclusive)" 

63 % (numFonts - 1) 

64 ) 

65 self.numFonts = numFonts 

66 self.file.seek(header.offsetTable[fontNumber]) 

67 data = self.file.read(sfntDirectorySize) 

68 if len(data) != sfntDirectorySize: 

69 raise TTLibError("Not a Font Collection (not enough data)") 

70 sstruct.unpack(sfntDirectoryFormat, data, self) 

71 elif self.sfntVersion == b"wOFF": 

72 self.flavor = "woff" 

73 self.DirectoryEntry = WOFFDirectoryEntry 

74 data = self.file.read(woffDirectorySize) 

75 if len(data) != woffDirectorySize: 

76 raise TTLibError("Not a WOFF font (not enough data)") 

77 sstruct.unpack(woffDirectoryFormat, data, self) 

78 else: 

79 data = self.file.read(sfntDirectorySize) 

80 if len(data) != sfntDirectorySize: 

81 raise TTLibError("Not a TrueType or OpenType font (not enough data)") 

82 sstruct.unpack(sfntDirectoryFormat, data, self) 

83 self.sfntVersion = Tag(self.sfntVersion) 

84 

85 if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"): 

86 raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") 

87 tables = {} 

88 for i in range(self.numTables): 

89 entry = self.DirectoryEntry() 

90 entry.fromFile(self.file) 

91 tag = Tag(entry.tag) 

92 tables[tag] = entry 

93 self.tables = OrderedDict(sorted(tables.items(), key=lambda i: i[1].offset)) 

94 

95 # Load flavor data if any 

96 if self.flavor == "woff": 

97 self.flavorData = WOFFFlavorData(self) 

98 

99 def has_key(self, tag): 

100 return tag in self.tables 

101 

102 __contains__ = has_key 

103 

104 def keys(self): 

105 return self.tables.keys() 

106 

107 def __getitem__(self, tag): 

108 """Fetch the raw table data.""" 

109 entry = self.tables[Tag(tag)] 

110 data = entry.loadData(self.file) 

111 if self.checkChecksums: 

112 if tag == "head": 

113 # Beh: we have to special-case the 'head' table. 

114 checksum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:]) 

115 else: 

116 checksum = calcChecksum(data) 

117 if self.checkChecksums > 1: 

118 # Be obnoxious, and barf when it's wrong 

119 assert checksum == entry.checkSum, "bad checksum for '%s' table" % tag 

120 elif checksum != entry.checkSum: 

121 # Be friendly, and just log a warning. 

122 log.warning("bad checksum for '%s' table", tag) 

123 return data 

124 

125 def __delitem__(self, tag): 

126 del self.tables[Tag(tag)] 

127 

128 def close(self): 

129 self.file.close() 

130 

131 # We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able 

132 # and deepcopy-able. When a TTFont is loaded as lazy=True, SFNTReader holds a 

133 # reference to an external file object which is not pickleable. So in __getstate__ 

134 # we store the file name and current position, and in __setstate__ we reopen the 

135 # same named file after unpickling. 

136 

137 def __getstate__(self): 

138 if isinstance(self.file, BytesIO): 

139 # BytesIO is already pickleable, return the state unmodified 

140 return self.__dict__ 

141 

142 # remove unpickleable file attribute, and only store its name and pos 

143 state = self.__dict__.copy() 

144 del state["file"] 

145 state["_filename"] = self.file.name 

146 state["_filepos"] = self.file.tell() 

147 return state 

148 

149 def __setstate__(self, state): 

150 if "file" not in state: 

151 self.file = open(state.pop("_filename"), "rb") 

152 self.file.seek(state.pop("_filepos")) 

153 self.__dict__.update(state) 

154 

155 

156# default compression level for WOFF 1.0 tables and metadata 

157ZLIB_COMPRESSION_LEVEL = 6 

158 

159# if set to True, use zopfli instead of zlib for compressing WOFF 1.0. 

160# The Python bindings are available at https://pypi.python.org/pypi/zopfli 

161USE_ZOPFLI = False 

162 

163# mapping between zlib's compression levels and zopfli's 'numiterations'. 

164# Use lower values for files over several MB in size or it will be too slow 

165ZOPFLI_LEVELS = { 

166 # 0: 0, # can't do 0 iterations... 

167 1: 1, 

168 2: 3, 

169 3: 5, 

170 4: 8, 

171 5: 10, 

172 6: 15, 

173 7: 25, 

174 8: 50, 

175 9: 100, 

176} 

177 

178 

179def compress(data, level=ZLIB_COMPRESSION_LEVEL): 

180 """Compress 'data' to Zlib format. If 'USE_ZOPFLI' variable is True, 

181 zopfli is used instead of the zlib module. 

182 The compression 'level' must be between 0 and 9. 1 gives best speed, 

183 9 gives best compression (0 gives no compression at all). 

184 The default value is a compromise between speed and compression (6). 

185 """ 

186 if not (0 <= level <= 9): 

187 raise ValueError("Bad compression level: %s" % level) 

188 if not USE_ZOPFLI or level == 0: 

189 from zlib import compress 

190 

191 return compress(data, level) 

192 else: 

193 from zopfli.zlib import compress 

194 

195 return compress(data, numiterations=ZOPFLI_LEVELS[level]) 

196 

197 

198class SFNTWriter(object): 

199 def __new__(cls, *args, **kwargs): 

200 """Return an instance of the SFNTWriter sub-class which is compatible 

201 with the specified 'flavor'. 

202 """ 

203 flavor = None 

204 if kwargs and "flavor" in kwargs: 

205 flavor = kwargs["flavor"] 

206 elif args and len(args) > 3: 

207 flavor = args[3] 

208 if cls is SFNTWriter: 

209 if flavor == "woff2": 

210 # return new WOFF2Writer object 

211 from fontTools.ttLib.woff2 import WOFF2Writer 

212 

213 return object.__new__(WOFF2Writer) 

214 # return default object 

215 return object.__new__(cls) 

216 

217 def __init__( 

218 self, 

219 file, 

220 numTables, 

221 sfntVersion="\000\001\000\000", 

222 flavor=None, 

223 flavorData=None, 

224 ): 

225 self.file = file 

226 self.numTables = numTables 

227 self.sfntVersion = Tag(sfntVersion) 

228 self.flavor = flavor 

229 self.flavorData = flavorData 

230 

231 if self.flavor == "woff": 

232 self.directoryFormat = woffDirectoryFormat 

233 self.directorySize = woffDirectorySize 

234 self.DirectoryEntry = WOFFDirectoryEntry 

235 

236 self.signature = "wOFF" 

237 

238 # to calculate WOFF checksum adjustment, we also need the original SFNT offsets 

239 self.origNextTableOffset = ( 

240 sfntDirectorySize + numTables * sfntDirectoryEntrySize 

241 ) 

242 else: 

243 assert not self.flavor, "Unknown flavor '%s'" % self.flavor 

244 self.directoryFormat = sfntDirectoryFormat 

245 self.directorySize = sfntDirectorySize 

246 self.DirectoryEntry = SFNTDirectoryEntry 

247 

248 from fontTools.ttLib import getSearchRange 

249 

250 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( 

251 numTables, 16 

252 ) 

253 

254 self.directoryOffset = self.file.tell() 

255 self.nextTableOffset = ( 

256 self.directoryOffset 

257 + self.directorySize 

258 + numTables * self.DirectoryEntry.formatSize 

259 ) 

260 # clear out directory area 

261 self.file.seek(self.nextTableOffset) 

262 # make sure we're actually where we want to be. (old cStringIO bug) 

263 self.file.write(b"\0" * (self.nextTableOffset - self.file.tell())) 

264 self.tables = OrderedDict() 

265 

266 def setEntry(self, tag, entry): 

267 if tag in self.tables: 

268 raise TTLibError("cannot rewrite '%s' table" % tag) 

269 

270 self.tables[tag] = entry 

271 

272 def __setitem__(self, tag, data): 

273 """Write raw table data to disk.""" 

274 if tag in self.tables: 

275 raise TTLibError("cannot rewrite '%s' table" % tag) 

276 

277 entry = self.DirectoryEntry() 

278 entry.tag = tag 

279 entry.offset = self.nextTableOffset 

280 if tag == "head": 

281 entry.checkSum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:]) 

282 self.headTable = data 

283 entry.uncompressed = True 

284 else: 

285 entry.checkSum = calcChecksum(data) 

286 entry.saveData(self.file, data) 

287 

288 if self.flavor == "woff": 

289 entry.origOffset = self.origNextTableOffset 

290 self.origNextTableOffset += (entry.origLength + 3) & ~3 

291 

292 self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3) 

293 # Add NUL bytes to pad the table data to a 4-byte boundary. 

294 # Don't depend on f.seek() as we need to add the padding even if no 

295 # subsequent write follows (seek is lazy), ie. after the final table 

296 # in the font. 

297 self.file.write(b"\0" * (self.nextTableOffset - self.file.tell())) 

298 assert self.nextTableOffset == self.file.tell() 

299 

300 self.setEntry(tag, entry) 

301 

302 def __getitem__(self, tag): 

303 return self.tables[tag] 

304 

305 def close(self): 

306 """All tables must have been written to disk. Now write the 

307 directory. 

308 """ 

309 tables = sorted(self.tables.items()) 

310 if len(tables) != self.numTables: 

311 raise TTLibError( 

312 "wrong number of tables; expected %d, found %d" 

313 % (self.numTables, len(tables)) 

314 ) 

315 

316 if self.flavor == "woff": 

317 self.signature = b"wOFF" 

318 self.reserved = 0 

319 

320 self.totalSfntSize = 12 

321 self.totalSfntSize += 16 * len(tables) 

322 for tag, entry in tables: 

323 self.totalSfntSize += (entry.origLength + 3) & ~3 

324 

325 data = self.flavorData if self.flavorData else WOFFFlavorData() 

326 if data.majorVersion is not None and data.minorVersion is not None: 

327 self.majorVersion = data.majorVersion 

328 self.minorVersion = data.minorVersion 

329 else: 

330 if hasattr(self, "headTable"): 

331 self.majorVersion, self.minorVersion = struct.unpack( 

332 ">HH", self.headTable[4:8] 

333 ) 

334 else: 

335 self.majorVersion = self.minorVersion = 0 

336 if data.metaData: 

337 self.metaOrigLength = len(data.metaData) 

338 self.file.seek(0, 2) 

339 self.metaOffset = self.file.tell() 

340 compressedMetaData = compress(data.metaData) 

341 self.metaLength = len(compressedMetaData) 

342 self.file.write(compressedMetaData) 

343 else: 

344 self.metaOffset = self.metaLength = self.metaOrigLength = 0 

345 if data.privData: 

346 self.file.seek(0, 2) 

347 off = self.file.tell() 

348 paddedOff = (off + 3) & ~3 

349 self.file.write(b"\0" * (paddedOff - off)) 

350 self.privOffset = self.file.tell() 

351 self.privLength = len(data.privData) 

352 self.file.write(data.privData) 

353 else: 

354 self.privOffset = self.privLength = 0 

355 

356 self.file.seek(0, 2) 

357 self.length = self.file.tell() 

358 

359 else: 

360 assert not self.flavor, "Unknown flavor '%s'" % self.flavor 

361 pass 

362 

363 directory = sstruct.pack(self.directoryFormat, self) 

364 

365 self.file.seek(self.directoryOffset + self.directorySize) 

366 seenHead = 0 

367 for tag, entry in tables: 

368 if tag == "head": 

369 seenHead = 1 

370 directory = directory + entry.toString() 

371 if seenHead: 

372 self.writeMasterChecksum(directory) 

373 self.file.seek(self.directoryOffset) 

374 self.file.write(directory) 

375 

376 def _calcMasterChecksum(self, directory): 

377 # calculate checkSumAdjustment 

378 tags = list(self.tables.keys()) 

379 checksums = [] 

380 for i in range(len(tags)): 

381 checksums.append(self.tables[tags[i]].checkSum) 

382 

383 if self.DirectoryEntry != SFNTDirectoryEntry: 

384 # Create a SFNT directory for checksum calculation purposes 

385 from fontTools.ttLib import getSearchRange 

386 

387 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( 

388 self.numTables, 16 

389 ) 

390 directory = sstruct.pack(sfntDirectoryFormat, self) 

391 tables = sorted(self.tables.items()) 

392 for tag, entry in tables: 

393 sfntEntry = SFNTDirectoryEntry() 

394 sfntEntry.tag = entry.tag 

395 sfntEntry.checkSum = entry.checkSum 

396 sfntEntry.offset = entry.origOffset 

397 sfntEntry.length = entry.origLength 

398 directory = directory + sfntEntry.toString() 

399 

400 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize 

401 assert directory_end == len(directory) 

402 

403 checksums.append(calcChecksum(directory)) 

404 checksum = sum(checksums) & 0xFFFFFFFF 

405 # BiboAfba! 

406 checksumadjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF 

407 return checksumadjustment 

408 

409 def writeMasterChecksum(self, directory): 

410 checksumadjustment = self._calcMasterChecksum(directory) 

411 # write the checksum to the file 

412 self.file.seek(self.tables["head"].offset + 8) 

413 self.file.write(struct.pack(">L", checksumadjustment)) 

414 

415 def reordersTables(self): 

416 return False 

417 

418 

419# -- sfnt directory helpers and cruft 

420 

421ttcHeaderFormat = """ 

422 > # big endian 

423 TTCTag: 4s # "ttcf" 

424 Version: L # 0x00010000 or 0x00020000 

425 numFonts: L # number of fonts 

426 # OffsetTable[numFonts]: L # array with offsets from beginning of file 

427 # ulDsigTag: L # version 2.0 only 

428 # ulDsigLength: L # version 2.0 only 

429 # ulDsigOffset: L # version 2.0 only 

430""" 

431 

432ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat) 

433 

434sfntDirectoryFormat = """ 

435 > # big endian 

436 sfntVersion: 4s 

437 numTables: H # number of tables 

438 searchRange: H # (max2 <= numTables)*16 

439 entrySelector: H # log2(max2 <= numTables) 

440 rangeShift: H # numTables*16-searchRange 

441""" 

442 

443sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat) 

444 

445sfntDirectoryEntryFormat = """ 

446 > # big endian 

447 tag: 4s 

448 checkSum: L 

449 offset: L 

450 length: L 

451""" 

452 

453sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat) 

454 

455woffDirectoryFormat = """ 

456 > # big endian 

457 signature: 4s # "wOFF" 

458 sfntVersion: 4s 

459 length: L # total woff file size 

460 numTables: H # number of tables 

461 reserved: H # set to 0 

462 totalSfntSize: L # uncompressed size 

463 majorVersion: H # major version of WOFF file 

464 minorVersion: H # minor version of WOFF file 

465 metaOffset: L # offset to metadata block 

466 metaLength: L # length of compressed metadata 

467 metaOrigLength: L # length of uncompressed metadata 

468 privOffset: L # offset to private data block 

469 privLength: L # length of private data block 

470""" 

471 

472woffDirectorySize = sstruct.calcsize(woffDirectoryFormat) 

473 

474woffDirectoryEntryFormat = """ 

475 > # big endian 

476 tag: 4s 

477 offset: L 

478 length: L # compressed length 

479 origLength: L # original length 

480 checkSum: L # original checksum 

481""" 

482 

483woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat) 

484 

485 

486class DirectoryEntry(object): 

487 def __init__(self): 

488 self.uncompressed = False # if True, always embed entry raw 

489 

490 def fromFile(self, file): 

491 sstruct.unpack(self.format, file.read(self.formatSize), self) 

492 

493 def fromString(self, str): 

494 sstruct.unpack(self.format, str, self) 

495 

496 def toString(self): 

497 return sstruct.pack(self.format, self) 

498 

499 def __repr__(self): 

500 if hasattr(self, "tag"): 

501 return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self)) 

502 else: 

503 return "<%s at %x>" % (self.__class__.__name__, id(self)) 

504 

505 def loadData(self, file): 

506 file.seek(self.offset) 

507 data = file.read(self.length) 

508 assert len(data) == self.length 

509 if hasattr(self.__class__, "decodeData"): 

510 data = self.decodeData(data) 

511 return data 

512 

513 def saveData(self, file, data): 

514 if hasattr(self.__class__, "encodeData"): 

515 data = self.encodeData(data) 

516 self.length = len(data) 

517 file.seek(self.offset) 

518 file.write(data) 

519 

520 def decodeData(self, rawData): 

521 return rawData 

522 

523 def encodeData(self, data): 

524 return data 

525 

526 

527class SFNTDirectoryEntry(DirectoryEntry): 

528 format = sfntDirectoryEntryFormat 

529 formatSize = sfntDirectoryEntrySize 

530 

531 

532class WOFFDirectoryEntry(DirectoryEntry): 

533 format = woffDirectoryEntryFormat 

534 formatSize = woffDirectoryEntrySize 

535 

536 def __init__(self): 

537 super(WOFFDirectoryEntry, self).__init__() 

538 # With fonttools<=3.1.2, the only way to set a different zlib 

539 # compression level for WOFF directory entries was to set the class 

540 # attribute 'zlibCompressionLevel'. This is now replaced by a globally 

541 # defined `ZLIB_COMPRESSION_LEVEL`, which is also applied when 

542 # compressing the metadata. For backward compatibility, we still 

543 # use the class attribute if it was already set. 

544 if not hasattr(WOFFDirectoryEntry, "zlibCompressionLevel"): 

545 self.zlibCompressionLevel = ZLIB_COMPRESSION_LEVEL 

546 

547 def decodeData(self, rawData): 

548 import zlib 

549 

550 if self.length == self.origLength: 

551 data = rawData 

552 else: 

553 assert self.length < self.origLength 

554 data = zlib.decompress(rawData) 

555 assert len(data) == self.origLength 

556 return data 

557 

558 def encodeData(self, data): 

559 self.origLength = len(data) 

560 if not self.uncompressed: 

561 compressedData = compress(data, self.zlibCompressionLevel) 

562 if self.uncompressed or len(compressedData) >= self.origLength: 

563 # Encode uncompressed 

564 rawData = data 

565 self.length = self.origLength 

566 else: 

567 rawData = compressedData 

568 self.length = len(rawData) 

569 return rawData 

570 

571 

572class WOFFFlavorData: 

573 Flavor = "woff" 

574 

575 def __init__(self, reader=None): 

576 self.majorVersion = None 

577 self.minorVersion = None 

578 self.metaData = None 

579 self.privData = None 

580 if reader: 

581 self.majorVersion = reader.majorVersion 

582 self.minorVersion = reader.minorVersion 

583 if reader.metaLength: 

584 reader.file.seek(reader.metaOffset) 

585 rawData = reader.file.read(reader.metaLength) 

586 assert len(rawData) == reader.metaLength 

587 data = self._decompress(rawData) 

588 assert len(data) == reader.metaOrigLength 

589 self.metaData = data 

590 if reader.privLength: 

591 reader.file.seek(reader.privOffset) 

592 data = reader.file.read(reader.privLength) 

593 assert len(data) == reader.privLength 

594 self.privData = data 

595 

596 def _decompress(self, rawData): 

597 import zlib 

598 

599 return zlib.decompress(rawData) 

600 

601 

602def calcChecksum(data): 

603 """Calculate the checksum for an arbitrary block of data. 

604 

605 If the data length is not a multiple of four, it assumes 

606 it is to be padded with null byte. 

607 

608 >>> print(calcChecksum(b"abcd")) 

609 1633837924 

610 >>> print(calcChecksum(b"abcdxyz")) 

611 3655064932 

612 """ 

613 remainder = len(data) % 4 

614 if remainder: 

615 data += b"\0" * (4 - remainder) 

616 value = 0 

617 blockSize = 4096 

618 assert blockSize % 4 == 0 

619 for i in range(0, len(data), blockSize): 

620 block = data[i : i + blockSize] 

621 longs = struct.unpack(">%dL" % (len(block) // 4), block) 

622 value = (value + sum(longs)) & 0xFFFFFFFF 

623 return value 

624 

625 

626def readTTCHeader(file): 

627 file.seek(0) 

628 data = file.read(ttcHeaderSize) 

629 if len(data) != ttcHeaderSize: 

630 raise TTLibError("Not a Font Collection (not enough data)") 

631 self = SimpleNamespace() 

632 sstruct.unpack(ttcHeaderFormat, data, self) 

633 if self.TTCTag != "ttcf": 

634 raise TTLibError("Not a Font Collection") 

635 assert self.Version == 0x00010000 or self.Version == 0x00020000, ( 

636 "unrecognized TTC version 0x%08x" % self.Version 

637 ) 

638 self.offsetTable = struct.unpack( 

639 ">%dL" % self.numFonts, file.read(self.numFonts * 4) 

640 ) 

641 if self.Version == 0x00020000: 

642 pass # ignoring version 2.0 signatures 

643 return self 

644 

645 

646def writeTTCHeader(file, numFonts): 

647 self = SimpleNamespace() 

648 self.TTCTag = "ttcf" 

649 self.Version = 0x00010000 

650 self.numFonts = numFonts 

651 file.seek(0) 

652 file.write(sstruct.pack(ttcHeaderFormat, self)) 

653 offset = file.tell() 

654 file.write(struct.pack(">%dL" % self.numFonts, *([0] * self.numFonts))) 

655 return offset 

656 

657 

658if __name__ == "__main__": 

659 import sys 

660 import doctest 

661 

662 sys.exit(doctest.testmod().failed)