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

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

968 statements  

1from io import BytesIO 

2import sys 

3import array 

4import struct 

5from collections import OrderedDict 

6from fontTools.misc import sstruct 

7from fontTools.misc.arrayTools import calcIntBounds 

8from fontTools.misc.textTools import Tag, bytechr, byteord, bytesjoin, pad 

9from fontTools.ttLib import ( 

10 TTFont, 

11 TTLibError, 

12 getTableModule, 

13 getTableClass, 

14 getSearchRange, 

15) 

16from fontTools.ttLib.sfnt import ( 

17 SFNTReader, 

18 SFNTWriter, 

19 DirectoryEntry, 

20 WOFFFlavorData, 

21 sfntDirectoryFormat, 

22 sfntDirectorySize, 

23 SFNTDirectoryEntry, 

24 sfntDirectoryEntrySize, 

25 calcChecksum, 

26) 

27from fontTools.ttLib.tables import ttProgram, _g_l_y_f 

28import logging 

29 

30 

31log = logging.getLogger("fontTools.ttLib.woff2") 

32 

33haveBrotli = False 

34try: 

35 try: 

36 import brotlicffi as brotli 

37 except ImportError: 

38 import brotli 

39 haveBrotli = True 

40except ImportError: 

41 pass 

42 

43 

44class WOFF2Reader(SFNTReader): 

45 flavor = "woff2" 

46 

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

48 if not haveBrotli: 

49 log.error( 

50 "The WOFF2 decoder requires the Brotli Python extension, available at: " 

51 "https://github.com/google/brotli" 

52 ) 

53 raise ImportError("No module named brotli") 

54 

55 self.file = file 

56 

57 signature = Tag(self.file.read(4)) 

58 if signature != b"wOF2": 

59 raise TTLibError("Not a WOFF2 font (bad signature)") 

60 

61 self.file.seek(0) 

62 self.DirectoryEntry = WOFF2DirectoryEntry 

63 data = self.file.read(woff2DirectorySize) 

64 if len(data) != woff2DirectorySize: 

65 raise TTLibError("Not a WOFF2 font (not enough data)") 

66 sstruct.unpack(woff2DirectoryFormat, data, self) 

67 

68 self.tables = OrderedDict() 

69 offset = 0 

70 for i in range(self.numTables): 

71 entry = self.DirectoryEntry() 

72 entry.fromFile(self.file) 

73 tag = Tag(entry.tag) 

74 self.tables[tag] = entry 

75 entry.offset = offset 

76 offset += entry.length 

77 

78 totalUncompressedSize = offset 

79 compressedData = self.file.read(self.totalCompressedSize) 

80 decompressedData = brotli.decompress(compressedData) 

81 if len(decompressedData) != totalUncompressedSize: 

82 raise TTLibError( 

83 "unexpected size for decompressed font data: expected %d, found %d" 

84 % (totalUncompressedSize, len(decompressedData)) 

85 ) 

86 self.transformBuffer = BytesIO(decompressedData) 

87 

88 self.file.seek(0, 2) 

89 if self.length != self.file.tell(): 

90 raise TTLibError("reported 'length' doesn't match the actual file size") 

91 

92 self.flavorData = WOFF2FlavorData(self) 

93 

94 # make empty TTFont to store data while reconstructing tables 

95 self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) 

96 

97 def __getitem__(self, tag): 

98 """Fetch the raw table data. Reconstruct transformed tables.""" 

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

100 if not hasattr(entry, "data"): 

101 if entry.transformed: 

102 entry.data = self.reconstructTable(tag) 

103 else: 

104 entry.data = entry.loadData(self.transformBuffer) 

105 return entry.data 

106 

107 def reconstructTable(self, tag): 

108 """Reconstruct table named 'tag' from transformed data.""" 

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

110 rawData = entry.loadData(self.transformBuffer) 

111 if tag == "glyf": 

112 # no need to pad glyph data when reconstructing 

113 padding = self.padding if hasattr(self, "padding") else None 

114 data = self._reconstructGlyf(rawData, padding) 

115 elif tag == "loca": 

116 data = self._reconstructLoca() 

117 elif tag == "hmtx": 

118 data = self._reconstructHmtx(rawData) 

119 else: 

120 raise TTLibError("transform for table '%s' is unknown" % tag) 

121 return data 

122 

123 def _reconstructGlyf(self, data, padding=None): 

124 """Return recostructed glyf table data, and set the corresponding loca's 

125 locations. Optionally pad glyph offsets to the specified number of bytes. 

126 """ 

127 self.ttFont["loca"] = WOFF2LocaTable() 

128 glyfTable = self.ttFont["glyf"] = WOFF2GlyfTable() 

129 glyfTable.reconstruct(data, self.ttFont) 

130 if padding: 

131 glyfTable.padding = padding 

132 data = glyfTable.compile(self.ttFont) 

133 return data 

134 

135 def _reconstructLoca(self): 

136 """Return reconstructed loca table data.""" 

137 if "loca" not in self.ttFont: 

138 # make sure glyf is reconstructed first 

139 self.tables["glyf"].data = self.reconstructTable("glyf") 

140 locaTable = self.ttFont["loca"] 

141 data = locaTable.compile(self.ttFont) 

142 if len(data) != self.tables["loca"].origLength: 

143 raise TTLibError( 

144 "reconstructed 'loca' table doesn't match original size: " 

145 "expected %d, found %d" % (self.tables["loca"].origLength, len(data)) 

146 ) 

147 return data 

148 

149 def _reconstructHmtx(self, data): 

150 """Return reconstructed hmtx table data.""" 

151 # Before reconstructing 'hmtx' table we need to parse other tables: 

152 # 'glyf' is required for reconstructing the sidebearings from the glyphs' 

153 # bounding box; 'hhea' is needed for the numberOfHMetrics field. 

154 if "glyf" in self.flavorData.transformedTables: 

155 # transformed 'glyf' table is self-contained, thus 'loca' not needed 

156 tableDependencies = ("maxp", "hhea", "glyf") 

157 else: 

158 # decompiling untransformed 'glyf' requires 'loca', which requires 'head' 

159 tableDependencies = ("maxp", "head", "hhea", "loca", "glyf") 

160 for tag in tableDependencies: 

161 self._decompileTable(tag) 

162 hmtxTable = self.ttFont["hmtx"] = WOFF2HmtxTable() 

163 hmtxTable.reconstruct(data, self.ttFont) 

164 data = hmtxTable.compile(self.ttFont) 

165 return data 

166 

167 def _decompileTable(self, tag): 

168 """Decompile table data and store it inside self.ttFont.""" 

169 data = self[tag] 

170 if self.ttFont.isLoaded(tag): 

171 return self.ttFont[tag] 

172 tableClass = getTableClass(tag) 

173 table = tableClass(tag) 

174 self.ttFont.tables[tag] = table 

175 table.decompile(data, self.ttFont) 

176 

177 

178class WOFF2Writer(SFNTWriter): 

179 flavor = "woff2" 

180 

181 def __init__( 

182 self, 

183 file, 

184 numTables, 

185 sfntVersion="\000\001\000\000", 

186 flavor=None, 

187 flavorData=None, 

188 ): 

189 if not haveBrotli: 

190 log.error( 

191 "The WOFF2 encoder requires the Brotli Python extension, available at: " 

192 "https://github.com/google/brotli" 

193 ) 

194 raise ImportError("No module named brotli") 

195 

196 self.file = file 

197 self.numTables = numTables 

198 self.sfntVersion = Tag(sfntVersion) 

199 self.flavorData = WOFF2FlavorData(data=flavorData) 

200 

201 self.directoryFormat = woff2DirectoryFormat 

202 self.directorySize = woff2DirectorySize 

203 self.DirectoryEntry = WOFF2DirectoryEntry 

204 

205 self.signature = Tag("wOF2") 

206 

207 self.nextTableOffset = 0 

208 self.transformBuffer = BytesIO() 

209 

210 self.tables = OrderedDict() 

211 

212 # make empty TTFont to store data while normalising and transforming tables 

213 self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) 

214 

215 def __setitem__(self, tag, data): 

216 """Associate new entry named 'tag' with raw table data.""" 

217 if tag in self.tables: 

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

