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

970 statements  

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

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 

46 flavor = "woff2" 

47 

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

49 if not haveBrotli: 

50 log.error( 

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

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

53 ) 

54 raise ImportError("No module named brotli") 

55 

56 self.file = file 

57 

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

59 if signature != b"wOF2": 

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

61 

62 self.file.seek(0) 

63 self.DirectoryEntry = WOFF2DirectoryEntry 

64 data = self.file.read(woff2DirectorySize) 

65 if len(data) != woff2DirectorySize: 

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

67 sstruct.unpack(woff2DirectoryFormat, data, self) 

68 

69 self.tables = OrderedDict() 

70 offset = 0 

71 for i in range(self.numTables): 

72 entry = self.DirectoryEntry() 

73 entry.fromFile(self.file) 

74 tag = Tag(entry.tag) 

75 self.tables[tag] = entry 

76 entry.offset = offset 

77 offset += entry.length 

78 

79 totalUncompressedSize = offset 

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

81 decompressedData = brotli.decompress(compressedData) 

82 if len(decompressedData) != totalUncompressedSize: 

83 raise TTLibError( 

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

85 % (totalUncompressedSize, len(decompressedData)) 

86 ) 

87 self.transformBuffer = BytesIO(decompressedData) 

88 

89 self.file.seek(0, 2) 

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

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

92 

93 self.flavorData = WOFF2FlavorData(self) 

94 

95 # make empty TTFont to store data while reconstructing tables 

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

97 

98 def __getitem__(self, tag): 

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

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

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

102 if entry.transformed: 

103 entry.data = self.reconstructTable(tag) 

104 else: 

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

106 return entry.data 

107 

108 def reconstructTable(self, tag): 

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

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

111 rawData = entry.loadData(self.transformBuffer) 

112 if tag == "glyf": 

113 # no need to pad glyph data when reconstructing 

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

115 data = self._reconstructGlyf(rawData, padding) 

116 elif tag == "loca": 

117 data = self._reconstructLoca() 

118 elif tag == "hmtx": 

119 data = self._reconstructHmtx(rawData) 

120 else: 

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

122 return data 

123 

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

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

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

127 """ 

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

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

130 glyfTable.reconstruct(data, self.ttFont) 

131 if padding: 

132 glyfTable.padding = padding 

133 data = glyfTable.compile(self.ttFont) 

134 return data 

135 

136 def _reconstructLoca(self): 

137 """Return reconstructed loca table data.""" 

138 if "loca" not in self.ttFont: 

139 # make sure glyf is reconstructed first 

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

141 locaTable = self.ttFont["loca"] 

142 data = locaTable.compile(self.ttFont) 

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

144 raise TTLibError( 

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

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

147 ) 

148 return data 

149 

150 def _reconstructHmtx(self, data): 

151 """Return reconstructed hmtx table data.""" 

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

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

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

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

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

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

158 else: 

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

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

161 for tag in tableDependencies: 

162 self._decompileTable(tag) 

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

164 hmtxTable.reconstruct(data, self.ttFont) 

165 data = hmtxTable.compile(self.ttFont) 

166 return data 

167 

168 def _decompileTable(self, tag): 

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

170 data = self[tag] 

171 if self.ttFont.isLoaded(tag): 

172 return self.ttFont[tag] 

173 tableClass = getTableClass(tag) 

174 table = tableClass(tag) 

175 self.ttFont.tables[tag] = table 

176 table.decompile(data, self.ttFont) 

177 

178 

179class WOFF2Writer(SFNTWriter): 

180 

181 flavor = "woff2" 

182 

183 def __init__( 

184 self, 

185 file, 

186 numTables, 

187 sfntVersion="\000\001\000\000", 

188 flavor=None, 

189 flavorData=None, 

190 ): 

191 if not haveBrotli: 

192 log.error( 

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

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

195 ) 

196 raise ImportError("No module named brotli") 

197 

198 self.file = file 

199 self.numTables = numTables 

200 self.sfntVersion = Tag(sfntVersion) 

201 self.flavorData = WOFF2FlavorData(data=flavorData) 

202 

203 self.directoryFormat = woff2DirectoryFormat 

204 self.directorySize = woff2DirectorySize 

205 self.DirectoryEntry = WOFF2DirectoryEntry 

206 

207 self.signature = Tag("wOF2") 

208 

209 self.nextTableOffset = 0 

210 self.transformBuffer = BytesIO() 

211 

212 self.tables = OrderedDict() 

213 

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

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

216 

217 def __setitem__(self, tag, data): 

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

219 if tag in self.tables: 

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

221 if tag == "DSIG": 

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

223 self.numTables -= 1 

224 return 

225 

226 entry = self.DirectoryEntry() 

227 entry.tag = Tag(tag) 

228 entry.flags = getKnownTagIndex(entry.tag) 

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

230 # have been specified 

231 entry.data = data 

232 

233 self.tables[tag] = entry 

234 

235 def close(self): 

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

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

238 raise TTLibError( 

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

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

241 ) 

242 

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

244 isTrueType = True 

245 elif self.sfntVersion == "OTTO": 

246 isTrueType = False 

247 else: 

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

249 

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

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

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

253 # See: 

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

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

256 if ( 

257 isTrueType 

258 and "glyf" in self.flavorData.transformedTables 

259 and "glyf" in self.tables 

260 ): 

261 self._normaliseGlyfAndLoca(padding=4) 

262 self._setHeadTransformFlag() 

263 

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

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

266 # See: 

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

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

269 # 

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

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

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

273 

274 self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() 

275 

276 fontData = self._transformTables() 

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

278 

279 self.totalCompressedSize = len(compressedFont) 

280 self.length = self._calcTotalSize() 

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

282 self.reserved = 0 

283 

284 directory = self._packTableDirectory() 

285 self.file.seek(0) 

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

287 self._writeFlavorData() 

288 

289 def _normaliseGlyfAndLoca(self, padding=4): 

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

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

292 compiling loca. 

293 """ 

