blob: 62915100ced464b9ff5ceb176c0f86403cc05265 [file] [log] [blame]
Just7842e561999-12-16 21:34:53 +00001"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format.
2
3Defines two public classes:
4 SFNTReader
5 SFNTWriter
6
7(Normally you don't have to use these classes explicitly; they are
8used automatically by ttLib.TTFont.)
9
10The reading and writing of sfnt files is separated in two distinct
11classes, since whenever to number of tables changes or whenever
12a table's length chages you need to rewrite the whole file anyway.
13"""
14
Behdad Esfahbod32c10ee2013-11-27 17:46:17 -050015from __future__ import print_function, division
Behdad Esfahbod7ed91ec2013-11-27 15:16:28 -050016from fontTools.misc.py23 import *
Behdad Esfahbod30e691e2013-11-27 17:27:45 -050017from fontTools.misc import sstruct
18import struct
Just7842e561999-12-16 21:34:53 +000019
jvr04b32042002-05-14 12:09:10 +000020
Behdad Esfahbode388db52013-11-28 14:26:58 -050021class SFNTReader(object):
Just7842e561999-12-16 21:34:53 +000022
pabs37e91e772009-02-22 08:55:00 +000023 def __init__(self, file, checkChecksums=1, fontNumber=-1):
Just7842e561999-12-16 21:34:53 +000024 self.file = file
jvrea9dfa92002-05-12 17:14:50 +000025 self.checkChecksums = checkChecksums
Behdad Esfahbod58d74162013-08-15 15:30:55 -040026
27 self.flavor = None
28 self.flavorData = None
29 self.DirectoryEntry = SFNTDirectoryEntry
30 self.sfntVersion = self.file.read(4)
31 self.file.seek(0)
Behdad Esfahbodac4672e2013-11-27 16:44:53 -050032 if self.sfntVersion == b"ttcf":
Behdad Esfahbod58d74162013-08-15 15:30:55 -040033 sstruct.unpack(ttcHeaderFormat, self.file.read(ttcHeaderSize), self)
pabs37e91e772009-02-22 08:55:00 +000034 assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version
35 if not 0 <= fontNumber < self.numFonts:
36 from fontTools import ttLib
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -050037 raise ttLib.TTLibError("specify a font number between 0 and %d (inclusive)" % (self.numFonts - 1))
pabs37e91e772009-02-22 08:55:00 +000038 offsetTable = struct.unpack(">%dL" % self.numFonts, self.file.read(self.numFonts * 4))
39 if self.Version == 0x00020000:
40 pass # ignoring version 2.0 signatures
41 self.file.seek(offsetTable[fontNumber])
Behdad Esfahbod58d74162013-08-15 15:30:55 -040042 sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self)
Behdad Esfahbodac4672e2013-11-27 16:44:53 -050043 elif self.sfntVersion == b"wOFF":
Behdad Esfahbod58d74162013-08-15 15:30:55 -040044 self.flavor = "woff"
45 self.DirectoryEntry = WOFFDirectoryEntry
46 sstruct.unpack(woffDirectoryFormat, self.file.read(woffDirectorySize), self)
47 else:
48 sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self)
Behdad Esfahbodac4672e2013-11-27 16:44:53 -050049 self.sfntVersion = Tag(self.sfntVersion)
Behdad Esfahbod58d74162013-08-15 15:30:55 -040050
Behdad Esfahbodac4672e2013-11-27 16:44:53 -050051 if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"):
Just7842e561999-12-16 21:34:53 +000052 from fontTools import ttLib
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -050053 raise ttLib.TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
Just7842e561999-12-16 21:34:53 +000054 self.tables = {}
55 for i in range(self.numTables):
Behdad Esfahbod58d74162013-08-15 15:30:55 -040056 entry = self.DirectoryEntry()
jvrea9dfa92002-05-12 17:14:50 +000057 entry.fromFile(self.file)
jvrce1d50a2002-05-12 17:02:50 +000058 if entry.length > 0:
Behdad Esfahbod5cf40082013-11-27 19:51:59 -050059 self.tables[Tag(entry.tag)] = entry
jvrce1d50a2002-05-12 17:02:50 +000060 else:
61 # Ignore zero-length tables. This doesn't seem to be documented,
62 # yet it's apparently how the Windows TT rasterizer behaves.
63 # Besides, at least one font has been sighted which actually
64 # *has* a zero-length table.
65 pass
Behdad Esfahbod58d74162013-08-15 15:30:55 -040066
67 # Load flavor data if any
68 if self.flavor == "woff":
69 self.flavorData = WOFFFlavorData(self)
70
Just7842e561999-12-16 21:34:53 +000071 def has_key(self, tag):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -050072 return tag in self.tables
73
74 __contains__ = has_key
Just7842e561999-12-16 21:34:53 +000075
76 def keys(self):
77 return self.tables.keys()
78
79 def __getitem__(self, tag):
80 """Fetch the raw table data."""
Behdad Esfahbodac4672e2013-11-27 16:44:53 -050081 entry = self.tables[Tag(tag)]
Behdad Esfahbod58d74162013-08-15 15:30:55 -040082 data = entry.loadData (self.file)
jvrea9dfa92002-05-12 17:14:50 +000083 if self.checkChecksums:
Just7842e561999-12-16 21:34:53 +000084 if tag == 'head':
85 # Beh: we have to special-case the 'head' table.
Behdad Esfahbod821572c2013-11-27 21:09:03 -050086 checksum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +000087 else:
jvrea9dfa92002-05-12 17:14:50 +000088 checksum = calcChecksum(data)
89 if self.checkChecksums > 1:
Just7842e561999-12-16 21:34:53 +000090 # Be obnoxious, and barf when it's wrong
91 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
Behdad Esfahbod180ace62013-11-27 02:40:30 -050092 elif checksum != entry.checkSum:
Just7842e561999-12-16 21:34:53 +000093 # Be friendly, and just print a warning.
Behdad Esfahbod3ec6a252013-11-27 04:57:33 -050094 print("bad checksum for '%s' table" % tag)
Just7842e561999-12-16 21:34:53 +000095 return data
96
jvrf7074632002-05-04 22:04:02 +000097 def __delitem__(self, tag):
Behdad Esfahbodac4672e2013-11-27 16:44:53 -050098 del self.tables[Tag(tag)]
jvrf7074632002-05-04 22:04:02 +000099
Just7842e561999-12-16 21:34:53 +0000100 def close(self):
101 self.file.close()
102
103
Behdad Esfahbode388db52013-11-28 14:26:58 -0500104class SFNTWriter(object):
Just7842e561999-12-16 21:34:53 +0000105
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400106 def __init__(self, file, numTables, sfntVersion="\000\001\000\000",
107 flavor=None, flavorData=None):
Just7842e561999-12-16 21:34:53 +0000108 self.file = file
109 self.numTables = numTables
Behdad Esfahbodac4672e2013-11-27 16:44:53 -0500110 self.sfntVersion = Tag(sfntVersion)
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400111 self.flavor = flavor
112 self.flavorData = flavorData
113
114 if self.flavor == "woff":
115 self.directoryFormat = woffDirectoryFormat
116 self.directorySize = woffDirectorySize
117 self.DirectoryEntry = WOFFDirectoryEntry
118
119 self.signature = "wOFF"
120 else:
121 assert not self.flavor, "Unknown flavor '%s'" % self.flavor
122 self.directoryFormat = sfntDirectoryFormat
123 self.directorySize = sfntDirectorySize
124 self.DirectoryEntry = SFNTDirectoryEntry
125
126 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
127
128 self.nextTableOffset = self.directorySize + numTables * self.DirectoryEntry.formatSize
Just7842e561999-12-16 21:34:53 +0000129 # clear out directory area
130 self.file.seek(self.nextTableOffset)
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400131 # make sure we're actually where we want to be. (old cStringIO bug)
Behdad Esfahbod821572c2013-11-27 21:09:03 -0500132 self.file.write(b'\0' * (self.nextTableOffset - self.file.tell()))
Just7842e561999-12-16 21:34:53 +0000133 self.tables = {}
134
135 def __setitem__(self, tag, data):
136 """Write raw table data to disk."""
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400137 reuse = False
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500138 if tag in self.tables:
Just7842e561999-12-16 21:34:53 +0000139 # We've written this table to file before. If the length
jvr04b32042002-05-14 12:09:10 +0000140 # of the data is still the same, we allow overwriting it.
Just7842e561999-12-16 21:34:53 +0000141 entry = self.tables[tag]
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400142 assert not hasattr(entry.__class__, 'encodeData')
Behdad Esfahbod180ace62013-11-27 02:40:30 -0500143 if len(data) != entry.length:
Just7842e561999-12-16 21:34:53 +0000144 from fontTools import ttLib
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500145 raise ttLib.TTLibError("cannot rewrite '%s' table: length does not match directory entry" % tag)
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400146 reuse = True
Just7842e561999-12-16 21:34:53 +0000147 else:
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400148 entry = self.DirectoryEntry()
Just7842e561999-12-16 21:34:53 +0000149 entry.tag = tag
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400150
151 if tag == 'head':
Behdad Esfahbod821572c2013-11-27 21:09:03 -0500152 entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:])
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400153 self.headTable = data
154 entry.uncompressed = True
155 else:
156 entry.checkSum = calcChecksum(data)
157
158 entry.offset = self.nextTableOffset
159 entry.saveData (self.file, data)
160
161 if not reuse:
162 self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3)
163
jvrc63ac642008-06-17 20:41:15 +0000164 # Add NUL bytes to pad the table data to a 4-byte boundary.
165 # Don't depend on f.seek() as we need to add the padding even if no
166 # subsequent write follows (seek is lazy), ie. after the final table
167 # in the font.
Behdad Esfahbod821572c2013-11-27 21:09:03 -0500168 self.file.write(b'\0' * (self.nextTableOffset - self.file.tell()))
jvrc63ac642008-06-17 20:41:15 +0000169 assert self.nextTableOffset == self.file.tell()
Just7842e561999-12-16 21:34:53 +0000170
Just7842e561999-12-16 21:34:53 +0000171 self.tables[tag] = entry
172
jvr28ae1962004-11-16 10:37:59 +0000173 def close(self):
Just7842e561999-12-16 21:34:53 +0000174 """All tables must have been written to disk. Now write the
175 directory.
176 """
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500177 tables = sorted(self.tables.items())
Behdad Esfahbod180ace62013-11-27 02:40:30 -0500178 if len(tables) != self.numTables:
Just7842e561999-12-16 21:34:53 +0000179 from fontTools import ttLib
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500180 raise ttLib.TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables)))
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400181
182 if self.flavor == "woff":
Behdad Esfahbodac4672e2013-11-27 16:44:53 -0500183 self.signature = b"wOFF"
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400184 self.reserved = 0
185
186 self.totalSfntSize = 12
187 self.totalSfntSize += 16 * len(tables)
188 for tag, entry in tables:
189 self.totalSfntSize += (entry.origLength + 3) & ~3
190
191 data = self.flavorData if self.flavorData else WOFFFlavorData()
Behdad Esfahbod9e6ef942013-12-04 16:31:44 -0500192 if data.majorVersion is not None and data.minorVersion is not None:
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400193 self.majorVersion = data.majorVersion
194 self.minorVersion = data.minorVersion
195 else:
196 if hasattr(self, 'headTable'):
197 self.majorVersion, self.minorVersion = struct.unpack(">HH", self.headTable[4:8])
198 else:
199 self.majorVersion = self.minorVersion = 0
200 if data.metaData:
201 self.metaOrigLength = len(data.metaData)
202 self.file.seek(0,2)
203 self.metaOffset = self.file.tell()
Behdad Esfahbod153ec402013-12-04 01:15:46 -0500204 import zlib
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400205 compressedMetaData = zlib.compress(data.metaData)
206 self.metaLength = len(compressedMetaData)
207 self.file.write(compressedMetaData)
208 else:
209 self.metaOffset = self.metaLength = self.metaOrigLength = 0
210 if data.privData:
211 self.file.seek(0,2)
212 off = self.file.tell()
213 paddedOff = (off + 3) & ~3
214 self.file.write('\0' * (paddedOff - off))
215 self.privOffset = self.file.tell()
216 self.privLength = len(data.privData)
217 self.file.write(data.privData)
218 else:
219 self.privOffset = self.privLength = 0
220
221 self.file.seek(0,2)
222 self.length = self.file.tell()
223
224 else:
225 assert not self.flavor, "Unknown flavor '%s'" % self.flavor
226 pass
Just7842e561999-12-16 21:34:53 +0000227
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400228 directory = sstruct.pack(self.directoryFormat, self)
Just7842e561999-12-16 21:34:53 +0000229
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400230 self.file.seek(self.directorySize)
jvrf509c0f2003-08-22 19:38:37 +0000231 seenHead = 0
Just7842e561999-12-16 21:34:53 +0000232 for tag, entry in tables:
jvrf509c0f2003-08-22 19:38:37 +0000233 if tag == "head":
234 seenHead = 1
jvrea9dfa92002-05-12 17:14:50 +0000235 directory = directory + entry.toString()
jvrf509c0f2003-08-22 19:38:37 +0000236 if seenHead:
jvr91bca422012-10-18 12:49:22 +0000237 self.writeMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000238 self.file.seek(0)
239 self.file.write(directory)
jvr91bca422012-10-18 12:49:22 +0000240
241 def _calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000242 # calculate checkSumAdjustment
Behdad Esfahbodc2297cd2013-11-27 06:26:55 -0500243 tags = list(self.tables.keys())
jvr91bca422012-10-18 12:49:22 +0000244 checksums = []
Just7842e561999-12-16 21:34:53 +0000245 for i in range(len(tags)):
jvr91bca422012-10-18 12:49:22 +0000246 checksums.append(self.tables[tags[i]].checkSum)
247
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400248 # TODO(behdad) I'm fairly sure the checksum for woff is not working correctly.
249 # Haven't debugged.
250 if self.DirectoryEntry != SFNTDirectoryEntry:
251 # Create a SFNT directory for checksum calculation purposes
252 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables)
253 directory = sstruct.pack(sfntDirectoryFormat, self)
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500254 tables = sorted(self.tables.items())
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400255 for tag, entry in tables:
256 sfntEntry = SFNTDirectoryEntry()
257 for item in ['tag', 'checkSum', 'offset', 'length']:
258 setattr(sfntEntry, item, getattr(entry, item))
259 directory = directory + sfntEntry.toString()
260
Just7842e561999-12-16 21:34:53 +0000261 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
262 assert directory_end == len(directory)
jvr91bca422012-10-18 12:49:22 +0000263
264 checksums.append(calcChecksum(directory))
265 checksum = sum(checksums) & 0xffffffff
Just7842e561999-12-16 21:34:53 +0000266 # BiboAfba!
jvr91bca422012-10-18 12:49:22 +0000267 checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff
268 return checksumadjustment
269
270 def writeMasterChecksum(self, directory):
271 checksumadjustment = self._calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000272 # write the checksum to the file
273 self.file.seek(self.tables['head'].offset + 8)
pabs30e2aece2009-03-24 09:42:15 +0000274 self.file.write(struct.pack(">L", checksumadjustment))
jvr1ebda672008-03-08 20:29:30 +0000275
Just7842e561999-12-16 21:34:53 +0000276
277# -- sfnt directory helpers and cruft
278
pabs37e91e772009-02-22 08:55:00 +0000279ttcHeaderFormat = """
280 > # big endian
281 TTCTag: 4s # "ttcf"
282 Version: L # 0x00010000 or 0x00020000
283 numFonts: L # number of fonts
284 # OffsetTable[numFonts]: L # array with offsets from beginning of file
285 # ulDsigTag: L # version 2.0 only
286 # ulDsigLength: L # version 2.0 only
287 # ulDsigOffset: L # version 2.0 only
288"""
289
290ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat)
291
Just7842e561999-12-16 21:34:53 +0000292sfntDirectoryFormat = """
293 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000294 sfntVersion: 4s
295 numTables: H # number of tables
296 searchRange: H # (max2 <= numTables)*16
297 entrySelector: H # log2(max2 <= numTables)
298 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000299"""
300
301sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
302
303sfntDirectoryEntryFormat = """
304 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000305 tag: 4s
pabs30e2aece2009-03-24 09:42:15 +0000306 checkSum: L
307 offset: L
308 length: L
Just7842e561999-12-16 21:34:53 +0000309"""
310
311sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
312
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400313woffDirectoryFormat = """
314 > # big endian
315 signature: 4s # "wOFF"
316 sfntVersion: 4s
317 length: L # total woff file size
318 numTables: H # number of tables
319 reserved: H # set to 0
320 totalSfntSize: L # uncompressed size
321 majorVersion: H # major version of WOFF file
322 minorVersion: H # minor version of WOFF file
323 metaOffset: L # offset to metadata block
324 metaLength: L # length of compressed metadata
325 metaOrigLength: L # length of uncompressed metadata
326 privOffset: L # offset to private data block
327 privLength: L # length of private data block
328"""
329
330woffDirectorySize = sstruct.calcsize(woffDirectoryFormat)
331
332woffDirectoryEntryFormat = """
333 > # big endian
334 tag: 4s
335 offset: L
336 length: L # compressed length
337 origLength: L # original length
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400338 checkSum: L # original checksum
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400339"""
340
341woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat)
342
343
Behdad Esfahbode388db52013-11-28 14:26:58 -0500344class DirectoryEntry(object):
Just7842e561999-12-16 21:34:53 +0000345
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400346 def __init__(self):
347 self.uncompressed = False # if True, always embed entry raw
348
jvrea9dfa92002-05-12 17:14:50 +0000349 def fromFile(self, file):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400350 sstruct.unpack(self.format, file.read(self.formatSize), self)
Just7842e561999-12-16 21:34:53 +0000351
jvrea9dfa92002-05-12 17:14:50 +0000352 def fromString(self, str):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400353 sstruct.unpack(self.format, str, self)
Just7842e561999-12-16 21:34:53 +0000354
jvrea9dfa92002-05-12 17:14:50 +0000355 def toString(self):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400356 return sstruct.pack(self.format, self)
Just7842e561999-12-16 21:34:53 +0000357
358 def __repr__(self):
359 if hasattr(self, "tag"):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400360 return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self))
Just7842e561999-12-16 21:34:53 +0000361 else:
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400362 return "<%s at %x>" % (self.__class__.__name__, id(self))
363
364 def loadData(self, file):
365 file.seek(self.offset)
366 data = file.read(self.length)
367 assert len(data) == self.length
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400368 if hasattr(self.__class__, 'decodeData'):
369 data = self.decodeData(data)
370 return data
371
372 def saveData(self, file, data):
373 if hasattr(self.__class__, 'encodeData'):
374 data = self.encodeData(data)
375 self.length = len(data)
376 file.seek(self.offset)
377 file.write(data)
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400378
379 def decodeData(self, rawData):
380 return rawData
381
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400382 def encodeData(self, data):
383 return data
384
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400385class SFNTDirectoryEntry(DirectoryEntry):
386
387 format = sfntDirectoryEntryFormat
388 formatSize = sfntDirectoryEntrySize
389
390class WOFFDirectoryEntry(DirectoryEntry):
391
392 format = woffDirectoryEntryFormat
393 formatSize = woffDirectoryEntrySize
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400394 zlibCompressionLevel = 6
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400395
396 def decodeData(self, rawData):
397 import zlib
398 if self.length == self.origLength:
399 data = rawData
400 else:
401 assert self.length < self.origLength
402 data = zlib.decompress(rawData)
403 assert len (data) == self.origLength
404 return data
405
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400406 def encodeData(self, data):
407 import zlib
408 self.origLength = len(data)
409 if not self.uncompressed:
410 compressedData = zlib.compress(data, self.zlibCompressionLevel)
411 if self.uncompressed or len(compressedData) >= self.origLength:
412 # Encode uncompressed
413 rawData = data
414 self.length = self.origLength
415 else:
416 rawData = compressedData
417 self.length = len(rawData)
418 return rawData
419
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400420class WOFFFlavorData():
421
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400422 Flavor = 'woff'
423
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400424 def __init__(self, reader=None):
425 self.majorVersion = None
426 self.minorVersion = None
427 self.metaData = None
428 self.privData = None
429 if reader:
430 self.majorVersion = reader.majorVersion
431 self.minorVersion = reader.minorVersion
432 if reader.metaLength:
433 reader.file.seek(reader.metaOffset)
Behdad Esfahbod153ec402013-12-04 01:15:46 -0500434 rawData = reader.file.read(reader.metaLength)
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400435 assert len(rawData) == reader.metaLength
Behdad Esfahbod153ec402013-12-04 01:15:46 -0500436 import zlib
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400437 data = zlib.decompress(rawData)
438 assert len(data) == reader.metaOrigLength
439 self.metaData = data
440 if reader.privLength:
441 reader.file.seek(reader.privOffset)
Behdad Esfahbod153ec402013-12-04 01:15:46 -0500442 data = reader.file.read(reader.privLength)
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400443 assert len(data) == reader.privLength
444 self.privData = data
Just7842e561999-12-16 21:34:53 +0000445
446
jvr91bca422012-10-18 12:49:22 +0000447def calcChecksum(data):
Just7842e561999-12-16 21:34:53 +0000448 """Calculate the checksum for an arbitrary block of data.
449 Optionally takes a 'start' argument, which allows you to
450 calculate a checksum in chunks by feeding it a previous
451 result.
452
453 If the data length is not a multiple of four, it assumes
454 it is to be padded with null byte.
jvr91bca422012-10-18 12:49:22 +0000455
Behdad Esfahbod821572c2013-11-27 21:09:03 -0500456 >>> print calcChecksum(b"abcd")
jvr91bca422012-10-18 12:49:22 +0000457 1633837924
Behdad Esfahbod821572c2013-11-27 21:09:03 -0500458 >>> print calcChecksum(b"abcdxyz")
jvr91bca422012-10-18 12:49:22 +0000459 3655064932
Just7842e561999-12-16 21:34:53 +0000460 """
Just7842e561999-12-16 21:34:53 +0000461 remainder = len(data) % 4
462 if remainder:
Behdad Esfahbod821572c2013-11-27 21:09:03 -0500463 data += b"\0" * (4 - remainder)
jvr91bca422012-10-18 12:49:22 +0000464 value = 0
465 blockSize = 4096
466 assert blockSize % 4 == 0
Behdad Esfahbod97dea0a2013-11-27 03:34:48 -0500467 for i in range(0, len(data), blockSize):
jvr91bca422012-10-18 12:49:22 +0000468 block = data[i:i+blockSize]
469 longs = struct.unpack(">%dL" % (len(block) // 4), block)
470 value = (value + sum(longs)) & 0xffffffff
471 return value
Just7842e561999-12-16 21:34:53 +0000472
473
jvrea9dfa92002-05-12 17:14:50 +0000474def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000475 """Return the highest exponent of two, so that
476 (2 ** exponent) <= x
477 """
478 exponent = 0
479 while x:
480 x = x >> 1
481 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000482 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000483
484
jvrea9dfa92002-05-12 17:14:50 +0000485def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000486 """Calculate searchRange, entrySelector, rangeShift for the
487 sfnt directory. 'n' is the number of tables.
488 """
489 # This stuff needs to be stored in the file, because?
jvrea9dfa92002-05-12 17:14:50 +0000490 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000491 searchRange = (2 ** exponent) * 16
492 entrySelector = exponent
493 rangeShift = n * 16 - searchRange
494 return searchRange, entrySelector, rangeShift
495
jvr91bca422012-10-18 12:49:22 +0000496
497if __name__ == "__main__":
498 import doctest
499 doctest.testmod()