219 if tag == "DSIG": 

220 # always drop DSIG table, since the encoding process can invalidate it 

221 self.numTables -= 1 

222 return 

223 

224 entry = self.DirectoryEntry() 

225 entry.tag = Tag(tag) 

226 entry.flags = getKnownTagIndex(entry.tag) 

227 # WOFF2 table data are written to disk only on close(), after all tags 

228 # have been specified 

229 entry.data = data 

230 

231 self.tables[tag] = entry 

232 

233 def close(self): 

234 """All tags must have been specified. Now write the table data and directory.""" 

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

236 raise TTLibError( 

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

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

239 ) 

240 

241 if self.sfntVersion in ("\x00\x01\x00\x00", "true"): 

242 isTrueType = True 

243 elif self.sfntVersion == "OTTO": 

244 isTrueType = False 

245 else: 

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

247 

248 # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned. 

249 # However, the reference WOFF2 implementation still fails to reconstruct 

250 # 'unpadded' glyf tables, therefore we need to 'normalise' them. 

251 # See: 

252 # https://github.com/khaledhosny/ots/issues/60 

253 # https://github.com/google/woff2/issues/15 

254 if ( 

255 isTrueType 

256 and "glyf" in self.flavorData.transformedTables 

257 and "glyf" in self.tables 

258 ): 

259 self._normaliseGlyfAndLoca(padding=4) 

260 self._setHeadTransformFlag() 

261 

262 # To pass the legacy OpenType Sanitiser currently included in browsers, 

263 # we must sort the table directory and data alphabetically by tag. 

264 # See: 

265 # https://github.com/google/woff2/pull/3 

266 # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html 

267 # 

268 # 2023: We rely on this in _transformTables where we expect that 

269 # "loca" comes after "glyf" table. 

270 self.tables = OrderedDict(sorted(self.tables.items())) 

271 

272 self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() 

273 

274 fontData = self._transformTables() 

275 compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT) 

276 

277 self.totalCompressedSize = len(compressedFont) 

278 self.length = self._calcTotalSize() 

279 self.majorVersion, self.minorVersion = self._getVersion() 

280 self.reserved = 0 

281 

282 directory = self._packTableDirectory() 

283 self.file.seek(0) 

284 self.file.write(pad(directory + compressedFont, size=4)) 

285 self._writeFlavorData() 

286 

287 def _normaliseGlyfAndLoca(self, padding=4): 

288 """Recompile glyf and loca tables, aligning glyph offsets to multiples of 

289 'padding' size. Update the head table's 'indexToLocFormat' accordingly while 

290 compiling loca. 

291 """ 

292 if self.sfntVersion == "OTTO": 

293 return 

294 

295 for tag in ("maxp", "head", "loca", "glyf", "fvar"): 

296 if tag in self.tables: 

297 self._decompileTable(tag) 

298 self.ttFont["glyf"].padding = padding 

299 for tag in ("glyf", "loca"): 

300 self._compileTable(tag) 

301 

302 def _setHeadTransformFlag(self): 

303 """Set bit 11 of 'head' table flags to indicate that the font has undergone 

304 a lossless modifying transform. Re-compile head table data.""" 

305 self._decompileTable("head") 

306 self.ttFont["head"].flags |= 1 << 11 

307 self._compileTable("head") 

308 

309 def _decompileTable(self, tag): 

310 """Fetch table data, decompile it, and store it inside self.ttFont.""" 

311 tag = Tag(tag) 

312 if tag not in self.tables: 

313 raise TTLibError("missing required table: %s" % tag) 

314 if self.ttFont.isLoaded(tag): 

315 return 

316 data = self.tables[tag].data 

317 if tag == "loca": 

318 tableClass = WOFF2LocaTable 

319 elif tag == "glyf": 

320 tableClass = WOFF2GlyfTable 

321 elif tag == "hmtx": 

322 tableClass = WOFF2HmtxTable 

323 else: 

324 tableClass = getTableClass(tag) 

325 table = tableClass(tag) 

326 self.ttFont.tables[tag] = table 

327 table.decompile(data, self.ttFont) 

328 

329 def _compileTable(self, tag): 

330 """Compile table and store it in its 'data' attribute.""" 

331 self.tables[tag].data = self.ttFont[tag].compile(self.ttFont) 

332 

333 def _calcSFNTChecksumsLengthsAndOffsets(self): 

334 """Compute the 'original' SFNT checksums, lengths and offsets for checksum 

335 adjustment calculation. Return the total size of the uncompressed font. 

336 """ 

337 offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables) 

338 for tag, entry in self.tables.items(): 

339 data = entry.data 

340 entry.origOffset = offset 

341 entry.origLength = len(data) 

342 if tag == "head": 

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

344 else: 

345 entry.checkSum = calcChecksum(data) 

346 offset += (entry.origLength + 3) & ~3 

347 return offset 

348 

349 def _transformTables(self): 

350 """Return transformed font data.""" 

351 transformedTables = self.flavorData.transformedTables 

352 for tag, entry in self.tables.items(): 

353 data = None 

354 if tag in transformedTables: 

355 data = self.transformTable(tag) 

356 if data is not None: 

357 entry.transformed = True 

358 if data is None: 

359 if tag == "glyf": 

360 # Currently we always sort table tags so 

361 # 'loca' comes after 'glyf'. 

362 transformedTables.discard("loca") 

363 # pass-through the table data without transformation 

364 data = entry.data 

365 entry.transformed = False 

366 entry.offset = self.nextTableOffset 

367 entry.saveData(self.transformBuffer, data) 

368 self.nextTableOffset += entry.length 

369 self.writeMasterChecksum() 

370 fontData = self.transformBuffer.getvalue() 

371 return fontData 

372 

373 def transformTable(self, tag): 

374 """Return transformed table data, or None if some pre-conditions aren't 

375 met -- in which case, the non-transformed table data will be used. 

376 """ 

377 if tag == "loca": 

378 data = b"" 

379 elif tag == "glyf": 

380 for tag in ("maxp", "head", "loca", "glyf"): 

381 self._decompileTable(tag) 

382 glyfTable = self.ttFont["glyf"] 

383 data = glyfTable.transform(self.ttFont) 

384 elif tag == "hmtx": 

385 if "glyf" not in self.tables: 

386 return 

387 for tag in ("maxp", "head", "hhea", "loca", "glyf", "hmtx"): 

388 self._decompileTable(tag) 

389 hmtxTable = self.ttFont["hmtx"] 

390 data = hmtxTable.transform(self.ttFont) # can be None 

391 else: 

392 raise TTLibError("Transform for table '%s' is unknown" % tag) 

393 return data 

394 

395 def _calcMasterChecksum(self): 

396 """Calculate checkSumAdjustment.""" 

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

398 checksums = [] 

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

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

401 

402 # Create a SFNT directory for checksum calculation purposes 

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

404 self.numTables, 16 

405 ) 

406 directory = sstruct.pack(sfntDirectoryFormat, self) 

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

408 for tag, entry in tables: 

409 sfntEntry = SFNTDirectoryEntry() 

410 sfntEntry.tag = entry.tag 

411 sfntEntry.checkSum = entry.checkSum 

412 sfntEntry.offset = entry.origOffset 

413 sfntEntry.length = entry.origLength 

414 directory = directory + sfntEntry.toString() 

415 

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

417 assert directory_end == len(directory) 

418 

419 checksums.append(calcChecksum(directory)) 

420 checksum = sum(checksums) & 0xFFFFFFFF 

421 # BiboAfba! 

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

423 return checksumadjustment 

424 

425 def writeMasterChecksum(self): 

426 """Write checkSumAdjustment to the transformBuffer.""" 

427 checksumadjustment = self._calcMasterChecksum() 

428 self.transformBuffer.seek(self.tables["head"].offset + 8) 

429 self.transformBuffer.write(struct.pack(">L", checksumadjustment)) 

430 

431 def _calcTotalSize(self): 

432 """Calculate total size of WOFF2 font, including any meta- and/or private data.""" 

433 offset = self.directorySize 

434 for entry in self.tables.values(): 

435 offset += len(entry.toString()) 

436 offset += self.totalCompressedSize 

