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

965 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 checksums = [] 

398 for tag in self.tables.keys(): 

399 checksums.append(self.tables[tag].checkSum) 

400 

401 # Create a SFNT directory for checksum calculation purposes 

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

403 self.numTables, 16 

404 ) 

405 directory = sstruct.pack(sfntDirectoryFormat, self) 

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

407 for tag, entry in tables: 

408 sfntEntry = SFNTDirectoryEntry() 

409 sfntEntry.tag = entry.tag 

410 sfntEntry.checkSum = entry.checkSum 

411 sfntEntry.offset = entry.origOffset 

412 sfntEntry.length = entry.origLength 

413 directory = directory + sfntEntry.toString() 

414 

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

416 assert directory_end == len(directory) 

417 

418 checksums.append(calcChecksum(directory)) 

419 checksum = sum(checksums) & 0xFFFFFFFF 

420 # BiboAfba! 

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

422 return checksumadjustment 

423 

424 def writeMasterChecksum(self): 

425 """Write checkSumAdjustment to the transformBuffer.""" 

426 checksumadjustment = self._calcMasterChecksum() 

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

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

429 

430 def _calcTotalSize(self): 

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

432 offset = self.directorySize 

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

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

435 offset += self.totalCompressedSize 

436 offset = (offset + 3) & ~3 

437 offset = self._calcFlavorDataOffsetsAndSize(offset) 

438 return offset 

439 

440 def _calcFlavorDataOffsetsAndSize(self, start): 

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

442 offset = start 

443 data = self.flavorData 

444 if data.metaData: 

445 self.metaOrigLength = len(data.metaData) 

446 self.metaOffset = offset 

447 self.compressedMetaData = brotli.compress( 

448 data.metaData, mode=brotli.MODE_TEXT 

449 ) 

450 self.metaLength = len(self.compressedMetaData) 

451 offset += self.metaLength 

452 else: 

453 self.metaOffset = self.metaLength = self.metaOrigLength = 0 

454 self.compressedMetaData = b"" 

455 if data.privData: 

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

457 offset = (offset + 3) & ~3 

458 self.privOffset = offset 

459 self.privLength = len(data.privData) 

460 offset += self.privLength 

461 else: 

462 self.privOffset = self.privLength = 0 

463 return offset 

464 

465 def _getVersion(self): 

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

467 data = self.flavorData 

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

469 return data.majorVersion, data.minorVersion 

470 else: 

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

472 if "head" in self.tables: 

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

474 else: 

475 return 0, 0 

476 

477 def _packTableDirectory(self): 

478 """Return WOFF2 table directory data.""" 

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

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

481 directory = directory + entry.toString() 

482 return directory 

483 

484 def _writeFlavorData(self): 

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

486 compressedMetaData = self.compressedMetaData 

487 privData = self.flavorData.privData 

488 if compressedMetaData and privData: 

489 compressedMetaData = pad(compressedMetaData, size=4) 

490 if compressedMetaData: 

491 self.file.seek(self.metaOffset) 

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

493 self.file.write(compressedMetaData) 

494 if privData: 

495 self.file.seek(self.privOffset) 

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

497 self.file.write(privData) 

498 

499 def reordersTables(self): 

500 return True 

501 

502 

503# -- woff2 directory helpers and cruft 

504 

505woff2DirectoryFormat = """ 

506 > # big endian 

507 signature: 4s # "wOF2" 

508 sfntVersion: 4s 

509 length: L # total woff2 file size 

510 numTables: H # number of tables 

511 reserved: H # set to 0 

512 totalSfntSize: L # uncompressed size 

513 totalCompressedSize: L # compressed size 

514 majorVersion: H # major version of WOFF file 

515 minorVersion: H # minor version of WOFF file 

516 metaOffset: L # offset to metadata block 

517 metaLength: L # length of compressed metadata 

518 metaOrigLength: L # length of uncompressed metadata 

519 privOffset: L # offset to private data block 

520 privLength: L # length of private data block 

521""" 

522 

523woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat) 

524 

