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