437 offset = (offset + 3) & ~3 

438 offset = self._calcFlavorDataOffsetsAndSize(offset) 

439 return offset 

440 

441 def _calcFlavorDataOffsetsAndSize(self, start): 

442 """Calculate offsets and lengths for any meta- and/or private data.""" 

443 offset = start 

444 data = self.flavorData 

445 if data.metaData: 

446 self.metaOrigLength = len(data.metaData) 

447 self.metaOffset = offset 

448 self.compressedMetaData = brotli.compress( 

449 data.metaData, mode=brotli.MODE_TEXT 

450 ) 

451 self.metaLength = len(self.compressedMetaData) 

452 offset += self.metaLength 

453 else: 

454 self.metaOffset = self.metaLength = self.metaOrigLength = 0 

455 self.compressedMetaData = b"" 

456 if data.privData: 

457 # make sure private data is padded to 4-byte boundary 

458 offset = (offset + 3) & ~3 

459 self.privOffset = offset 

460 self.privLength = len(data.privData) 

461 offset += self.privLength 

462 else: 

463 self.privOffset = self.privLength = 0 

464 return offset 

465 

466 def _getVersion(self): 

467 """Return the WOFF2 font's (majorVersion, minorVersion) tuple.""" 

468 data = self.flavorData 

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

470 return data.majorVersion, data.minorVersion 

471 else: 

472 # if None, return 'fontRevision' from 'head' table 

473 if "head" in self.tables: 

474 return struct.unpack(">HH", self.tables["head"].data[4:8]) 

475 else: 

476 return 0, 0 

477 

478 def _packTableDirectory(self): 

479 """Return WOFF2 table directory data.""" 

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

481 for entry in self.tables.values(): 

482 directory = directory + entry.toString() 

483 return directory 

484 

485 def _writeFlavorData(self): 

486 """Write metadata and/or private data using appropiate padding.""" 

487 compressedMetaData = self.compressedMetaData 

488 privData = self.flavorData.privData 

489 if compressedMetaData and privData: 

490 compressedMetaData = pad(compressedMetaData, size=4) 

491 if compressedMetaData: 

492 self.file.seek(self.metaOffset) 

493 assert self.file.tell() == self.metaOffset 

494 self.file.write(compressedMetaData) 

495 if privData: 

496 self.file.seek(self.privOffset) 

497 assert self.file.tell() == self.privOffset 

498 self.file.write(privData) 

499 

500 def reordersTables(self): 

501 return True 

502 

503 

504# -- woff2 directory helpers and cruft 

505 

506woff2DirectoryFormat = """ 

507 > # big endian 

508 signature: 4s # "wOF2" 

509 sfntVersion: 4s 

510 length: L # total woff2 file size 

511 numTables: H # number of tables 

512 reserved: H # set to 0 

513 totalSfntSize: L # uncompressed size 

514 totalCompressedSize: L # compressed size 

515 majorVersion: H # major version of WOFF file 

516 minorVersion: H # minor version of WOFF file 

517 metaOffset: L # offset to metadata block 

518 metaLength: L # length of compressed metadata 

519 metaOrigLength: L # length of uncompressed metadata 

520 privOffset: L # offset to private data block 

521 privLength: L # length of private data block 

522""" 

523 

524woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat) 

525 

526woff2KnownTags = ( 

527 "cmap", 

528 "head", 

529 "hhea", 

530 "hmtx", 

531 "maxp", 

532 "name", 

533 "OS/2", 

534 "post", 

535 "cvt ", 

536 "fpgm", 

537 "glyf", 

538 "loca", 

539 "prep", 

540 "CFF ", 

541 "VORG", 

542 "EBDT", 

543 "EBLC", 

544 "gasp", 

545 "hdmx", 

546 "kern", 

547 "LTSH", 

548 "PCLT", 

549 "VDMX", 

550 "vhea", 

551 "vmtx", 

552 "BASE", 

553 "GDEF", 

554 "GPOS", 

555 "GSUB", 

556 "EBSC", 

557 "JSTF", 

558 "MATH", 

559 "CBDT", 

560 "CBLC", 

561 "COLR", 

562 "CPAL", 

563 "SVG ", 

564 "sbix", 

565 "acnt", 

566 "avar", 

567 "bdat", 

568 "bloc", 

569 "bsln", 

570 "cvar", 

571 "fdsc", 

572 "feat", 

573 "fmtx", 

574 "fvar", 

575 "gvar", 

576 "hsty", 

577 "just", 

578 "lcar", 

579 "mort", 

580 "morx", 

581 "opbd", 

582 "prop", 

583 "trak", 

584 "Zapf", 

585 "Silf", 

586 "Glat", 

587 "Gloc", 

588 "Feat", 

589 "Sill", 

590) 

591 

592woff2FlagsFormat = """ 

593 > # big endian 

594 flags: B # table type and flags 

595""" 

596 

597woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat) 

598 

599woff2UnknownTagFormat = """ 

600 > # big endian 

601 tag: 4s # 4-byte tag (optional) 

602""" 

603 

604woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat) 

605 

606woff2UnknownTagIndex = 0x3F 

607 

608woff2Base128MaxSize = 5 

609woff2DirectoryEntryMaxSize = ( 

610 woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize 

611) 

612 

613woff2TransformedTableTags = ("glyf", "loca") 

614 

615woff2GlyfTableFormat = """ 

616 > # big endian 

617 version: H # = 0x0000 

618 optionFlags: H # Bit 0: we have overlapSimpleBitmap[], Bits 1-15: reserved 

619 numGlyphs: H # Number of glyphs 

620 indexFormat: H # Offset format for loca table 

621 nContourStreamSize: L # Size of nContour stream 

622 nPointsStreamSize: L # Size of nPoints stream 

623 flagStreamSize: L # Size of flag stream 

624 glyphStreamSize: L # Size of glyph stream 

625 compositeStreamSize: L # Size of composite stream 

626 bboxStreamSize: L # Comnined size of bboxBitmap and bboxStream 

627 instructionStreamSize: L # Size of instruction stream 

628""" 

629 

630woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat) 

631 

632bboxFormat = """ 

633 > # big endian 

634 xMin: h 

635 yMin: h 

636 xMax: h 

637 yMax: h 

638""" 

639 

640woff2OverlapSimpleBitmapFlag = 0x0001 

641 

642 

643def getKnownTagIndex(tag): 

644 """Return index of 'tag' in woff2KnownTags list. Return 63 if not found.""" 

645 for i in range(len(woff2KnownTags)): 

646 if tag == woff2KnownTags[i]: 

647 return i 

648 return woff2UnknownTagIndex 

649 

650 

651class WOFF2DirectoryEntry(DirectoryEntry): 

652 def fromFile(self, file): 

653 pos = file.tell() 

654 data = file.read(woff2DirectoryEntryMaxSize) 

655 left = self.fromString(data) 

656 consumed = len(data) - len(left) 

657 file.seek(pos + consumed) 

658 

659 def fromString(self, data): 

660 if len(data) < 1: 

661 raise TTLibError("can't read table 'flags': not enough data") 

662 dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self) 

663 if self.flags & 0x3F == 0x3F: 

664 # if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value 

665 if len(data) < woff2UnknownTagSize: 

666 raise TTLibError("can't read table 'tag': not enough data") 

667 dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self) 

668 else: 

669 # otherwise, tag is derived from a fixed 'Known Tags' table 

670 self.tag = woff2KnownTags[self.flags & 0x3F] 

671 self.tag = Tag(self.tag) 

672 self.origLength, data = unpackBase128(data) 

673 self.length = self.origLength 

674 if self.transformed: 

675 self.length, data = unpackBase128(data) 

676 if self.tag == "loca" and self.length != 0: 

677 raise TTLibError("the transformLength of the 'loca' table must be 0") 

678 # return left over data 

679 return data 

680 

681 def toString(self): 

682 data = bytechr(self.flags) 

683 if (self.flags & 0x3F) == 0x3F: 

684 data += struct.pack(">4s", self.tag.tobytes()) 

685 data += packBase128(self.origLength) 

686 if self.transformed: 

687 data += packBase128(self.length) 

688 return data 

689 

690 @property 

691 def transformVersion(self): 

