1*e1fe3e4aSElliott Hughes"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format. 2*e1fe3e4aSElliott Hughes 3*e1fe3e4aSElliott HughesDefines two public classes: 4*e1fe3e4aSElliott Hughes SFNTReader 5*e1fe3e4aSElliott Hughes SFNTWriter 6*e1fe3e4aSElliott Hughes 7*e1fe3e4aSElliott Hughes(Normally you don't have to use these classes explicitly; they are 8*e1fe3e4aSElliott Hughesused automatically by ttLib.TTFont.) 9*e1fe3e4aSElliott Hughes 10*e1fe3e4aSElliott HughesThe reading and writing of sfnt files is separated in two distinct 11*e1fe3e4aSElliott Hughesclasses, since whenever the number of tables changes or whenever 12*e1fe3e4aSElliott Hughesa table's length changes you need to rewrite the whole file anyway. 13*e1fe3e4aSElliott Hughes""" 14*e1fe3e4aSElliott Hughes 15*e1fe3e4aSElliott Hughesfrom io import BytesIO 16*e1fe3e4aSElliott Hughesfrom types import SimpleNamespace 17*e1fe3e4aSElliott Hughesfrom fontTools.misc.textTools import Tag 18*e1fe3e4aSElliott Hughesfrom fontTools.misc import sstruct 19*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTLibError, TTLibFileIsCollectionError 20*e1fe3e4aSElliott Hughesimport struct 21*e1fe3e4aSElliott Hughesfrom collections import OrderedDict 22*e1fe3e4aSElliott Hughesimport logging 23*e1fe3e4aSElliott Hughes 24*e1fe3e4aSElliott Hughes 25*e1fe3e4aSElliott Hugheslog = logging.getLogger(__name__) 26*e1fe3e4aSElliott Hughes 27*e1fe3e4aSElliott Hughes 28*e1fe3e4aSElliott Hughesclass SFNTReader(object): 29*e1fe3e4aSElliott Hughes def __new__(cls, *args, **kwargs): 30*e1fe3e4aSElliott Hughes """Return an instance of the SFNTReader sub-class which is compatible 31*e1fe3e4aSElliott Hughes with the input file type. 32*e1fe3e4aSElliott Hughes """ 33*e1fe3e4aSElliott Hughes if args and cls is SFNTReader: 34*e1fe3e4aSElliott Hughes infile = args[0] 35*e1fe3e4aSElliott Hughes infile.seek(0) 36*e1fe3e4aSElliott Hughes sfntVersion = Tag(infile.read(4)) 37*e1fe3e4aSElliott Hughes infile.seek(0) 38*e1fe3e4aSElliott Hughes if sfntVersion == "wOF2": 39*e1fe3e4aSElliott Hughes # return new WOFF2Reader object 40*e1fe3e4aSElliott Hughes from fontTools.ttLib.woff2 import WOFF2Reader 41*e1fe3e4aSElliott Hughes 42*e1fe3e4aSElliott Hughes return object.__new__(WOFF2Reader) 43*e1fe3e4aSElliott Hughes # return default object 44*e1fe3e4aSElliott Hughes return object.__new__(cls) 45*e1fe3e4aSElliott Hughes 46*e1fe3e4aSElliott Hughes def __init__(self, file, checkChecksums=0, fontNumber=-1): 47*e1fe3e4aSElliott Hughes self.file = file 48*e1fe3e4aSElliott Hughes self.checkChecksums = checkChecksums 49*e1fe3e4aSElliott Hughes 50*e1fe3e4aSElliott Hughes self.flavor = None 51*e1fe3e4aSElliott Hughes self.flavorData = None 52*e1fe3e4aSElliott Hughes self.DirectoryEntry = SFNTDirectoryEntry 53*e1fe3e4aSElliott Hughes self.file.seek(0) 54*e1fe3e4aSElliott Hughes self.sfntVersion = self.file.read(4) 55*e1fe3e4aSElliott Hughes self.file.seek(0) 56*e1fe3e4aSElliott Hughes if self.sfntVersion == b"ttcf": 57*e1fe3e4aSElliott Hughes header = readTTCHeader(self.file) 58*e1fe3e4aSElliott Hughes numFonts = header.numFonts 59*e1fe3e4aSElliott Hughes if not 0 <= fontNumber < numFonts: 60*e1fe3e4aSElliott Hughes raise TTLibFileIsCollectionError( 61*e1fe3e4aSElliott Hughes "specify a font number between 0 and %d (inclusive)" 62*e1fe3e4aSElliott Hughes % (numFonts - 1) 63*e1fe3e4aSElliott Hughes ) 64*e1fe3e4aSElliott Hughes self.numFonts = numFonts 65*e1fe3e4aSElliott Hughes self.file.seek(header.offsetTable[fontNumber]) 66*e1fe3e4aSElliott Hughes data = self.file.read(sfntDirectorySize) 67*e1fe3e4aSElliott Hughes if len(data) != sfntDirectorySize: 68*e1fe3e4aSElliott Hughes raise TTLibError("Not a Font Collection (not enough data)") 69*e1fe3e4aSElliott Hughes sstruct.unpack(sfntDirectoryFormat, data, self) 70*e1fe3e4aSElliott Hughes elif self.sfntVersion == b"wOFF": 71*e1fe3e4aSElliott Hughes self.flavor = "woff" 72*e1fe3e4aSElliott Hughes self.DirectoryEntry = WOFFDirectoryEntry 73*e1fe3e4aSElliott Hughes data = self.file.read(woffDirectorySize) 74*e1fe3e4aSElliott Hughes if len(data) != woffDirectorySize: 75*e1fe3e4aSElliott Hughes raise TTLibError("Not a WOFF font (not enough data)") 76*e1fe3e4aSElliott Hughes sstruct.unpack(woffDirectoryFormat, data, self) 77*e1fe3e4aSElliott Hughes else: 78*e1fe3e4aSElliott Hughes data = self.file.read(sfntDirectorySize) 79*e1fe3e4aSElliott Hughes if len(data) != sfntDirectorySize: 80*e1fe3e4aSElliott Hughes raise TTLibError("Not a TrueType or OpenType font (not enough data)") 81*e1fe3e4aSElliott Hughes sstruct.unpack(sfntDirectoryFormat, data, self) 82*e1fe3e4aSElliott Hughes self.sfntVersion = Tag(self.sfntVersion) 83*e1fe3e4aSElliott Hughes 84*e1fe3e4aSElliott Hughes if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"): 85*e1fe3e4aSElliott Hughes raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") 86*e1fe3e4aSElliott Hughes tables = {} 87*e1fe3e4aSElliott Hughes for i in range(self.numTables): 88*e1fe3e4aSElliott Hughes entry = self.DirectoryEntry() 89*e1fe3e4aSElliott Hughes entry.fromFile(self.file) 90*e1fe3e4aSElliott Hughes tag = Tag(entry.tag) 91*e1fe3e4aSElliott Hughes tables[tag] = entry 92*e1fe3e4aSElliott Hughes self.tables = OrderedDict(sorted(tables.items(), key=lambda i: i[1].offset)) 93*e1fe3e4aSElliott Hughes 94*e1fe3e4aSElliott Hughes # Load flavor data if any 95*e1fe3e4aSElliott Hughes if self.flavor == "woff": 96*e1fe3e4aSElliott Hughes self.flavorData = WOFFFlavorData(self) 97*e1fe3e4aSElliott Hughes 98*e1fe3e4aSElliott Hughes def has_key(self, tag): 99*e1fe3e4aSElliott Hughes return tag in self.tables 100*e1fe3e4aSElliott Hughes 101*e1fe3e4aSElliott Hughes __contains__ = has_key 102*e1fe3e4aSElliott Hughes 103*e1fe3e4aSElliott Hughes def keys(self): 104*e1fe3e4aSElliott Hughes return self.tables.keys() 105*e1fe3e4aSElliott Hughes 106*e1fe3e4aSElliott Hughes def __getitem__(self, tag): 107*e1fe3e4aSElliott Hughes """Fetch the raw table data.""" 108*e1fe3e4aSElliott Hughes entry = self.tables[Tag(tag)] 109*e1fe3e4aSElliott Hughes data = entry.loadData(self.file) 110*e1fe3e4aSElliott Hughes if self.checkChecksums: 111*e1fe3e4aSElliott Hughes if tag == "head": 112*e1fe3e4aSElliott Hughes # Beh: we have to special-case the 'head' table. 113*e1fe3e4aSElliott Hughes checksum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:]) 114*e1fe3e4aSElliott Hughes else: 115*e1fe3e4aSElliott Hughes checksum = calcChecksum(data) 116*e1fe3e4aSElliott Hughes if self.checkChecksums > 1: 117*e1fe3e4aSElliott Hughes # Be obnoxious, and barf when it's wrong 118*e1fe3e4aSElliott Hughes assert checksum == entry.checkSum, "bad checksum for '%s' table" % tag 119*e1fe3e4aSElliott Hughes elif checksum != entry.checkSum: 120*e1fe3e4aSElliott Hughes # Be friendly, and just log a warning. 121*e1fe3e4aSElliott Hughes log.warning("bad checksum for '%s' table", tag) 122*e1fe3e4aSElliott Hughes return data 123*e1fe3e4aSElliott Hughes 124*e1fe3e4aSElliott Hughes def __delitem__(self, tag): 125*e1fe3e4aSElliott Hughes del self.tables[Tag(tag)] 126*e1fe3e4aSElliott Hughes 127*e1fe3e4aSElliott Hughes def close(self): 128*e1fe3e4aSElliott Hughes self.file.close() 129*e1fe3e4aSElliott Hughes 130*e1fe3e4aSElliott Hughes # We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able 131*e1fe3e4aSElliott Hughes # and deepcopy-able. When a TTFont is loaded as lazy=True, SFNTReader holds a 132*e1fe3e4aSElliott Hughes # reference to an external file object which is not pickleable. So in __getstate__ 133*e1fe3e4aSElliott Hughes # we store the file name and current position, and in __setstate__ we reopen the 134*e1fe3e4aSElliott Hughes # same named file after unpickling. 135*e1fe3e4aSElliott Hughes 136*e1fe3e4aSElliott Hughes def __getstate__(self): 137*e1fe3e4aSElliott Hughes if isinstance(self.file, BytesIO): 138*e1fe3e4aSElliott Hughes # BytesIO is already pickleable, return the state unmodified 139*e1fe3e4aSElliott Hughes return self.__dict__ 140*e1fe3e4aSElliott Hughes 141*e1fe3e4aSElliott Hughes # remove unpickleable file attribute, and only store its name and pos 142*e1fe3e4aSElliott Hughes state = self.__dict__.copy() 143*e1fe3e4aSElliott Hughes del state["file"] 144*e1fe3e4aSElliott Hughes state["_filename"] = self.file.name 145*e1fe3e4aSElliott Hughes state["_filepos"] = self.file.tell() 146*e1fe3e4aSElliott Hughes return state 147*e1fe3e4aSElliott Hughes 148*e1fe3e4aSElliott Hughes def __setstate__(self, state): 149*e1fe3e4aSElliott Hughes if "file" not in state: 150*e1fe3e4aSElliott Hughes self.file = open(state.pop("_filename"), "rb") 151*e1fe3e4aSElliott Hughes self.file.seek(state.pop("_filepos")) 152*e1fe3e4aSElliott Hughes self.__dict__.update(state) 153*e1fe3e4aSElliott Hughes 154*e1fe3e4aSElliott Hughes 155*e1fe3e4aSElliott Hughes# default compression level for WOFF 1.0 tables and metadata 156*e1fe3e4aSElliott HughesZLIB_COMPRESSION_LEVEL = 6 157*e1fe3e4aSElliott Hughes 158*e1fe3e4aSElliott Hughes# if set to True, use zopfli instead of zlib for compressing WOFF 1.0. 159*e1fe3e4aSElliott Hughes# The Python bindings are available at https://pypi.python.org/pypi/zopfli 160*e1fe3e4aSElliott HughesUSE_ZOPFLI = False 161*e1fe3e4aSElliott Hughes 162*e1fe3e4aSElliott Hughes# mapping between zlib's compression levels and zopfli's 'numiterations'. 163*e1fe3e4aSElliott Hughes# Use lower values for files over several MB in size or it will be too slow 164*e1fe3e4aSElliott HughesZOPFLI_LEVELS = { 165*e1fe3e4aSElliott Hughes # 0: 0, # can't do 0 iterations... 166*e1fe3e4aSElliott Hughes 1: 1, 167*e1fe3e4aSElliott Hughes 2: 3, 168*e1fe3e4aSElliott Hughes 3: 5, 169*e1fe3e4aSElliott Hughes 4: 8, 170*e1fe3e4aSElliott Hughes 5: 10, 171*e1fe3e4aSElliott Hughes 6: 15, 172*e1fe3e4aSElliott Hughes 7: 25, 173*e1fe3e4aSElliott Hughes 8: 50, 174*e1fe3e4aSElliott Hughes 9: 100, 175*e1fe3e4aSElliott Hughes} 176*e1fe3e4aSElliott Hughes 177*e1fe3e4aSElliott Hughes 178*e1fe3e4aSElliott Hughesdef compress(data, level=ZLIB_COMPRESSION_LEVEL): 179*e1fe3e4aSElliott Hughes """Compress 'data' to Zlib format. If 'USE_ZOPFLI' variable is True, 180*e1fe3e4aSElliott Hughes zopfli is used instead of the zlib module. 181*e1fe3e4aSElliott Hughes The compression 'level' must be between 0 and 9. 1 gives best speed, 182*e1fe3e4aSElliott Hughes 9 gives best compression (0 gives no compression at all). 183*e1fe3e4aSElliott Hughes The default value is a compromise between speed and compression (6). 184*e1fe3e4aSElliott Hughes """ 185*e1fe3e4aSElliott Hughes if not (0 <= level <= 9): 186*e1fe3e4aSElliott Hughes raise ValueError("Bad compression level: %s" % level) 187*e1fe3e4aSElliott Hughes if not USE_ZOPFLI or level == 0: 188*e1fe3e4aSElliott Hughes from zlib import compress 189*e1fe3e4aSElliott Hughes 190*e1fe3e4aSElliott Hughes return compress(data, level) 191*e1fe3e4aSElliott Hughes else: 192*e1fe3e4aSElliott Hughes from zopfli.zlib import compress 193*e1fe3e4aSElliott Hughes 194*e1fe3e4aSElliott Hughes return compress(data, numiterations=ZOPFLI_LEVELS[level]) 195*e1fe3e4aSElliott Hughes 196*e1fe3e4aSElliott Hughes 197*e1fe3e4aSElliott Hughesclass SFNTWriter(object): 198*e1fe3e4aSElliott Hughes def __new__(cls, *args, **kwargs): 199*e1fe3e4aSElliott Hughes """Return an instance of the SFNTWriter sub-class which is compatible 200*e1fe3e4aSElliott Hughes with the specified 'flavor'. 201*e1fe3e4aSElliott Hughes """ 202*e1fe3e4aSElliott Hughes flavor = None 203*e1fe3e4aSElliott Hughes if kwargs and "flavor" in kwargs: 204*e1fe3e4aSElliott Hughes flavor = kwargs["flavor"] 205*e1fe3e4aSElliott Hughes elif args and len(args) > 3: 206*e1fe3e4aSElliott Hughes flavor = args[3] 207*e1fe3e4aSElliott Hughes if cls is SFNTWriter: 208*e1fe3e4aSElliott Hughes if flavor == "woff2": 209*e1fe3e4aSElliott Hughes # return new WOFF2Writer object 210*e1fe3e4aSElliott Hughes from fontTools.ttLib.woff2 import WOFF2Writer 211*e1fe3e4aSElliott Hughes 212*e1fe3e4aSElliott Hughes return object.__new__(WOFF2Writer) 213*e1fe3e4aSElliott Hughes # return default object 214*e1fe3e4aSElliott Hughes return object.__new__(cls) 215*e1fe3e4aSElliott Hughes 216*e1fe3e4aSElliott Hughes def __init__( 217*e1fe3e4aSElliott Hughes self, 218*e1fe3e4aSElliott Hughes file, 219*e1fe3e4aSElliott Hughes numTables, 220*e1fe3e4aSElliott Hughes sfntVersion="\000\001\000\000", 221*e1fe3e4aSElliott Hughes flavor=None, 222*e1fe3e4aSElliott Hughes flavorData=None, 223*e1fe3e4aSElliott Hughes ): 224*e1fe3e4aSElliott Hughes self.file = file 225*e1fe3e4aSElliott Hughes self.numTables = numTables 226*e1fe3e4aSElliott Hughes self.sfntVersion = Tag(sfntVersion) 227*e1fe3e4aSElliott Hughes self.flavor = flavor 228*e1fe3e4aSElliott Hughes self.flavorData = flavorData 229*e1fe3e4aSElliott Hughes 230*e1fe3e4aSElliott Hughes if self.flavor == "woff": 231*e1fe3e4aSElliott Hughes self.directoryFormat = woffDirectoryFormat 232*e1fe3e4aSElliott Hughes self.directorySize = woffDirectorySize 233*e1fe3e4aSElliott Hughes self.DirectoryEntry = WOFFDirectoryEntry 234*e1fe3e4aSElliott Hughes 235*e1fe3e4aSElliott Hughes self.signature = "wOFF" 236*e1fe3e4aSElliott Hughes 237*e1fe3e4aSElliott Hughes # to calculate WOFF checksum adjustment, we also need the original SFNT offsets 238*e1fe3e4aSElliott Hughes self.origNextTableOffset = ( 239*e1fe3e4aSElliott Hughes sfntDirectorySize + numTables * sfntDirectoryEntrySize 240*e1fe3e4aSElliott Hughes ) 241*e1fe3e4aSElliott Hughes else: 242*e1fe3e4aSElliott Hughes assert not self.flavor, "Unknown flavor '%s'" % self.flavor 243*e1fe3e4aSElliott Hughes self.directoryFormat = sfntDirectoryFormat 244*e1fe3e4aSElliott Hughes self.directorySize = sfntDirectorySize 245*e1fe3e4aSElliott Hughes self.DirectoryEntry = SFNTDirectoryEntry 246*e1fe3e4aSElliott Hughes 247*e1fe3e4aSElliott Hughes from fontTools.ttLib import getSearchRange 248*e1fe3e4aSElliott Hughes 249*e1fe3e4aSElliott Hughes self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( 250*e1fe3e4aSElliott Hughes numTables, 16 251*e1fe3e4aSElliott Hughes ) 252*e1fe3e4aSElliott Hughes 253*e1fe3e4aSElliott Hughes self.directoryOffset = self.file.tell() 254*e1fe3e4aSElliott Hughes self.nextTableOffset = ( 255*e1fe3e4aSElliott Hughes self.directoryOffset 256*e1fe3e4aSElliott Hughes + self.directorySize 257*e1fe3e4aSElliott Hughes + numTables * self.DirectoryEntry.formatSize 258*e1fe3e4aSElliott Hughes ) 259*e1fe3e4aSElliott Hughes # clear out directory area 260*e1fe3e4aSElliott Hughes self.file.seek(self.nextTableOffset) 261*e1fe3e4aSElliott Hughes # make sure we're actually where we want to be. (old cStringIO bug) 262*e1fe3e4aSElliott Hughes self.file.write(b"\0" * (self.nextTableOffset - self.file.tell())) 263*e1fe3e4aSElliott Hughes self.tables = OrderedDict() 264*e1fe3e4aSElliott Hughes 265*e1fe3e4aSElliott Hughes def setEntry(self, tag, entry): 266*e1fe3e4aSElliott Hughes if tag in self.tables: 267*e1fe3e4aSElliott Hughes raise TTLibError("cannot rewrite '%s' table" % tag) 268*e1fe3e4aSElliott Hughes 269*e1fe3e4aSElliott Hughes self.tables[tag] = entry 270*e1fe3e4aSElliott Hughes 271*e1fe3e4aSElliott Hughes def __setitem__(self, tag, data): 272*e1fe3e4aSElliott Hughes """Write raw table data to disk.""" 273*e1fe3e4aSElliott Hughes if tag in self.tables: 274*e1fe3e4aSElliott Hughes raise TTLibError("cannot rewrite '%s' table" % tag) 275*e1fe3e4aSElliott Hughes 276*e1fe3e4aSElliott Hughes entry = self.DirectoryEntry() 277*e1fe3e4aSElliott Hughes entry.tag = tag 278*e1fe3e4aSElliott Hughes entry.offset = self.nextTableOffset 279*e1fe3e4aSElliott Hughes if tag == "head": 280*e1fe3e4aSElliott Hughes entry.checkSum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:]) 281*e1fe3e4aSElliott Hughes self.headTable = data 282*e1fe3e4aSElliott Hughes entry.uncompressed = True 283*e1fe3e4aSElliott Hughes else: 284*e1fe3e4aSElliott Hughes entry.checkSum = calcChecksum(data) 285*e1fe3e4aSElliott Hughes entry.saveData(self.file, data) 286*e1fe3e4aSElliott Hughes 287*e1fe3e4aSElliott Hughes if self.flavor == "woff": 288*e1fe3e4aSElliott Hughes entry.origOffset = self.origNextTableOffset 289*e1fe3e4aSElliott Hughes self.origNextTableOffset += (entry.origLength + 3) & ~3 290*e1fe3e4aSElliott Hughes 291*e1fe3e4aSElliott Hughes self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3) 292*e1fe3e4aSElliott Hughes # Add NUL bytes to pad the table data to a 4-byte boundary. 293*e1fe3e4aSElliott Hughes # Don't depend on f.seek() as we need to add the padding even if no 294*e1fe3e4aSElliott Hughes # subsequent write follows (seek is lazy), ie. after the final table 295*e1fe3e4aSElliott Hughes # in the font. 296*e1fe3e4aSElliott Hughes self.file.write(b"\0" * (self.nextTableOffset - self.file.tell())) 297*e1fe3e4aSElliott Hughes assert self.nextTableOffset == self.file.tell() 298*e1fe3e4aSElliott Hughes 299*e1fe3e4aSElliott Hughes self.setEntry(tag, entry) 300*e1fe3e4aSElliott Hughes 301*e1fe3e4aSElliott Hughes def __getitem__(self, tag): 302*e1fe3e4aSElliott Hughes return self.tables[tag] 303*e1fe3e4aSElliott Hughes 304*e1fe3e4aSElliott Hughes def close(self): 305*e1fe3e4aSElliott Hughes """All tables must have been written to disk. Now write the 306*e1fe3e4aSElliott Hughes directory. 307*e1fe3e4aSElliott Hughes """ 308*e1fe3e4aSElliott Hughes tables = sorted(self.tables.items()) 309*e1fe3e4aSElliott Hughes if len(tables) != self.numTables: 310*e1fe3e4aSElliott Hughes raise TTLibError( 311*e1fe3e4aSElliott Hughes "wrong number of tables; expected %d, found %d" 312*e1fe3e4aSElliott Hughes % (self.numTables, len(tables)) 313*e1fe3e4aSElliott Hughes ) 314*e1fe3e4aSElliott Hughes 315*e1fe3e4aSElliott Hughes if self.flavor == "woff": 316*e1fe3e4aSElliott Hughes self.signature = b"wOFF" 317*e1fe3e4aSElliott Hughes self.reserved = 0 318*e1fe3e4aSElliott Hughes 319*e1fe3e4aSElliott Hughes self.totalSfntSize = 12 320*e1fe3e4aSElliott Hughes self.totalSfntSize += 16 * len(tables) 321*e1fe3e4aSElliott Hughes for tag, entry in tables: 322*e1fe3e4aSElliott Hughes self.totalSfntSize += (entry.origLength + 3) & ~3 323*e1fe3e4aSElliott Hughes 324*e1fe3e4aSElliott Hughes data = self.flavorData if self.flavorData else WOFFFlavorData() 325*e1fe3e4aSElliott Hughes if data.majorVersion is not None and data.minorVersion is not None: 326*e1fe3e4aSElliott Hughes self.majorVersion = data.majorVersion 327*e1fe3e4aSElliott Hughes self.minorVersion = data.minorVersion 328*e1fe3e4aSElliott Hughes else: 329*e1fe3e4aSElliott Hughes if hasattr(self, "headTable"): 330*e1fe3e4aSElliott Hughes self.majorVersion, self.minorVersion = struct.unpack( 331*e1fe3e4aSElliott Hughes ">HH", self.headTable[4:8] 332*e1fe3e4aSElliott Hughes ) 333*e1fe3e4aSElliott Hughes else: 334*e1fe3e4aSElliott Hughes self.majorVersion = self.minorVersion = 0 335*e1fe3e4aSElliott Hughes if data.metaData: 336*e1fe3e4aSElliott Hughes self.metaOrigLength = len(data.metaData) 337*e1fe3e4aSElliott Hughes self.file.seek(0, 2) 338*e1fe3e4aSElliott Hughes self.metaOffset = self.file.tell() 339*e1fe3e4aSElliott Hughes compressedMetaData = compress(data.metaData) 340*e1fe3e4aSElliott Hughes self.metaLength = len(compressedMetaData) 341*e1fe3e4aSElliott Hughes self.file.write(compressedMetaData) 342*e1fe3e4aSElliott Hughes else: 343*e1fe3e4aSElliott Hughes self.metaOffset = self.metaLength = self.metaOrigLength = 0 344*e1fe3e4aSElliott Hughes if data.privData: 345*e1fe3e4aSElliott Hughes self.file.seek(0, 2) 346*e1fe3e4aSElliott Hughes off = self.file.tell() 347*e1fe3e4aSElliott Hughes paddedOff = (off + 3) & ~3 348*e1fe3e4aSElliott Hughes self.file.write(b"\0" * (paddedOff - off)) 349*e1fe3e4aSElliott Hughes self.privOffset = self.file.tell() 350*e1fe3e4aSElliott Hughes self.privLength = len(data.privData) 351*e1fe3e4aSElliott Hughes self.file.write(data.privData) 352*e1fe3e4aSElliott Hughes else: 353*e1fe3e4aSElliott Hughes self.privOffset = self.privLength = 0 354*e1fe3e4aSElliott Hughes 355*e1fe3e4aSElliott Hughes self.file.seek(0, 2) 356*e1fe3e4aSElliott Hughes self.length = self.file.tell() 357*e1fe3e4aSElliott Hughes 358*e1fe3e4aSElliott Hughes else: 359*e1fe3e4aSElliott Hughes assert not self.flavor, "Unknown flavor '%s'" % self.flavor 360*e1fe3e4aSElliott Hughes pass 361*e1fe3e4aSElliott Hughes 362*e1fe3e4aSElliott Hughes directory = sstruct.pack(self.directoryFormat, self) 363*e1fe3e4aSElliott Hughes 364*e1fe3e4aSElliott Hughes self.file.seek(self.directoryOffset + self.directorySize) 365*e1fe3e4aSElliott Hughes seenHead = 0 366*e1fe3e4aSElliott Hughes for tag, entry in tables: 367*e1fe3e4aSElliott Hughes if tag == "head": 368*e1fe3e4aSElliott Hughes seenHead = 1 369*e1fe3e4aSElliott Hughes directory = directory + entry.toString() 370*e1fe3e4aSElliott Hughes if seenHead: 371*e1fe3e4aSElliott Hughes self.writeMasterChecksum(directory) 372*e1fe3e4aSElliott Hughes self.file.seek(self.directoryOffset) 373*e1fe3e4aSElliott Hughes self.file.write(directory) 374*e1fe3e4aSElliott Hughes 375*e1fe3e4aSElliott Hughes def _calcMasterChecksum(self, directory): 376*e1fe3e4aSElliott Hughes # calculate checkSumAdjustment 377*e1fe3e4aSElliott Hughes tags = list(self.tables.keys()) 378*e1fe3e4aSElliott Hughes checksums = [] 379*e1fe3e4aSElliott Hughes for i in range(len(tags)): 380*e1fe3e4aSElliott Hughes checksums.append(self.tables[tags[i]].checkSum) 381*e1fe3e4aSElliott Hughes 382*e1fe3e4aSElliott Hughes if self.DirectoryEntry != SFNTDirectoryEntry: 383*e1fe3e4aSElliott Hughes # Create a SFNT directory for checksum calculation purposes 384*e1fe3e4aSElliott Hughes from fontTools.ttLib import getSearchRange 385*e1fe3e4aSElliott Hughes 386*e1fe3e4aSElliott Hughes self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( 387*e1fe3e4aSElliott Hughes self.numTables, 16 388*e1fe3e4aSElliott Hughes ) 389*e1fe3e4aSElliott Hughes directory = sstruct.pack(sfntDirectoryFormat, self) 390*e1fe3e4aSElliott Hughes tables = sorted(self.tables.items()) 391*e1fe3e4aSElliott Hughes for tag, entry in tables: 392*e1fe3e4aSElliott Hughes sfntEntry = SFNTDirectoryEntry() 393*e1fe3e4aSElliott Hughes sfntEntry.tag = entry.tag 394*e1fe3e4aSElliott Hughes sfntEntry.checkSum = entry.checkSum 395*e1fe3e4aSElliott Hughes sfntEntry.offset = entry.origOffset 396*e1fe3e4aSElliott Hughes sfntEntry.length = entry.origLength 397*e1fe3e4aSElliott Hughes directory = directory + sfntEntry.toString() 398*e1fe3e4aSElliott Hughes 399*e1fe3e4aSElliott Hughes directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize 400*e1fe3e4aSElliott Hughes assert directory_end == len(directory) 401*e1fe3e4aSElliott Hughes 402*e1fe3e4aSElliott Hughes checksums.append(calcChecksum(directory)) 403*e1fe3e4aSElliott Hughes checksum = sum(checksums) & 0xFFFFFFFF 404*e1fe3e4aSElliott Hughes # BiboAfba! 405*e1fe3e4aSElliott Hughes checksumadjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF 406*e1fe3e4aSElliott Hughes return checksumadjustment 407*e1fe3e4aSElliott Hughes 408*e1fe3e4aSElliott Hughes def writeMasterChecksum(self, directory): 409*e1fe3e4aSElliott Hughes checksumadjustment = self._calcMasterChecksum(directory) 410*e1fe3e4aSElliott Hughes # write the checksum to the file 411*e1fe3e4aSElliott Hughes self.file.seek(self.tables["head"].offset + 8) 412*e1fe3e4aSElliott Hughes self.file.write(struct.pack(">L", checksumadjustment)) 413*e1fe3e4aSElliott Hughes 414*e1fe3e4aSElliott Hughes def reordersTables(self): 415*e1fe3e4aSElliott Hughes return False 416*e1fe3e4aSElliott Hughes 417*e1fe3e4aSElliott Hughes 418*e1fe3e4aSElliott Hughes# -- sfnt directory helpers and cruft 419*e1fe3e4aSElliott Hughes 420*e1fe3e4aSElliott HughesttcHeaderFormat = """ 421*e1fe3e4aSElliott Hughes > # big endian 422*e1fe3e4aSElliott Hughes TTCTag: 4s # "ttcf" 423*e1fe3e4aSElliott Hughes Version: L # 0x00010000 or 0x00020000 424*e1fe3e4aSElliott Hughes numFonts: L # number of fonts 425*e1fe3e4aSElliott Hughes # OffsetTable[numFonts]: L # array with offsets from beginning of file 426*e1fe3e4aSElliott Hughes # ulDsigTag: L # version 2.0 only 427*e1fe3e4aSElliott Hughes # ulDsigLength: L # version 2.0 only 428*e1fe3e4aSElliott Hughes # ulDsigOffset: L # version 2.0 only 429*e1fe3e4aSElliott Hughes""" 430*e1fe3e4aSElliott Hughes 431*e1fe3e4aSElliott HughesttcHeaderSize = sstruct.calcsize(ttcHeaderFormat) 432*e1fe3e4aSElliott Hughes 433*e1fe3e4aSElliott HughessfntDirectoryFormat = """ 434*e1fe3e4aSElliott Hughes > # big endian 435*e1fe3e4aSElliott Hughes sfntVersion: 4s 436*e1fe3e4aSElliott Hughes numTables: H # number of tables 437*e1fe3e4aSElliott Hughes searchRange: H # (max2 <= numTables)*16 438*e1fe3e4aSElliott Hughes entrySelector: H # log2(max2 <= numTables) 439*e1fe3e4aSElliott Hughes rangeShift: H # numTables*16-searchRange 440*e1fe3e4aSElliott Hughes""" 441*e1fe3e4aSElliott Hughes 442*e1fe3e4aSElliott HughessfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat) 443*e1fe3e4aSElliott Hughes 444*e1fe3e4aSElliott HughessfntDirectoryEntryFormat = """ 445*e1fe3e4aSElliott Hughes > # big endian 446*e1fe3e4aSElliott Hughes tag: 4s 447*e1fe3e4aSElliott Hughes checkSum: L 448*e1fe3e4aSElliott Hughes offset: L 449*e1fe3e4aSElliott Hughes length: L 450*e1fe3e4aSElliott Hughes""" 451*e1fe3e4aSElliott Hughes 452*e1fe3e4aSElliott HughessfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat) 453*e1fe3e4aSElliott Hughes 454*e1fe3e4aSElliott HugheswoffDirectoryFormat = """ 455*e1fe3e4aSElliott Hughes > # big endian 456*e1fe3e4aSElliott Hughes signature: 4s # "wOFF" 457*e1fe3e4aSElliott Hughes sfntVersion: 4s 458*e1fe3e4aSElliott Hughes length: L # total woff file size 459*e1fe3e4aSElliott Hughes numTables: H # number of tables 460*e1fe3e4aSElliott Hughes reserved: H # set to 0 461*e1fe3e4aSElliott Hughes totalSfntSize: L # uncompressed size 462*e1fe3e4aSElliott Hughes majorVersion: H # major version of WOFF file 463*e1fe3e4aSElliott Hughes minorVersion: H # minor version of WOFF file 464*e1fe3e4aSElliott Hughes metaOffset: L # offset to metadata block 465*e1fe3e4aSElliott Hughes metaLength: L # length of compressed metadata 466*e1fe3e4aSElliott Hughes metaOrigLength: L # length of uncompressed metadata 467*e1fe3e4aSElliott Hughes privOffset: L # offset to private data block 468*e1fe3e4aSElliott Hughes privLength: L # length of private data block 469*e1fe3e4aSElliott Hughes""" 470*e1fe3e4aSElliott Hughes 471*e1fe3e4aSElliott HugheswoffDirectorySize = sstruct.calcsize(woffDirectoryFormat) 472*e1fe3e4aSElliott Hughes 473*e1fe3e4aSElliott HugheswoffDirectoryEntryFormat = """ 474*e1fe3e4aSElliott Hughes > # big endian 475*e1fe3e4aSElliott Hughes tag: 4s 476*e1fe3e4aSElliott Hughes offset: L 477*e1fe3e4aSElliott Hughes length: L # compressed length 478*e1fe3e4aSElliott Hughes origLength: L # original length 479*e1fe3e4aSElliott Hughes checkSum: L # original checksum 480*e1fe3e4aSElliott Hughes""" 481*e1fe3e4aSElliott Hughes 482*e1fe3e4aSElliott HugheswoffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat) 483*e1fe3e4aSElliott Hughes 484*e1fe3e4aSElliott Hughes 485*e1fe3e4aSElliott Hughesclass DirectoryEntry(object): 486*e1fe3e4aSElliott Hughes def __init__(self): 487*e1fe3e4aSElliott Hughes self.uncompressed = False # if True, always embed entry raw 488*e1fe3e4aSElliott Hughes 489*e1fe3e4aSElliott Hughes def fromFile(self, file): 490*e1fe3e4aSElliott Hughes sstruct.unpack(self.format, file.read(self.formatSize), self) 491*e1fe3e4aSElliott Hughes 492*e1fe3e4aSElliott Hughes def fromString(self, str): 493*e1fe3e4aSElliott Hughes sstruct.unpack(self.format, str, self) 494*e1fe3e4aSElliott Hughes 495*e1fe3e4aSElliott Hughes def toString(self): 496*e1fe3e4aSElliott Hughes return sstruct.pack(self.format, self) 497*e1fe3e4aSElliott Hughes 498*e1fe3e4aSElliott Hughes def __repr__(self): 499*e1fe3e4aSElliott Hughes if hasattr(self, "tag"): 500*e1fe3e4aSElliott Hughes return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self)) 501*e1fe3e4aSElliott Hughes else: 502*e1fe3e4aSElliott Hughes return "<%s at %x>" % (self.__class__.__name__, id(self)) 503*e1fe3e4aSElliott Hughes 504*e1fe3e4aSElliott Hughes def loadData(self, file): 505*e1fe3e4aSElliott Hughes file.seek(self.offset) 506*e1fe3e4aSElliott Hughes data = file.read(self.length) 507*e1fe3e4aSElliott Hughes assert len(data) == self.length 508*e1fe3e4aSElliott Hughes if hasattr(self.__class__, "decodeData"): 509*e1fe3e4aSElliott Hughes data = self.decodeData(data) 510*e1fe3e4aSElliott Hughes return data 511*e1fe3e4aSElliott Hughes 512*e1fe3e4aSElliott Hughes def saveData(self, file, data): 513*e1fe3e4aSElliott Hughes if hasattr(self.__class__, "encodeData"): 514*e1fe3e4aSElliott Hughes data = self.encodeData(data) 515*e1fe3e4aSElliott Hughes self.length = len(data) 516*e1fe3e4aSElliott Hughes file.seek(self.offset) 517*e1fe3e4aSElliott Hughes file.write(data) 518*e1fe3e4aSElliott Hughes 519*e1fe3e4aSElliott Hughes def decodeData(self, rawData): 520*e1fe3e4aSElliott Hughes return rawData 521*e1fe3e4aSElliott Hughes 522*e1fe3e4aSElliott Hughes def encodeData(self, data): 523*e1fe3e4aSElliott Hughes return data 524*e1fe3e4aSElliott Hughes 525*e1fe3e4aSElliott Hughes 526*e1fe3e4aSElliott Hughesclass SFNTDirectoryEntry(DirectoryEntry): 527*e1fe3e4aSElliott Hughes format = sfntDirectoryEntryFormat 528*e1fe3e4aSElliott Hughes formatSize = sfntDirectoryEntrySize 529*e1fe3e4aSElliott Hughes 530*e1fe3e4aSElliott Hughes 531*e1fe3e4aSElliott Hughesclass WOFFDirectoryEntry(DirectoryEntry): 532*e1fe3e4aSElliott Hughes format = woffDirectoryEntryFormat 533*e1fe3e4aSElliott Hughes formatSize = woffDirectoryEntrySize 534*e1fe3e4aSElliott Hughes 535*e1fe3e4aSElliott Hughes def __init__(self): 536*e1fe3e4aSElliott Hughes super(WOFFDirectoryEntry, self).__init__() 537*e1fe3e4aSElliott Hughes # With fonttools<=3.1.2, the only way to set a different zlib 538*e1fe3e4aSElliott Hughes # compression level for WOFF directory entries was to set the class 539*e1fe3e4aSElliott Hughes # attribute 'zlibCompressionLevel'. This is now replaced by a globally 540*e1fe3e4aSElliott Hughes # defined `ZLIB_COMPRESSION_LEVEL`, which is also applied when 541*e1fe3e4aSElliott Hughes # compressing the metadata. For backward compatibility, we still 542*e1fe3e4aSElliott Hughes # use the class attribute if it was already set. 543*e1fe3e4aSElliott Hughes if not hasattr(WOFFDirectoryEntry, "zlibCompressionLevel"): 544*e1fe3e4aSElliott Hughes self.zlibCompressionLevel = ZLIB_COMPRESSION_LEVEL 545*e1fe3e4aSElliott Hughes 546*e1fe3e4aSElliott Hughes def decodeData(self, rawData): 547*e1fe3e4aSElliott Hughes import zlib 548*e1fe3e4aSElliott Hughes 549*e1fe3e4aSElliott Hughes if self.length == self.origLength: 550*e1fe3e4aSElliott Hughes data = rawData 551*e1fe3e4aSElliott Hughes else: 552*e1fe3e4aSElliott Hughes assert self.length < self.origLength 553*e1fe3e4aSElliott Hughes data = zlib.decompress(rawData) 554*e1fe3e4aSElliott Hughes assert len(data) == self.origLength 555*e1fe3e4aSElliott Hughes return data 556*e1fe3e4aSElliott Hughes 557*e1fe3e4aSElliott Hughes def encodeData(self, data): 558*e1fe3e4aSElliott Hughes self.origLength = len(data) 559*e1fe3e4aSElliott Hughes if not self.uncompressed: 560*e1fe3e4aSElliott Hughes compressedData = compress(data, self.zlibCompressionLevel) 561*e1fe3e4aSElliott Hughes if self.uncompressed or len(compressedData) >= self.origLength: 562*e1fe3e4aSElliott Hughes # Encode uncompressed 563*e1fe3e4aSElliott Hughes rawData = data 564*e1fe3e4aSElliott Hughes self.length = self.origLength 565*e1fe3e4aSElliott Hughes else: 566*e1fe3e4aSElliott Hughes rawData = compressedData 567*e1fe3e4aSElliott Hughes self.length = len(rawData) 568*e1fe3e4aSElliott Hughes return rawData 569*e1fe3e4aSElliott Hughes 570*e1fe3e4aSElliott Hughes 571*e1fe3e4aSElliott Hughesclass WOFFFlavorData: 572*e1fe3e4aSElliott Hughes Flavor = "woff" 573*e1fe3e4aSElliott Hughes 574*e1fe3e4aSElliott Hughes def __init__(self, reader=None): 575*e1fe3e4aSElliott Hughes self.majorVersion = None 576*e1fe3e4aSElliott Hughes self.minorVersion = None 577*e1fe3e4aSElliott Hughes self.metaData = None 578*e1fe3e4aSElliott Hughes self.privData = None 579*e1fe3e4aSElliott Hughes if reader: 580*e1fe3e4aSElliott Hughes self.majorVersion = reader.majorVersion 581*e1fe3e4aSElliott Hughes self.minorVersion = reader.minorVersion 582*e1fe3e4aSElliott Hughes if reader.metaLength: 583*e1fe3e4aSElliott Hughes reader.file.seek(reader.metaOffset) 584*e1fe3e4aSElliott Hughes rawData = reader.file.read(reader.metaLength) 585*e1fe3e4aSElliott Hughes assert len(rawData) == reader.metaLength 586*e1fe3e4aSElliott Hughes data = self._decompress(rawData) 587*e1fe3e4aSElliott Hughes assert len(data) == reader.metaOrigLength 588*e1fe3e4aSElliott Hughes self.metaData = data 589*e1fe3e4aSElliott Hughes if reader.privLength: 590*e1fe3e4aSElliott Hughes reader.file.seek(reader.privOffset) 591*e1fe3e4aSElliott Hughes data = reader.file.read(reader.privLength) 592*e1fe3e4aSElliott Hughes assert len(data) == reader.privLength 593*e1fe3e4aSElliott Hughes self.privData = data 594*e1fe3e4aSElliott Hughes 595*e1fe3e4aSElliott Hughes def _decompress(self, rawData): 596*e1fe3e4aSElliott Hughes import zlib 597*e1fe3e4aSElliott Hughes 598*e1fe3e4aSElliott Hughes return zlib.decompress(rawData) 599*e1fe3e4aSElliott Hughes 600*e1fe3e4aSElliott Hughes 601*e1fe3e4aSElliott Hughesdef calcChecksum(data): 602*e1fe3e4aSElliott Hughes """Calculate the checksum for an arbitrary block of data. 603*e1fe3e4aSElliott Hughes 604*e1fe3e4aSElliott Hughes If the data length is not a multiple of four, it assumes 605*e1fe3e4aSElliott Hughes it is to be padded with null byte. 606*e1fe3e4aSElliott Hughes 607*e1fe3e4aSElliott Hughes >>> print(calcChecksum(b"abcd")) 608*e1fe3e4aSElliott Hughes 1633837924 609*e1fe3e4aSElliott Hughes >>> print(calcChecksum(b"abcdxyz")) 610*e1fe3e4aSElliott Hughes 3655064932 611*e1fe3e4aSElliott Hughes """ 612*e1fe3e4aSElliott Hughes remainder = len(data) % 4 613*e1fe3e4aSElliott Hughes if remainder: 614*e1fe3e4aSElliott Hughes data += b"\0" * (4 - remainder) 615*e1fe3e4aSElliott Hughes value = 0 616*e1fe3e4aSElliott Hughes blockSize = 4096 617*e1fe3e4aSElliott Hughes assert blockSize % 4 == 0 618*e1fe3e4aSElliott Hughes for i in range(0, len(data), blockSize): 619*e1fe3e4aSElliott Hughes block = data[i : i + blockSize] 620*e1fe3e4aSElliott Hughes longs = struct.unpack(">%dL" % (len(block) // 4), block) 621*e1fe3e4aSElliott Hughes value = (value + sum(longs)) & 0xFFFFFFFF 622*e1fe3e4aSElliott Hughes return value 623*e1fe3e4aSElliott Hughes 624*e1fe3e4aSElliott Hughes 625*e1fe3e4aSElliott Hughesdef readTTCHeader(file): 626*e1fe3e4aSElliott Hughes file.seek(0) 627*e1fe3e4aSElliott Hughes data = file.read(ttcHeaderSize) 628*e1fe3e4aSElliott Hughes if len(data) != ttcHeaderSize: 629*e1fe3e4aSElliott Hughes raise TTLibError("Not a Font Collection (not enough data)") 630*e1fe3e4aSElliott Hughes self = SimpleNamespace() 631*e1fe3e4aSElliott Hughes sstruct.unpack(ttcHeaderFormat, data, self) 632*e1fe3e4aSElliott Hughes if self.TTCTag != "ttcf": 633*e1fe3e4aSElliott Hughes raise TTLibError("Not a Font Collection") 634*e1fe3e4aSElliott Hughes assert self.Version == 0x00010000 or self.Version == 0x00020000, ( 635*e1fe3e4aSElliott Hughes "unrecognized TTC version 0x%08x" % self.Version 636*e1fe3e4aSElliott Hughes ) 637*e1fe3e4aSElliott Hughes self.offsetTable = struct.unpack( 638*e1fe3e4aSElliott Hughes ">%dL" % self.numFonts, file.read(self.numFonts * 4) 639*e1fe3e4aSElliott Hughes ) 640*e1fe3e4aSElliott Hughes if self.Version == 0x00020000: 641*e1fe3e4aSElliott Hughes pass # ignoring version 2.0 signatures 642*e1fe3e4aSElliott Hughes return self 643*e1fe3e4aSElliott Hughes 644*e1fe3e4aSElliott Hughes 645*e1fe3e4aSElliott Hughesdef writeTTCHeader(file, numFonts): 646*e1fe3e4aSElliott Hughes self = SimpleNamespace() 647*e1fe3e4aSElliott Hughes self.TTCTag = "ttcf" 648*e1fe3e4aSElliott Hughes self.Version = 0x00010000 649*e1fe3e4aSElliott Hughes self.numFonts = numFonts 650*e1fe3e4aSElliott Hughes file.seek(0) 651*e1fe3e4aSElliott Hughes file.write(sstruct.pack(ttcHeaderFormat, self)) 652*e1fe3e4aSElliott Hughes offset = file.tell() 653*e1fe3e4aSElliott Hughes file.write(struct.pack(">%dL" % self.numFonts, *([0] * self.numFonts))) 654*e1fe3e4aSElliott Hughes return offset 655*e1fe3e4aSElliott Hughes 656*e1fe3e4aSElliott Hughes 657*e1fe3e4aSElliott Hughesif __name__ == "__main__": 658*e1fe3e4aSElliott Hughes import sys 659*e1fe3e4aSElliott Hughes import doctest 660*e1fe3e4aSElliott Hughes 661*e1fe3e4aSElliott Hughes sys.exit(doctest.testmod().failed) 662