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

378 statements  

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

2 

3Defines two public classes: 

4 SFNTReader 

5 SFNTWriter 

6 

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

8used automatically by ttLib.TTFont.) 

9 

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

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

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

13""" 

14 

15from io import BytesIO 

16from types import SimpleNamespace 

17from fontTools.misc.textTools import Tag 

18from fontTools.misc import sstruct 

19from fontTools.ttLib import TTLibError, TTLibFileIsCollectionError 

20import struct 

21from collections import OrderedDict 

22import logging 

23 

24 

25log = logging.getLogger(__name__) 

26 

27 

28class SFNTReader(object): 

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

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

31 with the input file type. 

32 """ 

33 if args and cls is SFNTReader: 

34 infile = args[0] 

35 infile.seek(0) 

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

37 infile.seek(0) 

38 if sfntVersion == "wOF2": 

39 # return new WOFF2Reader object 

40 from fontTools.ttLib.woff2 import WOFF2Reader 

41 

42 return object.__new__(WOFF2Reader) 

43 # return default object 

44 return object.__new__(cls) 

45 

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

47 self.file = file 

48 self.checkChecksums = checkChecksums 

49 

50 self.flavor = None 

51 self.flavorData = None 

52 self.DirectoryEntry = SFNTDirectoryEntry 

53 self.file.seek(0) 

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

55 self.file.seek(0) 

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

57 header = readTTCHeader(self.file) 

58 numFonts = header.numFonts 

59 if not 0 <= fontNumber < numFonts: 

60 raise TTLibFileIsCollectionError( 

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

62 % (numFonts - 1) 

63 ) 

64 self.numFonts = numFonts 

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

66 data = self.file.read(sfntDirectorySize) 

67 if len(data) != sfntDirectorySize: 

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

69 sstruct.unpack(sfntDirectoryFormat, data, self) 

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

71 self.flavor = "woff" 

72 self.DirectoryEntry = WOFFDirectoryEntry 

73 data = self.file.read(woffDirectorySize) 

74 if len(data) != woffDirectorySize: 

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

76 sstruct.unpack(woffDirectoryFormat, data, self) 

77 else: 

78 data = self.file.read(sfntDirectorySize) 

79 if len(data) != sfntDirectorySize: 

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

81 sstruct.unpack(sfntDirectoryFormat, data, self) 

82 self.sfntVersion = Tag(self.sfntVersion) 

83 

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

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

86 tables = {} 

87 for i in range(self.numTables): 

88 entry = self.DirectoryEntry() 

89 entry.fromFile(self.file) 

90 tag = Tag(entry.tag) 

91 tables[tag] = entry 

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

93 

94 # Load flavor data if any 

95 if self.flavor == "woff": 

96 self.flavorData = WOFFFlavorData(self) 

97 

98 def has_key(self, tag): 

99 return tag in self.tables 

100 

101 __contains__ = has_key 

102 

103 def keys(self): 

104 return self.tables.keys() 

105 

106 def __getitem__(self, tag): 

107 """Fetch the raw table data.""" 

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

109 data = entry.loadData(self.file) 

110 if self.checkChecksums: 

111 if tag == "head": 

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

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

114 else: 

115 checksum = calcChecksum(data) 

116 if self.checkChecksums > 1: 

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

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

119 elif checksum != entry.checkSum: 

120 # Be friendly, and just log a warning. 

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

122 return data 

123 

124 def __delitem__(self, tag): 

125 del self.tables[Tag(tag)] 

126 

127 def close(self): 

128 self.file.close() 

129 

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

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

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

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

134 # same named file after unpickling. 

135 

136 def __getstate__(self): 

137 if isinstance(self.file, BytesIO): 

138 # BytesIO is already pickleable, return the state unmodified 

139 return self.__dict__ 

140 

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

142 state = self.__dict__.copy() 

143 del state["file"] 

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

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

146 return state 

147 

148 def __setstate__(self, state): 

149 if "file" not in state: 

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

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

152 self.__dict__.update(state) 

153 

154 

155# default compression level for WOFF 1.0 tables and metadata 

156ZLIB_COMPRESSION_LEVEL = 6 

157 

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

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

160USE_ZOPFLI = False 

161 

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

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