692 """Return bits 6-7 of table entry's flags, which indicate the preprocessing 

693 transformation version number (between 0 and 3). 

694 """ 

695 return self.flags >> 6 

696 

697 @transformVersion.setter 

698 def transformVersion(self, value): 

699 assert 0 <= value <= 3 

700 self.flags |= value << 6 

701 

702 @property 

703 def transformed(self): 

704 """Return True if the table has any transformation, else return False.""" 

705 # For all tables in a font, except for 'glyf' and 'loca', the transformation 

706 # version 0 indicates the null transform (where the original table data is 

707 # passed directly to the Brotli compressor). For 'glyf' and 'loca' tables, 

708 # transformation version 3 indicates the null transform 

709 if self.tag in {"glyf", "loca"}: 

710 return self.transformVersion != 3 

711 else: 

712 return self.transformVersion != 0 

713 

714 @transformed.setter 

715 def transformed(self, booleanValue): 

716 # here we assume that a non-null transform means version 0 for 'glyf' and 

717 # 'loca' and 1 for every other table (e.g. hmtx); but that may change as 

718 # new transformation formats are introduced in the future (if ever). 

719 if self.tag in {"glyf", "loca"}: 

720 self.transformVersion = 3 if not booleanValue else 0 

721 else: 

722 self.transformVersion = int(booleanValue) 

723 

724 

725class WOFF2LocaTable(getTableClass("loca")): 

726 """Same as parent class. The only difference is that it attempts to preserve 

727 the 'indexFormat' as encoded in the WOFF2 glyf table. 

728 """ 

729 

730 def __init__(self, tag=None): 

731 self.tableTag = Tag(tag or "loca") 

732 

733 def compile(self, ttFont): 

734 try: 

735 max_location = max(self.locations) 

736 except AttributeError: 

737 self.set([]) 

738 max_location = 0 

739 if "glyf" in ttFont and hasattr(ttFont["glyf"], "indexFormat"): 

740 # copile loca using the indexFormat specified in the WOFF2 glyf table 

741 indexFormat = ttFont["glyf"].indexFormat 

742 if indexFormat == 0: 

743 if max_location >= 0x20000: 

744 raise TTLibError("indexFormat is 0 but local offsets > 0x20000") 

745 if not all(l % 2 == 0 for l in self.locations): 

746 raise TTLibError( 

747 "indexFormat is 0 but local offsets not multiples of 2" 

748 ) 

749 locations = array.array("H") 

750 for i in range(len(self.locations)): 

751 locations.append(self.locations[i] // 2) 

752 else: 

753 locations = array.array("I", self.locations) 

754 if sys.byteorder != "big": 

755 locations.byteswap() 

756 data = locations.tobytes() 

757 else: 

758 # use the most compact indexFormat given the current glyph offsets 

759 data = super(WOFF2LocaTable, self).compile(ttFont) 

760 return data 

761 

762 

763class WOFF2GlyfTable(getTableClass("glyf")): 

764 """Decoder/Encoder for WOFF2 'glyf' table transform.""" 

765 

766 subStreams = ( 

767 "nContourStream", 

768 "nPointsStream", 

769 "flagStream", 

770 "glyphStream", 

771 "compositeStream", 

772 "bboxStream", 

773 "instructionStream", 

774 ) 

775 

776 def __init__(self, tag=None): 

777 self.tableTag = Tag(tag or "glyf") 

778 

779 def reconstruct(self, data, ttFont): 

780 """Decompile transformed 'glyf' data.""" 

781 inputDataSize = len(data) 

782 

783 if inputDataSize < woff2GlyfTableFormatSize: 

784 raise TTLibError("not enough 'glyf' data") 

785 dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self) 

786 offset = woff2GlyfTableFormatSize 

787 

788 for stream in self.subStreams: 

789 size = getattr(self, stream + "Size") 

790 setattr(self, stream, data[:size]) 

791 data = data[size:] 

792 offset += size 

793 

794 hasOverlapSimpleBitmap = self.optionFlags & woff2OverlapSimpleBitmapFlag 

795 self.overlapSimpleBitmap = None 

796 if hasOverlapSimpleBitmap: 

797 overlapSimpleBitmapSize = (self.numGlyphs + 7) >> 3 

798 self.overlapSimpleBitmap = array.array("B", data[:overlapSimpleBitmapSize]) 

799 offset += overlapSimpleBitmapSize 

800 

801 if offset != inputDataSize: 

802 raise TTLibError( 

803 "incorrect size of transformed 'glyf' table: expected %d, received %d bytes" 

804 % (offset, inputDataSize) 

805 ) 

806 

807 bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 

808 bboxBitmap = self.bboxStream[:bboxBitmapSize] 

809 self.bboxBitmap = array.array("B", bboxBitmap) 

810 self.bboxStream = self.bboxStream[bboxBitmapSize:] 

811 

812 self.nContourStream = array.array("h", self.nContourStream) 

813 if sys.byteorder != "big": 

814 self.nContourStream.byteswap() 

815 assert len(self.nContourStream) == self.numGlyphs 

816 

817 if "head" in ttFont: 

818 ttFont["head"].indexToLocFormat = self.indexFormat 

819 try: 

820 self.glyphOrder = ttFont.getGlyphOrder() 

821 except: 

822 self.glyphOrder = None 

823 if self.glyphOrder is None: 

824 self.glyphOrder = [".notdef"] 

825 self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)]) 

826 else: 

827 if len(self.glyphOrder) != self.numGlyphs: 

828 raise TTLibError( 

829 "incorrect glyphOrder: expected %d glyphs, found %d" 

830 % (len(self.glyphOrder), self.numGlyphs) 

831 ) 

832 

833 glyphs = self.glyphs = {} 

834 for glyphID, glyphName in enumerate(self.glyphOrder): 

835 glyph = self._decodeGlyph(glyphID) 

836 glyphs[glyphName] = glyph 

837 

838 def transform(self, ttFont): 

839 """Return transformed 'glyf' data""" 

840 self.numGlyphs = len(self.glyphs) 

841 assert len(self.glyphOrder) == self.numGlyphs 

842 if "maxp" in ttFont: 

843 ttFont["maxp"].numGlyphs = self.numGlyphs 

844 self.indexFormat = ttFont["head"].indexToLocFormat 

845 

846 for stream in self.subStreams: 

847 setattr(self, stream, b"") 

848 bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 

849 self.bboxBitmap = array.array("B", [0] * bboxBitmapSize) 

850 

851 self.overlapSimpleBitmap = array.array("B", [0] * ((self.numGlyphs + 7) >> 3)) 

852 for glyphID in range(self.numGlyphs): 

853 try: 

854 self._encodeGlyph(glyphID) 

855 except NotImplementedError: 

856 return None 

857 hasOverlapSimpleBitmap = any(self.overlapSimpleBitmap) 

858 

859 self.bboxStream = self.bboxBitmap.tobytes() + self.bboxStream 

860 for stream in self.subStreams: 

861 setattr(self, stream + "Size", len(getattr(self, stream))) 

862 self.version = 0 

863 self.optionFlags = 0 

864 if hasOverlapSimpleBitmap: 

865 self.optionFlags |= woff2OverlapSimpleBitmapFlag 

866 data = sstruct.pack(woff2GlyfTableFormat, self) 

867 data += bytesjoin([getattr(self, s) for s in self.subStreams]) 

868 if hasOverlapSimpleBitmap: 

869 data += self.overlapSimpleBitmap.tobytes() 

870 return data 

871 

872 def _decodeGlyph(self, glyphID): 

873 glyph = getTableModule("glyf").Glyph() 

874 glyph.numberOfContours = self.nContourStream[glyphID] 

875 if glyph.numberOfContours == 0: 

876 return glyph 

877 elif glyph.isComposite(): 

878 self._decodeComponents(glyph) 

879 else: 

880 self._decodeCoordinates(glyph) 

881 self._decodeOverlapSimpleFlag(glyph, glyphID) 

882 self._decodeBBox(glyphID, glyph) 

883 return glyph 

884 

885 def _decodeComponents(self, glyph): 

886 data = self.compositeStream 

887 glyph.components = [] 

888 more = 1 

889 haveInstructions = 0 

