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(__name__) 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 tag in woff2TransformedTableTags: 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 if tag not in woff2TransformedTableTags: 94 raise TTLibError("transform for table '%s' is unknown" % tag) 95 entry = self.tables[Tag(tag)] 96 rawData = entry.loadData(self.transformBuffer) 97 if tag == 'glyf': 98 # no need to pad glyph data when reconstructing 99 padding = self.padding if hasattr(self, 'padding') else None 100 data = self._reconstructGlyf(rawData, padding) 101 elif tag == 'loca': 102 data = self._reconstructLoca() 103 else: 104 raise NotImplementedError 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 134class WOFF2Writer(SFNTWriter): 135 136 flavor = "woff2" 137 138 def __init__(self, file, numTables, sfntVersion="\000\001\000\000", 139 flavor=None, flavorData=None): 140 if not haveBrotli: 141 log.error( 142 'The WOFF2 encoder requires the Brotli Python extension, available at: ' 143 'https://github.com/google/brotli') 144 raise ImportError("No module named brotli") 145 146 self.file = file 147 self.numTables = numTables 148 self.sfntVersion = Tag(sfntVersion) 149 self.flavorData = flavorData or WOFF2FlavorData() 150 151 self.directoryFormat = woff2DirectoryFormat 152 self.directorySize = woff2DirectorySize 153 self.DirectoryEntry = WOFF2DirectoryEntry 154 155 self.signature = Tag("wOF2") 156 157 self.nextTableOffset = 0 158 self.transformBuffer = BytesIO() 159 160 self.tables = OrderedDict() 161 162 # make empty TTFont to store data while normalising and transforming tables 163 self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) 164 165 def __setitem__(self, tag, data): 166 """Associate new entry named 'tag' with raw table data.""" 167 if tag in self.tables: 168 raise TTLibError("cannot rewrite '%s' table" % tag) 169 if tag == 'DSIG': 170 # always drop DSIG table, since the encoding process can invalidate it 171 self.numTables -= 1 172 return 173 174 entry = self.DirectoryEntry() 175 entry.tag = Tag(tag) 176 entry.flags = getKnownTagIndex(entry.tag) 177 # WOFF2 table data are written to disk only on close(), after all tags 178 # have been specified 179 entry.data = data 180 181 self.tables[tag] = entry 182 183 def close(self): 184 """ All tags must have been specified. Now write the table data and directory. 185 """ 186 if len(self.tables) != self.numTables: 187 raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(self.tables))) 188 189 if self.sfntVersion in ("\x00\x01\x00\x00", "true"): 190 isTrueType = True 191 elif self.sfntVersion == "OTTO": 192 isTrueType = False 193 else: 194 raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") 195 196 # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned. 197 # However, the reference WOFF2 implementation still fails to reconstruct 198 # 'unpadded' glyf tables, therefore we need to 'normalise' them. 199 # See: 200 # https://github.com/khaledhosny/ots/issues/60 201 # https://github.com/google/woff2/issues/15 202 if isTrueType: 203 self._normaliseGlyfAndLoca(padding=4) 204 self._setHeadTransformFlag() 205 206 # To pass the legacy OpenType Sanitiser currently included in browsers, 207 # we must sort the table directory and data alphabetically by tag. 208 # See: 209 # https://github.com/google/woff2/pull/3 210 # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html 211 # TODO(user): remove to match spec once browsers are on newer OTS 212 self.tables = OrderedDict(sorted(self.tables.items())) 213 214 self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() 215 216 fontData = self._transformTables() 217 compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT) 218 219 self.totalCompressedSize = len(compressedFont) 220 self.length = self._calcTotalSize() 221 self.majorVersion, self.minorVersion = self._getVersion() 222 self.reserved = 0 223 224 directory = self._packTableDirectory() 225 self.file.seek(0) 226 self.file.write(pad(directory + compressedFont, size=4)) 227 self._writeFlavorData() 228 229 def _normaliseGlyfAndLoca(self, padding=4): 230 """ Recompile glyf and loca tables, aligning glyph offsets to multiples of 231 'padding' size. Update the head table's 'indexToLocFormat' accordingly while 232 compiling loca. 233 """ 234 if self.sfntVersion == "OTTO": 235 return 236 237 # make up glyph names required to decompile glyf table 238 self._decompileTable('maxp') 239 numGlyphs = self.ttFont['maxp'].numGlyphs 240 glyphOrder = ['.notdef'] + ["glyph%.5d" % i for i in range(1, numGlyphs)] 241 self.ttFont.setGlyphOrder(glyphOrder) 242 243 for tag in ('head', 'loca', 'glyf'): 244 self._decompileTable(tag) 245 self.ttFont['glyf'].padding = padding 246 for tag in ('glyf', 'loca'): 247 self._compileTable(tag) 248 249 def _setHeadTransformFlag(self): 250 """ Set bit 11 of 'head' table flags to indicate that the font has undergone 251 a lossless modifying transform. Re-compile head table data.""" 252 self._decompileTable('head') 253 self.ttFont['head'].flags |= (1 << 11) 254 self._compileTable('head') 255 256 def _decompileTable(self, tag): 257 """ Fetch table data, decompile it, and store it inside self.ttFont. """ 258 tag = Tag(tag) 259 if tag not in self.tables: 260 raise TTLibError("missing required table: %s" % tag) 261 if self.ttFont.isLoaded(tag): 262 return 263 data = self.tables[tag].data 264 if tag == 'loca': 265 tableClass = WOFF2LocaTable 266 elif tag == 'glyf': 267 tableClass = WOFF2GlyfTable 268 else: 269 tableClass = getTableClass(tag) 270 table = tableClass(tag) 271 self.ttFont.tables[tag] = table 272 table.decompile(data, self.ttFont) 273 274 def _compileTable(self, tag): 275 """ Compile table and store it in its 'data' attribute. """ 276 self.tables[tag].data = self.ttFont[tag].compile(self.ttFont) 277 278 def _calcSFNTChecksumsLengthsAndOffsets(self): 279 """ Compute the 'original' SFNT checksums, lengths and offsets for checksum 280 adjustment calculation. Return the total size of the uncompressed font. 281 """ 282 offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables) 283 for tag, entry in self.tables.items(): 284 data = entry.data 285 entry.origOffset = offset 286 entry.origLength = len(data) 287 if tag == 'head': 288 entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) 289 else: 290 entry.checkSum = calcChecksum(data) 291 offset += (entry.origLength + 3) & ~3 292 return offset 293 294 def _transformTables(self): 295 """Return transformed font data.""" 296 for tag, entry in self.tables.items(): 297 if tag in woff2TransformedTableTags: 298 data = self.transformTable(tag) 299 else: 300 data = entry.data 301 entry.offset = self.nextTableOffset 302 entry.saveData(self.transformBuffer, data) 303 self.nextTableOffset += entry.length 304 self.writeMasterChecksum() 305 fontData = self.transformBuffer.getvalue() 306 return fontData 307 308 def transformTable(self, tag): 309 """Return transformed table data.""" 310 if tag not in woff2TransformedTableTags: 311 raise TTLibError("Transform for table '%s' is unknown" % tag) 312 if tag == "loca": 313 data = b"" 314 elif tag == "glyf": 315 for tag in ('maxp', 'head', 'loca', 'glyf'): 316 self._decompileTable(tag) 317 glyfTable = self.ttFont['glyf'] 318 data = glyfTable.transform(self.ttFont) 319 else: 320 raise NotImplementedError 321 return data 322 323 def _calcMasterChecksum(self): 324 """Calculate checkSumAdjustment.""" 325 tags = list(self.tables.keys()) 326 checksums = [] 327 for i in range(len(tags)): 328 checksums.append(self.tables[tags[i]].checkSum) 329 330 # Create a SFNT directory for checksum calculation purposes 331 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16) 332 directory = sstruct.pack(sfntDirectoryFormat, self) 333 tables = sorted(self.tables.items()) 334 for tag, entry in tables: 335 sfntEntry = SFNTDirectoryEntry() 336 sfntEntry.tag = entry.tag 337 sfntEntry.checkSum = entry.checkSum 338 sfntEntry.offset = entry.origOffset 339 sfntEntry.length = entry.origLength 340 directory = directory + sfntEntry.toString() 341 342 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize 343 assert directory_end == len(directory) 344 345 checksums.append(calcChecksum(directory)) 346 checksum = sum(checksums) & 0xffffffff 347 # BiboAfba! 348 checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff 349 return checksumadjustment 350 351 def writeMasterChecksum(self): 352 """Write checkSumAdjustment to the transformBuffer.""" 353 checksumadjustment = self._calcMasterChecksum() 354 self.transformBuffer.seek(self.tables['head'].offset + 8) 355 self.transformBuffer.write(struct.pack(">L", checksumadjustment)) 356 357 def _calcTotalSize(self): 358 """Calculate total size of WOFF2 font, including any meta- and/or private data.""" 359 offset = self.directorySize 360 for entry in self.tables.values(): 361 offset += len(entry.toString()) 362 offset += self.totalCompressedSize 363 offset = (offset + 3) & ~3 364 offset = self._calcFlavorDataOffsetsAndSize(offset) 365 return offset 366 367 def _calcFlavorDataOffsetsAndSize(self, start): 368 """Calculate offsets and lengths for any meta- and/or private data.""" 369 offset = start 370 data = self.flavorData 371 if data.metaData: 372 self.metaOrigLength = len(data.metaData) 373 self.metaOffset = offset 374 self.compressedMetaData = brotli.compress( 375 data.metaData, mode=brotli.MODE_TEXT) 376 self.metaLength = len(self.compressedMetaData) 377 offset += self.metaLength 378 else: 379 self.metaOffset = self.metaLength = self.metaOrigLength = 0 380 self.compressedMetaData = b"" 381 if data.privData: 382 # make sure private data is padded to 4-byte boundary 383 offset = (offset + 3) & ~3 384 self.privOffset = offset 385 self.privLength = len(data.privData) 386 offset += self.privLength 387 else: 388 self.privOffset = self.privLength = 0 389 return offset 390 391 def _getVersion(self): 392 """Return the WOFF2 font's (majorVersion, minorVersion) tuple.""" 393 data = self.flavorData 394 if data.majorVersion is not None and data.minorVersion is not None: 395 return data.majorVersion, data.minorVersion 396 else: 397 # if None, return 'fontRevision' from 'head' table 398 if 'head' in self.tables: 399 return struct.unpack(">HH", self.tables['head'].data[4:8]) 400 else: 401 return 0, 0 402 403 def _packTableDirectory(self): 404 """Return WOFF2 table directory data.""" 405 directory = sstruct.pack(self.directoryFormat, self) 406 for entry in self.tables.values(): 407 directory = directory + entry.toString() 408 return directory 409 410 def _writeFlavorData(self): 411 """Write metadata and/or private data using appropiate padding.""" 412 compressedMetaData = self.compressedMetaData 413 privData = self.flavorData.privData 414 if compressedMetaData and privData: 415 compressedMetaData = pad(compressedMetaData, size=4) 416 if compressedMetaData: 417 self.file.seek(self.metaOffset) 418 assert self.file.tell() == self.metaOffset 419 self.file.write(compressedMetaData) 420 if privData: 421 self.file.seek(self.privOffset) 422 assert self.file.tell() == self.privOffset 423 self.file.write(privData) 424 425 def reordersTables(self): 426 return True 427 428 429# -- woff2 directory helpers and cruft 430 431woff2DirectoryFormat = """ 432 > # big endian 433 signature: 4s # "wOF2" 434 sfntVersion: 4s 435 length: L # total woff2 file size 436 numTables: H # number of tables 437 reserved: H # set to 0 438 totalSfntSize: L # uncompressed size 439 totalCompressedSize: L # compressed size 440 majorVersion: H # major version of WOFF file 441 minorVersion: H # minor version of WOFF file 442 metaOffset: L # offset to metadata block 443 metaLength: L # length of compressed metadata 444 metaOrigLength: L # length of uncompressed metadata 445 privOffset: L # offset to private data block 446 privLength: L # length of private data block 447""" 448 449woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat) 450 451woff2KnownTags = ( 452 "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post", "cvt ", 453 "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT", "EBLC", "gasp", 454 "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea", "vmtx", "BASE", "GDEF", 455 "GPOS", "GSUB", "EBSC", "JSTF", "MATH", "CBDT", "CBLC", "COLR", "CPAL", 456 "SVG ", "sbix", "acnt", "avar", "bdat", "bloc", "bsln", "cvar", "fdsc", 457 "feat", "fmtx", "fvar", "gvar", "hsty", "just", "lcar", "mort", "morx", 458 "opbd", "prop", "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill") 459 460woff2FlagsFormat = """ 461 > # big endian 462 flags: B # table type and flags 463""" 464 465woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat) 466 467woff2UnknownTagFormat = """ 468 > # big endian 469 tag: 4s # 4-byte tag (optional) 470""" 471 472woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat) 473 474woff2UnknownTagIndex = 0x3F 475 476woff2Base128MaxSize = 5 477woff2DirectoryEntryMaxSize = woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize 478 479woff2TransformedTableTags = ('glyf', 'loca') 480 481woff2GlyfTableFormat = """ 482 > # big endian 483 version: L # = 0x00000000 484 numGlyphs: H # Number of glyphs 485 indexFormat: H # Offset format for loca table 486 nContourStreamSize: L # Size of nContour stream 487 nPointsStreamSize: L # Size of nPoints stream 488 flagStreamSize: L # Size of flag stream 489 glyphStreamSize: L # Size of glyph stream 490 compositeStreamSize: L # Size of composite stream 491 bboxStreamSize: L # Comnined size of bboxBitmap and bboxStream 492 instructionStreamSize: L # Size of instruction stream 493""" 494 495woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat) 496 497bboxFormat = """ 498 > # big endian 499 xMin: h 500 yMin: h 501 xMax: h 502 yMax: h 503""" 504 505 506def getKnownTagIndex(tag): 507 """Return index of 'tag' in woff2KnownTags list. Return 63 if not found.""" 508 for i in range(len(woff2KnownTags)): 509 if tag == woff2KnownTags[i]: 510 return i 511 return woff2UnknownTagIndex 512 513 514class WOFF2DirectoryEntry(DirectoryEntry): 515 516 def fromFile(self, file): 517 pos = file.tell() 518 data = file.read(woff2DirectoryEntryMaxSize) 519 left = self.fromString(data) 520 consumed = len(data) - len(left) 521 file.seek(pos + consumed) 522 523 def fromString(self, data): 524 if len(data) < 1: 525 raise TTLibError("can't read table 'flags': not enough data") 526 dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self) 527 if self.flags & 0x3F == 0x3F: 528 # if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value 529 if len(data) < woff2UnknownTagSize: 530 raise TTLibError("can't read table 'tag': not enough data") 531 dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self) 532 else: 533 # otherwise, tag is derived from a fixed 'Known Tags' table 534 self.tag = woff2KnownTags[self.flags & 0x3F] 535 self.tag = Tag(self.tag) 536 if self.flags & 0xC0 != 0: 537 raise TTLibError('bits 6-7 are reserved and must be 0') 538 self.origLength, data = unpackBase128(data) 539 self.length = self.origLength 540 if self.tag in woff2TransformedTableTags: 541 self.length, data = unpackBase128(data) 542 if self.tag == 'loca' and self.length != 0: 543 raise TTLibError( 544 "the transformLength of the 'loca' table must be 0") 545 # return left over data 546 return data 547 548 def toString(self): 549 data = bytechr(self.flags) 550 if (self.flags & 0x3F) == 0x3F: 551 data += struct.pack('>4s', self.tag.tobytes()) 552 data += packBase128(self.origLength) 553 if self.tag in woff2TransformedTableTags: 554 data += packBase128(self.length) 555 return data 556 557 558class WOFF2LocaTable(getTableClass('loca')): 559 """Same as parent class. The only difference is that it attempts to preserve 560 the 'indexFormat' as encoded in the WOFF2 glyf table. 561 """ 562 563 def __init__(self, tag=None): 564 self.tableTag = Tag(tag or 'loca') 565 566 def compile(self, ttFont): 567 try: 568 max_location = max(self.locations) 569 except AttributeError: 570 self.set([]) 571 max_location = 0 572 if 'glyf' in ttFont and hasattr(ttFont['glyf'], 'indexFormat'): 573 # copile loca using the indexFormat specified in the WOFF2 glyf table 574 indexFormat = ttFont['glyf'].indexFormat 575 if indexFormat == 0: 576 if max_location >= 0x20000: 577 raise TTLibError("indexFormat is 0 but local offsets > 0x20000") 578 if not all(l % 2 == 0 for l in self.locations): 579 raise TTLibError("indexFormat is 0 but local offsets not multiples of 2") 580 locations = array.array("H") 581 for i in range(len(self.locations)): 582 locations.append(self.locations[i] // 2) 583 else: 584 locations = array.array("I", self.locations) 585 if sys.byteorder != "big": locations.byteswap() 586 data = locations.tostring() 587 else: 588 # use the most compact indexFormat given the current glyph offsets 589 data = super(WOFF2LocaTable, self).compile(ttFont) 590 return data 591 592 593class WOFF2GlyfTable(getTableClass('glyf')): 594 """Decoder/Encoder for WOFF2 'glyf' table transform.""" 595 596 subStreams = ( 597 'nContourStream', 'nPointsStream', 'flagStream', 'glyphStream', 598 'compositeStream', 'bboxStream', 'instructionStream') 599 600 def __init__(self, tag=None): 601 self.tableTag = Tag(tag or 'glyf') 602 603 def reconstruct(self, data, ttFont): 604 """ Decompile transformed 'glyf' data. """ 605 inputDataSize = len(data) 606 607 if inputDataSize < woff2GlyfTableFormatSize: 608 raise TTLibError("not enough 'glyf' data") 609 dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self) 610 offset = woff2GlyfTableFormatSize 611 612 for stream in self.subStreams: 613 size = getattr(self, stream + 'Size') 614 setattr(self, stream, data[:size]) 615 data = data[size:] 616 offset += size 617 618 if offset != inputDataSize: 619 raise TTLibError( 620 "incorrect size of transformed 'glyf' table: expected %d, received %d bytes" 621 % (offset, inputDataSize)) 622 623 bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 624 bboxBitmap = self.bboxStream[:bboxBitmapSize] 625 self.bboxBitmap = array.array('B', bboxBitmap) 626 self.bboxStream = self.bboxStream[bboxBitmapSize:] 627 628 self.nContourStream = array.array("h", self.nContourStream) 629 if sys.byteorder != "big": self.nContourStream.byteswap() 630 assert len(self.nContourStream) == self.numGlyphs 631 632 if 'head' in ttFont: 633 ttFont['head'].indexToLocFormat = self.indexFormat 634 try: 635 self.glyphOrder = ttFont.getGlyphOrder() 636 except: 637 self.glyphOrder = None 638 if self.glyphOrder is None: 639 self.glyphOrder = [".notdef"] 640 self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)]) 641 else: 642 if len(self.glyphOrder) != self.numGlyphs: 643 raise TTLibError( 644 "incorrect glyphOrder: expected %d glyphs, found %d" % 645 (len(self.glyphOrder), self.numGlyphs)) 646 647 glyphs = self.glyphs = {} 648 for glyphID, glyphName in enumerate(self.glyphOrder): 649 glyph = self._decodeGlyph(glyphID) 650 glyphs[glyphName] = glyph 651 652 def transform(self, ttFont): 653 """ Return transformed 'glyf' data """ 654 self.numGlyphs = len(self.glyphs) 655 if not hasattr(self, "glyphOrder"): 656 try: 657 self.glyphOrder = ttFont.getGlyphOrder() 658 except: 659 self.glyphOrder = None 660 if self.glyphOrder is None: 661 self.glyphOrder = [".notdef"] 662 self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)]) 663 if len(self.glyphOrder) != self.numGlyphs: 664 raise TTLibError( 665 "incorrect glyphOrder: expected %d glyphs, found %d" % 666 (len(self.glyphOrder), self.numGlyphs)) 667 668 if 'maxp' in ttFont: 669 ttFont['maxp'].numGlyphs = self.numGlyphs 670 self.indexFormat = ttFont['head'].indexToLocFormat 671 672 for stream in self.subStreams: 673 setattr(self, stream, b"") 674 bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 675 self.bboxBitmap = array.array('B', [0]*bboxBitmapSize) 676 677 for glyphID in range(self.numGlyphs): 678 self._encodeGlyph(glyphID) 679 680 self.bboxStream = self.bboxBitmap.tostring() + self.bboxStream 681 for stream in self.subStreams: 682 setattr(self, stream + 'Size', len(getattr(self, stream))) 683 self.version = 0 684 data = sstruct.pack(woff2GlyfTableFormat, self) 685 data += bytesjoin([getattr(self, s) for s in self.subStreams]) 686 return data 687 688 def _decodeGlyph(self, glyphID): 689 glyph = getTableModule('glyf').Glyph() 690 glyph.numberOfContours = self.nContourStream[glyphID] 691 if glyph.numberOfContours == 0: 692 return glyph 693 elif glyph.isComposite(): 694 self._decodeComponents(glyph) 695 else: 696 self._decodeCoordinates(glyph) 697 self._decodeBBox(glyphID, glyph) 698 return glyph 699 700 def _decodeComponents(self, glyph): 701 data = self.compositeStream 702 glyph.components = [] 703 more = 1 704 haveInstructions = 0 705 while more: 706 component = getTableModule('glyf').GlyphComponent() 707 more, haveInstr, data = component.decompile(data, self) 708 haveInstructions = haveInstructions | haveInstr 709 glyph.components.append(component) 710 self.compositeStream = data 711 if haveInstructions: 712 self._decodeInstructions(glyph) 713 714 def _decodeCoordinates(self, glyph): 715 data = self.nPointsStream 716 endPtsOfContours = [] 717 endPoint = -1 718 for i in range(glyph.numberOfContours): 719 ptsOfContour, data = unpack255UShort(data) 720 endPoint += ptsOfContour 721 endPtsOfContours.append(endPoint) 722 glyph.endPtsOfContours = endPtsOfContours 723 self.nPointsStream = data 724 self._decodeTriplets(glyph) 725 self._decodeInstructions(glyph) 726 727 def _decodeInstructions(self, glyph): 728 glyphStream = self.glyphStream 729 instructionStream = self.instructionStream 730 instructionLength, glyphStream = unpack255UShort(glyphStream) 731 glyph.program = ttProgram.Program() 732 glyph.program.fromBytecode(instructionStream[:instructionLength]) 733 self.glyphStream = glyphStream 734 self.instructionStream = instructionStream[instructionLength:] 735 736 def _decodeBBox(self, glyphID, glyph): 737 haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7))) 738 if glyph.isComposite() and not haveBBox: 739 raise TTLibError('no bbox values for composite glyph %d' % glyphID) 740 if haveBBox: 741 dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph) 742 else: 743 glyph.recalcBounds(self) 744 745 def _decodeTriplets(self, glyph): 746 747 def withSign(flag, baseval): 748 assert 0 <= baseval and baseval < 65536, 'integer overflow' 749 return baseval if flag & 1 else -baseval 750 751 nPoints = glyph.endPtsOfContours[-1] + 1 752 flagSize = nPoints 753 if flagSize > len(self.flagStream): 754 raise TTLibError("not enough 'flagStream' data") 755 flagsData = self.flagStream[:flagSize] 756 self.flagStream = self.flagStream[flagSize:] 757 flags = array.array('B', flagsData) 758 759 triplets = array.array('B', self.glyphStream) 760 nTriplets = len(triplets) 761 assert nPoints <= nTriplets 762 763 x = 0 764 y = 0 765 glyph.coordinates = getTableModule('glyf').GlyphCoordinates.zeros(nPoints) 766 glyph.flags = array.array("B") 767 tripletIndex = 0 768 for i in range(nPoints): 769 flag = flags[i] 770 onCurve = not bool(flag >> 7) 771 flag &= 0x7f 772 if flag < 84: 773 nBytes = 1 774 elif flag < 120: 775 nBytes = 2 776 elif flag < 124: 777 nBytes = 3 778 else: 779 nBytes = 4 780 assert ((tripletIndex + nBytes) <= nTriplets) 781 if flag < 10: 782 dx = 0 783 dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex]) 784 elif flag < 20: 785 dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex]) 786 dy = 0 787 elif flag < 84: 788 b0 = flag - 20 789 b1 = triplets[tripletIndex] 790 dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4)) 791 dy = withSign(flag >> 1, 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f)) 792 elif flag < 120: 793 b0 = flag - 84 794 dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex]) 795 dy = withSign(flag >> 1, 796 1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1]) 797 elif flag < 124: 798 b2 = triplets[tripletIndex + 1] 799 dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4)) 800 dy = withSign(flag >> 1, 801 ((b2 & 0x0f) << 8) + triplets[tripletIndex + 2]) 802 else: 803 dx = withSign(flag, 804 (triplets[tripletIndex] << 8) + triplets[tripletIndex + 1]) 805 dy = withSign(flag >> 1, 806 (triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3]) 807 tripletIndex += nBytes 808 x += dx 809 y += dy 810 glyph.coordinates[i] = (x, y) 811 glyph.flags.append(int(onCurve)) 812 bytesConsumed = tripletIndex 813 self.glyphStream = self.glyphStream[bytesConsumed:] 814 815 def _encodeGlyph(self, glyphID): 816 glyphName = self.getGlyphName(glyphID) 817 glyph = self[glyphName] 818 self.nContourStream += struct.pack(">h", glyph.numberOfContours) 819 if glyph.numberOfContours == 0: 820 return 821 elif glyph.isComposite(): 822 self._encodeComponents(glyph) 823 else: 824 self._encodeCoordinates(glyph) 825 self._encodeBBox(glyphID, glyph) 826 827 def _encodeComponents(self, glyph): 828 lastcomponent = len(glyph.components) - 1 829 more = 1 830 haveInstructions = 0 831 for i in range(len(glyph.components)): 832 if i == lastcomponent: 833 haveInstructions = hasattr(glyph, "program") 834 more = 0 835 component = glyph.components[i] 836 self.compositeStream += component.compile(more, haveInstructions, self) 837 if haveInstructions: 838 self._encodeInstructions(glyph) 839 840 def _encodeCoordinates(self, glyph): 841 lastEndPoint = -1 842 for endPoint in glyph.endPtsOfContours: 843 ptsOfContour = endPoint - lastEndPoint 844 self.nPointsStream += pack255UShort(ptsOfContour) 845 lastEndPoint = endPoint 846 self._encodeTriplets(glyph) 847 self._encodeInstructions(glyph) 848 849 def _encodeInstructions(self, glyph): 850 instructions = glyph.program.getBytecode() 851 self.glyphStream += pack255UShort(len(instructions)) 852 self.instructionStream += instructions 853 854 def _encodeBBox(self, glyphID, glyph): 855 assert glyph.numberOfContours != 0, "empty glyph has no bbox" 856 if not glyph.isComposite(): 857 # for simple glyphs, compare the encoded bounding box info with the calculated 858 # values, and if they match omit the bounding box info 859 currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax 860 calculatedBBox = calcIntBounds(glyph.coordinates) 861 if currentBBox == calculatedBBox: 862 return 863 self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7) 864 self.bboxStream += sstruct.pack(bboxFormat, glyph) 865 866 def _encodeTriplets(self, glyph): 867 assert len(glyph.coordinates) == len(glyph.flags) 868 coordinates = glyph.coordinates.copy() 869 coordinates.absoluteToRelative() 870 871 flags = array.array('B') 872 triplets = array.array('B') 873 for i in range(len(coordinates)): 874 onCurve = glyph.flags[i] 875 x, y = coordinates[i] 876 absX = abs(x) 877 absY = abs(y) 878 onCurveBit = 0 if onCurve else 128 879 xSignBit = 0 if (x < 0) else 1 880 ySignBit = 0 if (y < 0) else 1 881 xySignBits = xSignBit + 2 * ySignBit 882 883 if x == 0 and absY < 1280: 884 flags.append(onCurveBit + ((absY & 0xf00) >> 7) + ySignBit) 885 triplets.append(absY & 0xff) 886 elif y == 0 and absX < 1280: 887 flags.append(onCurveBit + 10 + ((absX & 0xf00) >> 7) + xSignBit) 888 triplets.append(absX & 0xff) 889 elif absX < 65 and absY < 65: 890 flags.append(onCurveBit + 20 + ((absX - 1) & 0x30) + (((absY - 1) & 0x30) >> 2) + xySignBits) 891 triplets.append((((absX - 1) & 0xf) << 4) | ((absY - 1) & 0xf)) 892 elif absX < 769 and absY < 769: 893 flags.append(onCurveBit + 84 + 12 * (((absX - 1) & 0x300) >> 8) + (((absY - 1) & 0x300) >> 6) + xySignBits) 894 triplets.append((absX - 1) & 0xff) 895 triplets.append((absY - 1) & 0xff) 896 elif absX < 4096 and absY < 4096: 897 flags.append(onCurveBit + 120 + xySignBits) 898 triplets.append(absX >> 4) 899 triplets.append(((absX & 0xf) << 4) | (absY >> 8)) 900 triplets.append(absY & 0xff) 901 else: 902 flags.append(onCurveBit + 124 + xySignBits) 903 triplets.append(absX >> 8) 904 triplets.append(absX & 0xff) 905 triplets.append(absY >> 8) 906 triplets.append(absY & 0xff) 907 908 self.flagStream += flags.tostring() 909 self.glyphStream += triplets.tostring() 910 911 912class WOFF2FlavorData(WOFFFlavorData): 913 914 Flavor = 'woff2' 915 916 def __init__(self, reader=None): 917 if not haveBrotli: 918 raise ImportError("No module named brotli") 919 self.majorVersion = None 920 self.minorVersion = None 921 self.metaData = None 922 self.privData = None 923 if reader: 924 self.majorVersion = reader.majorVersion 925 self.minorVersion = reader.minorVersion 926 if reader.metaLength: 927 reader.file.seek(reader.metaOffset) 928 rawData = reader.file.read(reader.metaLength) 929 assert len(rawData) == reader.metaLength 930 data = brotli.decompress(rawData) 931 assert len(data) == reader.metaOrigLength 932 self.metaData = data 933 if reader.privLength: 934 reader.file.seek(reader.privOffset) 935 data = reader.file.read(reader.privLength) 936 assert len(data) == reader.privLength 937 self.privData = data 938 939 940def unpackBase128(data): 941 r""" Read one to five bytes from UIntBase128-encoded input string, and return 942 a tuple containing the decoded integer plus any leftover data. 943 944 >>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00") 945 True 946 >>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295 947 True 948 >>> unpackBase128(b'\x80\x80\x3f') # doctest: +IGNORE_EXCEPTION_DETAIL 949 Traceback (most recent call last): 950 File "<stdin>", line 1, in ? 951 TTLibError: UIntBase128 value must not start with leading zeros 952 >>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0] # doctest: +IGNORE_EXCEPTION_DETAIL 953 Traceback (most recent call last): 954 File "<stdin>", line 1, in ? 955 TTLibError: UIntBase128-encoded sequence is longer than 5 bytes 956 >>> unpackBase128(b'\x90\x80\x80\x80\x00')[0] # doctest: +IGNORE_EXCEPTION_DETAIL 957 Traceback (most recent call last): 958 File "<stdin>", line 1, in ? 959 TTLibError: UIntBase128 value exceeds 2**32-1 960 """ 961 if len(data) == 0: 962 raise TTLibError('not enough data to unpack UIntBase128') 963 result = 0 964 if byteord(data[0]) == 0x80: 965 # font must be rejected if UIntBase128 value starts with 0x80 966 raise TTLibError('UIntBase128 value must not start with leading zeros') 967 for i in range(woff2Base128MaxSize): 968 if len(data) == 0: 969 raise TTLibError('not enough data to unpack UIntBase128') 970 code = byteord(data[0]) 971 data = data[1:] 972 # if any of the top seven bits are set then we're about to overflow 973 if result & 0xFE000000: 974 raise TTLibError('UIntBase128 value exceeds 2**32-1') 975 # set current value = old value times 128 bitwise-or (byte bitwise-and 127) 976 result = (result << 7) | (code & 0x7f) 977 # repeat until the most significant bit of byte is false 978 if (code & 0x80) == 0: 979 # return result plus left over data 980 return result, data 981 # make sure not to exceed the size bound 982 raise TTLibError('UIntBase128-encoded sequence is longer than 5 bytes') 983 984 985def base128Size(n): 986 """ Return the length in bytes of a UIntBase128-encoded sequence with value n. 987 988 >>> base128Size(0) 989 1 990 >>> base128Size(24567) 991 3 992 >>> base128Size(2**32-1) 993 5 994 """ 995 assert n >= 0 996 size = 1 997 while n >= 128: 998 size += 1 999 n >>= 7 1000 return size 1001 1002 1003def packBase128(n): 1004 r""" Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of 1005 bytes using UIntBase128 variable-length encoding. Produce the shortest possible 1006 encoding. 1007 1008 >>> packBase128(63) == b"\x3f" 1009 True 1010 >>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f' 1011 True 1012 """ 1013 if n < 0 or n >= 2**32: 1014 raise TTLibError( 1015 "UIntBase128 format requires 0 <= integer <= 2**32-1") 1016 data = b'' 1017 size = base128Size(n) 1018 for i in range(size): 1019 b = (n >> (7 * (size - i - 1))) & 0x7f 1020 if i < size - 1: 1021 b |= 0x80 1022 data += struct.pack('B', b) 1023 return data 1024 1025 1026def unpack255UShort(data): 1027 """ Read one to three bytes from 255UInt16-encoded input string, and return a 1028 tuple containing the decoded integer plus any leftover data. 1029 1030 >>> unpack255UShort(bytechr(252))[0] 1031 252 1032 1033 Note that some numbers (e.g. 506) can have multiple encodings: 1034 >>> unpack255UShort(struct.pack("BB", 254, 0))[0] 1035 506 1036 >>> unpack255UShort(struct.pack("BB", 255, 253))[0] 1037 506 1038 >>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0] 1039 506 1040 """ 1041 code = byteord(data[:1]) 1042 data = data[1:] 1043 if code == 253: 1044 # read two more bytes as an unsigned short 1045 if len(data) < 2: 1046 raise TTLibError('not enough data to unpack 255UInt16') 1047 result, = struct.unpack(">H", data[:2]) 1048 data = data[2:] 1049 elif code == 254: 1050 # read another byte, plus 253 * 2 1051 if len(data) == 0: 1052 raise TTLibError('not enough data to unpack 255UInt16') 1053 result = byteord(data[:1]) 1054 result += 506 1055 data = data[1:] 1056 elif code == 255: 1057 # read another byte, plus 253 1058 if len(data) == 0: 1059 raise TTLibError('not enough data to unpack 255UInt16') 1060 result = byteord(data[:1]) 1061 result += 253 1062 data = data[1:] 1063 else: 1064 # leave as is if lower than 253 1065 result = code 1066 # return result plus left over data 1067 return result, data 1068 1069 1070def pack255UShort(value): 1071 r""" Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring 1072 using 255UInt16 variable-length encoding. 1073 1074 >>> pack255UShort(252) == b'\xfc' 1075 True 1076 >>> pack255UShort(506) == b'\xfe\x00' 1077 True 1078 >>> pack255UShort(762) == b'\xfd\x02\xfa' 1079 True 1080 """ 1081 if value < 0 or value > 0xFFFF: 1082 raise TTLibError( 1083 "255UInt16 format requires 0 <= integer <= 65535") 1084 if value < 253: 1085 return struct.pack(">B", value) 1086 elif value < 506: 1087 return struct.pack(">BB", 255, value - 253) 1088 elif value < 762: 1089 return struct.pack(">BB", 254, value - 506) 1090 else: 1091 return struct.pack(">BH", 253, value) 1092 1093 1094if __name__ == "__main__": 1095 import doctest 1096 sys.exit(doctest.testmod().failed) 1097