164ZOPFLI_LEVELS = { 

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

166 1: 1, 

167 2: 3, 

168 3: 5, 

169 4: 8, 

170 5: 10, 

171 6: 15, 

172 7: 25, 

173 8: 50, 

174 9: 100, 

175} 

176 

177 

178def compress(data, level=ZLIB_COMPRESSION_LEVEL): 

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

180 zopfli is used instead of the zlib module. 

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

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

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

184 """ 

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

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

187 if not USE_ZOPFLI or level == 0: 

188 from zlib import compress 

189 

190 return compress(data, level) 

191 else: 

192 from zopfli.zlib import compress 

193 

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

195 

196 

197class SFNTWriter(object): 

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

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

200 with the specified 'flavor'. 

201 """ 

202 flavor = None 

203 if kwargs and "flavor" in kwargs: 

204 flavor = kwargs["flavor"] 

205 elif args and len(args) > 3: 

206 flavor = args[3] 

207 if cls is SFNTWriter: 

208 if flavor == "woff2": 

209 # return new WOFF2Writer object 

210 from fontTools.ttLib.woff2 import WOFF2Writer 

211 

212 return object.__new__(WOFF2Writer) 

213 # return default object 

214 return object.__new__(cls) 

215 

216 def __init__( 

217 self, 

218 file, 

219 numTables, 

220 sfntVersion="\000\001\000\000", 

221 flavor=None, 

222 flavorData=None, 

223 ): 

224 self.file = file 

225 self.numTables = numTables 

226 self.sfntVersion = Tag(sfntVersion) 

227 self.flavor = flavor 

228 self.flavorData = flavorData 

229 

230 if self.flavor == "woff": 

231 self.directoryFormat = woffDirectoryFormat 

232 self.directorySize = woffDirectorySize 

233 self.DirectoryEntry = WOFFDirectoryEntry 

234 

235 self.signature = "wOFF" 

236 

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

238 self.origNextTableOffset = ( 

239 sfntDirectorySize + numTables * sfntDirectoryEntrySize 

240 ) 

241 else: 

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

243 self.directoryFormat = sfntDirectoryFormat 

244 self.directorySize = sfntDirectorySize 

245 self.DirectoryEntry = SFNTDirectoryEntry 

246 

247 from fontTools.ttLib import getSearchRange 

248 

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

250 numTables, 16 

251 ) 

252 

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

254 self.nextTableOffset = ( 

255 self.directoryOffset 

256 + self.directorySize 

257 + numTables * self.DirectoryEntry.formatSize 

258 ) 

259 # clear out directory area 

260 self.file.seek(self.nextTableOffset) 

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

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

263 self.tables = OrderedDict() 

264 

265 def setEntry(self, tag, entry): 

266 if tag in self.tables: 

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

268 

269 self.tables[tag] = entry 

270 

271 def __setitem__(self, tag, data): 

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

273 if tag in self.tables: 

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

275 

276 entry = self.DirectoryEntry() 

277 entry.tag = tag 

278 entry.offset = self.nextTableOffset 

279 if tag == "head": 

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

281 self.headTable = data 

282 entry.uncompressed = True 

283 else: 

284 entry.checkSum = calcChecksum(data) 

285 entry.saveData(self.file, data) 

286 

287 if self.flavor == "woff": 

288 entry.origOffset = self.origNextTableOffset 

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

290 

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

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

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

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

295 # in the font. 

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

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

298 

299 self.setEntry(tag, entry) 

300 

301 def __getitem__(self, tag): 

302 return self.tables[tag] 

303 

304 def close(self): 

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

306 directory. 