890 while more: 

891 component = getTableModule("glyf").GlyphComponent() 

892 more, haveInstr, data = component.decompile(data, self) 

893 haveInstructions = haveInstructions | haveInstr 

894 glyph.components.append(component) 

895 self.compositeStream = data 

896 if haveInstructions: 

897 self._decodeInstructions(glyph) 

898 

899 def _decodeCoordinates(self, glyph): 

900 data = self.nPointsStream 

901 endPtsOfContours = [] 

902 endPoint = -1 

903 for i in range(glyph.numberOfContours): 

904 ptsOfContour, data = unpack255UShort(data) 

905 endPoint += ptsOfContour 

906 endPtsOfContours.append(endPoint) 

907 glyph.endPtsOfContours = endPtsOfContours 

908 self.nPointsStream = data 

909 self._decodeTriplets(glyph) 

910 self._decodeInstructions(glyph) 

911 

912 def _decodeOverlapSimpleFlag(self, glyph, glyphID): 

913 if self.overlapSimpleBitmap is None or glyph.numberOfContours <= 0: 

914 return 

915 byte = glyphID >> 3 

916 bit = glyphID & 7 

917 if self.overlapSimpleBitmap[byte] & (0x80 >> bit): 

918 glyph.flags[0] |= _g_l_y_f.flagOverlapSimple 

919 

920 def _decodeInstructions(self, glyph): 

921 glyphStream = self.glyphStream 

922 instructionStream = self.instructionStream 

923 instructionLength, glyphStream = unpack255UShort(glyphStream) 

924 glyph.program = ttProgram.Program() 

925 glyph.program.fromBytecode(instructionStream[:instructionLength]) 

926 self.glyphStream = glyphStream 

927 self.instructionStream = instructionStream[instructionLength:] 

928 

929 def _decodeBBox(self, glyphID, glyph): 

930 haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7))) 

931 if glyph.isComposite() and not haveBBox: 

932 raise TTLibError("no bbox values for composite glyph %d" % glyphID) 

933 if haveBBox: 

934 dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph) 

935 else: 

936 glyph.recalcBounds(self) 

937 

938 def _decodeTriplets(self, glyph): 

939 def withSign(flag, baseval): 

940 assert 0 <= baseval and baseval < 65536, "integer overflow" 

941 return baseval if flag & 1 else -baseval 

942 

943 nPoints = glyph.endPtsOfContours[-1] + 1 

944 flagSize = nPoints 

945 if flagSize > len(self.flagStream): 

946 raise TTLibError("not enough 'flagStream' data") 

947 flagsData = self.flagStream[:flagSize] 

948 self.flagStream = self.flagStream[flagSize:] 

949 flags = array.array("B", flagsData) 

950 

951 triplets = array.array("B", self.glyphStream) 

952 nTriplets = len(triplets) 

953 assert nPoints <= nTriplets 

954 

955 x = 0 

956 y = 0 

957 glyph.coordinates = getTableModule("glyf").GlyphCoordinates.zeros(nPoints) 

958 glyph.flags = array.array("B") 

959 tripletIndex = 0 

960 for i in range(nPoints): 

961 flag = flags[i] 

962 onCurve = not bool(flag >> 7) 

963 flag &= 0x7F 

964 if flag < 84: 

965 nBytes = 1 

966 elif flag < 120: 

967 nBytes = 2 

968 elif flag < 124: 

969 nBytes = 3 

970 else: 

971 nBytes = 4 

972 assert (tripletIndex + nBytes) <= nTriplets 

973 if flag < 10: 

974 dx = 0 

975 dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex]) 

976 elif flag < 20: 

977 dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex]) 

978 dy = 0 

979 elif flag < 84: 

980 b0 = flag - 20 

981 b1 = triplets[tripletIndex] 

982 dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4)) 

983 dy = withSign(flag >> 1, 1 + ((b0 & 0x0C) << 2) + (b1 & 0x0F)) 

984 elif flag < 120: 

985 b0 = flag - 84 

986 dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex]) 

987 dy = withSign( 

988 flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1] 

989 ) 

990 elif flag < 124: 

991 b2 = triplets[tripletIndex + 1] 

992 dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4)) 

993 dy = withSign( 

994 flag >> 1, ((b2 & 0x0F) << 8) + triplets[tripletIndex + 2] 

995 ) 

996 else: 

997 dx = withSign( 

998 flag, (triplets[tripletIndex] << 8) + triplets[tripletIndex + 1] 

999 ) 

1000 dy = withSign( 

1001 flag >> 1, 

1002 (triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3], 

1003 ) 

1004 tripletIndex += nBytes 

1005 x += dx 

1006 y += dy 

1007 glyph.coordinates[i] = (x, y) 

1008 glyph.flags.append(int(onCurve)) 

1009 bytesConsumed = tripletIndex 

1010 self.glyphStream = self.glyphStream[bytesConsumed:] 

1011 

1012 def _encodeGlyph(self, glyphID): 

1013 glyphName = self.getGlyphName(glyphID) 

1014 glyph = self[glyphName] 

1015 self.nContourStream += struct.pack(">h", glyph.numberOfContours) 

1016 if glyph.numberOfContours == 0: 

1017 return 

1018 elif glyph.isComposite(): 

1019 self._encodeComponents(glyph) 

1020 else: 

1021 self._encodeCoordinates(glyph) 

1022 self._encodeOverlapSimpleFlag(glyph, glyphID) 

1023 self._encodeBBox(glyphID, glyph) 

1024 

1025 def _encodeComponents(self, glyph): 

1026 lastcomponent = len(glyph.components) - 1 

1027 more = 1 

1028 haveInstructions = 0 

1029 for i in range(len(glyph.components)): 

1030 if i == lastcomponent: 

1031 haveInstructions = hasattr(glyph, "program") 

1032 more = 0 

1033 component = glyph.components[i] 

1034 self.compositeStream += component.compile(more, haveInstructions, self) 

1035 if haveInstructions: 

1036 self._encodeInstructions(glyph) 

1037 

1038 def _encodeCoordinates(self, glyph): 

1039 lastEndPoint = -1 

1040 if _g_l_y_f.flagCubic in glyph.flags: 

1041 raise NotImplementedError 

1042 for endPoint in glyph.endPtsOfContours: 

1043 ptsOfContour = endPoint - lastEndPoint 

1044 self.nPointsStream += pack255UShort(ptsOfContour) 

1045 lastEndPoint = endPoint 

1046 self._encodeTriplets(glyph) 

1047 self._encodeInstructions(glyph) 

1048 

1049 def _encodeOverlapSimpleFlag(self, glyph, glyphID): 

1050 if glyph.numberOfContours <= 0: 

1051 return 

1052 if glyph.flags[0] & _g_l_y_f.flagOverlapSimple: 

1053 byte = glyphID >> 3 

1054 bit = glyphID & 7 

1055 self.overlapSimpleBitmap[byte] |= 0x80 >> bit 

1056 

1057 def _encodeInstructions(self, glyph): 

1058 instructions = glyph.program.getBytecode() 

1059 self.glyphStream += pack255UShort(len(instructions)) 

1060 self.instructionStream += instructions 

1061 

1062 def _encodeBBox(self, glyphID, glyph): 

1063 assert glyph.numberOfContours != 0, "empty glyph has no bbox" 

1064 if not glyph.isComposite(): 

1065 # for simple glyphs, compare the encoded bounding box info with the calculated 

1066 # values, and if they match omit the bounding box info 

1067 currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax 

1068 calculatedBBox = calcIntBounds(glyph.coordinates) 

1069 if currentBBox == calculatedBBox: 

1070 return 

1071 self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7) 

1072 self.bboxStream += sstruct.pack(bboxFormat, glyph) 

1073 

1074 def _encodeTriplets(self, glyph): 

1075 assert len(glyph.coordinates) == len(glyph.flags) 

1076 coordinates = glyph.coordinates.copy() 

1077 coordinates.absoluteToRelative() 

1078 

1079 flags = array.array("B") 

1080 triplets = array.array("B") 

1081 for i in range(len(coordinates)): 

1082 onCurve = glyph.flags[i] & _g_l_y_f.flagOnCurve 