294 if self.sfntVersion == "OTTO": 

295 return 

296 

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

298 if tag in self.tables: 

299 self._decompileTable(tag) 

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

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

302 self._compileTable(tag) 

303 

304 def _setHeadTransformFlag(self): 

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

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

307 self._decompileTable("head") 

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

309 self._compileTable("head") 

310 

311 def _decompileTable(self, tag): 

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

313 tag = Tag(tag) 

314 if tag not in self.tables: 

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

316 if self.ttFont.isLoaded(tag): 

317 return 

318 data = self.tables[tag].data 

319 if tag == "loca": 

320 tableClass = WOFF2LocaTable 

321 elif tag == "glyf": 

322 tableClass = WOFF2GlyfTable 

323 elif tag == "hmtx": 

324 tableClass = WOFF2HmtxTable 

325 else: 

326 tableClass = getTableClass(tag) 

327 table = tableClass(tag) 

328 self.ttFont.tables[tag] = table 

329 table.decompile(data, self.ttFont) 

330 

331 def _compileTable(self, tag): 

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

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

334 

335 def _calcSFNTChecksumsLengthsAndOffsets(self): 

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

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

338 """ 

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

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

341 data = entry.data 

342 entry.origOffset = offset 

343 entry.origLength = len(data) 

344 if tag == "head": 

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

346 else: 

347 entry.checkSum = calcChecksum(data) 

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

349 return offset 

350 

351 def _transformTables(self): 

352 """Return transformed font data.""" 

353 transformedTables = self.flavorData.transformedTables 

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

355 data = None 

356 if tag in transformedTables: 

357 data = self.transformTable(tag) 

358 if data is not None: 

359 entry.transformed = True 

360 if data is None: 

361 if tag == "glyf": 

362 # Currently we always sort table tags so 

363 # 'loca' comes after 'glyf'. 

364 transformedTables.discard("loca") 

365 # pass-through the table data without transformation 

366 data = entry.data 

367 entry.transformed = False 

368 entry.offset = self.nextTableOffset 

369 entry.saveData(self.transformBuffer, data) 

370 self.nextTableOffset += entry.length 

371 self.writeMasterChecksum() 

372 fontData = self.transformBuffer.getvalue() 

373 return fontData 

374 

375 def transformTable(self, tag): 

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

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

378 """ 

379 if tag == "loca": 

380 data = b"" 

381 elif tag == "glyf": 

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

383 self._decompileTable(tag) 

384 glyfTable = self.ttFont["glyf"] 

385 data = glyfTable.transform(self.ttFont) 

386 elif tag == "hmtx": 

387 if "glyf" not in self.tables: 

388 return 

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

390 self._decompileTable(tag) 

391 hmtxTable = self.ttFont["hmtx"] 

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

393 else: 

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

395 return data 

396 

397 def _calcMasterChecksum(self): 

398 """Calculate checkSumAdjustment.""" 

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

400 checksums = [] 

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

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

403 

404 # Create a SFNT directory for checksum calculation purposes 

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

406 self.numTables, 16 

407 ) 

408 directory = sstruct.pack(sfntDirectoryFormat, self) 

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

410 for tag, entry in tables: 

411 sfntEntry = SFNTDirectoryEntry() 

412 sfntEntry.tag = entry.tag 

413 sfntEntry.checkSum = entry.checkSum 

414 sfntEntry.offset = entry.origOffset 

415 sfntEntry.length = entry.origLength 

416 directory = directory + sfntEntry.toString() 

417 

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

419 assert directory_end == len(directory) 

420 

421 checksums.append(calcChecksum(directory)) 

422 checksum = sum(checksums) & 0xFFFFFFFF 

423 # BiboAfba! 

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

425 return checksumadjustment 

426 

427 def writeMasterChecksum(self): 

428 """Write checkSumAdjustment to the transformBuffer.""" 

429 checksumadjustment = self._calcMasterChecksum() 

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

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

432 

433 def _calcTotalSize(self): 

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

435 offset = self.directorySize 

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

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

438 offset += self.totalCompressedSize 

439 offset = (offset + 3) & ~3 

440 offset = self._calcFlavorDataOffsetsAndSize(offset) 

441 return offset 

442 

443 def _calcFlavorDataOffsetsAndSize(self, start): 

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

445 offset = start 

446 data = self.flavorData 

447 if data.metaData: 

448 self.metaOrigLength = len(data.metaData) 

449 self.metaOffset = offset 

450 self.compressedMetaData = brotli.compress( 

451 data.metaData, mode=brotli.MODE_TEXT 

452 ) 

453 self.metaLength = len(self.compressedMetaData) 

454 offset += self.metaLength 

455 else: 

456 self.metaOffset = self.metaLength = self.metaOrigLength = 0 

