1from io import BytesIO 2import sys 3import array 4import struct 5from collections import OrderedDict 6from fontTools.misc import sstruct 7from fontTools.misc.arrayTools import calcIntBounds 8from fontTools.misc.textTools import Tag, bytechr, byteord, bytesjoin, pad 9from fontTools.ttLib import ( 10 TTFont, 11 TTLibError, 12 getTableModule, 13 getTableClass, 14 getSearchRange, 15) 16from fontTools.ttLib.sfnt import ( 17 SFNTReader, 18 SFNTWriter, 19 DirectoryEntry, 20 WOFFFlavorData, 21 sfntDirectoryFormat, 22 sfntDirectorySize, 23 SFNTDirectoryEntry, 24 sfntDirectoryEntrySize, 25 calcChecksum, 26) 27from fontTools.ttLib.tables import ttProgram, _g_l_y_f 28import logging 29 30 31log = logging.getLogger("fontTools.ttLib.woff2") 32 33haveBrotli = False 34try: 35 try: 36 import brotlicffi as brotli 37 except ImportError: 38 import brotli 39 haveBrotli = True 40except ImportError: 41 pass 42 43 44class WOFF2Reader(SFNTReader): 45 flavor = "woff2" 46 47 def __init__(self, file, checkChecksums=0, fontNumber=-1): 48 if not haveBrotli: 49 log.error( 50 "The WOFF2 decoder requires the Brotli Python extension, available at: " 51 "https://github.com/google/brotli" 52 ) 53 raise ImportError("No module named brotli") 54 55 self.file = file 56 57 signature = Tag(self.file.read(4)) 58 if signature != b"wOF2": 59 raise TTLibError("Not a WOFF2 font (bad signature)") 60 61 self.file.seek(0) 62 self.DirectoryEntry = WOFF2DirectoryEntry 63 data = self.file.read(woff2DirectorySize) 64 if len(data) != woff2DirectorySize: 65 raise TTLibError("Not a WOFF2 font (not enough data)") 66 sstruct.unpack(woff2DirectoryFormat, data, self) 67 68 self.tables = OrderedDict() 69 offset = 0 70 for i in range(self.numTables): 71 entry = self.DirectoryEntry() 72 entry.fromFile(self.file) 73 tag = Tag(entry.tag) 74 self.tables[tag] = entry 75 entry.offset = offset 76 offset += entry.length 77 78 totalUncompressedSize = offset 79 compressedData = self.file.read(self.totalCompressedSize) 80 decompressedData = brotli.decompress(compressedData) 81 if len(decompressedData) != totalUncompressedSize: 82 raise TTLibError( 83 "unexpected size for decompressed font data: expected %d, found %d" 84 % (totalUncompressedSize, len(decompressedData)) 85 ) 86 self.transformBuffer = BytesIO(decompressedData) 87 88 self.file.seek(0, 2) 89 if self.length != self.file.tell(): 90 raise TTLibError("reported 'length' doesn't match the actual file size") 91 92 self.flavorData = WOFF2FlavorData(self) 93 94 # make empty TTFont to store data while reconstructing tables 95 self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) 96 97 def __getitem__(self, tag): 98 """Fetch the raw table data. Reconstruct transformed tables.""" 99 entry = self.tables[Tag(tag)] 100 if not hasattr(entry, "data"): 101 if entry.transformed: 102 entry.data = self.reconstructTable(tag) 103 else: 104 entry.data = entry.loadData(self.transformBuffer) 105 return entry.data 106 107 def reconstructTable(self, tag): 108 """Reconstruct table named 'tag' from transformed data.""" 109 entry = self.tables[Tag(tag)] 110 rawData = entry.loadData(self.transformBuffer) 111 if tag == "glyf": 112 # no need to pad glyph data when reconstructing 113 padding = self.padding if hasattr(self, "padding") else None 114 data = self._reconstructGlyf(rawData, padding) 115 elif tag == "loca": 116 data = self._reconstructLoca() 117 elif tag == "hmtx": 118 data = self._reconstructHmtx(rawData) 119 else: 120 raise TTLibError("transform for table '%s' is unknown" % tag) 121 return data 122 123 def _reconstructGlyf(self, data, padding=None): 124 """Return recostructed glyf table data, and set the corresponding loca's 125 locations. Optionally pad glyph offsets to the specified number of bytes. 126 """ 127 self.ttFont["loca"] = WOFF2LocaTable() 128 glyfTable = self.ttFont["glyf"] = WOFF2GlyfTable() 129 glyfTable.reconstruct(data, self.ttFont) 130 if padding: 131 glyfTable.padding = padding 132 data = glyfTable.compile(self.ttFont) 133 return data 134 135 def _reconstructLoca(self): 136 """Return reconstructed loca table data.""" 137 if "loca" not in self.ttFont: 138 # make sure glyf is reconstructed first 139 self.tables["glyf"].data = self.reconstructTable("glyf") 140 locaTable = self.ttFont["loca"] 141 data = locaTable.compile(self.ttFont) 142 if len(data) != self.tables["loca"].origLength: 143 raise TTLibError( 144 "reconstructed 'loca' table doesn't match original size: " 145 "expected %d, found %d" % (self.tables["loca"].origLength, len(data)) 146 ) 147 return data 148 149 def _reconstructHmtx(self, data): 150 """Return reconstructed hmtx table data.""" 151 # Before reconstructing 'hmtx' table we need to parse other tables: 152 # 'glyf' is required for reconstructing the sidebearings from the glyphs' 153 # bounding box; 'hhea' is needed for the numberOfHMetrics field. 154 if "glyf" in self.flavorData.transformedTables: 155 # transformed 'glyf' table is self-contained, thus 'loca' not needed 156 tableDependencies = ("maxp", "hhea", "glyf") 157 else: 158 # decompiling untransformed 'glyf' requires 'loca', which requires 'head' 159 tableDependencies = ("maxp", "head", "hhea", "loca", "glyf") 160 for tag in tableDependencies: 161 self._decompileTable(tag) 162 hmtxTable = self.ttFont["hmtx"] = WOFF2HmtxTable() 163 hmtxTable.reconstruct(data, self.ttFont) 164 data = hmtxTable.compile(self.ttFont) 165 return data 166 167 def _decompileTable(self, tag): 168 """Decompile table data and store it inside self.ttFont.""" 169 data = self[tag] 170 if self.ttFont.isLoaded(tag): 171 return self.ttFont[tag] 172 tableClass = getTableClass(tag) 173 table = tableClass(tag) 174 self.ttFont.tables[tag] = table 175 table.decompile(data, self.ttFont) 176 177 178class WOFF2Writer(SFNTWriter): 179 flavor = "woff2" 180 181 def __init__( 182 self, 183 file, 184 numTables, 185 sfntVersion="\000\001\000\000", 186 flavor=None, 187 flavorData=None, 188 ): 189 if not haveBrotli: 190 log.error( 191 "The WOFF2 encoder requires the Brotli Python extension, available at: " 192 "https://github.com/google/brotli" 193 ) 194 raise ImportError("No module named brotli") 195 196 self.file = file 197 self.numTables = numTables 198 self.sfntVersion = Tag(sfntVersion) 199 self.flavorData = WOFF2FlavorData(data=flavorData) 200 201 self.directoryFormat = woff2DirectoryFormat 202 self.directorySize = woff2DirectorySize 203 self.DirectoryEntry = WOFF2DirectoryEntry 204 205 self.signature = Tag("wOF2") 206 207 self.nextTableOffset = 0 208 self.transformBuffer = BytesIO() 209 210 self.tables = OrderedDict() 211 212 # make empty TTFont to store data while normalising and transforming tables 213 self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) 214 215 def __setitem__(self, tag, data): 216 """Associate new entry named 'tag' with raw table data.""" 217 if tag in self.tables: 218 raise TTLibError("cannot rewrite '%s' table" % tag) 219 if tag == "DSIG": 220 # always drop DSIG table, since the encoding process can invalidate it 221 self.numTables -= 1 222 return 223 224 entry = self.DirectoryEntry() 225 entry.tag = Tag(tag) 226 entry.flags = getKnownTagIndex(entry.tag) 227 # WOFF2 table data are written to disk only on close(), after all tags 228 # have been specified 229 entry.data = data 230 231 self.tables[tag] = entry 232 233 def close(self): 234 """All tags must have been specified. Now write the table data and directory.""" 235 if len(self.tables) != self.numTables: 236 raise TTLibError( 237 "wrong number of tables; expected %d, found %d" 238 % (self.numTables, len(self.tables)) 239 ) 240 241 if self.sfntVersion in ("\x00\x01\x00\x00", "true"): 242 isTrueType = True 243 elif self.sfntVersion == "OTTO": 244 isTrueType = False 245 else: 246 raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") 247 248 # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned. 249 # However, the reference WOFF2 implementation still fails to reconstruct 250 # 'unpadded' glyf tables, therefore we need to 'normalise' them. 251 # See: 252 # https://github.com/khaledhosny/ots/issues/60 253 # https://github.com/google/woff2/issues/15 254 if ( 255 isTrueType 256 and "glyf" in self.flavorData.transformedTables 257 and "glyf" in self.tables 258 ): 259 self._normaliseGlyfAndLoca(padding=4) 260 self._setHeadTransformFlag() 261 262 # To pass the legacy OpenType Sanitiser currently included in browsers, 263 # we must sort the table directory and data alphabetically by tag. 264 # See: 265 # https://github.com/google/woff2/pull/3 266 # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html 267 # 268 # 2023: We rely on this in _transformTables where we expect that 269 # "loca" comes after "glyf" table. 270 self.tables = OrderedDict(sorted(self.tables.items())) 271 272 self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() 273 274 fontData = self._transformTables() 275 compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT) 276 277 self.totalCompressedSize = len(compressedFont) 278 self.length = self._calcTotalSize() 279 self.majorVersion, self.minorVersion = self._getVersion() 280 self.reserved = 0 281 282 directory = self._packTableDirectory() 283 self.file.seek(0) 284 self.file.write(pad(directory + compressedFont, size=4)) 285 self._writeFlavorData() 286 287 def _normaliseGlyfAndLoca(self, padding=4): 288 """Recompile glyf and loca tables, aligning glyph offsets to multiples of 289 'padding' size. Update the head table's 'indexToLocFormat' accordingly while 290 compiling loca. 291 """ 292 if self.sfntVersion == "OTTO": 293 return 294 295 for tag in ("maxp", "head", "loca", "glyf", "fvar"): 296 if tag in self.tables: 297 self._decompileTable(tag) 298 self.ttFont["glyf"].padding = padding 299 for tag in ("glyf", "loca"): 300 self._compileTable(tag) 301 302 def _setHeadTransformFlag(self): 303 """Set bit 11 of 'head' table flags to indicate that the font has undergone 304 a lossless modifying transform. Re-compile head table data.""" 305 self._decompileTable("head") 306 self.ttFont["head"].flags |= 1 << 11 307 self._compileTable("head") 308 309 def _decompileTable(self, tag): 310 """Fetch table data, decompile it, and store it inside self.ttFont.""" 311 tag = Tag(tag) 312 if tag not in self.tables: 313 raise TTLibError("missing required table: %s" % tag) 314 if self.ttFont.isLoaded(tag): 315 return 316 data = self.tables[tag].data 317 if tag == "loca": 318 tableClass = WOFF2LocaTable 319 elif tag == "glyf": 320 tableClass = WOFF2GlyfTable 321 elif tag == "hmtx": 322 tableClass = WOFF2HmtxTable 323 else: 324 tableClass = getTableClass(tag) 325 table = tableClass(tag) 326 self.ttFont.tables[tag] = table 327 table.decompile(data, self.ttFont) 328 329 def _compileTable(self, tag): 330 """Compile table and store it in its 'data' attribute.""" 331 self.tables[tag].data = self.ttFont[tag].compile(self.ttFont) 332 333 def _calcSFNTChecksumsLengthsAndOffsets(self): 334 """Compute the 'original' SFNT checksums, lengths and offsets for checksum 335 adjustment calculation. Return the total size of the uncompressed font. 336 """ 337 offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables) 338 for tag, entry in self.tables.items(): 339 data = entry.data 340 entry.origOffset = offset 341 entry.origLength = len(data) 342 if tag == "head": 343 entry.checkSum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:]) 344 else: 345 entry.checkSum = calcChecksum(data) 346 offset += (entry.origLength + 3) & ~3 347 return offset 348 349 def _transformTables(self): 350 """Return transformed font data.""" 351 transformedTables = self.flavorData.transformedTables 352 for tag, entry in self.tables.items(): 353 data = None 354 if tag in transformedTables: 355 data = self.transformTable(tag) 356 if data is not None: 357 entry.transformed = True 358 if data is None: 359 if tag == "glyf": 360 # Currently we always sort table tags so 361 # 'loca' comes after 'glyf'. 362 transformedTables.discard("loca") 363 # pass-through the table data without transformation 364 data = entry.data 365 entry.transformed = False 366 entry.offset = self.nextTableOffset 367 entry.saveData(self.transformBuffer, data) 368 self.nextTableOffset += entry.length 369 self.writeMasterChecksum() 370 fontData = self.transformBuffer.getvalue() 371 return fontData 372 373 def transformTable(self, tag): 374 """Return transformed table data, or None if some pre-conditions aren't 375 met -- in which case, the non-transformed table data will be used. 376 """ 377 if tag == "loca": 378 data = b"" 379 elif tag == "glyf": 380 for tag in ("maxp", "head", "loca", "glyf"): 381 self._decompileTable(tag) 382 glyfTable = self.ttFont["glyf"] 383 data = glyfTable.transform(self.ttFont) 384 elif tag == "hmtx": 385 if "glyf" not in self.tables: 386 return 387 for tag in ("maxp", "head", "hhea", "loca", "glyf", "hmtx"): 388 self._decompileTable(tag) 389 hmtxTable = self.ttFont["hmtx"] 390 data = hmtxTable.transform(self.ttFont) # can be None 391 else: 392 raise TTLibError("Transform for table '%s' is unknown" % tag) 393 return data 394 395 def _calcMasterChecksum(self): 396 """Calculate checkSumAdjustment.""" 397 tags = list(self.tables.keys()) 398 checksums = [] 399 for i in range(len(tags)): 400 checksums.append(self.tables[tags[i]].checkSum) 401 402 # Create a SFNT directory for checksum calculation purposes 403 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( 404 self.numTables, 16 405 ) 406 directory = sstruct.pack(sfntDirectoryFormat, self) 407 tables = sorted(self.tables.items()) 408 for tag, entry in tables: 409 sfntEntry = SFNTDirectoryEntry() 410 sfntEntry.tag = entry.tag 411 sfntEntry.checkSum = entry.checkSum 412 sfntEntry.offset = entry.origOffset 413 sfntEntry.length = entry.origLength 414 directory = directory + sfntEntry.toString() 415 416 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize 417 assert directory_end == len(directory) 418 419 checksums.append(calcChecksum(directory)) 420 checksum = sum(checksums) & 0xFFFFFFFF 421 # BiboAfba! 422 checksumadjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF 423 return checksumadjustment 424 425 def writeMasterChecksum(self): 426 """Write checkSumAdjustment to the transformBuffer.""" 427 checksumadjustment = self._calcMasterChecksum() 428 self.transformBuffer.seek(self.tables["head"].offset + 8) 429 self.transformBuffer.write(struct.pack(">L", checksumadjustment)) 430 431 def _calcTotalSize(self): 432 """Calculate total size of WOFF2 font, including any meta- and/or private data.""" 433 offset = self.directorySize 434 for entry in self.tables.values(): 435 offset += len(entry.toString()) 436 offset += self.totalCompressedSize 437 offset = (offset + 3) & ~3 438 offset = self._calcFlavorDataOffsetsAndSize(offset) 439 return offset 440 441 def _calcFlavorDataOffsetsAndSize(self, start): 442 """Calculate offsets and lengths for any meta- and/or private data.""" 443 offset = start 444 data = self.flavorData 445 if data.metaData: 446 self.metaOrigLength = len(data.metaData) 447 self.metaOffset = offset 448 self.compressedMetaData = brotli.compress( 449 data.metaData, mode=brotli.MODE_TEXT 450 ) 451 self.metaLength = len(self.compressedMetaData) 452 offset += self.metaLength 453 else: 454 self.metaOffset = self.metaLength = self.metaOrigLength = 0 455 self.compressedMetaData = b"" 456 if data.privData: 457 # make sure private data is padded to 4-byte boundary 458 offset = (offset + 3) & ~3 459 self.privOffset = offset 460 self.privLength = len(data.privData) 461 offset += self.privLength 462 else: 463 self.privOffset = self.privLength = 0 464 return offset 465 466 def _getVersion(self): 467 """Return the WOFF2 font's (majorVersion, minorVersion) tuple.""" 468 data = self.flavorData 469 if data.majorVersion is not None and data.minorVersion is not None: 470 return data.majorVersion, data.minorVersion 471 else: 472 # if None, return 'fontRevision' from 'head' table 473 if "head" in self.tables: 474 return struct.unpack(">HH", self.tables["head"].data[4:8]) 475 else: 476 return 0, 0 477 478 def _packTableDirectory(self): 479 """Return WOFF2 table directory data.""" 480 directory = sstruct.pack(self.directoryFormat, self) 481 for entry in self.tables.values(): 482 directory = directory + entry.toString() 483 return directory 484 485 def _writeFlavorData(self): 486 """Write metadata and/or private data using appropiate padding.""" 487 compressedMetaData = self.compressedMetaData 488 privData = self.flavorData.privData 489 if compressedMetaData and privData: 490 compressedMetaData = pad(compressedMetaData, size=4) 491 if compressedMetaData: 492 self.file.seek(self.metaOffset) 493 assert self.file.tell() == self.metaOffset 494 self.file.write(compressedMetaData) 495 if privData: 496 self.file.seek(self.privOffset) 497 assert self.file.tell() == self.privOffset 498 self.file.write(privData) 499 500 def reordersTables(self): 501 return True 502 503 504# -- woff2 directory helpers and cruft 505 506woff2DirectoryFormat = """ 507 > # big endian 508 signature: 4s # "wOF2" 509 sfntVersion: 4s 510 length: L # total woff2 file size 511 numTables: H # number of tables 512 reserved: H # set to 0 513 totalSfntSize: L # uncompressed size 514 totalCompressedSize: L # compressed size 515 majorVersion: H # major version of WOFF file 516 minorVersion: H # minor version of WOFF file 517 metaOffset: L # offset to metadata block 518 metaLength: L # length of compressed metadata 519 metaOrigLength: L # length of uncompressed metadata 520 privOffset: L # offset to private data block 521 privLength: L # length of private data block 522""" 523 524woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat) 525 526woff2KnownTags = ( 527 "cmap", 528 "head", 529 "hhea", 530 "hmtx", 531 "maxp", 532 "name", 533 "OS/2", 534 "post", 535 "cvt ", 536 "fpgm", 537 "glyf", 538 "loca", 539 "prep", 540 "CFF ", 541 "VORG", 542 "EBDT", 543 "EBLC", 544 "gasp", 545 "hdmx", 546 "kern", 547 "LTSH", 548 "PCLT", 549 "VDMX", 550 "vhea", 551 "vmtx", 552 "BASE", 553 "GDEF", 554 "GPOS", 555 "GSUB", 556 "EBSC", 557 "JSTF", 558 "MATH", 559 "CBDT", 560 "CBLC", 561 "COLR", 562 "CPAL", 563 "SVG ", 564 "sbix", 565 "acnt", 566 "avar", 567 "bdat", 568 "bloc", 569 "bsln", 570 "cvar", 571 "fdsc", 572 "feat", 573 "fmtx", 574 "fvar", 575 "gvar", 576 "hsty", 577 "just", 578 "lcar", 579 "mort", 580 "morx", 581 "opbd", 582 "prop", 583 "trak", 584 "Zapf", 585 "Silf", 586 "Glat", 587 "Gloc", 588 "Feat", 589 "Sill", 590) 591 592woff2FlagsFormat = """ 593 > # big endian 594 flags: B # table type and flags 595""" 596 597woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat) 598 599woff2UnknownTagFormat = """ 600 > # big endian 601 tag: 4s # 4-byte tag (optional) 602""" 603 604woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat) 605 606woff2UnknownTagIndex = 0x3F 607 608woff2Base128MaxSize = 5 609woff2DirectoryEntryMaxSize = ( 610 woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize 611) 612 613woff2TransformedTableTags = ("glyf", "loca") 614 615woff2GlyfTableFormat = """ 616 > # big endian 617 version: H # = 0x0000 618 optionFlags: H # Bit 0: we have overlapSimpleBitmap[], Bits 1-15: reserved 619 numGlyphs: H # Number of glyphs 620 indexFormat: H # Offset format for loca table 621 nContourStreamSize: L # Size of nContour stream 622 nPointsStreamSize: L # Size of nPoints stream 623 flagStreamSize: L # Size of flag stream 624 glyphStreamSize: L # Size of glyph stream 625 compositeStreamSize: L # Size of composite stream 626 bboxStreamSize: L # Comnined size of bboxBitmap and bboxStream 627 instructionStreamSize: L # Size of instruction stream 628""" 629 630woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat) 631 632bboxFormat = """ 633 > # big endian 634 xMin: h 635 yMin: h 636 xMax: h 637 yMax: h 638""" 639 640woff2OverlapSimpleBitmapFlag = 0x0001 641 642 643def getKnownTagIndex(tag): 644 """Return index of 'tag' in woff2KnownTags list. Return 63 if not found.""" 645 for i in range(len(woff2KnownTags)): 646 if tag == woff2KnownTags[i]: 647 return i 648 return woff2UnknownTagIndex 649 650 651class WOFF2DirectoryEntry(DirectoryEntry): 652 def fromFile(self, file): 653 pos = file.tell() 654 data = file.read(woff2DirectoryEntryMaxSize) 655 left = self.fromString(data) 656 consumed = len(data) - len(left) 657 file.seek(pos + consumed) 658 659 def fromString(self, data): 660 if len(data) < 1: 661 raise TTLibError("can't read table 'flags': not enough data") 662 dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self) 663 if self.flags & 0x3F == 0x3F: 664 # if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value 665 if len(data) < woff2UnknownTagSize: 666 raise TTLibError("can't read table 'tag': not enough data") 667 dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self) 668 else: 669 # otherwise, tag is derived from a fixed 'Known Tags' table 670 self.tag = woff2KnownTags[self.flags & 0x3F] 671 self.tag = Tag(self.tag) 672 self.origLength, data = unpackBase128(data) 673 self.length = self.origLength 674 if self.transformed: 675 self.length, data = unpackBase128(data) 676 if self.tag == "loca" and self.length != 0: 677 raise TTLibError("the transformLength of the 'loca' table must be 0") 678 # return left over data 679 return data 680 681 def toString(self): 682 data = bytechr(self.flags) 683 if (self.flags & 0x3F) == 0x3F: 684 data += struct.pack(">4s", self.tag.tobytes()) 685 data += packBase128(self.origLength) 686 if self.transformed: 687 data += packBase128(self.length) 688 return data 689 690 @property 691 def transformVersion(self): 692 """Return bits 6-7 of table entry's flags, which indicate the preprocessing 693 transformation version number (between 0 and 3). 694 """ 695 return self.flags >> 6 696 697 @transformVersion.setter 698 def transformVersion(self, value): 699 assert 0 <= value <= 3 700 self.flags |= value << 6 701 702 @property 703 def transformed(self): 704 """Return True if the table has any transformation, else return False.""" 705 # For all tables in a font, except for 'glyf' and 'loca', the transformation 706 # version 0 indicates the null transform (where the original table data is 707 # passed directly to the Brotli compressor). For 'glyf' and 'loca' tables, 708 # transformation version 3 indicates the null transform 709 if self.tag in {"glyf", "loca"}: 710 return self.transformVersion != 3 711 else: 712 return self.transformVersion != 0 713 714 @transformed.setter 715 def transformed(self, booleanValue): 716 # here we assume that a non-null transform means version 0 for 'glyf' and 717 # 'loca' and 1 for every other table (e.g. hmtx); but that may change as 718 # new transformation formats are introduced in the future (if ever). 719 if self.tag in {"glyf", "loca"}: 720 self.transformVersion = 3 if not booleanValue else 0 721 else: 722 self.transformVersion = int(booleanValue) 723 724 725class WOFF2LocaTable(getTableClass("loca")): 726 """Same as parent class. The only difference is that it attempts to preserve 727 the 'indexFormat' as encoded in the WOFF2 glyf table. 728 """ 729 730 def __init__(self, tag=None): 731 self.tableTag = Tag(tag or "loca") 732 733 def compile(self, ttFont): 734 try: 735 max_location = max(self.locations) 736 except AttributeError: 737 self.set([]) 738 max_location = 0 739 if "glyf" in ttFont and hasattr(ttFont["glyf"], "indexFormat"): 740 # copile loca using the indexFormat specified in the WOFF2 glyf table 741 indexFormat = ttFont["glyf"].indexFormat 742 if indexFormat == 0: 743 if max_location >= 0x20000: 744 raise TTLibError("indexFormat is 0 but local offsets > 0x20000") 745 if not all(l % 2 == 0 for l in self.locations): 746 raise TTLibError( 747 "indexFormat is 0 but local offsets not multiples of 2" 748 ) 749 locations = array.array("H") 750 for i in range(len(self.locations)): 751 locations.append(self.locations[i] // 2) 752 else: 753 locations = array.array("I", self.locations) 754 if sys.byteorder != "big": 755 locations.byteswap() 756 data = locations.tobytes() 757 else: 758 # use the most compact indexFormat given the current glyph offsets 759 data = super(WOFF2LocaTable, self).compile(ttFont) 760 return data 761 762 763class WOFF2GlyfTable(getTableClass("glyf")): 764 """Decoder/Encoder for WOFF2 'glyf' table transform.""" 765 766 subStreams = ( 767 "nContourStream", 768 "nPointsStream", 769 "flagStream", 770 "glyphStream", 771 "compositeStream", 772 "bboxStream", 773 "instructionStream", 774 ) 775 776 def __init__(self, tag=None): 777 self.tableTag = Tag(tag or "glyf") 778 779 def reconstruct(self, data, ttFont): 780 """Decompile transformed 'glyf' data.""" 781 inputDataSize = len(data) 782 783 if inputDataSize < woff2GlyfTableFormatSize: 784 raise TTLibError("not enough 'glyf' data") 785 dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self) 786 offset = woff2GlyfTableFormatSize 787 788 for stream in self.subStreams: 789 size = getattr(self, stream + "Size") 790 setattr(self, stream, data[:size]) 791 data = data[size:] 792 offset += size 793 794 hasOverlapSimpleBitmap = self.optionFlags & woff2OverlapSimpleBitmapFlag 795 self.overlapSimpleBitmap = None 796 if hasOverlapSimpleBitmap: 797 overlapSimpleBitmapSize = (self.numGlyphs + 7) >> 3 798 self.overlapSimpleBitmap = array.array("B", data[:overlapSimpleBitmapSize]) 799 offset += overlapSimpleBitmapSize 800 801 if offset != inputDataSize: 802 raise TTLibError( 803 "incorrect size of transformed 'glyf' table: expected %d, received %d bytes" 804 % (offset, inputDataSize) 805 ) 806 807 bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 808 bboxBitmap = self.bboxStream[:bboxBitmapSize] 809 self.bboxBitmap = array.array("B", bboxBitmap) 810 self.bboxStream = self.bboxStream[bboxBitmapSize:] 811 812 self.nContourStream = array.array("h", self.nContourStream) 813 if sys.byteorder != "big": 814 self.nContourStream.byteswap() 815 assert len(self.nContourStream) == self.numGlyphs 816 817 if "head" in ttFont: 818 ttFont["head"].indexToLocFormat = self.indexFormat 819 try: 820 self.glyphOrder = ttFont.getGlyphOrder() 821 except: 822 self.glyphOrder = None 823 if self.glyphOrder is None: 824 self.glyphOrder = [".notdef"] 825 self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)]) 826 else: 827 if len(self.glyphOrder) != self.numGlyphs: 828 raise TTLibError( 829 "incorrect glyphOrder: expected %d glyphs, found %d" 830 % (len(self.glyphOrder), self.numGlyphs) 831 ) 832 833 glyphs = self.glyphs = {} 834 for glyphID, glyphName in enumerate(self.glyphOrder): 835 glyph = self._decodeGlyph(glyphID) 836 glyphs[glyphName] = glyph 837 838 def transform(self, ttFont): 839 """Return transformed 'glyf' data""" 840 self.numGlyphs = len(self.glyphs) 841 assert len(self.glyphOrder) == self.numGlyphs 842 if "maxp" in ttFont: 843 ttFont["maxp"].numGlyphs = self.numGlyphs 844 self.indexFormat = ttFont["head"].indexToLocFormat 845 846 for stream in self.subStreams: 847 setattr(self, stream, b"") 848 bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 849 self.bboxBitmap = array.array("B", [0] * bboxBitmapSize) 850 851 self.overlapSimpleBitmap = array.array("B", [0] * ((self.numGlyphs + 7) >> 3)) 852 for glyphID in range(self.numGlyphs): 853 try: 854 self._encodeGlyph(glyphID) 855 except NotImplementedError: 856 return None 857 hasOverlapSimpleBitmap = any(self.overlapSimpleBitmap) 858 859 self.bboxStream = self.bboxBitmap.tobytes() + self.bboxStream 860 for stream in self.subStreams: 861 setattr(self, stream + "Size", len(getattr(self, stream))) 862 self.version = 0 863 self.optionFlags = 0 864 if hasOverlapSimpleBitmap: 865 self.optionFlags |= woff2OverlapSimpleBitmapFlag 866 data = sstruct.pack(woff2GlyfTableFormat, self) 867 data += bytesjoin([getattr(self, s) for s in self.subStreams]) 868 if hasOverlapSimpleBitmap: 869 data += self.overlapSimpleBitmap.tobytes() 870 return data 871 872 def _decodeGlyph(self, glyphID): 873 glyph = getTableModule("glyf").Glyph() 874 glyph.numberOfContours = self.nContourStream[glyphID] 875 if glyph.numberOfContours == 0: 876 return glyph 877 elif glyph.isComposite(): 878 self._decodeComponents(glyph) 879 else: 880 self._decodeCoordinates(glyph) 881 self._decodeOverlapSimpleFlag(glyph, glyphID) 882 self._decodeBBox(glyphID, glyph) 883 return glyph 884 885 def _decodeComponents(self, glyph): 886 data = self.compositeStream 887 glyph.components = [] 888 more = 1 889 haveInstructions = 0 890 while more: 891 component = getTableModule("glyf").GlyphComponent() 892 more, haveInstr, data = component.decompile(data, self) 893 haveInstructions = haveInstructions | haveInstr 894 glyph.components.append(component) 895 self.compositeStream = data 896 if haveInstructions: 897 self._decodeInstructions(glyph) 898 899 def _decodeCoordinates(self, glyph): 900 data = self.nPointsStream 901 endPtsOfContours = [] 902 endPoint = -1 903 for i in range(glyph.numberOfContours): 904 ptsOfContour, data = unpack255UShort(data) 905 endPoint += ptsOfContour 906 endPtsOfContours.append(endPoint) 907 glyph.endPtsOfContours = endPtsOfContours 908 self.nPointsStream = data 909 self._decodeTriplets(glyph) 910 self._decodeInstructions(glyph) 911 912 def _decodeOverlapSimpleFlag(self, glyph, glyphID): 913 if self.overlapSimpleBitmap is None or glyph.numberOfContours <= 0: 914 return 915 byte = glyphID >> 3 916 bit = glyphID & 7 917 if self.overlapSimpleBitmap[byte] & (0x80 >> bit): 918 glyph.flags[0] |= _g_l_y_f.flagOverlapSimple 919 920 def _decodeInstructions(self, glyph): 921 glyphStream = self.glyphStream 922 instructionStream = self.instructionStream 923 instructionLength, glyphStream = unpack255UShort(glyphStream) 924 glyph.program = ttProgram.Program() 925 glyph.program.fromBytecode(instructionStream[:instructionLength]) 926 self.glyphStream = glyphStream 927 self.instructionStream = instructionStream[instructionLength:] 928 929 def _decodeBBox(self, glyphID, glyph): 930 haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7))) 931 if glyph.isComposite() and not haveBBox: 932 raise TTLibError("no bbox values for composite glyph %d" % glyphID) 933 if haveBBox: 934 dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph) 935 else: 936 glyph.recalcBounds(self) 937 938 def _decodeTriplets(self, glyph): 939 def withSign(flag, baseval): 940 assert 0 <= baseval and baseval < 65536, "integer overflow" 941 return baseval if flag & 1 else -baseval 942 943 nPoints = glyph.endPtsOfContours[-1] + 1 944 flagSize = nPoints 945 if flagSize > len(self.flagStream): 946 raise TTLibError("not enough 'flagStream' data") 947 flagsData = self.flagStream[:flagSize] 948 self.flagStream = self.flagStream[flagSize:] 949 flags = array.array("B", flagsData) 950 951 triplets = array.array("B", self.glyphStream) 952 nTriplets = len(triplets) 953 assert nPoints <= nTriplets 954 955 x = 0 956 y = 0 957 glyph.coordinates = getTableModule("glyf").GlyphCoordinates.zeros(nPoints) 958 glyph.flags = array.array("B") 959 tripletIndex = 0 960 for i in range(nPoints): 961 flag = flags[i] 962 onCurve = not bool(flag >> 7) 963 flag &= 0x7F 964 if flag < 84: 965 nBytes = 1 966 elif flag < 120: 967 nBytes = 2 968 elif flag < 124: 969 nBytes = 3 970 else: 971 nBytes = 4 972 assert (tripletIndex + nBytes) <= nTriplets 973 if flag < 10: 974 dx = 0 975 dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex]) 976 elif flag < 20: 977 dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex]) 978 dy = 0 979 elif flag < 84: 980 b0 = flag - 20 981 b1 = triplets[tripletIndex] 982 dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4)) 983 dy = withSign(flag >> 1, 1 + ((b0 & 0x0C) << 2) + (b1 & 0x0F)) 984 elif flag < 120: 985 b0 = flag - 84 986 dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex]) 987 dy = withSign( 988 flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1] 989 ) 990 elif flag < 124: 991 b2 = triplets[tripletIndex + 1] 992 dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4)) 993 dy = withSign( 994 flag >> 1, ((b2 & 0x0F) << 8) + triplets[tripletIndex + 2] 995 ) 996 else: 997 dx = withSign( 998 flag, (triplets[tripletIndex] << 8) + triplets[tripletIndex + 1] 999 ) 1000 dy = withSign( 1001 flag >> 1, 1002 (triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3], 1003 ) 1004 tripletIndex += nBytes 1005 x += dx 1006 y += dy 1007 glyph.coordinates[i] = (x, y) 1008 glyph.flags.append(int(onCurve)) 1009 bytesConsumed = tripletIndex 1010 self.glyphStream = self.glyphStream[bytesConsumed:] 1011 1012 def _encodeGlyph(self, glyphID): 1013 glyphName = self.getGlyphName(glyphID) 1014 glyph = self[glyphName] 1015 self.nContourStream += struct.pack(">h", glyph.numberOfContours) 1016 if glyph.numberOfContours == 0: 1017 return 1018 elif glyph.isComposite(): 1019 self._encodeComponents(glyph) 1020 elif glyph.isVarComposite(): 1021 raise NotImplementedError 1022 else: 1023 self._encodeCoordinates(glyph) 1024 self._encodeOverlapSimpleFlag(glyph, glyphID) 1025 self._encodeBBox(glyphID, glyph) 1026 1027 def _encodeComponents(self, glyph): 1028 lastcomponent = len(glyph.components) - 1 1029 more = 1 1030 haveInstructions = 0 1031 for i in range(len(glyph.components)): 1032 if i == lastcomponent: 1033 haveInstructions = hasattr(glyph, "program") 1034 more = 0 1035 component = glyph.components[i] 1036 self.compositeStream += component.compile(more, haveInstructions, self) 1037 if haveInstructions: 1038 self._encodeInstructions(glyph) 1039 1040 def _encodeCoordinates(self, glyph): 1041 lastEndPoint = -1 1042 if _g_l_y_f.flagCubic in glyph.flags: 1043 raise NotImplementedError 1044 for endPoint in glyph.endPtsOfContours: 1045 ptsOfContour = endPoint - lastEndPoint 1046 self.nPointsStream += pack255UShort(ptsOfContour) 1047 lastEndPoint = endPoint 1048 self._encodeTriplets(glyph) 1049 self._encodeInstructions(glyph) 1050 1051 def _encodeOverlapSimpleFlag(self, glyph, glyphID): 1052 if glyph.numberOfContours <= 0: 1053 return 1054 if glyph.flags[0] & _g_l_y_f.flagOverlapSimple: 1055 byte = glyphID >> 3 1056 bit = glyphID & 7 1057 self.overlapSimpleBitmap[byte] |= 0x80 >> bit 1058 1059 def _encodeInstructions(self, glyph): 1060 instructions = glyph.program.getBytecode() 1061 self.glyphStream += pack255UShort(len(instructions)) 1062 self.instructionStream += instructions 1063 1064 def _encodeBBox(self, glyphID, glyph): 1065 assert glyph.numberOfContours != 0, "empty glyph has no bbox" 1066 if not glyph.isComposite(): 1067 # for simple glyphs, compare the encoded bounding box info with the calculated 1068 # values, and if they match omit the bounding box info 1069 currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax 1070 calculatedBBox = calcIntBounds(glyph.coordinates) 1071 if currentBBox == calculatedBBox: 1072 return 1073 self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7) 1074 self.bboxStream += sstruct.pack(bboxFormat, glyph) 1075 1076 def _encodeTriplets(self, glyph): 1077 assert len(glyph.coordinates) == len(glyph.flags) 1078 coordinates = glyph.coordinates.copy() 1079 coordinates.absoluteToRelative() 1080 1081 flags = array.array("B") 1082 triplets = array.array("B") 1083 for i in range(len(coordinates)): 1084 onCurve = glyph.flags[i] & _g_l_y_f.flagOnCurve 1085 x, y = coordinates[i] 1086 absX = abs(x) 1087 absY = abs(y) 1088 onCurveBit = 0 if onCurve else 128 1089 xSignBit = 0 if (x < 0) else 1 1090 ySignBit = 0 if (y < 0) else 1 1091 xySignBits = xSignBit + 2 * ySignBit 1092 1093 if x == 0 and absY < 1280: 1094 flags.append(onCurveBit + ((absY & 0xF00) >> 7) + ySignBit) 1095 triplets.append(absY & 0xFF) 1096 elif y == 0 and absX < 1280: 1097 flags.append(onCurveBit + 10 + ((absX & 0xF00) >> 7) + xSignBit) 1098 triplets.append(absX & 0xFF) 1099 elif absX < 65 and absY < 65: 1100 flags.append( 1101 onCurveBit 1102 + 20 1103 + ((absX - 1) & 0x30) 1104 + (((absY - 1) & 0x30) >> 2) 1105 + xySignBits 1106 ) 1107 triplets.append((((absX - 1) & 0xF) << 4) | ((absY - 1) & 0xF)) 1108 elif absX < 769 and absY < 769: 1109 flags.append( 1110 onCurveBit 1111 + 84 1112 + 12 * (((absX - 1) & 0x300) >> 8) 1113 + (((absY - 1) & 0x300) >> 6) 1114 + xySignBits 1115 ) 1116 triplets.append((absX - 1) & 0xFF) 1117 triplets.append((absY - 1) & 0xFF) 1118 elif absX < 4096 and absY < 4096: 1119 flags.append(onCurveBit + 120 + xySignBits) 1120 triplets.append(absX >> 4) 1121 triplets.append(((absX & 0xF) << 4) | (absY >> 8)) 1122 triplets.append(absY & 0xFF) 1123 else: 1124 flags.append(onCurveBit + 124 + xySignBits) 1125 triplets.append(absX >> 8) 1126 triplets.append(absX & 0xFF) 1127 triplets.append(absY >> 8) 1128 triplets.append(absY & 0xFF) 1129 1130 self.flagStream += flags.tobytes() 1131 self.glyphStream += triplets.tobytes() 1132 1133 1134class WOFF2HmtxTable(getTableClass("hmtx")): 1135 def __init__(self, tag=None): 1136 self.tableTag = Tag(tag or "hmtx") 1137 1138 def reconstruct(self, data, ttFont): 1139 (flags,) = struct.unpack(">B", data[:1]) 1140 data = data[1:] 1141 if flags & 0b11111100 != 0: 1142 raise TTLibError("Bits 2-7 of '%s' flags are reserved" % self.tableTag) 1143 1144 # When bit 0 is _not_ set, the lsb[] array is present 1145 hasLsbArray = flags & 1 == 0 1146 # When bit 1 is _not_ set, the leftSideBearing[] array is present 1147 hasLeftSideBearingArray = flags & 2 == 0 1148 if hasLsbArray and hasLeftSideBearingArray: 1149 raise TTLibError( 1150 "either bits 0 or 1 (or both) must set in transformed '%s' flags" 1151 % self.tableTag 1152 ) 1153 1154 glyfTable = ttFont["glyf"] 1155 headerTable = ttFont["hhea"] 1156 glyphOrder = glyfTable.glyphOrder 1157 numGlyphs = len(glyphOrder) 1158 numberOfHMetrics = min(int(headerTable.numberOfHMetrics), numGlyphs) 1159 1160 assert len(data) >= 2 * numberOfHMetrics 1161 advanceWidthArray = array.array("H", data[: 2 * numberOfHMetrics]) 1162 if sys.byteorder != "big": 1163 advanceWidthArray.byteswap() 1164 data = data[2 * numberOfHMetrics :] 1165 1166 if hasLsbArray: 1167 assert len(data) >= 2 * numberOfHMetrics 1168 lsbArray = array.array("h", data[: 2 * numberOfHMetrics]) 1169 if sys.byteorder != "big": 1170 lsbArray.byteswap() 1171 data = data[2 * numberOfHMetrics :] 1172 else: 1173 # compute (proportional) glyphs' lsb from their xMin 1174 lsbArray = array.array("h") 1175 for i, glyphName in enumerate(glyphOrder): 1176 if i >= numberOfHMetrics: 1177 break 1178 glyph = glyfTable[glyphName] 1179 xMin = getattr(glyph, "xMin", 0) 1180 lsbArray.append(xMin) 1181 1182 numberOfSideBearings = numGlyphs - numberOfHMetrics 1183 if hasLeftSideBearingArray: 1184 assert len(data) >= 2 * numberOfSideBearings 1185 leftSideBearingArray = array.array("h", data[: 2 * numberOfSideBearings]) 1186 if sys.byteorder != "big": 1187 leftSideBearingArray.byteswap() 1188 data = data[2 * numberOfSideBearings :] 1189 else: 1190 # compute (monospaced) glyphs' leftSideBearing from their xMin 1191 leftSideBearingArray = array.array("h") 1192 for i, glyphName in enumerate(glyphOrder): 1193 if i < numberOfHMetrics: 1194 continue 1195 glyph = glyfTable[glyphName] 1196 xMin = getattr(glyph, "xMin", 0) 1197 leftSideBearingArray.append(xMin) 1198 1199 if data: 1200 raise TTLibError("too much '%s' table data" % self.tableTag) 1201 1202 self.metrics = {} 1203 for i in range(numberOfHMetrics): 1204 glyphName = glyphOrder[i] 1205 advanceWidth, lsb = advanceWidthArray[i], lsbArray[i] 1206 self.metrics[glyphName] = (advanceWidth, lsb) 1207 lastAdvance = advanceWidthArray[-1] 1208 for i in range(numberOfSideBearings): 1209 glyphName = glyphOrder[i + numberOfHMetrics] 1210 self.metrics[glyphName] = (lastAdvance, leftSideBearingArray[i]) 1211 1212 def transform(self, ttFont): 1213 glyphOrder = ttFont.getGlyphOrder() 1214 glyf = ttFont["glyf"] 1215 hhea = ttFont["hhea"] 1216 numberOfHMetrics = hhea.numberOfHMetrics 1217 1218 # check if any of the proportional glyphs has left sidebearings that 1219 # differ from their xMin bounding box values. 1220 hasLsbArray = False 1221 for i in range(numberOfHMetrics): 1222 glyphName = glyphOrder[i] 1223 lsb = self.metrics[glyphName][1] 1224 if lsb != getattr(glyf[glyphName], "xMin", 0): 1225 hasLsbArray = True 1226 break 1227 1228 # do the same for the monospaced glyphs (if any) at the end of hmtx table 1229 hasLeftSideBearingArray = False 1230 for i in range(numberOfHMetrics, len(glyphOrder)): 1231 glyphName = glyphOrder[i] 1232 lsb = self.metrics[glyphName][1] 1233 if lsb != getattr(glyf[glyphName], "xMin", 0): 1234 hasLeftSideBearingArray = True 1235 break 1236 1237 # if we need to encode both sidebearings arrays, then no transformation is 1238 # applicable, and we must use the untransformed hmtx data 1239 if hasLsbArray and hasLeftSideBearingArray: 1240 return 1241 1242 # set bit 0 and 1 when the respective arrays are _not_ present 1243 flags = 0 1244 if not hasLsbArray: 1245 flags |= 1 << 0 1246 if not hasLeftSideBearingArray: 1247 flags |= 1 << 1 1248 1249 data = struct.pack(">B", flags) 1250 1251 advanceWidthArray = array.array( 1252 "H", 1253 [ 1254 self.metrics[glyphName][0] 1255 for i, glyphName in enumerate(glyphOrder) 1256 if i < numberOfHMetrics 1257 ], 1258 ) 1259 if sys.byteorder != "big": 1260 advanceWidthArray.byteswap() 1261 data += advanceWidthArray.tobytes() 1262 1263 if hasLsbArray: 1264 lsbArray = array.array( 1265 "h", 1266 [ 1267 self.metrics[glyphName][1] 1268 for i, glyphName in enumerate(glyphOrder) 1269 if i < numberOfHMetrics 1270 ], 1271 ) 1272 if sys.byteorder != "big": 1273 lsbArray.byteswap() 1274 data += lsbArray.tobytes() 1275 1276 if hasLeftSideBearingArray: 1277 leftSideBearingArray = array.array( 1278 "h", 1279 [ 1280 self.metrics[glyphOrder[i]][1] 1281 for i in range(numberOfHMetrics, len(glyphOrder)) 1282 ], 1283 ) 1284 if sys.byteorder != "big": 1285 leftSideBearingArray.byteswap() 1286 data += leftSideBearingArray.tobytes() 1287 1288 return data 1289 1290 1291class WOFF2FlavorData(WOFFFlavorData): 1292 Flavor = "woff2" 1293 1294 def __init__(self, reader=None, data=None, transformedTables=None): 1295 """Data class that holds the WOFF2 header major/minor version, any 1296 metadata or private data (as bytes strings), and the set of 1297 table tags that have transformations applied (if reader is not None), 1298 or will have once the WOFF2 font is compiled. 1299 1300 Args: 1301 reader: an SFNTReader (or subclass) object to read flavor data from. 1302 data: another WOFFFlavorData object to initialise data from. 1303 transformedTables: set of strings containing table tags to be transformed. 1304 1305 Raises: 1306 ImportError if the brotli module is not installed. 1307 1308 NOTE: The 'reader' argument, on the one hand, and the 'data' and 1309 'transformedTables' arguments, on the other hand, are mutually exclusive. 1310 """ 1311 if not haveBrotli: 1312 raise ImportError("No module named brotli") 1313 1314 if reader is not None: 1315 if data is not None: 1316 raise TypeError("'reader' and 'data' arguments are mutually exclusive") 1317 if transformedTables is not None: 1318 raise TypeError( 1319 "'reader' and 'transformedTables' arguments are mutually exclusive" 1320 ) 1321 1322 if transformedTables is not None and ( 1323 "glyf" in transformedTables 1324 and "loca" not in transformedTables 1325 or "loca" in transformedTables 1326 and "glyf" not in transformedTables 1327 ): 1328 raise ValueError("'glyf' and 'loca' must be transformed (or not) together") 1329 super(WOFF2FlavorData, self).__init__(reader=reader) 1330 if reader: 1331 transformedTables = [ 1332 tag for tag, entry in reader.tables.items() if entry.transformed 1333 ] 1334 elif data: 1335 self.majorVersion = data.majorVersion 1336 self.majorVersion = data.minorVersion 1337 self.metaData = data.metaData 1338 self.privData = data.privData 1339 if transformedTables is None and hasattr(data, "transformedTables"): 1340 transformedTables = data.transformedTables 1341 1342 if transformedTables is None: 1343 transformedTables = woff2TransformedTableTags 1344 1345 self.transformedTables = set(transformedTables) 1346 1347 def _decompress(self, rawData): 1348 return brotli.decompress(rawData) 1349 1350 1351def unpackBase128(data): 1352 r"""Read one to five bytes from UIntBase128-encoded input string, and return 1353 a tuple containing the decoded integer plus any leftover data. 1354 1355 >>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00") 1356 True 1357 >>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295 1358 True 1359 >>> unpackBase128(b'\x80\x80\x3f') # doctest: +IGNORE_EXCEPTION_DETAIL 1360 Traceback (most recent call last): 1361 File "<stdin>", line 1, in ? 1362 TTLibError: UIntBase128 value must not start with leading zeros 1363 >>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0] # doctest: +IGNORE_EXCEPTION_DETAIL 1364 Traceback (most recent call last): 1365 File "<stdin>", line 1, in ? 1366 TTLibError: UIntBase128-encoded sequence is longer than 5 bytes 1367 >>> unpackBase128(b'\x90\x80\x80\x80\x00')[0] # doctest: +IGNORE_EXCEPTION_DETAIL 1368 Traceback (most recent call last): 1369 File "<stdin>", line 1, in ? 1370 TTLibError: UIntBase128 value exceeds 2**32-1 1371 """ 1372 if len(data) == 0: 1373 raise TTLibError("not enough data to unpack UIntBase128") 1374 result = 0 1375 if byteord(data[0]) == 0x80: 1376 # font must be rejected if UIntBase128 value starts with 0x80 1377 raise TTLibError("UIntBase128 value must not start with leading zeros") 1378 for i in range(woff2Base128MaxSize): 1379 if len(data) == 0: 1380 raise TTLibError("not enough data to unpack UIntBase128") 1381 code = byteord(data[0]) 1382 data = data[1:] 1383 # if any of the top seven bits are set then we're about to overflow 1384 if result & 0xFE000000: 1385 raise TTLibError("UIntBase128 value exceeds 2**32-1") 1386 # set current value = old value times 128 bitwise-or (byte bitwise-and 127) 1387 result = (result << 7) | (code & 0x7F) 1388 # repeat until the most significant bit of byte is false 1389 if (code & 0x80) == 0: 1390 # return result plus left over data 1391 return result, data 1392 # make sure not to exceed the size bound 1393 raise TTLibError("UIntBase128-encoded sequence is longer than 5 bytes") 1394 1395 1396def base128Size(n): 1397 """Return the length in bytes of a UIntBase128-encoded sequence with value n. 1398 1399 >>> base128Size(0) 1400 1 1401 >>> base128Size(24567) 1402 3 1403 >>> base128Size(2**32-1) 1404 5 1405 """ 1406 assert n >= 0 1407 size = 1 1408 while n >= 128: 1409 size += 1 1410 n >>= 7 1411 return size 1412 1413 1414def packBase128(n): 1415 r"""Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of 1416 bytes using UIntBase128 variable-length encoding. Produce the shortest possible 1417 encoding. 1418 1419 >>> packBase128(63) == b"\x3f" 1420 True 1421 >>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f' 1422 True 1423 """ 1424 if n < 0 or n >= 2**32: 1425 raise TTLibError("UIntBase128 format requires 0 <= integer <= 2**32-1") 1426 data = b"" 1427 size = base128Size(n) 1428 for i in range(size): 1429 b = (n >> (7 * (size - i - 1))) & 0x7F 1430 if i < size - 1: 1431 b |= 0x80 1432 data += struct.pack("B", b) 1433 return data 1434 1435 1436def unpack255UShort(data): 1437 """Read one to three bytes from 255UInt16-encoded input string, and return a 1438 tuple containing the decoded integer plus any leftover data. 1439 1440 >>> unpack255UShort(bytechr(252))[0] 1441 252 1442 1443 Note that some numbers (e.g. 506) can have multiple encodings: 1444 >>> unpack255UShort(struct.pack("BB", 254, 0))[0] 1445 506 1446 >>> unpack255UShort(struct.pack("BB", 255, 253))[0] 1447 506 1448 >>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0] 1449 506 1450 """ 1451 code = byteord(data[:1]) 1452 data = data[1:] 1453 if code == 253: 1454 # read two more bytes as an unsigned short 1455 if len(data) < 2: 1456 raise TTLibError("not enough data to unpack 255UInt16") 1457 (result,) = struct.unpack(">H", data[:2]) 1458 data = data[2:] 1459 elif code == 254: 1460 # read another byte, plus 253 * 2 1461 if len(data) == 0: 1462 raise TTLibError("not enough data to unpack 255UInt16") 1463 result = byteord(data[:1]) 1464 result += 506 1465 data = data[1:] 1466 elif code == 255: 1467 # read another byte, plus 253 1468 if len(data) == 0: 1469 raise TTLibError("not enough data to unpack 255UInt16") 1470 result = byteord(data[:1]) 1471 result += 253 1472 data = data[1:] 1473 else: 1474 # leave as is if lower than 253 1475 result = code 1476 # return result plus left over data 1477 return result, data 1478 1479 1480def pack255UShort(value): 1481 r"""Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring 1482 using 255UInt16 variable-length encoding. 1483 1484 >>> pack255UShort(252) == b'\xfc' 1485 True 1486 >>> pack255UShort(506) == b'\xfe\x00' 1487 True 1488 >>> pack255UShort(762) == b'\xfd\x02\xfa' 1489 True 1490 """ 1491 if value < 0 or value > 0xFFFF: 1492 raise TTLibError("255UInt16 format requires 0 <= integer <= 65535") 1493 if value < 253: 1494 return struct.pack(">B", value) 1495 elif value < 506: 1496 return struct.pack(">BB", 255, value - 253) 1497 elif value < 762: 1498 return struct.pack(">BB", 254, value - 506) 1499 else: 1500 return struct.pack(">BH", 253, value) 1501 1502 1503def compress(input_file, output_file, transform_tables=None): 1504 """Compress OpenType font to WOFF2. 1505 1506 Args: 1507 input_file: a file path, file or file-like object (open in binary mode) 1508 containing an OpenType font (either CFF- or TrueType-flavored). 1509 output_file: a file path, file or file-like object where to save the 1510 compressed WOFF2 font. 1511 transform_tables: Optional[Iterable[str]]: a set of table tags for which 1512 to enable preprocessing transformations. By default, only 'glyf' 1513 and 'loca' tables are transformed. An empty set means disable all 1514 transformations. 1515 """ 1516 log.info("Processing %s => %s" % (input_file, output_file)) 1517 1518 font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False) 1519 font.flavor = "woff2" 1520 1521 if transform_tables is not None: 1522 font.flavorData = WOFF2FlavorData( 1523 data=font.flavorData, transformedTables=transform_tables 1524 ) 1525 1526 font.save(output_file, reorderTables=False) 1527 1528 1529def decompress(input_file, output_file): 1530 """Decompress WOFF2 font to OpenType font. 1531 1532 Args: 1533 input_file: a file path, file or file-like object (open in binary mode) 1534 containing a compressed WOFF2 font. 1535 output_file: a file path, file or file-like object where to save the 1536 decompressed OpenType font. 1537 """ 1538 log.info("Processing %s => %s" % (input_file, output_file)) 1539 1540 font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False) 1541 font.flavor = None 1542 font.flavorData = None 1543 font.save(output_file, reorderTables=True) 1544 1545 1546def main(args=None): 1547 """Compress and decompress WOFF2 fonts""" 1548 import argparse 1549 from fontTools import configLogger 1550 from fontTools.ttx import makeOutputFileName 1551 1552 class _HelpAction(argparse._HelpAction): 1553 def __call__(self, parser, namespace, values, option_string=None): 1554 subparsers_actions = [ 1555 action 1556 for action in parser._actions 1557 if isinstance(action, argparse._SubParsersAction) 1558 ] 1559 for subparsers_action in subparsers_actions: 1560 for choice, subparser in subparsers_action.choices.items(): 1561 print(subparser.format_help()) 1562 parser.exit() 1563 1564 class _NoGlyfTransformAction(argparse.Action): 1565 def __call__(self, parser, namespace, values, option_string=None): 1566 namespace.transform_tables.difference_update({"glyf", "loca"}) 1567 1568 class _HmtxTransformAction(argparse.Action): 1569 def __call__(self, parser, namespace, values, option_string=None): 1570 namespace.transform_tables.add("hmtx") 1571 1572 parser = argparse.ArgumentParser( 1573 prog="fonttools ttLib.woff2", description=main.__doc__, add_help=False 1574 ) 1575 1576 parser.add_argument( 1577 "-h", "--help", action=_HelpAction, help="show this help message and exit" 1578 ) 1579 1580 parser_group = parser.add_subparsers(title="sub-commands") 1581 parser_compress = parser_group.add_parser( 1582 "compress", description="Compress a TTF or OTF font to WOFF2" 1583 ) 1584 parser_decompress = parser_group.add_parser( 1585 "decompress", description="Decompress a WOFF2 font to OTF" 1586 ) 1587 1588 for subparser in (parser_compress, parser_decompress): 1589 group = subparser.add_mutually_exclusive_group(required=False) 1590 group.add_argument( 1591 "-v", 1592 "--verbose", 1593 action="store_true", 1594 help="print more messages to console", 1595 ) 1596 group.add_argument( 1597 "-q", 1598 "--quiet", 1599 action="store_true", 1600 help="do not print messages to console", 1601 ) 1602 1603 parser_compress.add_argument( 1604 "input_file", 1605 metavar="INPUT", 1606 help="the input OpenType font (.ttf or .otf)", 1607 ) 1608 parser_decompress.add_argument( 1609 "input_file", 1610 metavar="INPUT", 1611 help="the input WOFF2 font", 1612 ) 1613 1614 parser_compress.add_argument( 1615 "-o", 1616 "--output-file", 1617 metavar="OUTPUT", 1618 help="the output WOFF2 font", 1619 ) 1620 parser_decompress.add_argument( 1621 "-o", 1622 "--output-file", 1623 metavar="OUTPUT", 1624 help="the output OpenType font", 1625 ) 1626 1627 transform_group = parser_compress.add_argument_group() 1628 transform_group.add_argument( 1629 "--no-glyf-transform", 1630 dest="transform_tables", 1631 nargs=0, 1632 action=_NoGlyfTransformAction, 1633 help="Do not transform glyf (and loca) tables", 1634 ) 1635 transform_group.add_argument( 1636 "--hmtx-transform", 1637 dest="transform_tables", 1638 nargs=0, 1639 action=_HmtxTransformAction, 1640 help="Enable optional transformation for 'hmtx' table", 1641 ) 1642 1643 parser_compress.set_defaults( 1644 subcommand=compress, 1645 transform_tables={"glyf", "loca"}, 1646 ) 1647 parser_decompress.set_defaults(subcommand=decompress) 1648 1649 options = vars(parser.parse_args(args)) 1650 1651 subcommand = options.pop("subcommand", None) 1652 if not subcommand: 1653 parser.print_help() 1654 return 1655 1656 quiet = options.pop("quiet") 1657 verbose = options.pop("verbose") 1658 configLogger( 1659 level=("ERROR" if quiet else "DEBUG" if verbose else "INFO"), 1660 ) 1661 1662 if not options["output_file"]: 1663 if subcommand is compress: 1664 extension = ".woff2" 1665 elif subcommand is decompress: 1666 # choose .ttf/.otf file extension depending on sfntVersion 1667 with open(options["input_file"], "rb") as f: 1668 f.seek(4) # skip 'wOF2' signature 1669 sfntVersion = f.read(4) 1670 assert len(sfntVersion) == 4, "not enough data" 1671 extension = ".otf" if sfntVersion == b"OTTO" else ".ttf" 1672 else: 1673 raise AssertionError(subcommand) 1674 options["output_file"] = makeOutputFileName( 1675 options["input_file"], outputDir=None, extension=extension 1676 ) 1677 1678 try: 1679 subcommand(**options) 1680 except TTLibError as e: 1681 parser.error(e) 1682 1683 1684if __name__ == "__main__": 1685 sys.exit(main()) 1686