1083 x, y = coordinates[i] 

1084 absX = abs(x) 

1085 absY = abs(y) 

1086 onCurveBit = 0 if onCurve else 128 

1087 xSignBit = 0 if (x < 0) else 1 

1088 ySignBit = 0 if (y < 0) else 1 

1089 xySignBits = xSignBit + 2 * ySignBit 

1090 

1091 if x == 0 and absY < 1280: 

1092 flags.append(onCurveBit + ((absY & 0xF00) >> 7) + ySignBit) 

1093 triplets.append(absY & 0xFF) 

1094 elif y == 0 and absX < 1280: 

1095 flags.append(onCurveBit + 10 + ((absX & 0xF00) >> 7) + xSignBit) 

1096 triplets.append(absX & 0xFF) 

1097 elif absX < 65 and absY < 65: 

1098 flags.append( 

1099 onCurveBit 

1100 + 20 

1101 + ((absX - 1) & 0x30) 

1102 + (((absY - 1) & 0x30) >> 2) 

1103 + xySignBits 

1104 ) 

1105 triplets.append((((absX - 1) & 0xF) << 4) | ((absY - 1) & 0xF)) 

1106 elif absX < 769 and absY < 769: 

1107 flags.append( 

1108 onCurveBit 

1109 + 84 

1110 + 12 * (((absX - 1) & 0x300) >> 8) 

1111 + (((absY - 1) & 0x300) >> 6) 

1112 + xySignBits 

1113 ) 

1114 triplets.append((absX - 1) & 0xFF) 

1115 triplets.append((absY - 1) & 0xFF) 

1116 elif absX < 4096 and absY < 4096: 

1117 flags.append(onCurveBit + 120 + xySignBits) 

1118 triplets.append(absX >> 4) 

1119 triplets.append(((absX & 0xF) << 4) | (absY >> 8)) 

1120 triplets.append(absY & 0xFF) 

1121 else: 

1122 flags.append(onCurveBit + 124 + xySignBits) 

1123 triplets.append(absX >> 8) 

1124 triplets.append(absX & 0xFF) 

1125 triplets.append(absY >> 8) 

1126 triplets.append(absY & 0xFF) 

1127 

1128 self.flagStream += flags.tobytes() 

1129 self.glyphStream += triplets.tobytes() 

1130 

1131 

1132class WOFF2HmtxTable(getTableClass("hmtx")): 

1133 def __init__(self, tag=None): 

1134 self.tableTag = Tag(tag or "hmtx") 

1135 

1136 def reconstruct(self, data, ttFont): 

1137 (flags,) = struct.unpack(">B", data[:1]) 

1138 data = data[1:] 

1139 if flags & 0b11111100 != 0: 

1140 raise TTLibError("Bits 2-7 of '%s' flags are reserved" % self.tableTag) 

1141 

1142 # When bit 0 is _not_ set, the lsb[] array is present 

1143 hasLsbArray = flags & 1 == 0 

1144 # When bit 1 is _not_ set, the leftSideBearing[] array is present 

1145 hasLeftSideBearingArray = flags & 2 == 0 

1146 if hasLsbArray and hasLeftSideBearingArray: 

1147 raise TTLibError( 

1148 "either bits 0 or 1 (or both) must set in transformed '%s' flags" 

1149 % self.tableTag 

1150 ) 

1151 

1152 glyfTable = ttFont["glyf"] 

1153 headerTable = ttFont["hhea"] 

1154 glyphOrder = glyfTable.glyphOrder 

1155 numGlyphs = len(glyphOrder) 

1156 numberOfHMetrics = min(int(headerTable.numberOfHMetrics), numGlyphs) 

1157 

1158 assert len(data) >= 2 * numberOfHMetrics 

1159 advanceWidthArray = array.array("H", data[: 2 * numberOfHMetrics]) 

1160 if sys.byteorder != "big": 

1161 advanceWidthArray.byteswap() 

1162 data = data[2 * numberOfHMetrics :] 

1163 

1164 if hasLsbArray: 

1165 assert len(data) >= 2 * numberOfHMetrics 

1166 lsbArray = array.array("h", data[: 2 * numberOfHMetrics]) 

1167 if sys.byteorder != "big": 

1168 lsbArray.byteswap() 

1169 data = data[2 * numberOfHMetrics :] 

1170 else: 

1171 # compute (proportional) glyphs' lsb from their xMin 

1172 lsbArray = array.array("h") 

1173 for i, glyphName in enumerate(glyphOrder): 

1174 if i >= numberOfHMetrics: 

1175 break 

1176 glyph = glyfTable[glyphName] 

1177 xMin = getattr(glyph, "xMin", 0) 

1178 lsbArray.append(xMin) 

1179 

1180 numberOfSideBearings = numGlyphs - numberOfHMetrics 

1181 if hasLeftSideBearingArray: 

1182 assert len(data) >= 2 * numberOfSideBearings 

1183 leftSideBearingArray = array.array("h", data[: 2 * numberOfSideBearings]) 

1184 if sys.byteorder != "big": 

1185 leftSideBearingArray.byteswap() 

1186 data = data[2 * numberOfSideBearings :] 

1187 else: 

1188 # compute (monospaced) glyphs' leftSideBearing from their xMin 

1189 leftSideBearingArray = array.array("h") 

1190 for i, glyphName in enumerate(glyphOrder): 

1191 if i < numberOfHMetrics: 

1192 continue 

1193 glyph = glyfTable[glyphName] 

1194 xMin = getattr(glyph, "xMin", 0) 

1195 leftSideBearingArray.append(xMin) 

1196 

1197 if data: 

1198 raise TTLibError("too much '%s' table data" % self.tableTag) 

1199 

1200 self.metrics = {} 

1201 for i in range(numberOfHMetrics): 

1202 glyphName = glyphOrder[i] 

1203 advanceWidth, lsb = advanceWidthArray[i], lsbArray[i] 

1204 self.metrics[glyphName] = (advanceWidth, lsb) 

1205 lastAdvance = advanceWidthArray[-1] 

1206 for i in range(numberOfSideBearings): 

1207 glyphName = glyphOrder[i + numberOfHMetrics] 

1208 self.metrics[glyphName] = (lastAdvance, leftSideBearingArray[i]) 

1209 

1210 def transform(self, ttFont): 

1211 glyphOrder = ttFont.getGlyphOrder() 

1212 glyf = ttFont["glyf"] 

1213 hhea = ttFont["hhea"] 

1214 numberOfHMetrics = hhea.numberOfHMetrics 

1215 

1216 # check if any of the proportional glyphs has left sidebearings that 

1217 # differ from their xMin bounding box values. 

1218 hasLsbArray = False 

1219 for i in range(numberOfHMetrics): 

1220 glyphName = glyphOrder[i] 

1221 lsb = self.metrics[glyphName][1] 

1222 if lsb != getattr(glyf[glyphName], "xMin", 0): 

1223 hasLsbArray = True 

1224 break 

1225 

1226 # do the same for the monospaced glyphs (if any) at the end of hmtx table 

1227 hasLeftSideBearingArray = False 

1228 for i in range(numberOfHMetrics, len(glyphOrder)): 

1229 glyphName = glyphOrder[i] 

1230 lsb = self.metrics[glyphName][1] 

1231 if lsb != getattr(glyf[glyphName], "xMin", 0): 

1232 hasLeftSideBearingArray = True 

1233 break 

1234 

1235 # if we need to encode both sidebearings arrays, then no transformation is 

1236 # applicable, and we must use the untransformed hmtx data 

1237 if hasLsbArray and hasLeftSideBearingArray: 

1238 return 

1239 

1240 # set bit 0 and 1 when the respective arrays are _not_ present 

1241 flags = 0 

1242 if not hasLsbArray: 

1243 flags |= 1 << 0 

1244 if not hasLeftSideBearingArray: 

1245 flags |= 1 << 1 

1246 

1247 data = struct.pack(">B", flags) 

1248 

1249 advanceWidthArray = array.array( 

1250 "H", 

1251 [ 

1252 self.metrics[glyphName][0] 

1253 for i, glyphName in enumerate(glyphOrder) 

1254 if i < numberOfHMetrics 

1255 ], 

1256 ) 

