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