307 """ 

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

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

310 raise TTLibError( 

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

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

313 ) 

314 

315 if self.flavor == "woff": 

316 self.signature = b"wOFF" 

317 self.reserved = 0 

318 

319 self.totalSfntSize = 12 

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

321 for tag, entry in tables: 

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

323 

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

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

326 self.majorVersion = data.majorVersion 

327 self.minorVersion = data.minorVersion 

328 else: 

329 if hasattr(self, "headTable"): 

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

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

332 ) 

333 else: 

334 self.majorVersion = self.minorVersion = 0 

335 if data.metaData: 

336 self.metaOrigLength = len(data.metaData) 

337 self.file.seek(0, 2) 

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

339 compressedMetaData = compress(data.metaData) 

340 self.metaLength = len(compressedMetaData) 

341 self.file.write(compressedMetaData) 

342 else: 

343 self.metaOffset = self.metaLength = self.metaOrigLength = 0 

344 if data.privData: 

345 self.file.seek(0, 2) 

346 off = self.file.tell() 

347 paddedOff = (off + 3) & ~3 

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

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

350 self.privLength = len(data.privData) 

351 self.file.write(data.privData) 

352 else: 

353 self.privOffset = self.privLength = 0 

354 

355 self.file.seek(0, 2) 

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

357 

358 else: 

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

360 pass 

361 

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

363 

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

365 seenHead = 0 

366 for tag, entry in tables: 

367 if tag == "head": 

368 seenHead = 1 

369 directory = directory + entry.toString() 

370 if seenHead: 

371 self.writeMasterChecksum(directory) 

372 self.file.seek(self.directoryOffset) 

373 self.file.write(directory) 

374 

375 def _calcMasterChecksum(self, directory): 

376 # calculate checkSumAdjustment 

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

378 checksums = [] 

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

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

381 

382 if self.DirectoryEntry != SFNTDirectoryEntry: 

383 # Create a SFNT directory for checksum calculation purposes 

384 from fontTools.ttLib import getSearchRange 

385 

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

387 self.numTables, 16 

388 ) 

389 directory = sstruct.pack(sfntDirectoryFormat, self) 

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

391 for tag, entry in tables: 

392 sfntEntry = SFNTDirectoryEntry() 

393 sfntEntry.tag = entry.tag 

394 sfntEntry.checkSum = entry.checkSum 

395 sfntEntry.offset = entry.origOffset 

396 sfntEntry.length = entry.origLength 

397 directory = directory + sfntEntry.toString() 

398 

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

400 assert directory_end == len(directory) 

401 

402 checksums.append(calcChecksum(directory)) 

403 checksum = sum(checksums) & 0xFFFFFFFF 

404 # BiboAfba! 

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

406 return checksumadjustment 

407 

408 def writeMasterChecksum(self, directory): 

409 checksumadjustment = self._calcMasterChecksum(directory) 

410 # write the checksum to the file 

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

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

413 

414 def reordersTables(self): 

415 return False 

416 

417 

418# -- sfnt directory helpers and cruft 

419 

420ttcHeaderFormat = """ 

421 > # big endian 

422 TTCTag: 4s # "ttcf" 

423 Version: L # 0x00010000 or 0x00020000 

424 numFonts: L # number of fonts 

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

426 # ulDsigTag: L # version 2.0 only 

427 # ulDsigLength: L # version 2.0 only 

428 # ulDsigOffset: L # version 2.0 only 

429""" 

430 

431ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat) 

432 

433sfntDirectoryFormat = """ 

434 > # big endian 

435 sfntVersion: 4s 

436 numTables: H # number of tables 

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

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

439 rangeShift: H # numTables*16-searchRange 

440""" 

441 

442sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat) 

443 

444sfntDirectoryEntryFormat = """ 

445 > # big endian 

446 tag: 4s 

447 checkSum: L 

448 offset: L 

449 length: L 

450""" 

451 

452sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat) 

453 

454woffDirectoryFormat = """ 

455 > # big endian 

456 signature: 4s # "wOFF" 

457 sfntVersion: 4s 

458 length: L # total woff file size 

459 numTables: H # number of tables 

460 reserved: H # set to 0 

461 totalSfntSize: L # uncompressed size 

462 majorVersion: H # major version of WOFF file 

463 minorVersion: H # minor version of WOFF file 

464 metaOffset: L # offset to metadata block 

465 metaLength: L # length of compressed metadata 

466 metaOrigLength: L # length of uncompressed metadata 

467 privOffset: L # offset to private data block 

468 privLength: L # length of private data block 

469""" 

470 

471woffDirectorySize = sstruct.calcsize(woffDirectoryFormat) 

472 

473woffDirectoryEntryFormat = """ 

474 > # big endian 

475 tag: 4s 

476 offset: L 

477 length: L # compressed length 

478 origLength: L # original length 

479 checkSum: L # original checksum 

