Spaces:
Paused
Paused
| from io import BytesIO | |
| import sys | |
| import array | |
| import struct | |
| from collections import OrderedDict | |
| from fontTools.misc import sstruct | |
| from fontTools.misc.arrayTools import calcIntBounds | |
| from fontTools.misc.textTools import Tag, bytechr, byteord, bytesjoin, pad | |
| from fontTools.ttLib import ( | |
| TTFont, | |
| TTLibError, | |
| getTableModule, | |
| getTableClass, | |
| getSearchRange, | |
| ) | |
| from fontTools.ttLib.sfnt import ( | |
| SFNTReader, | |
| SFNTWriter, | |
| DirectoryEntry, | |
| WOFFFlavorData, | |
| sfntDirectoryFormat, | |
| sfntDirectorySize, | |
| SFNTDirectoryEntry, | |
| sfntDirectoryEntrySize, | |
| calcChecksum, | |
| ) | |
| from fontTools.ttLib.tables import ttProgram, _g_l_y_f | |
| import logging | |
| log = logging.getLogger("fontTools.ttLib.woff2") | |
| haveBrotli = False | |
| try: | |
| try: | |
| import brotlicffi as brotli | |
| except ImportError: | |
| import brotli | |
| haveBrotli = True | |
| except ImportError: | |
| pass | |
| class WOFF2Reader(SFNTReader): | |
| flavor = "woff2" | |
| def __init__(self, file, checkChecksums=0, fontNumber=-1): | |
| if not haveBrotli: | |
| log.error( | |
| "The WOFF2 decoder requires the Brotli Python extension, available at: " | |
| "https://github.com/google/brotli" | |
| ) | |
| raise ImportError("No module named brotli") | |
| self.file = file | |
| signature = Tag(self.file.read(4)) | |
| if signature != b"wOF2": | |
| raise TTLibError("Not a WOFF2 font (bad signature)") | |
| self.file.seek(0) | |
| self.DirectoryEntry = WOFF2DirectoryEntry | |
| data = self.file.read(woff2DirectorySize) | |
| if len(data) != woff2DirectorySize: | |
| raise TTLibError("Not a WOFF2 font (not enough data)") | |
| sstruct.unpack(woff2DirectoryFormat, data, self) | |
| self.tables = OrderedDict() | |
| offset = 0 | |
| for i in range(self.numTables): | |
| entry = self.DirectoryEntry() | |
| entry.fromFile(self.file) | |
| tag = Tag(entry.tag) | |
| self.tables[tag] = entry | |
| entry.offset = offset | |
| offset += entry.length | |
| totalUncompressedSize = offset | |
| compressedData = self.file.read(self.totalCompressedSize) | |
| decompressedData = brotli.decompress(compressedData) | |
| if len(decompressedData) != totalUncompressedSize: | |
| raise TTLibError( | |
| "unexpected size for decompressed font data: expected %d, found %d" | |
| % (totalUncompressedSize, len(decompressedData)) | |
| ) | |
| self.transformBuffer = BytesIO(decompressedData) | |
| self.file.seek(0, 2) | |
| if self.length != self.file.tell(): | |
| raise TTLibError("reported 'length' doesn't match the actual file size") | |
| self.flavorData = WOFF2FlavorData(self) | |
| # make empty TTFont to store data while reconstructing tables | |
| self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) | |
| def __getitem__(self, tag): | |
| """Fetch the raw table data. Reconstruct transformed tables.""" | |
| entry = self.tables[Tag(tag)] | |
| if not hasattr(entry, "data"): | |
| if entry.transformed: | |
| entry.data = self.reconstructTable(tag) | |
| else: | |
| entry.data = entry.loadData(self.transformBuffer) | |
| return entry.data | |
| def reconstructTable(self, tag): | |
| """Reconstruct table named 'tag' from transformed data.""" | |
| entry = self.tables[Tag(tag)] | |
| rawData = entry.loadData(self.transformBuffer) | |
| if tag == "glyf": | |
| # no need to pad glyph data when reconstructing | |
| padding = self.padding if hasattr(self, "padding") else None | |
| data = self._reconstructGlyf(rawData, padding) | |
| elif tag == "loca": | |
| data = self._reconstructLoca() | |
| elif tag == "hmtx": | |
| data = self._reconstructHmtx(rawData) | |
| else: | |
| raise TTLibError("transform for table '%s' is unknown" % tag) | |
| return data | |
| def _reconstructGlyf(self, data, padding=None): | |
| """Return recostructed glyf table data, and set the corresponding loca's | |
| locations. Optionally pad glyph offsets to the specified number of bytes. | |
| """ | |
| self.ttFont["loca"] = WOFF2LocaTable() | |
| glyfTable = self.ttFont["glyf"] = WOFF2GlyfTable() | |
| glyfTable.reconstruct(data, self.ttFont) | |
| if padding: | |
| glyfTable.padding = padding | |
| data = glyfTable.compile(self.ttFont) | |
| return data | |
| def _reconstructLoca(self): | |
| """Return reconstructed loca table data.""" | |
| if "loca" not in self.ttFont: | |
| # make sure glyf is reconstructed first | |
| self.tables["glyf"].data = self.reconstructTable("glyf") | |
| locaTable = self.ttFont["loca"] | |
| data = locaTable.compile(self.ttFont) | |
| if len(data) != self.tables["loca"].origLength: | |
| raise TTLibError( | |
| "reconstructed 'loca' table doesn't match original size: " | |
| "expected %d, found %d" % (self.tables["loca"].origLength, len(data)) | |
| ) | |
| return data | |
| def _reconstructHmtx(self, data): | |
| """Return reconstructed hmtx table data.""" | |
| # Before reconstructing 'hmtx' table we need to parse other tables: | |
| # 'glyf' is required for reconstructing the sidebearings from the glyphs' | |
| # bounding box; 'hhea' is needed for the numberOfHMetrics field. | |
| if "glyf" in self.flavorData.transformedTables: | |
| # transformed 'glyf' table is self-contained, thus 'loca' not needed | |
| tableDependencies = ("maxp", "hhea", "glyf") | |
| else: | |
| # decompiling untransformed 'glyf' requires 'loca', which requires 'head' | |
| tableDependencies = ("maxp", "head", "hhea", "loca", "glyf") | |
| for tag in tableDependencies: | |
| self._decompileTable(tag) | |
| hmtxTable = self.ttFont["hmtx"] = WOFF2HmtxTable() | |
| hmtxTable.reconstruct(data, self.ttFont) | |
| data = hmtxTable.compile(self.ttFont) | |
| return data | |
| def _decompileTable(self, tag): | |
| """Decompile table data and store it inside self.ttFont.""" | |
| data = self[tag] | |
| if self.ttFont.isLoaded(tag): | |
| return self.ttFont[tag] | |
| tableClass = getTableClass(tag) | |
| table = tableClass(tag) | |
| self.ttFont.tables[tag] = table | |
| table.decompile(data, self.ttFont) | |
| class WOFF2Writer(SFNTWriter): | |
| flavor = "woff2" | |
| def __init__( | |
| self, | |
| file, | |
| numTables, | |
| sfntVersion="\000\001\000\000", | |
| flavor=None, | |
| flavorData=None, | |
| ): | |
| if not haveBrotli: | |
| log.error( | |
| "The WOFF2 encoder requires the Brotli Python extension, available at: " | |
| "https://github.com/google/brotli" | |
| ) | |
| raise ImportError("No module named brotli") | |
| self.file = file | |
| self.numTables = numTables | |
| self.sfntVersion = Tag(sfntVersion) | |
| self.flavorData = WOFF2FlavorData(data=flavorData) | |
| self.directoryFormat = woff2DirectoryFormat | |
| self.directorySize = woff2DirectorySize | |
| self.DirectoryEntry = WOFF2DirectoryEntry | |
| self.signature = Tag("wOF2") | |
| self.nextTableOffset = 0 | |
| self.transformBuffer = BytesIO() | |
| self.tables = OrderedDict() | |
| # make empty TTFont to store data while normalising and transforming tables | |
| self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) | |
| def __setitem__(self, tag, data): | |
| """Associate new entry named 'tag' with raw table data.""" | |
| if tag in self.tables: | |
| raise TTLibError("cannot rewrite '%s' table" % tag) | |
| if tag == "DSIG": | |
| # always drop DSIG table, since the encoding process can invalidate it | |
| self.numTables -= 1 | |
| return | |
| entry = self.DirectoryEntry() | |
| entry.tag = Tag(tag) | |
| entry.flags = getKnownTagIndex(entry.tag) | |
| # WOFF2 table data are written to disk only on close(), after all tags | |
| # have been specified | |
| entry.data = data | |
| self.tables[tag] = entry | |
| def close(self): | |
| """All tags must have been specified. Now write the table data and directory.""" | |
| if len(self.tables) != self.numTables: | |
| raise TTLibError( | |
| "wrong number of tables; expected %d, found %d" | |
| % (self.numTables, len(self.tables)) | |
| ) | |
| if self.sfntVersion in ("\x00\x01\x00\x00", "true"): | |
| isTrueType = True | |
| elif self.sfntVersion == "OTTO": | |
| isTrueType = False | |
| else: | |
| raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") | |
| # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned. | |
| # However, the reference WOFF2 implementation still fails to reconstruct | |
| # 'unpadded' glyf tables, therefore we need to 'normalise' them. | |
| # See: | |
| # https://github.com/khaledhosny/ots/issues/60 | |
| # https://github.com/google/woff2/issues/15 | |
| if ( | |
| isTrueType | |
| and "glyf" in self.flavorData.transformedTables | |
| and "glyf" in self.tables | |
| ): | |
| self._normaliseGlyfAndLoca(padding=4) | |
| self._setHeadTransformFlag() | |
| # To pass the legacy OpenType Sanitiser currently included in browsers, | |
| # we must sort the table directory and data alphabetically by tag. | |
| # See: | |
| # https://github.com/google/woff2/pull/3 | |
| # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html | |
| # | |
| # 2023: We rely on this in _transformTables where we expect that | |
| # "loca" comes after "glyf" table. | |
| self.tables = OrderedDict(sorted(self.tables.items())) | |
| self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() | |
| fontData = self._transformTables() | |
| compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT) | |
| self.totalCompressedSize = len(compressedFont) | |
| self.length = self._calcTotalSize() | |
| self.majorVersion, self.minorVersion = self._getVersion() | |
| self.reserved = 0 | |
| directory = self._packTableDirectory() | |
| self.file.seek(0) | |
| self.file.write(pad(directory + compressedFont, size=4)) | |
| self._writeFlavorData() | |
| def _normaliseGlyfAndLoca(self, padding=4): | |
| """Recompile glyf and loca tables, aligning glyph offsets to multiples of | |
| 'padding' size. Update the head table's 'indexToLocFormat' accordingly while | |
| compiling loca. | |
| """ | |
| if self.sfntVersion == "OTTO": | |
| return | |
| for tag in ("maxp", "head", "loca", "glyf", "fvar"): | |
| if tag in self.tables: | |
| self._decompileTable(tag) | |
| self.ttFont["glyf"].padding = padding | |
| for tag in ("glyf", "loca"): | |
| self._compileTable(tag) | |
| def _setHeadTransformFlag(self): | |
| """Set bit 11 of 'head' table flags to indicate that the font has undergone | |
| a lossless modifying transform. Re-compile head table data.""" | |
| self._decompileTable("head") | |
| self.ttFont["head"].flags |= 1 << 11 | |
| self._compileTable("head") | |
| def _decompileTable(self, tag): | |
| """Fetch table data, decompile it, and store it inside self.ttFont.""" | |
| tag = Tag(tag) | |
| if tag not in self.tables: | |
| raise TTLibError("missing required table: %s" % tag) | |
| if self.ttFont.isLoaded(tag): | |
| return | |
| data = self.tables[tag].data | |
| if tag == "loca": | |
| tableClass = WOFF2LocaTable | |
| elif tag == "glyf": | |
| tableClass = WOFF2GlyfTable | |
| elif tag == "hmtx": | |
| tableClass = WOFF2HmtxTable | |
| else: | |
| tableClass = getTableClass(tag) | |
| table = tableClass(tag) | |
| self.ttFont.tables[tag] = table | |
| table.decompile(data, self.ttFont) | |
| def _compileTable(self, tag): | |
| """Compile table and store it in its 'data' attribute.""" | |
| self.tables[tag].data = self.ttFont[tag].compile(self.ttFont) | |
| def _calcSFNTChecksumsLengthsAndOffsets(self): | |
| """Compute the 'original' SFNT checksums, lengths and offsets for checksum | |
| adjustment calculation. Return the total size of the uncompressed font. | |
| """ | |
| offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables) | |
| for tag, entry in self.tables.items(): | |
| data = entry.data | |
| entry.origOffset = offset | |
| entry.origLength = len(data) | |
| if tag == "head": | |
| entry.checkSum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:]) | |
| else: | |
| entry.checkSum = calcChecksum(data) | |
| offset += (entry.origLength + 3) & ~3 | |
| return offset | |
| def _transformTables(self): | |
| """Return transformed font data.""" | |
| transformedTables = self.flavorData.transformedTables | |
| for tag, entry in self.tables.items(): | |
| data = None | |
| if tag in transformedTables: | |
| data = self.transformTable(tag) | |
| if data is not None: | |
| entry.transformed = True | |
| if data is None: | |
| if tag == "glyf": | |
| # Currently we always sort table tags so | |
| # 'loca' comes after 'glyf'. | |
| transformedTables.discard("loca") | |
| # pass-through the table data without transformation | |
| data = entry.data | |
| entry.transformed = False | |
| entry.offset = self.nextTableOffset | |
| entry.saveData(self.transformBuffer, data) | |
| self.nextTableOffset += entry.length | |
| self.writeMasterChecksum() | |
| fontData = self.transformBuffer.getvalue() | |
| return fontData | |
| def transformTable(self, tag): | |
| """Return transformed table data, or None if some pre-conditions aren't | |
| met -- in which case, the non-transformed table data will be used. | |
| """ | |
| if tag == "loca": | |
| data = b"" | |
| elif tag == "glyf": | |
| for tag in ("maxp", "head", "loca", "glyf"): | |
| self._decompileTable(tag) | |
| glyfTable = self.ttFont["glyf"] | |
| data = glyfTable.transform(self.ttFont) | |
| elif tag == "hmtx": | |
| if "glyf" not in self.tables: | |
| return | |
| for tag in ("maxp", "head", "hhea", "loca", "glyf", "hmtx"): | |
| self._decompileTable(tag) | |
| hmtxTable = self.ttFont["hmtx"] | |
| data = hmtxTable.transform(self.ttFont) # can be None | |
| else: | |
| raise TTLibError("Transform for table '%s' is unknown" % tag) | |
| return data | |
| def _calcMasterChecksum(self): | |
| """Calculate checkSumAdjustment.""" | |
| tags = list(self.tables.keys()) | |
| checksums = [] | |
| for i in range(len(tags)): | |
| checksums.append(self.tables[tags[i]].checkSum) | |
| # Create a SFNT directory for checksum calculation purposes | |
| self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( | |
| self.numTables, 16 | |
| ) | |
| directory = sstruct.pack(sfntDirectoryFormat, self) | |
| tables = sorted(self.tables.items()) | |
| for tag, entry in tables: | |
| sfntEntry = SFNTDirectoryEntry() | |
| sfntEntry.tag = entry.tag | |
| sfntEntry.checkSum = entry.checkSum | |
| sfntEntry.offset = entry.origOffset | |
| sfntEntry.length = entry.origLength | |
| directory = directory + sfntEntry.toString() | |
| directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize | |
| assert directory_end == len(directory) | |
| checksums.append(calcChecksum(directory)) | |
| checksum = sum(checksums) & 0xFFFFFFFF | |
| # BiboAfba! | |
| checksumadjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF | |
| return checksumadjustment | |
| def writeMasterChecksum(self): | |
| """Write checkSumAdjustment to the transformBuffer.""" | |
| checksumadjustment = self._calcMasterChecksum() | |
| self.transformBuffer.seek(self.tables["head"].offset + 8) | |
| self.transformBuffer.write(struct.pack(">L", checksumadjustment)) | |
| def _calcTotalSize(self): | |
| """Calculate total size of WOFF2 font, including any meta- and/or private data.""" | |
| offset = self.directorySize | |
| for entry in self.tables.values(): | |
| offset += len(entry.toString()) | |
| offset += self.totalCompressedSize | |
| offset = (offset + 3) & ~3 | |
| offset = self._calcFlavorDataOffsetsAndSize(offset) | |
| return offset | |
| def _calcFlavorDataOffsetsAndSize(self, start): | |
| """Calculate offsets and lengths for any meta- and/or private data.""" | |
| offset = start | |
| data = self.flavorData | |
| if data.metaData: | |
| self.metaOrigLength = len(data.metaData) | |
| self.metaOffset = offset | |
| self.compressedMetaData = brotli.compress( | |
| data.metaData, mode=brotli.MODE_TEXT | |
| ) | |
| self.metaLength = len(self.compressedMetaData) | |
| offset += self.metaLength | |
| else: | |
| self.metaOffset = self.metaLength = self.metaOrigLength = 0 | |
| self.compressedMetaData = b"" | |
| if data.privData: | |
| # make sure private data is padded to 4-byte boundary | |
| offset = (offset + 3) & ~3 | |
| self.privOffset = offset | |
| self.privLength = len(data.privData) | |
| offset += self.privLength | |
| else: | |
| self.privOffset = self.privLength = 0 | |
| return offset | |
| def _getVersion(self): | |
| """Return the WOFF2 font's (majorVersion, minorVersion) tuple.""" | |
| data = self.flavorData | |
| if data.majorVersion is not None and data.minorVersion is not None: | |
| return data.majorVersion, data.minorVersion | |
| else: | |
| # if None, return 'fontRevision' from 'head' table | |
| if "head" in self.tables: | |
| return struct.unpack(">HH", self.tables["head"].data[4:8]) | |
| else: | |
| return 0, 0 | |
| def _packTableDirectory(self): | |
| """Return WOFF2 table directory data.""" | |
| directory = sstruct.pack(self.directoryFormat, self) | |
| for entry in self.tables.values(): | |
| directory = directory + entry.toString() | |
| return directory | |
| def _writeFlavorData(self): | |
| """Write metadata and/or private data using appropiate padding.""" | |
| compressedMetaData = self.compressedMetaData | |
| privData = self.flavorData.privData | |
| if compressedMetaData and privData: | |
| compressedMetaData = pad(compressedMetaData, size=4) | |
| if compressedMetaData: | |
| self.file.seek(self.metaOffset) | |
| assert self.file.tell() == self.metaOffset | |
| self.file.write(compressedMetaData) | |
| if privData: | |
| self.file.seek(self.privOffset) | |
| assert self.file.tell() == self.privOffset | |
| self.file.write(privData) | |
| def reordersTables(self): | |
| return True | |
| # -- woff2 directory helpers and cruft | |
| woff2DirectoryFormat = """ | |
| > # big endian | |
| signature: 4s # "wOF2" | |
| sfntVersion: 4s | |
| length: L # total woff2 file size | |
| numTables: H # number of tables | |
| reserved: H # set to 0 | |
| totalSfntSize: L # uncompressed size | |
| totalCompressedSize: L # compressed size | |
| majorVersion: H # major version of WOFF file | |
| minorVersion: H # minor version of WOFF file | |
| metaOffset: L # offset to metadata block | |
| metaLength: L # length of compressed metadata | |
| metaOrigLength: L # length of uncompressed metadata | |
| privOffset: L # offset to private data block | |
| privLength: L # length of private data block | |
| """ | |
| woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat) | |
| woff2KnownTags = ( | |
| "cmap", | |
| "head", | |
| "hhea", | |
| "hmtx", | |
| "maxp", | |
| "name", | |
| "OS/2", | |
| "post", | |
| "cvt ", | |
| "fpgm", | |
| "glyf", | |
| "loca", | |
| "prep", | |
| "CFF ", | |
| "VORG", | |
| "EBDT", | |
| "EBLC", | |
| "gasp", | |
| "hdmx", | |
| "kern", | |
| "LTSH", | |
| "PCLT", | |
| "VDMX", | |
| "vhea", | |
| "vmtx", | |
| "BASE", | |
| "GDEF", | |
| "GPOS", | |
| "GSUB", | |
| "EBSC", | |
| "JSTF", | |
| "MATH", | |
| "CBDT", | |
| "CBLC", | |
| "COLR", | |
| "CPAL", | |
| "SVG ", | |
| "sbix", | |
| "acnt", | |
| "avar", | |
| "bdat", | |
| "bloc", | |
| "bsln", | |
| "cvar", | |
| "fdsc", | |
| "feat", | |
| "fmtx", | |
| "fvar", | |
| "gvar", | |
| "hsty", | |
| "just", | |
| "lcar", | |
| "mort", | |
| "morx", | |
| "opbd", | |
| "prop", | |
| "trak", | |
| "Zapf", | |
| "Silf", | |
| "Glat", | |
| "Gloc", | |
| "Feat", | |
| "Sill", | |
| ) | |
| woff2FlagsFormat = """ | |
| > # big endian | |
| flags: B # table type and flags | |
| """ | |
| woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat) | |
| woff2UnknownTagFormat = """ | |
| > # big endian | |
| tag: 4s # 4-byte tag (optional) | |
| """ | |
| woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat) | |
| woff2UnknownTagIndex = 0x3F | |
| woff2Base128MaxSize = 5 | |
| woff2DirectoryEntryMaxSize = ( | |
| woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize | |
| ) | |
| woff2TransformedTableTags = ("glyf", "loca") | |
| woff2GlyfTableFormat = """ | |
| > # big endian | |
| version: H # = 0x0000 | |
| optionFlags: H # Bit 0: we have overlapSimpleBitmap[], Bits 1-15: reserved | |
| numGlyphs: H # Number of glyphs | |
| indexFormat: H # Offset format for loca table | |
| nContourStreamSize: L # Size of nContour stream | |
| nPointsStreamSize: L # Size of nPoints stream | |
| flagStreamSize: L # Size of flag stream | |
| glyphStreamSize: L # Size of glyph stream | |
| compositeStreamSize: L # Size of composite stream | |
| bboxStreamSize: L # Comnined size of bboxBitmap and bboxStream | |
| instructionStreamSize: L # Size of instruction stream | |
| """ | |
| woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat) | |
| bboxFormat = """ | |
| > # big endian | |
| xMin: h | |
| yMin: h | |
| xMax: h | |
| yMax: h | |
| """ | |
| woff2OverlapSimpleBitmapFlag = 0x0001 | |
| def getKnownTagIndex(tag): | |
| """Return index of 'tag' in woff2KnownTags list. Return 63 if not found.""" | |
| for i in range(len(woff2KnownTags)): | |
| if tag == woff2KnownTags[i]: | |
| return i | |
| return woff2UnknownTagIndex | |
| class WOFF2DirectoryEntry(DirectoryEntry): | |
| def fromFile(self, file): | |
| pos = file.tell() | |
| data = file.read(woff2DirectoryEntryMaxSize) | |
| left = self.fromString(data) | |
| consumed = len(data) - len(left) | |
| file.seek(pos + consumed) | |
| def fromString(self, data): | |
| if len(data) < 1: | |
| raise TTLibError("can't read table 'flags': not enough data") | |
| dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self) | |
| if self.flags & 0x3F == 0x3F: | |
| # if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value | |
| if len(data) < woff2UnknownTagSize: | |
| raise TTLibError("can't read table 'tag': not enough data") | |
| dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self) | |
| else: | |
| # otherwise, tag is derived from a fixed 'Known Tags' table | |
| self.tag = woff2KnownTags[self.flags & 0x3F] | |
| self.tag = Tag(self.tag) | |
| self.origLength, data = unpackBase128(data) | |
| self.length = self.origLength | |
| if self.transformed: | |
| self.length, data = unpackBase128(data) | |
| if self.tag == "loca" and self.length != 0: | |
| raise TTLibError("the transformLength of the 'loca' table must be 0") | |
| # return left over data | |
| return data | |
| def toString(self): | |
| data = bytechr(self.flags) | |
| if (self.flags & 0x3F) == 0x3F: | |
| data += struct.pack(">4s", self.tag.tobytes()) | |
| data += packBase128(self.origLength) | |
| if self.transformed: | |
| data += packBase128(self.length) | |
| return data | |
| def transformVersion(self): | |
| """Return bits 6-7 of table entry's flags, which indicate the preprocessing | |
| transformation version number (between 0 and 3). | |
| """ | |
| return self.flags >> 6 | |
| def transformVersion(self, value): | |
| assert 0 <= value <= 3 | |
| self.flags |= value << 6 | |
| def transformed(self): | |
| """Return True if the table has any transformation, else return False.""" | |
| # For all tables in a font, except for 'glyf' and 'loca', the transformation | |
| # version 0 indicates the null transform (where the original table data is | |
| # passed directly to the Brotli compressor). For 'glyf' and 'loca' tables, | |
| # transformation version 3 indicates the null transform | |
| if self.tag in {"glyf", "loca"}: | |
| return self.transformVersion != 3 | |
| else: | |
| return self.transformVersion != 0 | |
| def transformed(self, booleanValue): | |
| # here we assume that a non-null transform means version 0 for 'glyf' and | |
| # 'loca' and 1 for every other table (e.g. hmtx); but that may change as | |
| # new transformation formats are introduced in the future (if ever). | |
| if self.tag in {"glyf", "loca"}: | |
| self.transformVersion = 3 if not booleanValue else 0 | |
| else: | |
| self.transformVersion = int(booleanValue) | |
| class WOFF2LocaTable(getTableClass("loca")): | |
| """Same as parent class. The only difference is that it attempts to preserve | |
| the 'indexFormat' as encoded in the WOFF2 glyf table. | |
| """ | |
| def __init__(self, tag=None): | |
| self.tableTag = Tag(tag or "loca") | |
| def compile(self, ttFont): | |
| try: | |
| max_location = max(self.locations) | |
| except AttributeError: | |
| self.set([]) | |
| max_location = 0 | |
| if "glyf" in ttFont and hasattr(ttFont["glyf"], "indexFormat"): | |
| # copile loca using the indexFormat specified in the WOFF2 glyf table | |
| indexFormat = ttFont["glyf"].indexFormat | |
| if indexFormat == 0: | |
| if max_location >= 0x20000: | |
| raise TTLibError("indexFormat is 0 but local offsets > 0x20000") | |
| if not all(l % 2 == 0 for l in self.locations): | |
| raise TTLibError( | |
| "indexFormat is 0 but local offsets not multiples of 2" | |
| ) | |
| locations = array.array("H") | |
| for i in range(len(self.locations)): | |
| locations.append(self.locations[i] // 2) | |
| else: | |
| locations = array.array("I", self.locations) | |
| if sys.byteorder != "big": | |
| locations.byteswap() | |
| data = locations.tobytes() | |
| else: | |
| # use the most compact indexFormat given the current glyph offsets | |
| data = super(WOFF2LocaTable, self).compile(ttFont) | |
| return data | |
| class WOFF2GlyfTable(getTableClass("glyf")): | |
| """Decoder/Encoder for WOFF2 'glyf' table transform.""" | |
| subStreams = ( | |
| "nContourStream", | |
| "nPointsStream", | |
| "flagStream", | |
| "glyphStream", | |
| "compositeStream", | |
| "bboxStream", | |
| "instructionStream", | |
| ) | |
| def __init__(self, tag=None): | |
| self.tableTag = Tag(tag or "glyf") | |
| def reconstruct(self, data, ttFont): | |
| """Decompile transformed 'glyf' data.""" | |
| inputDataSize = len(data) | |
| if inputDataSize < woff2GlyfTableFormatSize: | |
| raise TTLibError("not enough 'glyf' data") | |
| dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self) | |
| offset = woff2GlyfTableFormatSize | |
| for stream in self.subStreams: | |
| size = getattr(self, stream + "Size") | |
| setattr(self, stream, data[:size]) | |
| data = data[size:] | |
| offset += size | |
| hasOverlapSimpleBitmap = self.optionFlags & woff2OverlapSimpleBitmapFlag | |
| self.overlapSimpleBitmap = None | |
| if hasOverlapSimpleBitmap: | |
| overlapSimpleBitmapSize = (self.numGlyphs + 7) >> 3 | |
| self.overlapSimpleBitmap = array.array("B", data[:overlapSimpleBitmapSize]) | |
| offset += overlapSimpleBitmapSize | |
| if offset != inputDataSize: | |
| raise TTLibError( | |
| "incorrect size of transformed 'glyf' table: expected %d, received %d bytes" | |
| % (offset, inputDataSize) | |
| ) | |
| bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 | |
| bboxBitmap = self.bboxStream[:bboxBitmapSize] | |
| self.bboxBitmap = array.array("B", bboxBitmap) | |
| self.bboxStream = self.bboxStream[bboxBitmapSize:] | |
| self.nContourStream = array.array("h", self.nContourStream) | |
| if sys.byteorder != "big": | |
| self.nContourStream.byteswap() | |
| assert len(self.nContourStream) == self.numGlyphs | |
| if "head" in ttFont: | |
| ttFont["head"].indexToLocFormat = self.indexFormat | |
| try: | |
| self.glyphOrder = ttFont.getGlyphOrder() | |
| except: | |
| self.glyphOrder = None | |
| if self.glyphOrder is None: | |
| self.glyphOrder = [".notdef"] | |
| self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)]) | |
| else: | |
| if len(self.glyphOrder) != self.numGlyphs: | |
| raise TTLibError( | |
| "incorrect glyphOrder: expected %d glyphs, found %d" | |
| % (len(self.glyphOrder), self.numGlyphs) | |
| ) | |
| glyphs = self.glyphs = {} | |
| for glyphID, glyphName in enumerate(self.glyphOrder): | |
| glyph = self._decodeGlyph(glyphID) | |
| glyphs[glyphName] = glyph | |
| def transform(self, ttFont): | |
| """Return transformed 'glyf' data""" | |
| self.numGlyphs = len(self.glyphs) | |
| assert len(self.glyphOrder) == self.numGlyphs | |
| if "maxp" in ttFont: | |
| ttFont["maxp"].numGlyphs = self.numGlyphs | |
| self.indexFormat = ttFont["head"].indexToLocFormat | |
| for stream in self.subStreams: | |
| setattr(self, stream, b"") | |
| bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 | |
| self.bboxBitmap = array.array("B", [0] * bboxBitmapSize) | |
| self.overlapSimpleBitmap = array.array("B", [0] * ((self.numGlyphs + 7) >> 3)) | |
| for glyphID in range(self.numGlyphs): | |
| try: | |
| self._encodeGlyph(glyphID) | |
| except NotImplementedError: | |
| return None | |
| hasOverlapSimpleBitmap = any(self.overlapSimpleBitmap) | |
| self.bboxStream = self.bboxBitmap.tobytes() + self.bboxStream | |
| for stream in self.subStreams: | |
| setattr(self, stream + "Size", len(getattr(self, stream))) | |
| self.version = 0 | |
| self.optionFlags = 0 | |
| if hasOverlapSimpleBitmap: | |
| self.optionFlags |= woff2OverlapSimpleBitmapFlag | |
| data = sstruct.pack(woff2GlyfTableFormat, self) | |
| data += bytesjoin([getattr(self, s) for s in self.subStreams]) | |
| if hasOverlapSimpleBitmap: | |
| data += self.overlapSimpleBitmap.tobytes() | |
| return data | |
| def _decodeGlyph(self, glyphID): | |
| glyph = getTableModule("glyf").Glyph() | |
| glyph.numberOfContours = self.nContourStream[glyphID] | |
| if glyph.numberOfContours == 0: | |
| return glyph | |
| elif glyph.isComposite(): | |
| self._decodeComponents(glyph) | |
| else: | |
| self._decodeCoordinates(glyph) | |
| self._decodeOverlapSimpleFlag(glyph, glyphID) | |
| self._decodeBBox(glyphID, glyph) | |
| return glyph | |
| def _decodeComponents(self, glyph): | |
| data = self.compositeStream | |
| glyph.components = [] | |
| more = 1 | |
| haveInstructions = 0 | |
| while more: | |
| component = getTableModule("glyf").GlyphComponent() | |
| more, haveInstr, data = component.decompile(data, self) | |
| haveInstructions = haveInstructions | haveInstr | |
| glyph.components.append(component) | |
| self.compositeStream = data | |
| if haveInstructions: | |
| self._decodeInstructions(glyph) | |
| def _decodeCoordinates(self, glyph): | |
| data = self.nPointsStream | |
| endPtsOfContours = [] | |
| endPoint = -1 | |
| for i in range(glyph.numberOfContours): | |
| ptsOfContour, data = unpack255UShort(data) | |
| endPoint += ptsOfContour | |
| endPtsOfContours.append(endPoint) | |
| glyph.endPtsOfContours = endPtsOfContours | |
| self.nPointsStream = data | |
| self._decodeTriplets(glyph) | |
| self._decodeInstructions(glyph) | |
| def _decodeOverlapSimpleFlag(self, glyph, glyphID): | |
| if self.overlapSimpleBitmap is None or glyph.numberOfContours <= 0: | |
| return | |
| byte = glyphID >> 3 | |
| bit = glyphID & 7 | |
| if self.overlapSimpleBitmap[byte] & (0x80 >> bit): | |
| glyph.flags[0] |= _g_l_y_f.flagOverlapSimple | |
| def _decodeInstructions(self, glyph): | |
| glyphStream = self.glyphStream | |
| instructionStream = self.instructionStream | |
| instructionLength, glyphStream = unpack255UShort(glyphStream) | |
| glyph.program = ttProgram.Program() | |
| glyph.program.fromBytecode(instructionStream[:instructionLength]) | |
| self.glyphStream = glyphStream | |
| self.instructionStream = instructionStream[instructionLength:] | |
| def _decodeBBox(self, glyphID, glyph): | |
| haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7))) | |
| if glyph.isComposite() and not haveBBox: | |
| raise TTLibError("no bbox values for composite glyph %d" % glyphID) | |
| if haveBBox: | |
| dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph) | |
| else: | |
| glyph.recalcBounds(self) | |
| def _decodeTriplets(self, glyph): | |
| def withSign(flag, baseval): | |
| assert 0 <= baseval and baseval < 65536, "integer overflow" | |
| return baseval if flag & 1 else -baseval | |
| nPoints = glyph.endPtsOfContours[-1] + 1 | |
| flagSize = nPoints | |
| if flagSize > len(self.flagStream): | |
| raise TTLibError("not enough 'flagStream' data") | |
| flagsData = self.flagStream[:flagSize] | |
| self.flagStream = self.flagStream[flagSize:] | |
| flags = array.array("B", flagsData) | |
| triplets = array.array("B", self.glyphStream) | |
| nTriplets = len(triplets) | |
| assert nPoints <= nTriplets | |
| x = 0 | |
| y = 0 | |
| glyph.coordinates = getTableModule("glyf").GlyphCoordinates.zeros(nPoints) | |
| glyph.flags = array.array("B") | |
| tripletIndex = 0 | |
| for i in range(nPoints): | |
| flag = flags[i] | |
| onCurve = not bool(flag >> 7) | |
| flag &= 0x7F | |
| if flag < 84: | |
| nBytes = 1 | |
| elif flag < 120: | |
| nBytes = 2 | |
| elif flag < 124: | |
| nBytes = 3 | |
| else: | |
| nBytes = 4 | |
| assert (tripletIndex + nBytes) <= nTriplets | |
| if flag < 10: | |
| dx = 0 | |
| dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex]) | |
| elif flag < 20: | |
| dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex]) | |
| dy = 0 | |
| elif flag < 84: | |
| b0 = flag - 20 | |
| b1 = triplets[tripletIndex] | |
| dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4)) | |
| dy = withSign(flag >> 1, 1 + ((b0 & 0x0C) << 2) + (b1 & 0x0F)) | |
| elif flag < 120: | |
| b0 = flag - 84 | |
| dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex]) | |
| dy = withSign( | |
| flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1] | |
| ) | |
| elif flag < 124: | |
| b2 = triplets[tripletIndex + 1] | |
| dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4)) | |
| dy = withSign( | |
| flag >> 1, ((b2 & 0x0F) << 8) + triplets[tripletIndex + 2] | |
| ) | |
| else: | |
| dx = withSign( | |
| flag, (triplets[tripletIndex] << 8) + triplets[tripletIndex + 1] | |
| ) | |
| dy = withSign( | |
| flag >> 1, | |
| (triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3], | |
| ) | |
| tripletIndex += nBytes | |
| x += dx | |
| y += dy | |
| glyph.coordinates[i] = (x, y) | |
| glyph.flags.append(int(onCurve)) | |
| bytesConsumed = tripletIndex | |
| self.glyphStream = self.glyphStream[bytesConsumed:] | |
| def _encodeGlyph(self, glyphID): | |
| glyphName = self.getGlyphName(glyphID) | |
| glyph = self[glyphName] | |
| self.nContourStream += struct.pack(">h", glyph.numberOfContours) | |
| if glyph.numberOfContours == 0: | |
| return | |
| elif glyph.isComposite(): | |
| self._encodeComponents(glyph) | |
| else: | |
| self._encodeCoordinates(glyph) | |
| self._encodeOverlapSimpleFlag(glyph, glyphID) | |
| self._encodeBBox(glyphID, glyph) | |
| def _encodeComponents(self, glyph): | |
| lastcomponent = len(glyph.components) - 1 | |
| more = 1 | |
| haveInstructions = 0 | |
| for i in range(len(glyph.components)): | |
| if i == lastcomponent: | |
| haveInstructions = hasattr(glyph, "program") | |
| more = 0 | |
| component = glyph.components[i] | |
| self.compositeStream += component.compile(more, haveInstructions, self) | |
| if haveInstructions: | |
| self._encodeInstructions(glyph) | |
| def _encodeCoordinates(self, glyph): | |
| lastEndPoint = -1 | |
| if _g_l_y_f.flagCubic in glyph.flags: | |
| raise NotImplementedError | |
| for endPoint in glyph.endPtsOfContours: | |
| ptsOfContour = endPoint - lastEndPoint | |
| self.nPointsStream += pack255UShort(ptsOfContour) | |
| lastEndPoint = endPoint | |
| self._encodeTriplets(glyph) | |
| self._encodeInstructions(glyph) | |
| def _encodeOverlapSimpleFlag(self, glyph, glyphID): | |
| if glyph.numberOfContours <= 0: | |
| return | |
| if glyph.flags[0] & _g_l_y_f.flagOverlapSimple: | |
| byte = glyphID >> 3 | |
| bit = glyphID & 7 | |
| self.overlapSimpleBitmap[byte] |= 0x80 >> bit | |
| def _encodeInstructions(self, glyph): | |
| instructions = glyph.program.getBytecode() | |
| self.glyphStream += pack255UShort(len(instructions)) | |
| self.instructionStream += instructions | |
| def _encodeBBox(self, glyphID, glyph): | |
| assert glyph.numberOfContours != 0, "empty glyph has no bbox" | |
| if not glyph.isComposite(): | |
| # for simple glyphs, compare the encoded bounding box info with the calculated | |
| # values, and if they match omit the bounding box info | |
| currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax | |
| calculatedBBox = calcIntBounds(glyph.coordinates) | |
| if currentBBox == calculatedBBox: | |
| return | |
| self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7) | |
| self.bboxStream += sstruct.pack(bboxFormat, glyph) | |
| def _encodeTriplets(self, glyph): | |
| assert len(glyph.coordinates) == len(glyph.flags) | |
| coordinates = glyph.coordinates.copy() | |
| coordinates.absoluteToRelative() | |
| flags = array.array("B") | |
| triplets = array.array("B") | |
| for i in range(len(coordinates)): | |
| onCurve = glyph.flags[i] & _g_l_y_f.flagOnCurve | |
| x, y = coordinates[i] | |
| absX = abs(x) | |
| absY = abs(y) | |
| onCurveBit = 0 if onCurve else 128 | |
| xSignBit = 0 if (x < 0) else 1 | |
| ySignBit = 0 if (y < 0) else 1 | |
| xySignBits = xSignBit + 2 * ySignBit | |
| if x == 0 and absY < 1280: | |
| flags.append(onCurveBit + ((absY & 0xF00) >> 7) + ySignBit) | |
| triplets.append(absY & 0xFF) | |
| elif y == 0 and absX < 1280: | |
| flags.append(onCurveBit + 10 + ((absX & 0xF00) >> 7) + xSignBit) | |
| triplets.append(absX & 0xFF) | |
| elif absX < 65 and absY < 65: | |
| flags.append( | |
| onCurveBit | |
| + 20 | |
| + ((absX - 1) & 0x30) | |
| + (((absY - 1) & 0x30) >> 2) | |
| + xySignBits | |
| ) | |
| triplets.append((((absX - 1) & 0xF) << 4) | ((absY - 1) & 0xF)) | |
| elif absX < 769 and absY < 769: | |
| flags.append( | |
| onCurveBit | |
| + 84 | |
| + 12 * (((absX - 1) & 0x300) >> 8) | |
| + (((absY - 1) & 0x300) >> 6) | |
| + xySignBits | |
| ) | |
| triplets.append((absX - 1) & 0xFF) | |
| triplets.append((absY - 1) & 0xFF) | |
| elif absX < 4096 and absY < 4096: | |
| flags.append(onCurveBit + 120 + xySignBits) | |
| triplets.append(absX >> 4) | |
| triplets.append(((absX & 0xF) << 4) | (absY >> 8)) | |
| triplets.append(absY & 0xFF) | |
| else: | |
| flags.append(onCurveBit + 124 + xySignBits) | |
| triplets.append(absX >> 8) | |
| triplets.append(absX & 0xFF) | |
| triplets.append(absY >> 8) | |
| triplets.append(absY & 0xFF) | |
| self.flagStream += flags.tobytes() | |
| self.glyphStream += triplets.tobytes() | |
| class WOFF2HmtxTable(getTableClass("hmtx")): | |
| def __init__(self, tag=None): | |
| self.tableTag = Tag(tag or "hmtx") | |
| def reconstruct(self, data, ttFont): | |
| (flags,) = struct.unpack(">B", data[:1]) | |
| data = data[1:] | |
| if flags & 0b11111100 != 0: | |
| raise TTLibError("Bits 2-7 of '%s' flags are reserved" % self.tableTag) | |
| # When bit 0 is _not_ set, the lsb[] array is present | |
| hasLsbArray = flags & 1 == 0 | |
| # When bit 1 is _not_ set, the leftSideBearing[] array is present | |
| hasLeftSideBearingArray = flags & 2 == 0 | |
| if hasLsbArray and hasLeftSideBearingArray: | |
| raise TTLibError( | |
| "either bits 0 or 1 (or both) must set in transformed '%s' flags" | |
| % self.tableTag | |
| ) | |
| glyfTable = ttFont["glyf"] | |
| headerTable = ttFont["hhea"] | |
| glyphOrder = glyfTable.glyphOrder | |
| numGlyphs = len(glyphOrder) | |
| numberOfHMetrics = min(int(headerTable.numberOfHMetrics), numGlyphs) | |
| assert len(data) >= 2 * numberOfHMetrics | |
| advanceWidthArray = array.array("H", data[: 2 * numberOfHMetrics]) | |
| if sys.byteorder != "big": | |
| advanceWidthArray.byteswap() | |
| data = data[2 * numberOfHMetrics :] | |
| if hasLsbArray: | |
| assert len(data) >= 2 * numberOfHMetrics | |
| lsbArray = array.array("h", data[: 2 * numberOfHMetrics]) | |
| if sys.byteorder != "big": | |
| lsbArray.byteswap() | |
| data = data[2 * numberOfHMetrics :] | |
| else: | |
| # compute (proportional) glyphs' lsb from their xMin | |
| lsbArray = array.array("h") | |
| for i, glyphName in enumerate(glyphOrder): | |
| if i >= numberOfHMetrics: | |
| break | |
| glyph = glyfTable[glyphName] | |
| xMin = getattr(glyph, "xMin", 0) | |
| lsbArray.append(xMin) | |
| numberOfSideBearings = numGlyphs - numberOfHMetrics | |
| if hasLeftSideBearingArray: | |
| assert len(data) >= 2 * numberOfSideBearings | |
| leftSideBearingArray = array.array("h", data[: 2 * numberOfSideBearings]) | |
| if sys.byteorder != "big": | |
| leftSideBearingArray.byteswap() | |
| data = data[2 * numberOfSideBearings :] | |
| else: | |
| # compute (monospaced) glyphs' leftSideBearing from their xMin | |
| leftSideBearingArray = array.array("h") | |
| for i, glyphName in enumerate(glyphOrder): | |
| if i < numberOfHMetrics: | |
| continue | |
| glyph = glyfTable[glyphName] | |
| xMin = getattr(glyph, "xMin", 0) | |
| leftSideBearingArray.append(xMin) | |
| if data: | |
| raise TTLibError("too much '%s' table data" % self.tableTag) | |
| self.metrics = {} | |
| for i in range(numberOfHMetrics): | |
| glyphName = glyphOrder[i] | |
| advanceWidth, lsb = advanceWidthArray[i], lsbArray[i] | |
| self.metrics[glyphName] = (advanceWidth, lsb) | |
| lastAdvance = advanceWidthArray[-1] | |
| for i in range(numberOfSideBearings): | |
| glyphName = glyphOrder[i + numberOfHMetrics] | |
| self.metrics[glyphName] = (lastAdvance, leftSideBearingArray[i]) | |
| def transform(self, ttFont): | |
| glyphOrder = ttFont.getGlyphOrder() | |
| glyf = ttFont["glyf"] | |
| hhea = ttFont["hhea"] | |
| numberOfHMetrics = hhea.numberOfHMetrics | |
| # check if any of the proportional glyphs has left sidebearings that | |
| # differ from their xMin bounding box values. | |
| hasLsbArray = False | |
| for i in range(numberOfHMetrics): | |
| glyphName = glyphOrder[i] | |
| lsb = self.metrics[glyphName][1] | |
| if lsb != getattr(glyf[glyphName], "xMin", 0): | |
| hasLsbArray = True | |
| break | |
| # do the same for the monospaced glyphs (if any) at the end of hmtx table | |
| hasLeftSideBearingArray = False | |
| for i in range(numberOfHMetrics, len(glyphOrder)): | |
| glyphName = glyphOrder[i] | |
| lsb = self.metrics[glyphName][1] | |
| if lsb != getattr(glyf[glyphName], "xMin", 0): | |
| hasLeftSideBearingArray = True | |
| break | |
| # if we need to encode both sidebearings arrays, then no transformation is | |
| # applicable, and we must use the untransformed hmtx data | |
| if hasLsbArray and hasLeftSideBearingArray: | |
| return | |
| # set bit 0 and 1 when the respective arrays are _not_ present | |
| flags = 0 | |
| if not hasLsbArray: | |
| flags |= 1 << 0 | |
| if not hasLeftSideBearingArray: | |
| flags |= 1 << 1 | |
| data = struct.pack(">B", flags) | |
| advanceWidthArray = array.array( | |
| "H", | |
| [ | |
| self.metrics[glyphName][0] | |
| for i, glyphName in enumerate(glyphOrder) | |
| if i < numberOfHMetrics | |
| ], | |
| ) | |
| if sys.byteorder != "big": | |
| advanceWidthArray.byteswap() | |
| data += advanceWidthArray.tobytes() | |
| if hasLsbArray: | |
| lsbArray = array.array( | |
| "h", | |
| [ | |
| self.metrics[glyphName][1] | |
| for i, glyphName in enumerate(glyphOrder) | |
| if i < numberOfHMetrics | |
| ], | |
| ) | |
| if sys.byteorder != "big": | |
| lsbArray.byteswap() | |
| data += lsbArray.tobytes() | |
| if hasLeftSideBearingArray: | |
| leftSideBearingArray = array.array( | |
| "h", | |
| [ | |
| self.metrics[glyphOrder[i]][1] | |
| for i in range(numberOfHMetrics, len(glyphOrder)) | |
| ], | |
| ) | |
| if sys.byteorder != "big": | |
| leftSideBearingArray.byteswap() | |
| data += leftSideBearingArray.tobytes() | |
| return data | |
| class WOFF2FlavorData(WOFFFlavorData): | |
| Flavor = "woff2" | |
| def __init__(self, reader=None, data=None, transformedTables=None): | |
| """Data class that holds the WOFF2 header major/minor version, any | |
| metadata or private data (as bytes strings), and the set of | |
| table tags that have transformations applied (if reader is not None), | |
| or will have once the WOFF2 font is compiled. | |
| Args: | |
| reader: an SFNTReader (or subclass) object to read flavor data from. | |
| data: another WOFFFlavorData object to initialise data from. | |
| transformedTables: set of strings containing table tags to be transformed. | |
| Raises: | |
| ImportError if the brotli module is not installed. | |
| NOTE: The 'reader' argument, on the one hand, and the 'data' and | |
| 'transformedTables' arguments, on the other hand, are mutually exclusive. | |
| """ | |
| if not haveBrotli: | |
| raise ImportError("No module named brotli") | |
| if reader is not None: | |
| if data is not None: | |
| raise TypeError("'reader' and 'data' arguments are mutually exclusive") | |
| if transformedTables is not None: | |
| raise TypeError( | |
| "'reader' and 'transformedTables' arguments are mutually exclusive" | |
| ) | |
| if transformedTables is not None and ( | |
| "glyf" in transformedTables | |
| and "loca" not in transformedTables | |
| or "loca" in transformedTables | |
| and "glyf" not in transformedTables | |
| ): | |
| raise ValueError("'glyf' and 'loca' must be transformed (or not) together") | |
| super(WOFF2FlavorData, self).__init__(reader=reader) | |
| if reader: | |
| transformedTables = [ | |
| tag for tag, entry in reader.tables.items() if entry.transformed | |
| ] | |
| elif data: | |
| self.majorVersion = data.majorVersion | |
| self.majorVersion = data.minorVersion | |
| self.metaData = data.metaData | |
| self.privData = data.privData | |
| if transformedTables is None and hasattr(data, "transformedTables"): | |
| transformedTables = data.transformedTables | |
| if transformedTables is None: | |
| transformedTables = woff2TransformedTableTags | |
| self.transformedTables = set(transformedTables) | |
| def _decompress(self, rawData): | |
| return brotli.decompress(rawData) | |
| def unpackBase128(data): | |
| r"""Read one to five bytes from UIntBase128-encoded input string, and return | |
| a tuple containing the decoded integer plus any leftover data. | |
| >>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00") | |
| True | |
| >>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295 | |
| True | |
| >>> unpackBase128(b'\x80\x80\x3f') # doctest: +IGNORE_EXCEPTION_DETAIL | |
| Traceback (most recent call last): | |
| File "<stdin>", line 1, in ? | |
| TTLibError: UIntBase128 value must not start with leading zeros | |
| >>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0] # doctest: +IGNORE_EXCEPTION_DETAIL | |
| Traceback (most recent call last): | |
| File "<stdin>", line 1, in ? | |
| TTLibError: UIntBase128-encoded sequence is longer than 5 bytes | |
| >>> unpackBase128(b'\x90\x80\x80\x80\x00')[0] # doctest: +IGNORE_EXCEPTION_DETAIL | |
| Traceback (most recent call last): | |
| File "<stdin>", line 1, in ? | |
| TTLibError: UIntBase128 value exceeds 2**32-1 | |
| """ | |
| if len(data) == 0: | |
| raise TTLibError("not enough data to unpack UIntBase128") | |
| result = 0 | |
| if byteord(data[0]) == 0x80: | |
| # font must be rejected if UIntBase128 value starts with 0x80 | |
| raise TTLibError("UIntBase128 value must not start with leading zeros") | |
| for i in range(woff2Base128MaxSize): | |
| if len(data) == 0: | |
| raise TTLibError("not enough data to unpack UIntBase128") | |
| code = byteord(data[0]) | |
| data = data[1:] | |
| # if any of the top seven bits are set then we're about to overflow | |
| if result & 0xFE000000: | |
| raise TTLibError("UIntBase128 value exceeds 2**32-1") | |
| # set current value = old value times 128 bitwise-or (byte bitwise-and 127) | |
| result = (result << 7) | (code & 0x7F) | |
| # repeat until the most significant bit of byte is false | |
| if (code & 0x80) == 0: | |
| # return result plus left over data | |
| return result, data | |
| # make sure not to exceed the size bound | |
| raise TTLibError("UIntBase128-encoded sequence is longer than 5 bytes") | |
| def base128Size(n): | |
| """Return the length in bytes of a UIntBase128-encoded sequence with value n. | |
| >>> base128Size(0) | |
| 1 | |
| >>> base128Size(24567) | |
| 3 | |
| >>> base128Size(2**32-1) | |
| 5 | |
| """ | |
| assert n >= 0 | |
| size = 1 | |
| while n >= 128: | |
| size += 1 | |
| n >>= 7 | |
| return size | |
| def packBase128(n): | |
| r"""Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of | |
| bytes using UIntBase128 variable-length encoding. Produce the shortest possible | |
| encoding. | |
| >>> packBase128(63) == b"\x3f" | |
| True | |
| >>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f' | |
| True | |
| """ | |
| if n < 0 or n >= 2**32: | |
| raise TTLibError("UIntBase128 format requires 0 <= integer <= 2**32-1") | |
| data = b"" | |
| size = base128Size(n) | |
| for i in range(size): | |
| b = (n >> (7 * (size - i - 1))) & 0x7F | |
| if i < size - 1: | |
| b |= 0x80 | |
| data += struct.pack("B", b) | |
| return data | |
| def unpack255UShort(data): | |
| """Read one to three bytes from 255UInt16-encoded input string, and return a | |
| tuple containing the decoded integer plus any leftover data. | |
| >>> unpack255UShort(bytechr(252))[0] | |
| 252 | |
| Note that some numbers (e.g. 506) can have multiple encodings: | |
| >>> unpack255UShort(struct.pack("BB", 254, 0))[0] | |
| 506 | |
| >>> unpack255UShort(struct.pack("BB", 255, 253))[0] | |
| 506 | |
| >>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0] | |
| 506 | |
| """ | |
| code = byteord(data[:1]) | |
| data = data[1:] | |
| if code == 253: | |
| # read two more bytes as an unsigned short | |
| if len(data) < 2: | |
| raise TTLibError("not enough data to unpack 255UInt16") | |
| (result,) = struct.unpack(">H", data[:2]) | |
| data = data[2:] | |
| elif code == 254: | |
| # read another byte, plus 253 * 2 | |
| if len(data) == 0: | |
| raise TTLibError("not enough data to unpack 255UInt16") | |
| result = byteord(data[:1]) | |
| result += 506 | |
| data = data[1:] | |
| elif code == 255: | |
| # read another byte, plus 253 | |
| if len(data) == 0: | |
| raise TTLibError("not enough data to unpack 255UInt16") | |
| result = byteord(data[:1]) | |
| result += 253 | |
| data = data[1:] | |
| else: | |
| # leave as is if lower than 253 | |
| result = code | |
| # return result plus left over data | |
| return result, data | |
| def pack255UShort(value): | |
| r"""Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring | |
| using 255UInt16 variable-length encoding. | |
| >>> pack255UShort(252) == b'\xfc' | |
| True | |
| >>> pack255UShort(506) == b'\xfe\x00' | |
| True | |
| >>> pack255UShort(762) == b'\xfd\x02\xfa' | |
| True | |
| """ | |
| if value < 0 or value > 0xFFFF: | |
| raise TTLibError("255UInt16 format requires 0 <= integer <= 65535") | |
| if value < 253: | |
| return struct.pack(">B", value) | |
| elif value < 506: | |
| return struct.pack(">BB", 255, value - 253) | |
| elif value < 762: | |
| return struct.pack(">BB", 254, value - 506) | |
| else: | |
| return struct.pack(">BH", 253, value) | |
| def compress(input_file, output_file, transform_tables=None): | |
| """Compress OpenType font to WOFF2. | |
| Args: | |
| input_file: a file path, file or file-like object (open in binary mode) | |
| containing an OpenType font (either CFF- or TrueType-flavored). | |
| output_file: a file path, file or file-like object where to save the | |
| compressed WOFF2 font. | |
| transform_tables: Optional[Iterable[str]]: a set of table tags for which | |
| to enable preprocessing transformations. By default, only 'glyf' | |
| and 'loca' tables are transformed. An empty set means disable all | |
| transformations. | |
| """ | |
| log.info("Processing %s => %s" % (input_file, output_file)) | |
| font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False) | |
| font.flavor = "woff2" | |
| if transform_tables is not None: | |
| font.flavorData = WOFF2FlavorData( | |
| data=font.flavorData, transformedTables=transform_tables | |
| ) | |
| font.save(output_file, reorderTables=False) | |
| def decompress(input_file, output_file): | |
| """Decompress WOFF2 font to OpenType font. | |
| Args: | |
| input_file: a file path, file or file-like object (open in binary mode) | |
| containing a compressed WOFF2 font. | |
| output_file: a file path, file or file-like object where to save the | |
| decompressed OpenType font. | |
| """ | |
| log.info("Processing %s => %s" % (input_file, output_file)) | |
| font = TTFont(input_file, recalcBBoxes=False, recalcTimestamp=False) | |
| font.flavor = None | |
| font.flavorData = None | |
| font.save(output_file, reorderTables=True) | |
| def main(args=None): | |
| """Compress and decompress WOFF2 fonts""" | |
| import argparse | |
| from fontTools import configLogger | |
| from fontTools.ttx import makeOutputFileName | |
| class _HelpAction(argparse._HelpAction): | |
| def __call__(self, parser, namespace, values, option_string=None): | |
| subparsers_actions = [ | |
| action | |
| for action in parser._actions | |
| if isinstance(action, argparse._SubParsersAction) | |
| ] | |
| for subparsers_action in subparsers_actions: | |
| for choice, subparser in subparsers_action.choices.items(): | |
| print(subparser.format_help()) | |
| parser.exit() | |
| class _NoGlyfTransformAction(argparse.Action): | |
| def __call__(self, parser, namespace, values, option_string=None): | |
| namespace.transform_tables.difference_update({"glyf", "loca"}) | |
| class _HmtxTransformAction(argparse.Action): | |
| def __call__(self, parser, namespace, values, option_string=None): | |
| namespace.transform_tables.add("hmtx") | |
| parser = argparse.ArgumentParser( | |
| prog="fonttools ttLib.woff2", description=main.__doc__, add_help=False | |
| ) | |
| parser.add_argument( | |
| "-h", "--help", action=_HelpAction, help="show this help message and exit" | |
| ) | |
| parser_group = parser.add_subparsers(title="sub-commands") | |
| parser_compress = parser_group.add_parser( | |
| "compress", description="Compress a TTF or OTF font to WOFF2" | |
| ) | |
| parser_decompress = parser_group.add_parser( | |
| "decompress", description="Decompress a WOFF2 font to OTF" | |
| ) | |
| for subparser in (parser_compress, parser_decompress): | |
| group = subparser.add_mutually_exclusive_group(required=False) | |
| group.add_argument( | |
| "-v", | |
| "--verbose", | |
| action="store_true", | |
| help="print more messages to console", | |
| ) | |
| group.add_argument( | |
| "-q", | |
| "--quiet", | |
| action="store_true", | |
| help="do not print messages to console", | |
| ) | |
| parser_compress.add_argument( | |
| "input_file", | |
| metavar="INPUT", | |
| help="the input OpenType font (.ttf or .otf)", | |
| ) | |
| parser_decompress.add_argument( | |
| "input_file", | |
| metavar="INPUT", | |
| help="the input WOFF2 font", | |
| ) | |
| parser_compress.add_argument( | |
| "-o", | |
| "--output-file", | |
| metavar="OUTPUT", | |
| help="the output WOFF2 font", | |
| ) | |
| parser_decompress.add_argument( | |
| "-o", | |
| "--output-file", | |
| metavar="OUTPUT", | |
| help="the output OpenType font", | |
| ) | |
| transform_group = parser_compress.add_argument_group() | |
| transform_group.add_argument( | |
| "--no-glyf-transform", | |
| dest="transform_tables", | |
| nargs=0, | |
| action=_NoGlyfTransformAction, | |
| help="Do not transform glyf (and loca) tables", | |
| ) | |
| transform_group.add_argument( | |
| "--hmtx-transform", | |
| dest="transform_tables", | |
| nargs=0, | |
| action=_HmtxTransformAction, | |
| help="Enable optional transformation for 'hmtx' table", | |
| ) | |
| parser_compress.set_defaults( | |
| subcommand=compress, | |
| transform_tables={"glyf", "loca"}, | |
| ) | |
| parser_decompress.set_defaults(subcommand=decompress) | |
| options = vars(parser.parse_args(args)) | |
| subcommand = options.pop("subcommand", None) | |
| if not subcommand: | |
| parser.print_help() | |
| return | |
| quiet = options.pop("quiet") | |
| verbose = options.pop("verbose") | |
| configLogger( | |
| level=("ERROR" if quiet else "DEBUG" if verbose else "INFO"), | |
| ) | |
| if not options["output_file"]: | |
| if subcommand is compress: | |
| extension = ".woff2" | |
| elif subcommand is decompress: | |
| # choose .ttf/.otf file extension depending on sfntVersion | |
| with open(options["input_file"], "rb") as f: | |
| f.seek(4) # skip 'wOF2' signature | |
| sfntVersion = f.read(4) | |
| assert len(sfntVersion) == 4, "not enough data" | |
| extension = ".otf" if sfntVersion == b"OTTO" else ".ttf" | |
| else: | |
| raise AssertionError(subcommand) | |
| options["output_file"] = makeOutputFileName( | |
| options["input_file"], outputDir=None, extension=extension | |
| ) | |
| try: | |
| subcommand(**options) | |
| except TTLibError as e: | |
| parser.error(e) | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |