blob: 82e3e04e08cb3ff2b221a9e530451cc65e2aba48 [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
jvr9be387c2008-03-01 11:43:01 +000015import sys
Behdad Esfahbod8413c102013-09-17 16:59:39 -040016import struct
17from fontTools.misc import sstruct
Just7842e561999-12-16 21:34:53 +000018import os
19
jvr04b32042002-05-14 12:09:10 +000020
Just7842e561999-12-16 21:34:53 +000021class SFNTReader:
22
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)
pabs37e91e772009-02-22 08:55:00 +000032 if self.sfntVersion == "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
37 raise ttLib.TTLibError, "specify a font number between 0 and %d (inclusive)" % (self.numFonts - 1)
38 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)
43 elif self.sfntVersion == "wOFF":
44 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)
49
Just7842e561999-12-16 21:34:53 +000050 if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
51 from fontTools import ttLib
52 raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
53 self.tables = {}
54 for i in range(self.numTables):
Behdad Esfahbod58d74162013-08-15 15:30:55 -040055 entry = self.DirectoryEntry()
jvrea9dfa92002-05-12 17:14:50 +000056 entry.fromFile(self.file)
jvrce1d50a2002-05-12 17:02:50 +000057 if entry.length > 0:
58 self.tables[entry.tag] = entry
59 else:
60 # Ignore zero-length tables. This doesn't seem to be documented,
61 # yet it's apparently how the Windows TT rasterizer behaves.
62 # Besides, at least one font has been sighted which actually
63 # *has* a zero-length table.
64 pass
Behdad Esfahbod58d74162013-08-15 15:30:55 -040065
66 # Load flavor data if any
67 if self.flavor == "woff":
68 self.flavorData = WOFFFlavorData(self)
69
Just7842e561999-12-16 21:34:53 +000070 def has_key(self, tag):
71 return self.tables.has_key(tag)
72
73 def keys(self):
74 return self.tables.keys()
75
76 def __getitem__(self, tag):
77 """Fetch the raw table data."""
78 entry = self.tables[tag]
Behdad Esfahbod58d74162013-08-15 15:30:55 -040079 data = entry.loadData (self.file)
jvrea9dfa92002-05-12 17:14:50 +000080 if self.checkChecksums:
Just7842e561999-12-16 21:34:53 +000081 if tag == 'head':
82 # Beh: we have to special-case the 'head' table.
jvrea9dfa92002-05-12 17:14:50 +000083 checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +000084 else:
jvrea9dfa92002-05-12 17:14:50 +000085 checksum = calcChecksum(data)
86 if self.checkChecksums > 1:
Just7842e561999-12-16 21:34:53 +000087 # Be obnoxious, and barf when it's wrong
88 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
89 elif checksum <> entry.checkSum:
90 # Be friendly, and just print a warning.
91 print "bad checksum for '%s' table" % tag
92 return data
93
jvrf7074632002-05-04 22:04:02 +000094 def __delitem__(self, tag):
95 del self.tables[tag]
96
Just7842e561999-12-16 21:34:53 +000097 def close(self):
98 self.file.close()
99
100
101class SFNTWriter:
102
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400103 def __init__(self, file, numTables, sfntVersion="\000\001\000\000",
104 flavor=None, flavorData=None):
Just7842e561999-12-16 21:34:53 +0000105 self.file = file
106 self.numTables = numTables
107 self.sfntVersion = sfntVersion
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400108 self.flavor = flavor
109 self.flavorData = flavorData
110
111 if self.flavor == "woff":
112 self.directoryFormat = woffDirectoryFormat
113 self.directorySize = woffDirectorySize
114 self.DirectoryEntry = WOFFDirectoryEntry
115
116 self.signature = "wOFF"
117 else:
118 assert not self.flavor, "Unknown flavor '%s'" % self.flavor
119 self.directoryFormat = sfntDirectoryFormat
120 self.directorySize = sfntDirectorySize
121 self.DirectoryEntry = SFNTDirectoryEntry
122
123 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
124
125 self.nextTableOffset = self.directorySize + numTables * self.DirectoryEntry.formatSize
Just7842e561999-12-16 21:34:53 +0000126 # clear out directory area
127 self.file.seek(self.nextTableOffset)
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400128 # make sure we're actually where we want to be. (old cStringIO bug)
Just7842e561999-12-16 21:34:53 +0000129 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
130 self.tables = {}
131
132 def __setitem__(self, tag, data):
133 """Write raw table data to disk."""
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400134 reuse = False
Just7842e561999-12-16 21:34:53 +0000135 if self.tables.has_key(tag):
136 # We've written this table to file before. If the length
jvr04b32042002-05-14 12:09:10 +0000137 # of the data is still the same, we allow overwriting it.
Just7842e561999-12-16 21:34:53 +0000138 entry = self.tables[tag]
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400139 assert not hasattr(entry.__class__, 'encodeData')
Just7842e561999-12-16 21:34:53 +0000140 if len(data) <> entry.length:
141 from fontTools import ttLib
142 raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400143 reuse = True
Just7842e561999-12-16 21:34:53 +0000144 else:
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400145 entry = self.DirectoryEntry()
Just7842e561999-12-16 21:34:53 +0000146 entry.tag = tag
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400147
148 if tag == 'head':
149 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
150 self.headTable = data
151 entry.uncompressed = True
152 else:
153 entry.checkSum = calcChecksum(data)
154
155 entry.offset = self.nextTableOffset
156 entry.saveData (self.file, data)
157
158 if not reuse:
159 self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3)
160
jvrc63ac642008-06-17 20:41:15 +0000161 # Add NUL bytes to pad the table data to a 4-byte boundary.
162 # Don't depend on f.seek() as we need to add the padding even if no
163 # subsequent write follows (seek is lazy), ie. after the final table
164 # in the font.
Just7842e561999-12-16 21:34:53 +0000165 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
jvrc63ac642008-06-17 20:41:15 +0000166 assert self.nextTableOffset == self.file.tell()
Just7842e561999-12-16 21:34:53 +0000167
Just7842e561999-12-16 21:34:53 +0000168 self.tables[tag] = entry
169
jvr28ae1962004-11-16 10:37:59 +0000170 def close(self):
Just7842e561999-12-16 21:34:53 +0000171 """All tables must have been written to disk. Now write the
172 directory.
173 """
174 tables = self.tables.items()
175 tables.sort()
176 if len(tables) <> self.numTables:
177 from fontTools import ttLib
178 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400179
180 if self.flavor == "woff":
181 self.signature = "wOFF"
182 self.reserved = 0
183
184 self.totalSfntSize = 12
185 self.totalSfntSize += 16 * len(tables)
186 for tag, entry in tables:
187 self.totalSfntSize += (entry.origLength + 3) & ~3
188
189 data = self.flavorData if self.flavorData else WOFFFlavorData()
190 if data.majorVersion != None and data.minorVersion != None:
191 self.majorVersion = data.majorVersion
192 self.minorVersion = data.minorVersion
193 else:
194 if hasattr(self, 'headTable'):
195 self.majorVersion, self.minorVersion = struct.unpack(">HH", self.headTable[4:8])
196 else:
197 self.majorVersion = self.minorVersion = 0
198 if data.metaData:
199 self.metaOrigLength = len(data.metaData)
200 self.file.seek(0,2)
201 self.metaOffset = self.file.tell()
202 compressedMetaData = zlib.compress(data.metaData)
203 self.metaLength = len(compressedMetaData)
204 self.file.write(compressedMetaData)
205 else:
206 self.metaOffset = self.metaLength = self.metaOrigLength = 0
207 if data.privData:
208 self.file.seek(0,2)
209 off = self.file.tell()
210 paddedOff = (off + 3) & ~3
211 self.file.write('\0' * (paddedOff - off))
212 self.privOffset = self.file.tell()
213 self.privLength = len(data.privData)
214 self.file.write(data.privData)
215 else:
216 self.privOffset = self.privLength = 0
217
218 self.file.seek(0,2)
219 self.length = self.file.tell()
220
221 else:
222 assert not self.flavor, "Unknown flavor '%s'" % self.flavor
223 pass
Just7842e561999-12-16 21:34:53 +0000224
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400225 directory = sstruct.pack(self.directoryFormat, self)
Just7842e561999-12-16 21:34:53 +0000226
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400227 self.file.seek(self.directorySize)
jvrf509c0f2003-08-22 19:38:37 +0000228 seenHead = 0
Just7842e561999-12-16 21:34:53 +0000229 for tag, entry in tables:
jvrf509c0f2003-08-22 19:38:37 +0000230 if tag == "head":
231 seenHead = 1
jvrea9dfa92002-05-12 17:14:50 +0000232 directory = directory + entry.toString()
jvrf509c0f2003-08-22 19:38:37 +0000233 if seenHead:
jvr91bca422012-10-18 12:49:22 +0000234 self.writeMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000235 self.file.seek(0)
236 self.file.write(directory)
jvr91bca422012-10-18 12:49:22 +0000237
238 def _calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000239 # calculate checkSumAdjustment
240 tags = self.tables.keys()
jvr91bca422012-10-18 12:49:22 +0000241 checksums = []
Just7842e561999-12-16 21:34:53 +0000242 for i in range(len(tags)):
jvr91bca422012-10-18 12:49:22 +0000243 checksums.append(self.tables[tags[i]].checkSum)
244
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400245 # TODO(behdad) I'm fairly sure the checksum for woff is not working correctly.
246 # Haven't debugged.
247 if self.DirectoryEntry != SFNTDirectoryEntry:
248 # Create a SFNT directory for checksum calculation purposes
249 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables)
250 directory = sstruct.pack(sfntDirectoryFormat, self)
251 tables = self.tables.items()
252 tables.sort()
253 for tag, entry in tables:
254 sfntEntry = SFNTDirectoryEntry()
255 for item in ['tag', 'checkSum', 'offset', 'length']:
256 setattr(sfntEntry, item, getattr(entry, item))
257 directory = directory + sfntEntry.toString()
258
Just7842e561999-12-16 21:34:53 +0000259 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
260 assert directory_end == len(directory)
jvr91bca422012-10-18 12:49:22 +0000261
262 checksums.append(calcChecksum(directory))
263 checksum = sum(checksums) & 0xffffffff
Just7842e561999-12-16 21:34:53 +0000264 # BiboAfba!
jvr91bca422012-10-18 12:49:22 +0000265 checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff
266 return checksumadjustment
267
268 def writeMasterChecksum(self, directory):
269 checksumadjustment = self._calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000270 # write the checksum to the file
271 self.file.seek(self.tables['head'].offset + 8)
pabs30e2aece2009-03-24 09:42:15 +0000272 self.file.write(struct.pack(">L", checksumadjustment))
jvr1ebda672008-03-08 20:29:30 +0000273
Just7842e561999-12-16 21:34:53 +0000274
275# -- sfnt directory helpers and cruft
276
pabs37e91e772009-02-22 08:55:00 +0000277ttcHeaderFormat = """
278 > # big endian
279 TTCTag: 4s # "ttcf"
280 Version: L # 0x00010000 or 0x00020000
281 numFonts: L # number of fonts
282 # OffsetTable[numFonts]: L # array with offsets from beginning of file
283 # ulDsigTag: L # version 2.0 only
284 # ulDsigLength: L # version 2.0 only
285 # ulDsigOffset: L # version 2.0 only
286"""
287
288ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat)
289
Just7842e561999-12-16 21:34:53 +0000290sfntDirectoryFormat = """
291 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000292 sfntVersion: 4s
293 numTables: H # number of tables
294 searchRange: H # (max2 <= numTables)*16
295 entrySelector: H # log2(max2 <= numTables)
296 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000297"""
298
299sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
300
301sfntDirectoryEntryFormat = """
302 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000303 tag: 4s
pabs30e2aece2009-03-24 09:42:15 +0000304 checkSum: L
305 offset: L
306 length: L
Just7842e561999-12-16 21:34:53 +0000307"""
308
309sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
310
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400311woffDirectoryFormat = """
312 > # big endian
313 signature: 4s # "wOFF"
314 sfntVersion: 4s
315 length: L # total woff file size
316 numTables: H # number of tables
317 reserved: H # set to 0
318 totalSfntSize: L # uncompressed size
319 majorVersion: H # major version of WOFF file
320 minorVersion: H # minor version of WOFF file
321 metaOffset: L # offset to metadata block
322 metaLength: L # length of compressed metadata
323 metaOrigLength: L # length of uncompressed metadata
324 privOffset: L # offset to private data block
325 privLength: L # length of private data block
326"""
327
328woffDirectorySize = sstruct.calcsize(woffDirectoryFormat)
329
330woffDirectoryEntryFormat = """
331 > # big endian
332 tag: 4s
333 offset: L
334 length: L # compressed length
335 origLength: L # original length
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400336 checkSum: L # original checksum
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400337"""
338
339woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat)
340
341
342class DirectoryEntry:
Just7842e561999-12-16 21:34:53 +0000343
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400344 def __init__(self):
345 self.uncompressed = False # if True, always embed entry raw
346
jvrea9dfa92002-05-12 17:14:50 +0000347 def fromFile(self, file):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400348 sstruct.unpack(self.format, file.read(self.formatSize), self)
Just7842e561999-12-16 21:34:53 +0000349
jvrea9dfa92002-05-12 17:14:50 +0000350 def fromString(self, str):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400351 sstruct.unpack(self.format, str, self)
Just7842e561999-12-16 21:34:53 +0000352
jvrea9dfa92002-05-12 17:14:50 +0000353 def toString(self):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400354 return sstruct.pack(self.format, self)
Just7842e561999-12-16 21:34:53 +0000355
356 def __repr__(self):
357 if hasattr(self, "tag"):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400358 return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self))
Just7842e561999-12-16 21:34:53 +0000359 else:
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400360 return "<%s at %x>" % (self.__class__.__name__, id(self))
361
362 def loadData(self, file):
363 file.seek(self.offset)
364 data = file.read(self.length)
365 assert len(data) == self.length
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400366 if hasattr(self.__class__, 'decodeData'):
367 data = self.decodeData(data)
368 return data
369
370 def saveData(self, file, data):
371 if hasattr(self.__class__, 'encodeData'):
372 data = self.encodeData(data)
373 self.length = len(data)
374 file.seek(self.offset)
375 file.write(data)
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400376
377 def decodeData(self, rawData):
378 return rawData
379
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400380 def encodeData(self, data):
381 return data
382
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400383class SFNTDirectoryEntry(DirectoryEntry):
384
385 format = sfntDirectoryEntryFormat
386 formatSize = sfntDirectoryEntrySize
387
388class WOFFDirectoryEntry(DirectoryEntry):
389
390 format = woffDirectoryEntryFormat
391 formatSize = woffDirectoryEntrySize
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400392 zlibCompressionLevel = 6
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400393
394 def decodeData(self, rawData):
395 import zlib
396 if self.length == self.origLength:
397 data = rawData
398 else:
399 assert self.length < self.origLength
400 data = zlib.decompress(rawData)
401 assert len (data) == self.origLength
402 return data
403
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400404 def encodeData(self, data):
405 import zlib
406 self.origLength = len(data)
407 if not self.uncompressed:
408 compressedData = zlib.compress(data, self.zlibCompressionLevel)
409 if self.uncompressed or len(compressedData) >= self.origLength:
410 # Encode uncompressed
411 rawData = data
412 self.length = self.origLength
413 else:
414 rawData = compressedData
415 self.length = len(rawData)
416 return rawData
417
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400418class WOFFFlavorData():
419
Behdad Esfahbodb0dc6df2013-08-15 17:39:16 -0400420 Flavor = 'woff'
421
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400422 def __init__(self, reader=None):
423 self.majorVersion = None
424 self.minorVersion = None
425 self.metaData = None
426 self.privData = None
427 if reader:
428 self.majorVersion = reader.majorVersion
429 self.minorVersion = reader.minorVersion
430 if reader.metaLength:
431 reader.file.seek(reader.metaOffset)
432 rawData = read.file.read(reader.metaLength)
433 assert len(rawData) == reader.metaLength
434 data = zlib.decompress(rawData)
435 assert len(data) == reader.metaOrigLength
436 self.metaData = data
437 if reader.privLength:
438 reader.file.seek(reader.privOffset)
439 data = read.file.read(reader.privLength)
440 assert len(data) == reader.privLength
441 self.privData = data
Just7842e561999-12-16 21:34:53 +0000442
443
jvr91bca422012-10-18 12:49:22 +0000444def calcChecksum(data):
Just7842e561999-12-16 21:34:53 +0000445 """Calculate the checksum for an arbitrary block of data.
446 Optionally takes a 'start' argument, which allows you to
447 calculate a checksum in chunks by feeding it a previous
448 result.
449
450 If the data length is not a multiple of four, it assumes
451 it is to be padded with null byte.
jvr91bca422012-10-18 12:49:22 +0000452
453 >>> print calcChecksum("abcd")
454 1633837924
455 >>> print calcChecksum("abcdxyz")
456 3655064932
Just7842e561999-12-16 21:34:53 +0000457 """
Just7842e561999-12-16 21:34:53 +0000458 remainder = len(data) % 4
459 if remainder:
jvr91bca422012-10-18 12:49:22 +0000460 data += "\0" * (4 - remainder)
461 value = 0
462 blockSize = 4096
463 assert blockSize % 4 == 0
464 for i in xrange(0, len(data), blockSize):
465 block = data[i:i+blockSize]
466 longs = struct.unpack(">%dL" % (len(block) // 4), block)
467 value = (value + sum(longs)) & 0xffffffff
468 return value
Just7842e561999-12-16 21:34:53 +0000469
470
jvrea9dfa92002-05-12 17:14:50 +0000471def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000472 """Return the highest exponent of two, so that
473 (2 ** exponent) <= x
474 """
475 exponent = 0
476 while x:
477 x = x >> 1
478 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000479 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000480
481
jvrea9dfa92002-05-12 17:14:50 +0000482def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000483 """Calculate searchRange, entrySelector, rangeShift for the
484 sfnt directory. 'n' is the number of tables.
485 """
486 # This stuff needs to be stored in the file, because?
487 import math
jvrea9dfa92002-05-12 17:14:50 +0000488 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000489 searchRange = (2 ** exponent) * 16
490 entrySelector = exponent
491 rangeShift = n * 16 - searchRange
492 return searchRange, entrySelector, rangeShift
493
jvr91bca422012-10-18 12:49:22 +0000494
495if __name__ == "__main__":
496 import doctest
497 doctest.testmod()