480""" 

481 

482woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat) 

483 

484 

485class DirectoryEntry(object): 

486 def __init__(self): 

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

488 

489 def fromFile(self, file): 

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

491 

492 def fromString(self, str): 

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

494 

495 def toString(self): 

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

497 

498 def __repr__(self): 

499 if hasattr(self, "tag"): 

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

501 else: 

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

503 

504 def loadData(self, file): 

505 file.seek(self.offset) 

506 data = file.read(self.length) 

507 assert len(data) == self.length 

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

509 data = self.decodeData(data) 

510 return data 

511 

512 def saveData(self, file, data): 

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

514 data = self.encodeData(data) 

515 self.length = len(data) 

516 file.seek(self.offset) 

517 file.write(data) 

518 

519 def decodeData(self, rawData): 

520 return rawData 

521 

522 def encodeData(self, data): 

523 return data 

524 

525 

526class SFNTDirectoryEntry(DirectoryEntry): 

527 format = sfntDirectoryEntryFormat 

528 formatSize = sfntDirectoryEntrySize 

529 

530 

531class WOFFDirectoryEntry(DirectoryEntry): 

532 format = woffDirectoryEntryFormat 

533 formatSize = woffDirectoryEntrySize 

534 

535 def __init__(self): 

536 super(WOFFDirectoryEntry, self).__init__() 

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

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

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

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

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

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

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

544 self.zlibCompressionLevel = ZLIB_COMPRESSION_LEVEL 

545 

546 def decodeData(self, rawData): 

547 import zlib 

548 

549 if self.length == self.origLength: 

550 data = rawData 

551 else: 

552 assert self.length < self.origLength 

553 data = zlib.decompress(rawData) 

554 assert len(data) == self.origLength 

555 return data 

556 

557 def encodeData(self, data): 

558 self.origLength = len(data) 

559 if not self.uncompressed: 

560 compressedData = compress(data, self.zlibCompressionLevel) 

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

562 # Encode uncompressed 

563 rawData = data 

564 self.length = self.origLength 

565 else: 

566 rawData = compressedData 

567 self.length = len(rawData) 

568 return rawData 

569 

570 

571class WOFFFlavorData: 

572 Flavor = "woff" 

573 

574 def __init__(self, reader=None): 

575 self.majorVersion = None 

576 self.minorVersion = None 

577 self.metaData = None 

578 self.privData = None 

579 if reader: 

580 self.majorVersion = reader.majorVersion 

581 self.minorVersion = reader.minorVersion 

582 if reader.metaLength: 

583 reader.file.seek(reader.metaOffset) 

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

585 assert len(rawData) == reader.metaLength 

586 data = self._decompress(rawData) 

587 assert len(data) == reader.metaOrigLength 

588 self.metaData = data 

589 if reader.privLength: 

590 reader.file.seek(reader.privOffset) 

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

592 assert len(data) == reader.privLength 

593 self.privData = data 

594 

595 def _decompress(self, rawData): 

596 import zlib 

597 

598 return zlib.decompress(rawData) 

599 

600 

601def calcChecksum(data): 

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

603 

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

605 it is to be padded with null byte. 

606 

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

608 1633837924 

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

610 3655064932 

611 """ 

612 remainder = len(data) % 4 

613 if remainder: 

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

615 value = 0 

616 blockSize = 4096 

617 assert blockSize % 4 == 0 

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

619 block = data[i : i + blockSize] 

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

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

622 return value 

623 

624 

625def readTTCHeader(file): 

626 file.seek(0) 

627 data = file.read(ttcHeaderSize) 

628 if len(data) != ttcHeaderSize: 

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

630 self = SimpleNamespace() 

631 sstruct.unpack(ttcHeaderFormat, data, self) 

632 if self.TTCTag != "ttcf": 

633 raise TTLibError("Not a Font Collection") 

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

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

636 ) 

637 self.offsetTable = struct.unpack( 

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

639 ) 

640 if self.Version == 0x00020000: 

641 pass # ignoring version 2.0 signatures 

642 return self 

643 

644 

645def writeTTCHeader(file, numFonts): 

646 self = SimpleNamespace() 

647 self.TTCTag = "ttcf" 

648 self.Version = 0x00010000 

649 self.numFonts = numFonts 

650 file.seek(0) 

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

652 offset = file.tell() 

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

654 return offset 

655 

656 

657if __name__ == "__main__": 

658 import sys 

659 import doctest 

660 

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