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