457 self.compressedMetaData = b"" 

458 if data.privData: 

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

460 offset = (offset + 3) & ~3 

461 self.privOffset = offset 

462 self.privLength = len(data.privData) 

463 offset += self.privLength 

464 else: 

465 self.privOffset = self.privLength = 0 

466 return offset 

467 

468 def _getVersion(self): 

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

470 data = self.flavorData 

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

472 return data.majorVersion, data.minorVersion 

473 else: 

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

475 if "head" in self.tables: 

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

477 else: 

478 return 0, 0 

479 

480 def _packTableDirectory(self): 

481 """Return WOFF2 table directory data.""" 

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

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

484 directory = directory + entry.toString() 

485 return directory 

486 

487 def _writeFlavorData(self): 

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

489 compressedMetaData = self.compressedMetaData 

490 privData = self.flavorData.privData 

491 if compressedMetaData and privData: 

492 compressedMetaData = pad(compressedMetaData, size=4) 

493 if compressedMetaData: 

494 self.file.seek(self.metaOffset) 

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

496 self.file.write(compressedMetaData) 

497 if privData: 

498 self.file.seek(self.privOffset) 

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

500 self.file.write(privData) 

501 

502 def reordersTables(self): 

503 return True 

504 

505 

506# -- woff2 directory helpers and cruft 

507 

508woff2DirectoryFormat = """ 

509 > # big endian 

510 signature: 4s # "wOF2" 

511 sfntVersion: 4s 

512 length: L # total woff2 file size 

513 numTables: H # number of tables 

514 reserved: H # set to 0 

515 totalSfntSize: L # uncompressed size 

516 totalCompressedSize: L # compressed size 

517 majorVersion: H # major version of WOFF file 

518 minorVersion: H # minor version of WOFF file 

519 metaOffset: L # offset to metadata block 

520 metaLength: L # length of compressed metadata 

521 metaOrigLength: L # length of uncompressed metadata 

522 privOffset: L # offset to private data block 

523 privLength: L # length of private data block 

524""" 

525 

526woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat) 

527 

528woff2KnownTags = ( 

529 "cmap", 

530 "head", 

531 "hhea", 

532 "hmtx", 

533 "maxp", 

534 "name", 

535 "OS/2", 

536 "post", 

537 "cvt ", 

538 "fpgm", 

539 "glyf", 

540 "loca", 

541 "prep", 

542 "CFF ", 

543 "VORG", 

544 "EBDT", 

545 "EBLC", 

546 "gasp", 

547 "hdmx", 

548 "kern", 

549 "LTSH", 

550 "PCLT", 

551 "VDMX", 

552 "vhea", 

553 "vmtx", 

554 "BASE", 

555 "GDEF", 

556 "GPOS", 

557 "GSUB", 

558 "EBSC", 

559 "JSTF", 

560 "MATH", 

561 "CBDT", 

562 "CBLC", 

563 "COLR", 

564 "CPAL", 

565 "SVG ", 

566 "sbix", 

567 "acnt", 

568 "avar", 

569 "bdat", 

570 "bloc", 

571 "bsln", 

572 "cvar", 

573 "fdsc", 

574 "feat", 

575 "fmtx", 

576 "fvar", 

577 "gvar", 

578 "hsty", 

579 "just", 

580 "lcar", 

581 "mort", 

582 "morx", 

583 "opbd", 

584 "prop", 

585 "trak", 

586 "Zapf", 

587 "Silf", 

588 "Glat", 

589 "Gloc", 

590 "Feat", 

591 "Sill", 

592) 

593 

594woff2FlagsFormat = """ 

595 > # big endian 

596 flags: B # table type and flags 

597""" 

598 

599woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat) 

600 

601woff2UnknownTagFormat = """ 

602 > # big endian 

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

604""" 

605 

606woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat) 

607 

608woff2UnknownTagIndex = 0x3F 

609 

610woff2Base128MaxSize = 5 

611woff2DirectoryEntryMaxSize = ( 

612 woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize 

613) 

614 

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

616 

617woff2GlyfTableFormat = """ 

618 > # big endian 

619 version: H # = 0x0000 

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

621 numGlyphs: H # Number of glyphs 

622 indexFormat: H # Offset format for loca table 

623 nContourStreamSize: L # Size of nContour stream 

624 nPointsStreamSize: L # Size of nPoints stream 

625 flagStreamSize: L # Size of flag stream 

626 glyphStreamSize: L # Size of glyph stream 

627 compositeStreamSize: L # Size of composite stream 

628 bboxStreamSize: L # Comnined size of bboxBitmap and bboxStream 

629 instructionStreamSize: L # Size of instruction stream 

630""" 

631 

632woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat) 

633 

634bboxFormat = """ 

635 > # big endian 

636 xMin: h 

637 yMin: h 

638 xMax: h 

639 yMax: h 

640""" 

641 

642woff2OverlapSimpleBitmapFlag = 0x0001 

643 

644 

645def getKnownTagIndex(tag): 

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

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

648 if tag == woff2KnownTags[i]: 

649 return i 

650 return woff2UnknownTagIndex 

651 

652 

653class WOFF2DirectoryEntry(DirectoryEntry): 

654 def fromFile(self, file): 

655 pos = file.tell() 

656 data = file.read(woff2DirectoryEntryMaxSize) 

657 left = self.fromString(data) 

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