1257 if sys.byteorder != "big": 

1258 advanceWidthArray.byteswap() 

1259 data += advanceWidthArray.tobytes() 

1260 

1261 if hasLsbArray: 

1262 lsbArray = array.array( 

1263 "h", 

1264 [ 

1265 self.metrics[glyphName][1] 

1266 for i, glyphName in enumerate(glyphOrder) 

1267 if i < numberOfHMetrics 

1268 ], 

1269 ) 

1270 if sys.byteorder != "big": 

1271 lsbArray.byteswap() 

1272 data += lsbArray.tobytes() 

1273 

1274 if hasLeftSideBearingArray: 

1275 leftSideBearingArray = array.array( 

1276 "h", 

1277 [ 

1278 self.metrics[glyphOrder[i]][1] 

1279 for i in range(numberOfHMetrics, len(glyphOrder)) 

1280 ], 

1281 ) 

1282 if sys.byteorder != "big": 

1283 leftSideBearingArray.byteswap() 

1284 data += leftSideBearingArray.tobytes() 

1285 

1286 return data 

1287 

1288 

1289class WOFF2FlavorData(WOFFFlavorData): 

1290 Flavor = "woff2" 

1291 

1292 def __init__(self, reader=None, data=None, transformedTables=None): 

1293 """Data class that holds the WOFF2 header major/minor version, any 

1294 metadata or private data (as bytes strings), and the set of 

1295 table tags that have transformations applied (if reader is not None), 

1296 or will have once the WOFF2 font is compiled. 

1297 

1298 Args: 

1299 reader: an SFNTReader (or subclass) object to read flavor data from. 

1300 data: another WOFFFlavorData object to initialise data from. 

1301 transformedTables: set of strings containing table tags to be transformed. 

1302 

1303 Raises: 

1304 ImportError if the brotli module is not installed. 

1305 

1306 NOTE: The 'reader' argument, on the one hand, and the 'data' and 

1307 'transformedTables' arguments, on the other hand, are mutually exclusive. 

1308 """ 

1309 if not haveBrotli: 

1310 raise ImportError("No module named brotli") 

1311 

1312 if reader is not None: 

1313 if data is not None: 

1314 raise TypeError("'reader' and 'data' arguments are mutually exclusive") 

1315 if transformedTables is not None: 

1316 raise TypeError( 

1317 "'reader' and 'transformedTables' arguments are mutually exclusive" 

1318 ) 

1319 

1320 if transformedTables is not None and ( 

1321 "glyf" in transformedTables 

1322 and "loca" not in transformedTables 

1323 or "loca" in transformedTables 

1324 and "glyf" not in transformedTables 

1325 ): 

1326 raise ValueError("'glyf' and 'loca' must be transformed (or not) together") 

1327 super(WOFF2FlavorData, self).__init__(reader=reader) 

1328 if reader: 

1329 transformedTables = [ 

1330 tag for tag, entry in reader.tables.items() if entry.transformed 

1331 ] 

1332 elif data: 

1333 self.majorVersion = data.majorVersion 

1334 self.majorVersion = data.minorVersion 

1335 self.metaData = data.metaData 

1336 self.privData = data.privData 

1337 if transformedTables is None and hasattr(data, "transformedTables"): 

1338 transformedTables = data.transformedTables 

1339 

1340 if transformedTables is None: 

1341 transformedTables = woff2TransformedTableTags 

1342 

1343 self.transformedTables = set(transformedTables) 

1344 

1345 def _decompress(self, rawData): 

1346 return brotli.decompress(rawData) 

1347 

1348 

1349def unpackBase128(data): 

1350 r"""Read one to five bytes from UIntBase128-encoded input string, and return 

1351 a tuple containing the decoded integer plus any leftover data. 

1352 

1353 >>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00") 

1354 True 

1355 >>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295 

1356 True 

1357 >>> unpackBase128(b'\x80\x80\x3f') # doctest: +IGNORE_EXCEPTION_DETAIL 

1358 Traceback (most recent call last): 

1359 File "<stdin>", line 1, in ? 

1360 TTLibError: UIntBase128 value must not start with leading zeros 

1361 >>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0] # doctest: +IGNORE_EXCEPTION_DETAIL 

1362 Traceback (most recent call last): 

1363 File "<stdin>", line 1, in ? 

1364 TTLibError: UIntBase128-encoded sequence is longer than 5 bytes 

1365 >>> unpackBase128(b'\x90\x80\x80\x80\x00')[0] # doctest: +IGNORE_EXCEPTION_DETAIL 

1366 Traceback (most recent call last): 

1367 File "<stdin>", line 1, in ? 

1368 TTLibError: UIntBase128 value exceeds 2**32-1 

1369 """ 

1370 if len(data) == 0: 

1371 raise TTLibError("not enough data to unpack UIntBase128") 

1372 result = 0 

1373 if byteord(data[0]) == 0x80: 

1374 # font must be rejected if UIntBase128 value starts with 0x80 

1375 raise TTLibError("UIntBase128 value must not start with leading zeros") 

1376 for i in range(woff2Base128MaxSize): 

1377 if len(data) == 0: 

1378 raise TTLibError("not enough data to unpack UIntBase128") 

1379 code = byteord(data[0]) 

1380 data = data[1:] 

1381 # if any of the top seven bits are set then we're about to overflow 

1382 if result & 0xFE000000: 

1383 raise TTLibError("UIntBase128 value exceeds 2**32-1") 

1384 # set current value = old value times 128 bitwise-or (byte bitwise-and 127) 

1385 result = (result << 7) | (code & 0x7F) 

1386 # repeat until the most significant bit of byte is false 

1387 if (code & 0x80) == 0: 

1388 # return result plus left over data 

1389 return result, data 

1390 # make sure not to exceed the size bound 

1391 raise TTLibError("UIntBase128-encoded sequence is longer than 5 bytes") 

1392 

1393 

1394def base128Size(n): 

1395 """Return the length in bytes of a UIntBase128-encoded sequence with value n. 

1396 

1397 >>> base128Size(0) 

1398 1 

1399 >>> base128Size(24567) 

1400 3 

1401 >>> base128Size(2**32-1) 

1402 5 

1403 """ 

1404 assert n >= 0 

1405 size = 1 

1406 while n >= 128: 

1407 size += 1 

1408 n >>= 7 

1409 return size 

1410 

1411 

1412def packBase128(n): 

1413 r"""Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of 

1414 bytes using UIntBase128 variable-length encoding. Produce the shortest possible 

1415 encoding. 

1416 

1417 >>> packBase128(63) == b"\x3f" 

1418 True 

1419 >>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f' 

1420 True 

1421 """ 

1422 if n < 0 or n >= 2**32: 

1423 raise TTLibError("UIntBase128 format requires 0 <= integer <= 2**32-1") 

1424 data = b"" 

1425 size = base128Size(n) 

1426 for i in range(size): 

1427 b = (n >> (7 * (size - i - 1))) & 0x7F 

1428 if i < size - 1: 

1429 b |= 0x80 

1430 data += struct.pack("B", b) 

1431 return data 

1432 

1433 

1434def unpack255UShort(data): 

1435 """Read one to three bytes from 255UInt16-encoded input string, and return a 

1436 tuple containing the decoded integer plus any leftover data. 

1437 

1438 >>> unpack255UShort(bytechr(252))[0] 

1439 252 

1440 

1441 Note that some numbers (e.g. 506) can have multiple encodings: 

1442 >>> unpack255UShort(struct.pack("BB", 254, 0))[0] 

1443 506 

1444 >>> unpack255UShort(struct.pack("BB", 255, 253))[0] 

1445 506 

1446 >>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0] 

1447 506 

1448 """ 

1449 code = byteord(data[:1]) 

1450 data = data[1:] 

1451 if code == 253: 

1452 # read two more bytes as an unsigned short 

1453 if len(data) < 2: 

1454 raise TTLibError("not enough data to unpack 255UInt16") 

1455 (result,) = struct.unpack(">H", data[:2]) 

1456 data = data[2:] 

1457 elif code == 254: 

1458 # read another byte, plus 253 * 2 

1459 if len(data) == 0: 

1460 raise TTLibError("not enough data to unpack 255UInt16") 