525woff2KnownTags = ( 

526 "cmap", 

527 "head", 

528 "hhea", 

529 "hmtx", 

530 "maxp", 

531 "name", 

532 "OS/2", 

533 "post", 

534 "cvt ", 

535 "fpgm", 

536 "glyf", 

537 "loca", 

538 "prep", 

539 "CFF ", 

540 "VORG", 

541 "EBDT", 

542 "EBLC", 

543 "gasp", 

544 "hdmx", 

545 "kern", 

546 "LTSH", 

547 "PCLT", 

548 "VDMX", 

549 "vhea", 

550 "vmtx", 

551 "BASE", 

552 "GDEF", 

553 "GPOS", 

554 "GSUB", 

555 "EBSC", 

556 "JSTF", 

557 "MATH", 

558 "CBDT", 

559 "CBLC", 

560 "COLR", 

561 "CPAL", 

562 "SVG ", 

563 "sbix", 

564 "acnt", 

565 "avar", 

566 "bdat", 

567 "bloc", 

568 "bsln", 

569 "cvar", 

570 "fdsc", 

571 "feat", 

572 "fmtx", 

573 "fvar", 

574 "gvar", 

575 "hsty", 

576 "just", 

577 "lcar", 

578 "mort", 

579 "morx", 

580 "opbd", 

581 "prop", 

582 "trak", 

583 "Zapf", 

584 "Silf", 

585 "Glat", 

586 "Gloc", 

587 "Feat", 

588 "Sill", 

589) 

590 

591woff2FlagsFormat = """ 

592 > # big endian 

593 flags: B # table type and flags 

594""" 

595 

596woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat) 

597 

598woff2UnknownTagFormat = """ 

599 > # big endian 

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

601""" 

602 

603woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat) 

604 

605woff2UnknownTagIndex = 0x3F 

606 

607woff2Base128MaxSize = 5 

608woff2DirectoryEntryMaxSize = ( 

609 woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize 

610) 

611 

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

613 

614woff2GlyfTableFormat = """ 

615 > # big endian 

616 version: H # = 0x0000 

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

618 numGlyphs: H # Number of glyphs 

619 indexFormat: H # Offset format for loca table 

620 nContourStreamSize: L # Size of nContour stream 

621 nPointsStreamSize: L # Size of nPoints stream 

622 flagStreamSize: L # Size of flag stream 

623 glyphStreamSize: L # Size of glyph stream 

624 compositeStreamSize: L # Size of composite stream 

625 bboxStreamSize: L # Comnined size of bboxBitmap and bboxStream 

626 instructionStreamSize: L # Size of instruction stream 

627""" 

628 

629woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat) 

630 

631bboxFormat = """ 

632 > # big endian 

633 xMin: h 

634 yMin: h 

635 xMax: h 

636 yMax: h 

637""" 

638 

639woff2OverlapSimpleBitmapFlag = 0x0001 

640 

641 

642def getKnownTagIndex(tag): 

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

644 try: 

645 return woff2KnownTags.index(tag) 

646 except ValueError: 

647 return woff2UnknownTagIndex 

648 

649 

650class WOFF2DirectoryEntry(DirectoryEntry): 

651 def fromFile(self, file): 

652 pos = file.tell() 

653 data = file.read(woff2DirectoryEntryMaxSize) 

654 left = self.fromString(data) 

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

656 file.seek(pos + consumed) 

657 

658 def fromString(self, data): 

659 if len(data) < 1: 

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

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

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

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

664 if len(data) < woff2UnknownTagSize: 

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

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

667 else: 

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

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

670 self.tag = Tag(self.tag) 

671 self.origLength, data = unpackBase128(data) 

672 self.length = self.origLength 

673 if self.transformed: 

674 self.length, data = unpackBase128(data) 

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

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

677 # return left over data 

678 return data 

679 

680 def toString(self): 

681 data = bytechr(self.flags) 

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

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

684 data += packBase128(self.origLength) 

685 if self.transformed: 

686 data += packBase128(self.length) 

687 return data 

688 

689 @property 

690 def transformVersion(self): 

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

692 transformation version number (between 0 and 3). 

693 """ 

694 return self.flags >> 6 

695 

696 @transformVersion.setter 

697 def transformVersion(self, value): 

698 assert 0 <= value <= 3 

699 self.flags |= value << 6 

700 

701 @property 

702 def transformed(self): 

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

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

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

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

707 # transformation version 3 indicates the null transform 

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

709 return self.transformVersion != 3 

710 else: 

711 return self.transformVersion != 0 

712 

713 @transformed.setter 

714 def transformed(self, booleanValue): 

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

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

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

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

719 self.transformVersion = 3 if not booleanValue else 0 

720 else: 

721 self.transformVersion = int(booleanValue) 

722 

723 

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

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

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

727 """ 