659 file.seek(pos + consumed) 

660 

661 def fromString(self, data): 

662 if len(data) < 1: 

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

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

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

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

667 if len(data) < woff2UnknownTagSize: 

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

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

670 else: 

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

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

673 self.tag = Tag(self.tag) 

674 self.origLength, data = unpackBase128(data) 

675 self.length = self.origLength 

676 if self.transformed: 

677 self.length, data = unpackBase128(data) 

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

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

680 # return left over data 

681 return data 

682 

683 def toString(self): 

684 data = bytechr(self.flags) 

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

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

687 data += packBase128(self.origLength) 

688 if self.transformed: 

689 data += packBase128(self.length) 

690 return data 

691 

692 @property 

693 def transformVersion(self): 

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

695 transformation version number (between 0 and 3). 

696 """ 

697 return self.flags >> 6 

698 

699 @transformVersion.setter 

700 def transformVersion(self, value): 

701 assert 0 <= value <= 3 

702 self.flags |= value << 6 

703 

704 @property 

705 def transformed(self): 

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

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

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

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

710 # transformation version 3 indicates the null transform 

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

712 return self.transformVersion != 3 

713 else: 

714 return self.transformVersion != 0 

715 

716 @transformed.setter 

717 def transformed(self, booleanValue): 

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

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

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

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

722 self.transformVersion = 3 if not booleanValue else 0 

723 else: 

724 self.transformVersion = int(booleanValue) 

725 

726 

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

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

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

730 """ 

731 

732 def __init__(self, tag=None): 

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

734 

735 def compile(self, ttFont): 

736 try: 

737 max_location = max(self.locations) 

738 except AttributeError: 

739 self.set([]) 

740 max_location = 0 

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

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

743 indexFormat = ttFont["glyf"].indexFormat 

744 if indexFormat == 0: 

745 if max_location >= 0x20000: 

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

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

748 raise TTLibError( 

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

750 ) 

751 locations = array.array("H") 

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

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

754 else: 

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

756 if sys.byteorder != "big": 

757 locations.byteswap() 

758 data = locations.tobytes() 

759 else: 

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

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

762 return data 

763 

764 

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

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

767 

768 subStreams = ( 

769 "nContourStream", 

770 "nPointsStream", 

771 "flagStream", 

772 "glyphStream", 

773 "compositeStream", 

774 "bboxStream", 

775 "instructionStream", 

776 ) 

777 

778 def __init__(self, tag=None): 

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

780 

781 def reconstruct(self, data, ttFont): 

782 """Decompile transformed 'glyf' data.""" 

783 inputDataSize = len(data) 

784 

785 if inputDataSize < woff2GlyfTableFormatSize: 

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

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

788 offset = woff2GlyfTableFormatSize 

789 

790 for stream in self.subStreams: 

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

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

793 data = data[size:] 

794 offset += size 

795 

796 hasOverlapSimpleBitmap = self.optionFlags & woff2OverlapSimpleBitmapFlag 

797 self.overlapSimpleBitmap = None 

798 if hasOverlapSimpleBitmap: 

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

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

801 offset += overlapSimpleBitmapSize 

802 

803 if offset != inputDataSize: 

804 raise TTLibError( 

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

806 % (offset, inputDataSize) 

807 ) 

808 

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

810 bboxBitmap = self.bboxStream[:bboxBitmapSize] 

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

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

813 

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

815 if sys.byteorder != "big": 

816 self.nContourStream.byteswap() 

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

818 

819 if "head" in ttFont: 

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

821 try: 

822 self.glyphOrder = ttFont.getGlyphOrder() 

823 except: 

824 self.glyphOrder = None 

825 if self.glyphOrder is None: 

826 self.glyphOrder = [".notdef"] 

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

828 else: 

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

830 raise TTLibError( 

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

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

833 ) 

834 

835 glyphs = self.glyphs = {} 

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

837 glyph = self._decodeGlyph(glyphID) 

838 glyphs[glyphName] = glyph 

839 

840 def transform(self, ttFont): 

841 """Return transformed 'glyf' data""" 

842 self.numGlyphs = len(self.glyphs) 

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

844 if "maxp" in ttFont: 

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

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

847 

848 for stream in self.subStreams: 

849 setattr(self, stream, b"") 

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

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

852 

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

854 for glyphID in range(self.numGlyphs): 

855 try: 

856 self._encodeGlyph(glyphID) 

857 except NotImplementedError: 

858 return None 

859 hasOverlapSimpleBitmap = any(self.overlapSimpleBitmap) 

860 

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

862 for stream in self.subStreams: 

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

864 self.version = 0 

865 self.optionFlags = 0 

866 if hasOverlapSimpleBitmap: 

867 self.optionFlags |= woff2OverlapSimpleBitmapFlag 

868 data = sstruct.pack(woff2GlyfTableFormat, self) 

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

870 if hasOverlapSimpleBitmap: 

871 data += self.overlapSimpleBitmap.tobytes() 

872 return data 

873 

874 def _decodeGlyph(self, glyphID): 

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

876 glyph.numberOfContours = self.nContourStream[glyphID] 

877 if glyph.numberOfContours == 0: 

878 return glyph 

879 elif glyph.isComposite(): 

880 self._decodeComponents(glyph) 

881 else: 

882 self._decodeCoordinates(glyph) 