1461 result = byteord(data[:1]) 

1462 result += 506 

1463 data = data[1:] 

1464 elif code == 255: 

1465 # read another byte, plus 253 

1466 if len(data) == 0: 

1467 raise TTLibError("not enough data to unpack 255UInt16") 

1468 result = byteord(data[:1]) 

1469 result += 253 

1470 data = data[1:] 

1471 else: 

1472 # leave as is if lower than 253 

1473 result = code 

1474 # return result plus left over data 

1475 return result, data 

1476 

1477 

1478def pack255UShort(value): 

1479 r"""Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring 

1480 using 255UInt16 variable-length encoding. 

1481 

1482 >>> pack255UShort(252) == b'\xfc' 

1483 True 

1484 >>> pack255UShort(506) == b'\xfe\x00' 

1485 True 

1486 >>> pack255UShort(762) == b'\xfd\x02\xfa' 

1487 True 

1488 """ 

1489 if value < 0 or value > 0xFFFF: 

1490 raise TTLibError("255UInt16 format requires 0 <= integer <= 65535") 

1491 if value < 253: 

1492 return struct.pack(">B", value) 

1493 elif value < 506: 

1494 return struct.pack(">BB", 255, value - 253) 

1495 elif value < 762: 

1496 return struct.pack(">BB", 254, value - 506) 

1497 else: 

1498 return struct.pack(">BH", 253, value) 

1499 

1500 

1501def compress(input_file, output_file, transform_tables=None): 

1502 """Compress OpenType font to WOFF2. 

1503 

1504 Args: 

1505 input_file: a file path, file or file-like object (open in binary mode) 

1506 containing an OpenType font (either CFF- or TrueType-flavored). 

1507 output_file: a file path, file or file-like object where to save the 

1508 compressed WOFF2 font. 

1509 transform_tables: Optional[Iterable[str]]: a set of table tags for which 

1510 to enable preprocessing transformations. By default, only 'glyf' 

1511 and 'loca' tables are transformed. An empty set means disable all 

1512 transformations. 

1513 """ 

1514 log.info("Processing %s => %s" % (input_file, output_file)) 

1515 

1516 font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False) 

1517 font.flavor = "woff2" 

1518 

1519 if transform_tables is not None: 

1520 font.flavorData = WOFF2FlavorData( 

1521 data=font.flavorData, transformedTables=transform_tables 

1522 ) 

1523 

1524 font.save(output_file, reorderTables=False) 

1525 

1526 

1527def decompress(input_file, output_file): 

1528 """Decompress WOFF2 font to OpenType font. 

1529 

1530 Args: 

1531 input_file: a file path, file or file-like object (open in binary mode) 

1532 containing a compressed WOFF2 font. 

1533 output_file: a file path, file or file-like object where to save the 

1534 decompressed OpenType font. 

1535 """ 

1536 log.info("Processing %s => %s" % (input_file, output_file)) 

1537 

1538 font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False) 

1539 font.flavor = None 

1540 font.flavorData = None 

1541 font.save(output_file, reorderTables=True) 

1542 

1543 

1544def main(args=None): 

1545 """Compress and decompress WOFF2 fonts""" 

1546 import argparse 

1547 from fontTools import configLogger 

1548 from fontTools.ttx import makeOutputFileName 

1549 

1550 class _HelpAction(argparse._HelpAction): 

1551 def __call__(self, parser, namespace, values, option_string=None): 

1552 subparsers_actions = [ 

1553 action 

1554 for action in parser._actions 

1555 if isinstance(action, argparse._SubParsersAction) 

1556 ] 

1557 for subparsers_action in subparsers_actions: 

1558 for choice, subparser in subparsers_action.choices.items(): 

1559 print(subparser.format_help()) 

1560 parser.exit() 

1561 

1562 class _NoGlyfTransformAction(argparse.Action): 

1563 def __call__(self, parser, namespace, values, option_string=None): 

1564 namespace.transform_tables.difference_update({"glyf", "loca"}) 

1565 

1566 class _HmtxTransformAction(argparse.Action): 

1567 def __call__(self, parser, namespace, values, option_string=None): 

1568 namespace.transform_tables.add("hmtx") 

1569 

1570 parser = argparse.ArgumentParser( 

1571 prog="fonttools ttLib.woff2", description=main.__doc__, add_help=False 

1572 ) 

1573 

1574 parser.add_argument( 

1575 "-h", "--help", action=_HelpAction, help="show this help message and exit" 

1576 ) 

1577 

1578 parser_group = parser.add_subparsers(title="sub-commands") 

1579 parser_compress = parser_group.add_parser( 

1580 "compress", description="Compress a TTF or OTF font to WOFF2" 

1581 ) 

1582 parser_decompress = parser_group.add_parser( 

1583 "decompress", description="Decompress a WOFF2 font to OTF" 

1584 ) 

1585 

1586 for subparser in (parser_compress, parser_decompress): 

1587 group = subparser.add_mutually_exclusive_group(required=False) 

1588 group.add_argument( 

1589 "-v", 

1590 "--verbose", 

1591 action="store_true", 

1592 help="print more messages to console", 

1593 ) 

1594 group.add_argument( 

1595 "-q", 

1596 "--quiet", 

1597 action="store_true", 

1598 help="do not print messages to console", 

1599 ) 

1600 

1601 parser_compress.add_argument( 

1602 "input_file", 

1603 metavar="INPUT", 

1604 help="the input OpenType font (.ttf or .otf)", 

1605 ) 

1606 parser_decompress.add_argument( 

1607 "input_file", 

1608 metavar="INPUT", 

1609 help="the input WOFF2 font", 

1610 ) 

1611 

1612 parser_compress.add_argument( 

1613 "-o", 

1614 "--output-file", 

1615 metavar="OUTPUT", 

1616 help="the output WOFF2 font", 

1617 ) 

1618 parser_decompress.add_argument( 

1619 "-o", 

1620 "--output-file", 

1621 metavar="OUTPUT", 

1622 help="the output OpenType font", 

1623 ) 

1624 

1625 transform_group = parser_compress.add_argument_group() 

1626 transform_group.add_argument( 

1627 "--no-glyf-transform", 

1628 dest="transform_tables", 

1629 nargs=0, 

1630 action=_NoGlyfTransformAction, 

1631 help="Do not transform glyf (and loca) tables", 

1632 ) 

1633 transform_group.add_argument( 

1634 "--hmtx-transform", 

1635 dest="transform_tables", 

1636 nargs=0, 

1637 action=_HmtxTransformAction, 

1638 help="Enable optional transformation for 'hmtx' table", 

1639 ) 

1640 

1641 parser_compress.set_defaults( 

1642 subcommand=compress, 

1643 transform_tables={"glyf", "loca"}, 

1644 ) 

1645 parser_decompress.set_defaults(subcommand=decompress) 

1646 

1647 options = vars(parser.parse_args(args)) 

1648 

1649 subcommand = options.pop("subcommand", None) 

1650 if not subcommand: 

1651 parser.print_help() 

1652 return 

1653 

1654 quiet = options.pop("quiet") 

1655 verbose = options.pop("verbose") 

1656 configLogger( 

1657 level=("ERROR" if quiet else "DEBUG" if verbose else "INFO"), 

1658 ) 

1659 

1660 if not options["output_file"]: 

1661 if subcommand is compress: 

1662 extension = ".woff2" 

1663 elif subcommand is decompress: 

1664 # choose .ttf/.otf file extension depending on sfntVersion 

1665 with open(options["input_file"], "rb") as f: 

1666 f.seek(4) # skip 'wOF2' signature 

1667 sfntVersion = f.read(4) 

1668 assert len(sfntVersion) == 4, "not enough data" 

1669 extension = ".otf" if sfntVersion == b"OTTO" else ".ttf" 

1670 else: 

1671 raise AssertionError(subcommand) 

1672 options["output_file"] = makeOutputFileName( 

1673 options["input_file"], outputDir=None, extension=extension 

1674 ) 

1675 

1676 try: 

1677 subcommand(**options) 

1678 except TTLibError as e: 

1679 parser.error(e) 

1680 

1681 

1682if __name__ == "__main__": 

1683 sys.exit(main())