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