883 self._decodeOverlapSimpleFlag(glyph, glyphID) 

884 self._decodeBBox(glyphID, glyph) 

885 return glyph 

886 

887 def _decodeComponents(self, glyph): 

888 data = self.compositeStream 

889 glyph.components = [] 

890 more = 1 

891 haveInstructions = 0 

892 while more: 

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

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

895 haveInstructions = haveInstructions | haveInstr 

896 glyph.components.append(component) 

897 self.compositeStream = data 

898 if haveInstructions: 

899 self._decodeInstructions(glyph) 

900 

901 def _decodeCoordinates(self, glyph): 

902 data = self.nPointsStream 

903 endPtsOfContours = [] 

904 endPoint = -1 

905 for i in range(glyph.numberOfContours): 

906 ptsOfContour, data = unpack255UShort(data) 

907 endPoint += ptsOfContour 

908 endPtsOfContours.append(endPoint) 

909 glyph.endPtsOfContours = endPtsOfContours 

910 self.nPointsStream = data 

911 self._decodeTriplets(glyph) 

912 self._decodeInstructions(glyph) 

913 

914 def _decodeOverlapSimpleFlag(self, glyph, glyphID): 

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

916 return 

917 byte = glyphID >> 3 

918 bit = glyphID & 7 

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

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

921 

922 def _decodeInstructions(self, glyph): 

923 glyphStream = self.glyphStream 

924 instructionStream = self.instructionStream 

925 instructionLength, glyphStream = unpack255UShort(glyphStream) 

926 glyph.program = ttProgram.Program() 

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

928 self.glyphStream = glyphStream 

929 self.instructionStream = instructionStream[instructionLength:] 

930 

931 def _decodeBBox(self, glyphID, glyph): 

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

933 if glyph.isComposite() and not haveBBox: 

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

935 if haveBBox: 

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

937 else: 

938 glyph.recalcBounds(self) 

939 

940 def _decodeTriplets(self, glyph): 

941 def withSign(flag, baseval): 

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

943 return baseval if flag & 1 else -baseval 

944 

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

946 flagSize = nPoints 

947 if flagSize > len(self.flagStream): 

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

949 flagsData = self.flagStream[:flagSize] 

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

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

952 

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

954 nTriplets = len(triplets) 

955 assert nPoints <= nTriplets 

956 

957 x = 0 

958 y = 0 

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

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

961 tripletIndex = 0 

962 for i in range(nPoints): 

963 flag = flags[i] 

964 onCurve = not bool(flag >> 7) 

965 flag &= 0x7F 

966 if flag < 84: 

967 nBytes = 1 

968 elif flag < 120: 

969 nBytes = 2 

970 elif flag < 124: 

971 nBytes = 3 

972 else: 

973 nBytes = 4 

974 assert (tripletIndex + nBytes) <= nTriplets 

975 if flag < 10: 

976 dx = 0 

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

978 elif flag < 20: 

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

980 dy = 0 

981 elif flag < 84: 

982 b0 = flag - 20 

983 b1 = triplets[tripletIndex] 

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

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

986 elif flag < 120: 

987 b0 = flag - 84 

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

989 dy = withSign( 

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

991 ) 

992 elif flag < 124: 

993 b2 = triplets[tripletIndex + 1] 

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

995 dy = withSign( 

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

997 ) 

998 else: 

999 dx = withSign( 

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

1001 ) 

1002 dy = withSign( 

1003 flag >> 1, 

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

1005 ) 

1006 tripletIndex += nBytes 

1007 x += dx 

1008 y += dy 

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

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

1011 bytesConsumed = tripletIndex 

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

1013 

1014 def _encodeGlyph(self, glyphID): 

1015 glyphName = self.getGlyphName(glyphID) 

1016 glyph = self[glyphName] 

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

1018 if glyph.numberOfContours == 0: 

1019 return 

1020 elif glyph.isComposite(): 

1021 self._encodeComponents(glyph) 

1022 elif glyph.isVarComposite(): 

1023 raise NotImplementedError 

1024 else: 

1025 self._encodeCoordinates(glyph) 

1026 self._encodeOverlapSimpleFlag(glyph, glyphID) 

1027 self._encodeBBox(glyphID, glyph) 

1028 

1029 def _encodeComponents(self, glyph): 

1030 lastcomponent = len(glyph.components) - 1 

1031 more = 1 

1032 haveInstructions = 0 

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

1034 if i == lastcomponent: 

1035 haveInstructions = hasattr(glyph, "program") 

1036 more = 0 

1037 component = glyph.components[i] 

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

1039 if haveInstructions: 

1040 self._encodeInstructions(glyph) 

1041 

1042 def _encodeCoordinates(self, glyph): 

1043 lastEndPoint = -1 

1044 if _g_l_y_f.flagCubic in glyph.flags: 

1045 raise NotImplementedError 

1046 for endPoint in glyph.endPtsOfContours: 

1047 ptsOfContour = endPoint - lastEndPoint 

1048 self.nPointsStream += pack255UShort(ptsOfContour) 

1049 lastEndPoint = endPoint 

1050 self._encodeTriplets(glyph) 

1051 self._encodeInstructions(glyph) 

1052 

1053 def _encodeOverlapSimpleFlag(self, glyph, glyphID): 

1054 if glyph.numberOfContours <= 0: 

1055 return 

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

1057 byte = glyphID >> 3 

1058 bit = glyphID & 7 

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

1060 

1061 def _encodeInstructions(self, glyph): 

1062 instructions = glyph.program.getBytecode() 

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

1064 self.instructionStream += instructions 

1065 

1066 def _encodeBBox(self, glyphID, glyph): 

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

1068 if not glyph.isComposite(): 

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

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

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

1072 calculatedBBox = calcIntBounds(glyph.coordinates) 

1073 if currentBBox == calculatedBBox: 

1074 return 

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

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

1077 

1078 def _encodeTriplets(self, glyph): 

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

1080 coordinates = glyph.coordinates.copy() 

1081 coordinates.absoluteToRelative() 

1082 

1083 flags = array.array("B") 

1084 triplets = array.array("B") 

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

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

1087 x, y = coordinates[i] 

1088 absX = abs(x) 

1089 absY = abs(y) 

1090 onCurveBit = 0 if onCurve else 128 

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

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

1093 xySignBits = xSignBit + 2 * ySignBit 

1094 

1095 if x == 0 and absY < 1280: 

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

1097 triplets.append(absY & 0xFF) 

1098 elif y == 0 and absX < 1280: 

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

1100 triplets.append(absX & 0xFF) 

1101 elif absX < 65 and absY < 65: 

1102 flags.append( 

1103 onCurveBit 

1104 + 20 

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

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

1107 + xySignBits 

1108 ) 

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

1110 elif absX < 769 and absY < 769: 

1111 flags.append( 

1112 onCurveBit 

1113 + 84 

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

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

1116 + xySignBits 

1117 ) 

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

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

1120 elif absX < 4096 and absY < 4096: 

1121 flags.append(onCurveBit + 120 + xySignBits) 

1122 triplets.append(absX >> 4) 

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

1124 triplets.append(absY & 0xFF) 

1125 else: 

1126 flags.append(onCurveBit + 124 + xySignBits) 

1127 triplets.append(absX >> 8) 

1128 triplets.append(absX & 0xFF) 

1129 triplets.append(absY >> 8) 

1130 triplets.append(absY & 0xFF) 

1131 

1132 self.flagStream += flags.tobytes() 

1133 self.glyphStream += triplets.tobytes() 

1134 

1135 

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

1137 def __init__(self, tag=None): 

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

1139 

1140 def reconstruct(self, data, ttFont): 

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

1142 data = data[1:] 

1143 if flags & 0b11111100 != 0: 

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

1145 

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

1147 hasLsbArray = flags & 1 == 0 

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

1149 hasLeftSideBearingArray = flags & 2 == 0 

1150 if hasLsbArray and hasLeftSideBearingArray: 

1151 raise TTLibError( 

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

1153 % self.tableTag 

1154 ) 

1155 

1156 glyfTable = ttFont["glyf"] 

1157 headerTable = ttFont["hhea"] 

1158 glyphOrder = glyfTable.glyphOrder 

1159 numGlyphs = len(glyphOrder) 

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

1161 

1162 assert len(data) >= 2 * numberOfHMetrics 

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

1164 if sys.byteorder != "big": 

1165 advanceWidthArray.byteswap() 

1166 data = data[2 * numberOfHMetrics :] 

1167 

1168 if hasLsbArray: 

1169 assert len(data) >= 2 * numberOfHMetrics 

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

1171 if sys.byteorder != "big": 

1172 lsbArray.byteswap() 

1173 data = data[2 * numberOfHMetrics :] 

1174 else: 

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

1176 lsbArray = array.array("h") 

1177 for i, glyphName in enumerate(glyphOrder): 

1178 if i >= numberOfHMetrics: 

1179 break 

1180 glyph = glyfTable[glyphName] 

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

1182 lsbArray.append(xMin) 

1183 

1184 numberOfSideBearings = numGlyphs - numberOfHMetrics 

1185 if hasLeftSideBearingArray: 

1186 assert len(data) >= 2 * numberOfSideBearings 

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

1188 if sys.byteorder != "big": 

1189 leftSideBearingArray.byteswap() 

1190 data = data[2 * numberOfSideBearings :] 

1191 else: 

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

1193 leftSideBearingArray = array.array("h") 

1194 for i, glyphName in enumerate(glyphOrder): 

1195 if i < numberOfHMetrics: 

1196 continue 

1197 glyph = glyfTable[glyphName] 

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

1199 leftSideBearingArray.append(xMin) 

1200 

1201 if data: 

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

1203 

1204 self.metrics = {} 

1205 for i in range(numberOfHMetrics): 

1206 glyphName = glyphOrder[i] 

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

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

1209 lastAdvance = advanceWidthArray[-1] 

1210 for i in range(numberOfSideBearings): 

1211 glyphName = glyphOrder[i + numberOfHMetrics] 

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

1213 

1214 def transform(self, ttFont): 

1215 glyphOrder = ttFont.getGlyphOrder() 

1216 glyf = ttFont["glyf"] 

1217 hhea = ttFont["hhea"] 

1218 numberOfHMetrics = hhea.numberOfHMetrics 

1219 

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

1221 # differ from their xMin bounding box values. 

1222 hasLsbArray = False 

1223 for i in range(numberOfHMetrics): 

1224 glyphName = glyphOrder[i] 

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

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

1227 hasLsbArray = True 

1228 break 

1229 

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

1231 hasLeftSideBearingArray = False 

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