728 

729 def __init__(self, tag=None): 

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

731 

732 def compile(self, ttFont): 

733 try: 

734 max_location = max(self.locations) 

735 except AttributeError: 

736 self.set([]) 

737 max_location = 0 

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

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

740 indexFormat = ttFont["glyf"].indexFormat 

741 if indexFormat == 0: 

742 if max_location >= 0x20000: 

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

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

745 raise TTLibError( 

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

747 ) 

748 locations = array.array("H") 

749 for location in self.locations: 

750 locations.append(location // 2) 

751 else: 

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

753 if sys.byteorder != "big": 

754 locations.byteswap() 

755 data = locations.tobytes() 

756 else: 

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

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

759 return data 

760 

761 

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

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

764 

765 subStreams = ( 

766 "nContourStream", 

767 "nPointsStream", 

768 "flagStream", 

769 "glyphStream", 

770 "compositeStream", 

771 "bboxStream", 

772 "instructionStream", 

773 ) 

774 

775 def __init__(self, tag=None): 

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

777 

778 def reconstruct(self, data, ttFont): 

779 """Decompile transformed 'glyf' data.""" 

780 inputDataSize = len(data) 

781 

782 if inputDataSize < woff2GlyfTableFormatSize: 

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

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

785 offset = woff2GlyfTableFormatSize 

786 

787 for stream in self.subStreams: 

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

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

790 data = data[size:] 

791 offset += size 

792 

793 hasOverlapSimpleBitmap = self.optionFlags & woff2OverlapSimpleBitmapFlag 

794 self.overlapSimpleBitmap = None 

795 if hasOverlapSimpleBitmap: 

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

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

798 offset += overlapSimpleBitmapSize 

799 

800 if offset != inputDataSize: 

801 raise TTLibError( 

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

803 % (offset, inputDataSize) 

804 ) 

805 

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

807 bboxBitmap = self.bboxStream[:bboxBitmapSize] 

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

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

810 

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

812 if sys.byteorder != "big": 

813 self.nContourStream.byteswap() 

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

815 

816 if "head" in ttFont: 

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

818 try: 

819 self.glyphOrder = ttFont.getGlyphOrder() 

820 except: 

821 self.glyphOrder = None 

822 if self.glyphOrder is None: 

823 self.glyphOrder = [".notdef"] 

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

825 else: 

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

827 raise TTLibError( 

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

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

830 ) 

831 

832 glyphs = self.glyphs = {} 

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

834 glyph = self._decodeGlyph(glyphID) 

835 glyphs[glyphName] = glyph 

836 

837 def transform(self, ttFont): 

838 """Return transformed 'glyf' data""" 

839 self.numGlyphs = len(self.glyphs) 

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

841 if "maxp" in ttFont: 

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

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

844 

845 for stream in self.subStreams: 

846 setattr(self, stream, b"") 

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

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

849 

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

851 for glyphID in range(self.numGlyphs): 

852 try: 

853 self._encodeGlyph(glyphID) 

854 except NotImplementedError: 

855 return None 

856 hasOverlapSimpleBitmap = any(self.overlapSimpleBitmap) 

857 

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

859 for stream in self.subStreams: 

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

861 self.version = 0 

862 self.optionFlags = 0 

863 if hasOverlapSimpleBitmap: 

864 self.optionFlags |= woff2OverlapSimpleBitmapFlag 

865 data = sstruct.pack(woff2GlyfTableFormat, self) 

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

867 if hasOverlapSimpleBitmap: 

868 data += self.overlapSimpleBitmap.tobytes() 

869 return data 

870 

871 def _decodeGlyph(self, glyphID): 

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

873 glyph.numberOfContours = self.nContourStream[glyphID] 

874 if glyph.numberOfContours == 0: 

875 return glyph 

876 elif glyph.isComposite(): 

877 self._decodeComponents(glyph) 

878 else: 

879 self._decodeCoordinates(glyph) 

880 self._decodeOverlapSimpleFlag(glyph, glyphID) 

881 self._decodeBBox(glyphID, glyph) 

882 return glyph 

883 

884 def _decodeComponents(self, glyph): 

885 data = self.compositeStream 

886 glyph.components = [] 

887 more = 1 

888 haveInstructions = 0 

889 while more: 

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

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

892 haveInstructions = haveInstructions | haveInstr 

893 glyph.components.append(component) 

894 self.compositeStream = data 

895 if haveInstructions: 

896 self._decodeInstructions(glyph) 

897 

898 def _decodeCoordinates(self, glyph): 

899 data = self.nPointsStream 

900 endPtsOfContours = [] 

901 endPoint = -1 

902 for i in range(glyph.numberOfContours): 

903 ptsOfContour, data = unpack255UShort(data) 

904 endPoint += ptsOfContour 

905 endPtsOfContours.append(endPoint) 

906 glyph.endPtsOfContours = endPtsOfContours 

907 self.nPointsStream = data 

908 self._decodeTriplets(glyph) 

909 self._decodeInstructions(glyph) 

910 

911 def _decodeOverlapSimpleFlag(self, glyph, glyphID): 

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

913 return 

914 byte = glyphID >> 3 

915 bit = glyphID & 7 

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

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

918 

919 def _decodeInstructions(self, glyph): 

920 glyphStream = self.glyphStream 

921 instructionStream = self.instructionStream 

922 instructionLength, glyphStream = unpack255UShort(glyphStream) 

923 glyph.program = ttProgram.Program() 

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

925 self.glyphStream = glyphStream 

926 self.instructionStream = instructionStream[instructionLength:] 

927 

928 def _decodeBBox(self, glyphID, glyph): 

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

930 if glyph.isComposite() and not haveBBox: 

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

932 if haveBBox: 

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

934 else: 

935 glyph.recalcBounds(self) 

936 

937 def _decodeTriplets(self, glyph): 

938 def withSign(flag, baseval): 

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

940 return baseval if flag & 1 else -baseval 

941 

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

943 flagSize = nPoints 

944 if flagSize > len(self.flagStream): 

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

946 flagsData = self.flagStream[:flagSize] 

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

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

949 

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

951 nTriplets = len(triplets) 

952 assert nPoints <= nTriplets 

953 

954 x = 0 

955 y = 0 

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

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

958 tripletIndex = 0 

959 for i in range(nPoints): 

960 flag = flags[i] 

961 onCurve = not bool(flag >> 7) 

962 flag &= 0x7F 

963 if flag < 84: 

964 nBytes = 1 

965 elif flag < 120: 

966 nBytes = 2 

967 elif flag < 124: 

968 nBytes = 3 

969 else: 

970 nBytes = 4 

971 assert (tripletIndex + nBytes) <= nTriplets 

972 if flag < 10: 

973 dx = 0 

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

975 elif flag < 20: 

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

977 dy = 0 

978 elif flag < 84: 

979 b0 = flag - 20 

980 b1 = triplets[tripletIndex] 

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

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

983 elif flag < 120: 

984 b0 = flag - 84 

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

986 dy = withSign( 

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

988 ) 

989 elif flag < 124: 

990 b2 = triplets[tripletIndex + 1] 

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

992 dy = withSign( 

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

994 ) 

995 else: 

996 dx = withSign( 

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

998 ) 

999 dy = withSign( 

1000 flag >> 1, 

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

1002 ) 

1003 tripletIndex += nBytes 

1004 x += dx 

1005 y += dy 

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

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

1008 bytesConsumed = tripletIndex 

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

1010 

1011 def _encodeGlyph(self, glyphID): 

1012 glyphName = self.getGlyphName(glyphID) 

1013 glyph = self[glyphName] 

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

1015 if glyph.numberOfContours == 0: 

1016 return 

1017 elif glyph.isComposite(): 

1018 self._encodeComponents(glyph) 

1019 else: 

1020 self._encodeCoordinates(glyph) 

1021 self._encodeOverlapSimpleFlag(glyph, glyphID) 

1022 self._encodeBBox(glyphID, glyph) 

1023 

1024 def _encodeComponents(self, glyph): 

1025 lastcomponent = len(glyph.components) - 1 

1026 more = 1 

1027 haveInstructions = 0 

1028 for i, component in enumerate(glyph.components): 

1029 if i == lastcomponent: 

1030 haveInstructions = hasattr(glyph, "program") 

1031 more = 0 

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

1033 if haveInstructions: 

1034 self._encodeInstructions(glyph) 

1035 

1036 def _encodeCoordinates(self, glyph): 

1037 lastEndPoint = -1 

1038 if _g_l_y_f.flagCubic in glyph.flags: 

1039 raise NotImplementedError 

1040 for endPoint in glyph.endPtsOfContours: 

1041 ptsOfContour = endPoint - lastEndPoint 

1042 self.nPointsStream += pack255UShort(ptsOfContour) 

1043 lastEndPoint = endPoint 

1044 self._encodeTriplets(glyph) 

1045 self._encodeInstructions(glyph) 

1046 

1047 def _encodeOverlapSimpleFlag(self, glyph, glyphID): 

1048 if glyph.numberOfContours <= 0: 

1049 return 

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

1051 byte = glyphID >> 3 

1052 bit = glyphID & 7 

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

1054 

1055 def _encodeInstructions(self, glyph): 

1056 instructions = glyph.program.getBytecode() 

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

1058 self.instructionStream += instructions 

1059 

1060 def _encodeBBox(self, glyphID, glyph): 

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

1062 if not glyph.isComposite(): 

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

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

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

1066 calculatedBBox = calcIntBounds(glyph.coordinates) 

1067 if currentBBox == calculatedBBox: 

1068 return 

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

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

1071 

1072 def _encodeTriplets(self, glyph): 

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

1074 coordinates = glyph.coordinates.copy() 

1075 coordinates.absoluteToRelative() 

1076 

1077 flags = array.array("B") 

1078 triplets = array.array("B") 

1079 for i, (x, y) in enumerate(coordinates): 

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

1081 absX = abs(x) 

1082 absY = abs(y) 

1083 onCurveBit = 0 if onCurve else 128 

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

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

1086 xySignBits = xSignBit + 2 * ySignBit 

1087 

1088 if x == 0 and absY < 1280: 

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

1090 triplets.append(absY & 0xFF) 

1091 elif y == 0 and absX < 1280: 

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

1093 triplets.append(absX & 0xFF) 

1094 elif absX < 65 and absY < 65: 

1095 flags.append( 

1096 onCurveBit 

1097 + 20 

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

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

1100 + xySignBits 

1101 ) 

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

1103 elif absX < 769 and absY < 769: 

1104 flags.append( 

1105 onCurveBit 

1106 + 84 

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

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

1109 + xySignBits 

1110 ) 

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

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

1113 elif absX < 4096 and absY < 4096: 

1114 flags.append(onCurveBit + 120 + xySignBits) 

1115 triplets.append(absX >> 4) 

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

1117 triplets.append(absY & 0xFF) 

1118 else: 

1119 flags.append(onCurveBit + 124 + xySignBits) 

1120 triplets.append(absX >> 8) 

1121 triplets.append(absX & 0xFF) 

1122 triplets.append(absY >> 8) 

1123 triplets.append(absY & 0xFF) 

1124 

1125 self.flagStream += flags.tobytes() 

1126 self.glyphStream += triplets.tobytes() 

1127 

1128 

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

1130 def __init__(self, tag=None): 

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

1132 

1133 def reconstruct(self, data, ttFont): 

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

1135 data = data[1:] 

1136 if flags & 0b11111100 != 0: 

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

1138 

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

1140 hasLsbArray = flags & 1 == 0 

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

1142 hasLeftSideBearingArray = flags & 2 == 0 

1143 if hasLsbArray and hasLeftSideBearingArray: 

1144 raise TTLibError( 

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

1146 % self.tableTag 

1147 ) 

1148 

1149 glyfTable = ttFont["glyf"] 

1150 headerTable = ttFont["hhea"] 

1151 glyphOrder = glyfTable.glyphOrder 

1152 numGlyphs = len(glyphOrder) 

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

1154 

1155 assert len(data) >= 2 * numberOfHMetrics 

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

1157 if sys.byteorder != "big": 

1158 advanceWidthArray.byteswap() 

1159 data = data[2 * numberOfHMetrics :] 

1160 

1161 if hasLsbArray: 

1162 assert len(data) >= 2 * numberOfHMetrics 

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

1164 if sys.byteorder != "big": 

1165 lsbArray.byteswap() 

1166 data = data[2 * numberOfHMetrics :] 

1167 else: 

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

1169 lsbArray = array.array("h") 

1170 for i, glyphName in enumerate(glyphOrder): 

1171 if i >= numberOfHMetrics: 

1172 break 

1173 glyph = glyfTable[glyphName] 

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

1175 lsbArray.append(xMin) 

1176 

1177 numberOfSideBearings = numGlyphs - numberOfHMetrics 

1178 if hasLeftSideBearingArray: 

1179 assert len(data) >= 2 * numberOfSideBearings 

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

1181 if sys.byteorder != "big": 

1182 leftSideBearingArray.byteswap() 

1183 data = data[2 * numberOfSideBearings :] 

1184 else: 

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

1186 leftSideBearingArray = array.array("h") 

1187 for i, glyphName in enumerate(glyphOrder): 

1188 if i < numberOfHMetrics: 

1189 continue 

1190 glyph = glyfTable[glyphName] 

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

1192 leftSideBearingArray.append(xMin) 

1193 

1194 if data: 

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

1196 

1197 self.metrics = {} 

1198 for i in range(numberOfHMetrics): 

1199 glyphName = glyphOrder[i] 

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

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

1202 lastAdvance = advanceWidthArray[-1] 

1203 for i in range(numberOfSideBearings): 

1204 glyphName = glyphOrder[i + numberOfHMetrics] 

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

1206 

1207 def transform(self, ttFont): 

1208 glyphOrder = ttFont.getGlyphOrder() 

1209 glyf = ttFont["glyf"] 

1210 hhea = ttFont["hhea"] 

1211 numberOfHMetrics = hhea.numberOfHMetrics 

1212 

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

1214 # differ from their xMin bounding box values. 

1215 hasLsbArray = False 

1216 for i in range(numberOfHMetrics): 

1217 glyphName = glyphOrder[i] 

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

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

1220 hasLsbArray = True 

1221 break 

1222 

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

1224 hasLeftSideBearingArray = False 

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

1226 glyphName = glyphOrder[i] 

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

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

1229 hasLeftSideBearingArray = True 

1230 break 

1231 

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

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

1234 if hasLsbArray and hasLeftSideBearingArray: 

1235 return 

1236 

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

1238 flags = 0 

1239 if not hasLsbArray: 

1240 flags |= 1 << 0 

1241 if not hasLeftSideBearingArray: 

1242 flags |= 1 << 1 

1243 

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

1245 

1246 advanceWidthArray = array.array( 

1247 "H", 

1248 [ 

1249 self.metrics[glyphName][0] 

1250 for i, glyphName in enumerate(glyphOrder) 

1251 if i < numberOfHMetrics 

1252 ], 

1253 ) 

1254 if sys.byteorder != "big": 

1255 advanceWidthArray.byteswap() 

1256 data += advanceWidthArray.tobytes() 

1257 

1258 if hasLsbArray: 

1259 lsbArray = array.array( 

1260 "h", 

1261 [ 

1262 self.metrics[glyphName][1] 

1263 for i, glyphName in enumerate(glyphOrder) 

1264 if i < numberOfHMetrics 

1265 ], 

1266 ) 

1267 if sys.byteorder != "big": 

1268 lsbArray.byteswap() 

1269 data += lsbArray.tobytes() 

1270 

1271 if hasLeftSideBearingArray: 

1272 leftSideBearingArray = array.array( 

1273 "h", 

1274 [ 

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

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

1277 ], 

1278 ) 

1279 if sys.byteorder != "big": 

1280 leftSideBearingArray.byteswap() 

1281 data += leftSideBearingArray.tobytes() 

1282 

1283 return data 

1284 

1285 

1286class WOFF2FlavorData(WOFFFlavorData): 

1287 Flavor = "woff2" 

1288 

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

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

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

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

1293 or will have once the WOFF2 font is compiled. 

1294 

1295 Args: 

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

1297 data: another WOFFFlavorData object to initialise data from. 

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

1299 

1300 Raises: 

1301 ImportError if the brotli module is not installed. 

1302 

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

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

1305 """ 

1306 if not haveBrotli: 

1307 raise ImportError("No module named brotli") 

1308 

1309 if reader is not None: 

1310 if data is not None: 

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

1312 if transformedTables is not None: 

1313 raise TypeError( 

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

1315 ) 

1316 

1317 if transformedTables is not None and ( 

1318 "glyf" in transformedTables 

1319 and "loca" not in transformedTables 

1320 or "loca" in transformedTables 

1321 and "glyf" not in transformedTables 

1322 ): 

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

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

1325 if reader: 

1326 transformedTables = [ 

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

1328 ] 

1329 elif data: 

1330 self.majorVersion = data.majorVersion 

1331 self.majorVersion = data.minorVersion 

1332 self.metaData = data.metaData 

1333 self.privData = data.privData 

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

1335 transformedTables = data.transformedTables 

1336 

1337 if transformedTables is None: 

1338 transformedTables = woff2TransformedTableTags 

1339 

1340 self.transformedTables = set(transformedTables) 

1341 

1342 def _decompress(self, rawData): 

1343 return brotli.decompress(rawData) 

1344 

1345 

1346def unpackBase128(data): 

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

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

1349 

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

1351 True 

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

1353 True 

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

1355 Traceback (most recent call last): 

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

1357 TTLibError: UIntBase128 value must not start with leading zeros 

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

1359 Traceback (most recent call last): 

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

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

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

1363 Traceback (most recent call last): 

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

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

1366 """ 

1367 if len(data) == 0: 

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

1369 result = 0 

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

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

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

1373 for i in range(woff2Base128MaxSize): 

1374 if len(data) == 0: 

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

1376 code = byteord(data[0]) 

1377 data = data[1:] 

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

1379 if result & 0xFE000000: 

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

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

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

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

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

1385 # return result plus left over data 

1386 return result, data 

1387 # make sure not to exceed the size bound 

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

1389 

1390 

1391def base128Size(n): 

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

1393 

1394 >>> base128Size(0) 

1395 1 

1396 >>> base128Size(24567) 

1397 3 

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

1399 5 

1400 """ 

1401 assert n >= 0 

1402 size = 1 

1403 while n >= 128: 

1404 size += 1 

1405 n >>= 7 

1406 return size 

1407 

1408 

1409def packBase128(n): 

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

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

1412 encoding. 

1413 

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

1415 True 

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

1417 True 

1418 """ 

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

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

1421 data = b"" 

1422 size = base128Size(n) 

1423 for i in range(size): 

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

1425 if i < size - 1: 

1426 b |= 0x80 

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

1428 return data 

1429 

1430 

1431def unpack255UShort(data): 

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

1433 tuple containing the decoded integer plus any leftover data. 

1434 

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

1436 252 

1437 

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

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

1440 506 

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

1442 506 

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

1444 506 

1445 """ 

1446 code = byteord(data[:1]) 

1447 data = data[1:] 

1448 if code == 253: 

1449 # read two more bytes as an unsigned short 

1450 if len(data) < 2: 

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

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

1453 data = data[2:] 

1454 elif code == 254: 

1455 # read another byte, plus 253 * 2 

1456 if len(data) == 0: 

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

1458 result = byteord(data[:1]) 

1459 result += 506 

1460 data = data[1:] 

1461 elif code == 255: 

1462 # read another byte, plus 253 

1463 if len(data) == 0: 

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

1465 result = byteord(data[:1]) 

1466 result += 253 

1467 data = data[1:] 

1468 else: 

1469 # leave as is if lower than 253 

1470 result = code 

1471 # return result plus left over data 

1472 return result, data 

1473 

1474 

1475def pack255UShort(value): 

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

1477 using 255UInt16 variable-length encoding. 

1478 

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

1480 True 

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

1482 True 

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

1484 True 

1485 """ 

1486 if value < 0 or value > 0xFFFF: 

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

1488 if value < 253: 

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

1490 elif value < 506: 

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

1492 elif value < 762: 

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

1494 else: 

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

1496 

1497 

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

1499 """Compress OpenType font to WOFF2. 

1500 

1501 Args: 

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

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

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

1505 compressed WOFF2 font. 

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

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

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

1509 transformations. 

1510 """ 

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

1512 

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

1514 font.flavor = "woff2" 

1515 

1516 if transform_tables is not None: 

1517 font.flavorData = WOFF2FlavorData( 

1518 data=font.flavorData, transformedTables=transform_tables 

1519 ) 

1520 

1521 font.save(output_file, reorderTables=False) 

1522 

1523 

1524def decompress(input_file, output_file): 

1525 """Decompress WOFF2 font to OpenType font. 

1526 

1527 Args: 

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

1529 containing a compressed WOFF2 font. 

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

1531 decompressed OpenType font. 

1532 """ 

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

1534 

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

1536 font.flavor = None 

1537 font.flavorData = None 

1538 font.save(output_file, reorderTables=True) 

1539 

1540 

1541def main(args=None): 

1542 """Compress and decompress WOFF2 fonts""" 

1543 import argparse 

1544 from fontTools import configLogger 

1545 from fontTools.ttx import makeOutputFileName 

1546 

1547 class _HelpAction(argparse._HelpAction): 

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

1549 subparsers_actions = [ 

1550 action 

1551 for action in parser._actions 

1552 if isinstance(action, argparse._SubParsersAction) 

1553 ] 

1554 for subparsers_action in subparsers_actions: 

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

1556 print(subparser.format_help()) 

1557 parser.exit() 

1558 

1559 class _NoGlyfTransformAction(argparse.Action): 

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

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

1562 

1563 class _HmtxTransformAction(argparse.Action): 

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

1565 namespace.transform_tables.add("hmtx") 

1566 

1567 parser = argparse.ArgumentParser( 

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

1569 ) 

1570 

1571 parser.add_argument( 

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

1573 ) 

1574 

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

1576 parser_compress = parser_group.add_parser( 

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

1578 ) 

1579 parser_decompress = parser_group.add_parser( 

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

1581 ) 

1582 

1583 for subparser in (parser_compress, parser_decompress): 

1584 group = subparser.add_mutually_exclusive_group(required=False) 

1585 group.add_argument( 

1586 "-v", 

1587 "--verbose", 

1588 action="store_true", 

1589 help="print more messages to console", 

1590 ) 

1591 group.add_argument( 

1592 "-q", 

1593 "--quiet", 

1594 action="store_true", 

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

1596 ) 

1597 

1598 parser_compress.add_argument( 

1599 "input_file", 

1600 metavar="INPUT", 

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

1602 ) 

1603 parser_decompress.add_argument( 

1604 "input_file", 

1605 metavar="INPUT", 

1606 help="the input WOFF2 font", 

1607 ) 

1608 

1609 parser_compress.add_argument( 

1610 "-o", 

1611 "--output-file", 

1612 metavar="OUTPUT", 

1613 help="the output WOFF2 font", 

1614 ) 

1615 parser_decompress.add_argument( 

1616 "-o", 

1617 "--output-file", 

1618 metavar="OUTPUT", 

1619 help="the output OpenType font", 

1620 ) 

1621 

1622 transform_group = parser_compress.add_argument_group() 

1623 transform_group.add_argument( 

1624 "--no-glyf-transform", 

1625 dest="transform_tables", 

1626 nargs=0, 

1627 action=_NoGlyfTransformAction, 

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

1629 ) 

1630 transform_group.add_argument( 

1631 "--hmtx-transform", 

1632 dest="transform_tables", 

1633 nargs=0, 

1634 action=_HmtxTransformAction, 

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

1636 ) 

1637 

1638 parser_compress.set_defaults( 

1639 subcommand=compress, 

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

1641 ) 

1642 parser_decompress.set_defaults(subcommand=decompress) 

1643 

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

1645 

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

1647 if not subcommand: 

1648 parser.print_help() 

1649 return 

1650 

1651 quiet = options.pop("quiet") 

1652 verbose = options.pop("verbose") 

1653 configLogger( 

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

1655 ) 

1656 

1657 if not options["output_file"]: 

1658 if subcommand is compress: 

1659 extension = ".woff2" 

1660 elif subcommand is decompress: 

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

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

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

1664 sfntVersion = f.read(4) 

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

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

1667 else: 

1668 raise AssertionError(subcommand) 

1669 options["output_file"] = makeOutputFileName( 

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

1671 ) 

1672 

1673 try: 

1674 subcommand(**options) 

1675 except TTLibError as e: 

1676 parser.error(e) 

1677 

1678 

1679if __name__ == "__main__": 

1680 sys.exit(main())