1233 glyphName = glyphOrder[i] 

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

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

1236 hasLeftSideBearingArray = True 

1237 break 

1238 

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

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

1241 if hasLsbArray and hasLeftSideBearingArray: 

1242 return 

1243 

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

1245 flags = 0 

1246 if not hasLsbArray: 

1247 flags |= 1 << 0 

1248 if not hasLeftSideBearingArray: 

1249 flags |= 1 << 1 

1250 

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

1252 

1253 advanceWidthArray = array.array( 

1254 "H", 

1255 [ 

1256 self.metrics[glyphName][0] 

1257 for i, glyphName in enumerate(glyphOrder) 

1258 if i < numberOfHMetrics 

1259 ], 

1260 ) 

1261 if sys.byteorder != "big": 

1262 advanceWidthArray.byteswap() 

1263 data += advanceWidthArray.tobytes() 

1264 

1265 if hasLsbArray: 

1266 lsbArray = array.array( 

1267 "h", 

1268 [ 

1269 self.metrics[glyphName][1] 

1270 for i, glyphName in enumerate(glyphOrder) 

1271 if i < numberOfHMetrics 

1272 ], 

1273 ) 

1274 if sys.byteorder != "big": 

1275 lsbArray.byteswap() 

1276 data += lsbArray.tobytes() 

1277 

1278 if hasLeftSideBearingArray: 

1279 leftSideBearingArray = array.array( 

1280 "h", 

1281 [ 

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

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

1284 ], 

1285 ) 

1286 if sys.byteorder != "big": 

1287 leftSideBearingArray.byteswap() 

1288 data += leftSideBearingArray.tobytes() 

1289 

1290 return data 

1291 

1292 

1293class WOFF2FlavorData(WOFFFlavorData): 

1294 

1295 Flavor = "woff2" 

1296 

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

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

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

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

1301 or will have once the WOFF2 font is compiled. 

1302 

1303 Args: 

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

1305 data: another WOFFFlavorData object to initialise data from. 

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

1307 

1308 Raises: 

1309 ImportError if the brotli module is not installed. 

1310 

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

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

1313 """ 

1314 if not haveBrotli: 

1315 raise ImportError("No module named brotli") 

1316 

1317 if reader is not None: 

1318 if data is not None: 

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

1320 if transformedTables is not None: 

1321 raise TypeError( 

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

1323 ) 

1324 

1325 if transformedTables is not None and ( 

1326 "glyf" in transformedTables 

1327 and "loca" not in transformedTables 

1328 or "loca" in transformedTables 

1329 and "glyf" not in transformedTables 

1330 ): 

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

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

1333 if reader: 

1334 transformedTables = [ 

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

1336 ] 

1337 elif data: 

1338 self.majorVersion = data.majorVersion 

1339 self.majorVersion = data.minorVersion 

1340 self.metaData = data.metaData 

1341 self.privData = data.privData 

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

1343 transformedTables = data.transformedTables 

1344 

1345 if transformedTables is None: 

1346 transformedTables = woff2TransformedTableTags 

1347 

1348 self.transformedTables = set(transformedTables) 

1349 

1350 def _decompress(self, rawData): 

1351 return brotli.decompress(rawData) 

1352 

1353 

1354def unpackBase128(data): 

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

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

1357 

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

1359 True 

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

1361 True 

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

1363 Traceback (most recent call last): 

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

1365 TTLibError: UIntBase128 value must not start with leading zeros 

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

1367 Traceback (most recent call last): 

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

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

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

1371 Traceback (most recent call last): 

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

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

1374 """ 

1375 if len(data) == 0: 

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

1377 result = 0 

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

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

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

1381 for i in range(woff2Base128MaxSize): 

1382 if len(data) == 0: 

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

1384 code = byteord(data[0]) 

1385 data = data[1:] 

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

1387 if result & 0xFE000000: 

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

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

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

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

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

1393 # return result plus left over data 

1394 return result, data 

1395 # make sure not to exceed the size bound 

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

1397 

1398 

1399def base128Size(n): 

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

1401 

1402 >>> base128Size(0) 

1403 1 

1404 >>> base128Size(24567) 

1405 3 

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

1407 5 

1408 """ 

1409 assert n >= 0 

1410 size = 1 

1411 while n >= 128: 

1412 size += 1 

1413 n >>= 7 

1414 return size 

1415 

1416 

1417def packBase128(n): 

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

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

1420 encoding. 

1421 

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

1423 True 

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

1425 True 

1426 """ 

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

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

1429 data = b"" 

1430 size = base128Size(n) 

1431 for i in range(size): 

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

1433 if i < size - 1: 

1434 b |= 0x80 

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

1436 return data 

1437 

1438 

1439def unpack255UShort(data): 

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

1441 tuple containing the decoded integer plus any leftover data. 

1442 

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

1444 252 

1445 

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

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

1448 506 

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

1450 506 

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

1452 506 

1453 """ 

1454 code = byteord(data[:1]) 

1455 data = data[1:] 

1456 if code == 253: 

1457 # read two more bytes as an unsigned short 

1458 if len(data) < 2: 

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

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

1461 data = data[2:] 

1462 elif code == 254: 

1463 # read another byte, plus 253 * 2 

1464 if len(data) == 0: 

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

1466 result = byteord(data[:1]) 

1467 result += 506 

1468 data = data[1:] 

1469 elif code == 255: 

1470 # read another byte, plus 253 

1471 if len(data) == 0: 

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

1473 result = byteord(data[:1]) 

1474 result += 253 

1475 data = data[1:] 

1476 else: 

1477 # leave as is if lower than 253 

1478 result = code 

1479 # return result plus left over data 

1480 return result, data 

1481 

1482 

1483def pack255UShort(value): 

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

1485 using 255UInt16 variable-length encoding. 

1486 

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

1488 True 

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

1490 True 

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

1492 True 

1493 """ 

1494 if value < 0 or value > 0xFFFF: 

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

1496 if value < 253: 

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

1498 elif value < 506: 

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

1500 elif value < 762: 

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

1502 else: 

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

1504 

1505 

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

1507 """Compress OpenType font to WOFF2. 

1508 

1509 Args: 

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

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

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

1513 compressed WOFF2 font. 

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

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

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

1517 transformations. 

1518 """ 

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

1520 

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

1522 font.flavor = "woff2" 

1523 

1524 if transform_tables is not None: 

1525 font.flavorData = WOFF2FlavorData( 

1526 data=font.flavorData, transformedTables=transform_tables 

1527 ) 

1528 

1529 font.save(output_file, reorderTables=False) 

1530 

1531 

1532def decompress(input_file, output_file): 

1533 """Decompress WOFF2 font to OpenType font. 

1534 

1535 Args: 

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

1537 containing a compressed WOFF2 font. 

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

1539 decompressed OpenType font. 

1540 """ 

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

1542 

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

1544 font.flavor = None 

1545 font.flavorData = None 

1546 font.save(output_file, reorderTables=True) 

1547 

1548 

1549def main(args=None): 

1550 """Compress and decompress WOFF2 fonts""" 

1551 import argparse 

1552 from fontTools import configLogger 

1553 from fontTools.ttx import makeOutputFileName 

1554 

1555 class _HelpAction(argparse._HelpAction): 

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

1557 subparsers_actions = [ 

1558 action 

1559 for action in parser._actions 

1560 if isinstance(action, argparse._SubParsersAction) 

1561 ] 

1562 for subparsers_action in subparsers_actions: 

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

1564 print(subparser.format_help()) 

1565 parser.exit() 

1566 

1567 class _NoGlyfTransformAction(argparse.Action): 

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

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

1570 

1571 class _HmtxTransformAction(argparse.Action): 

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

1573 namespace.transform_tables.add("hmtx") 

1574 

1575 parser = argparse.ArgumentParser( 

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

1577 ) 

1578 

1579 parser.add_argument( 

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

1581 ) 

1582 

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

1584 parser_compress = parser_group.add_parser( 

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

1586 ) 

1587 parser_decompress = parser_group.add_parser( 

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

1589 ) 

1590 

1591 for subparser in (parser_compress, parser_decompress): 

1592 group = subparser.add_mutually_exclusive_group(required=False) 

1593 group.add_argument( 

1594 "-v", 

1595 "--verbose", 

1596 action="store_true", 

1597 help="print more messages to console", 

1598 ) 

1599 group.add_argument( 

1600 "-q", 

1601 "--quiet", 

1602 action="store_true", 

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

1604 ) 

1605 

1606 parser_compress.add_argument( 

1607 "input_file", 

1608 metavar="INPUT", 

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

1610 ) 

1611 parser_decompress.add_argument( 

1612 "input_file", 

1613 metavar="INPUT", 

1614 help="the input WOFF2 font", 

1615 ) 

1616 

1617 parser_compress.add_argument( 

1618 "-o", 

1619 "--output-file", 

1620 metavar="OUTPUT", 

1621 help="the output WOFF2 font", 

1622 ) 

1623 parser_decompress.add_argument( 

1624 "-o", 

1625 "--output-file", 

1626 metavar="OUTPUT", 

1627 help="the output OpenType font", 

1628 ) 

1629 

1630 transform_group = parser_compress.add_argument_group() 

1631 transform_group.add_argument( 

1632 "--no-glyf-transform", 

1633 dest="transform_tables", 

1634 nargs=0, 

1635 action=_NoGlyfTransformAction, 

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

1637 ) 

1638 transform_group.add_argument( 

1639 "--hmtx-transform", 

1640 dest="transform_tables", 

1641 nargs=0, 

1642 action=_HmtxTransformAction, 

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

1644 ) 

1645 

1646 parser_compress.set_defaults( 

1647 subcommand=compress, 

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

1649 ) 

1650 parser_decompress.set_defaults(subcommand=decompress) 

1651 

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

1653 

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

1655 if not subcommand: 

1656 parser.print_help() 

1657 return 

1658 

1659 quiet = options.pop("quiet") 

1660 verbose = options.pop("verbose") 

1661 configLogger( 

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

1663 ) 

1664 

1665 if not options["output_file"]: 

1666 if subcommand is compress: 

1667 extension = ".woff2" 

1668 elif subcommand is decompress: 

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

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

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

1672 sfntVersion = f.read(4) 

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

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

1675 else: 

1676 raise AssertionError(subcommand) 

1677 options["output_file"] = makeOutputFileName( 

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

1679 ) 

1680 

1681 try: 

1682 subcommand(**options) 

1683 except TTLibError as e: 

1684 parser.error(e) 

1685 

1686 

1687if __name__ == "__main__": 

1688 sys.exit(main())