Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fontTools/ttLib/sfnt.py: 41%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

382 statements  

1"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format. 

2 

3Defines two public classes: 

4 

5- SFNTReader 

6- SFNTWriter 

7 

8(Normally you don't have to use these classes explicitly; they are 

9used automatically by ttLib.TTFont.) 

10 

11The reading and writing of sfnt files is separated in two distinct 

12classes, since whenever the number of tables changes or whenever 

13a table's length changes you need to rewrite the whole file anyway. 

14""" 

15 

16from __future__ import annotations 

17 

18from collections.abc import KeysView 

19from io import BytesIO 

20from types import SimpleNamespace 

21from fontTools.misc.textTools import Tag 

22from fontTools.misc import sstruct 

23from fontTools.ttLib import TTLibError, TTLibFileIsCollectionError 

24import struct 

25from collections import OrderedDict 

26import logging 

27 

28 

29log = logging.getLogger(__name__) 

30 

31 

32class SFNTReader(object): 

33 def __new__(cls, *args, **kwargs): 

34 """Return an instance of the SFNTReader sub-class which is compatible 

35 with the input file type. 

36 """ 

37 if args and cls is SFNTReader: 

38 infile = args[0] 

39 infile.seek(0) 

40 sfntVersion = Tag(infile.read(4)) 

41 infile.seek(0) 

42 if sfntVersion == "wOF2": 

43 # return new WOFF2Reader object 

44 from fontTools.ttLib.woff2 import WOFF2Reader 

45 

46 return object.__new__(WOFF2Reader) 

47 # return default object 

48 return object.__new__(cls) 

49 

50 def __init__(self, file, checkChecksums=0, fontNumber=-1): 

51 self.file = file 

52 self.checkChecksums = checkChecksums 

53 

54 self.flavor = None 

55 self.flavorData = None 

56 self.DirectoryEntry = SFNTDirectoryEntry 

57 self.file.seek(0) 

58 self.sfntVersion = self.file.read(4) 

59 self.file.seek(0) 

60 if self.sfntVersion == b"ttcf": 

61 header = readTTCHeader(self.file) 

62 numFonts = header.numFonts 

63 if not 0 <= fontNumber < numFonts: 

64 raise TTLibFileIsCollectionError( 

65 "specify a font number between 0 and %d (inclusive)" 

66 % (numFonts - 1) 

67 ) 

68 self.numFonts = numFonts 

69 self.file.seek(header.offsetTable[fontNumber]) 

70 data = self.file.read(sfntDirectorySize) 

71 if len(data) != sfntDirectorySize: 

72 raise TTLibError("Not a Font Collection (not enough data)") 

73 sstruct.unpack(sfntDirectoryFormat, data, self) 

74 elif self.sfntVersion == b"wOFF": 

75 self.flavor = "woff" 

76 self.DirectoryEntry = WOFFDirectoryEntry 

77 data = self.file.read(woffDirectorySize) 

78 if len(data) != woffDirectorySize: 

79 raise TTLibError("Not a WOFF font (not enough data)") 

80 sstruct.unpack(woffDirectoryFormat, data, self) 

81 else: 

82 data = self.file.read(sfntDirectorySize) 

83 if len(data) != sfntDirectorySize: 

84 raise TTLibError("Not a TrueType or OpenType font (not enough data)") 

85 sstruct.unpack(sfntDirectoryFormat, data, self) 

86 self.sfntVersion = Tag(self.sfntVersion) 

87 

88 if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"): 

89 raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") 

90 tables: dict[Tag, DirectoryEntry] = {} 

91 for i in range(self.numTables): 

92 entry = self.DirectoryEntry() 

93 entry.fromFile(self.file) 

94 tag = Tag(entry.tag) 

95 tables[tag] = entry 

96 self.tables = OrderedDict(sorted(tables.items(), key=lambda i: i[1].offset)) 

97 

98 # Load flavor data if any 

99 if self.flavor == "woff": 

100 self.flavorData = WOFFFlavorData(self) 

101 

102 def has_key(self, tag: str | bytes) -> bool: 

103 return tag in self.tables 

104 

105 __contains__ = has_key 

106 

107 def keys(self) -> KeysView[Tag]: 

108 return self.tables.keys() 

109 

110 def __getitem__(self, tag: str | bytes) -> bytes: 

111 """Fetch the raw table data.""" 

112 entry = self.tables[Tag(tag)] 

113 data = entry.loadData(self.file) 

114 if self.checkChecksums: 

115 if tag == "head": 

116 # Beh: we have to special-case the 'head' table. 

117 checksum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:]) 

118 else: 

119 checksum = calcChecksum(data) 

120 if self.checkChecksums > 1: 

121 # Be obnoxious, and barf when it's wrong 

122 assert checksum == entry.checkSum, "bad checksum for '%s' table" % tag 

123 elif checksum != entry.checkSum: 

124 # Be friendly, and just log a warning. 

125 log.warning("bad checksum for '%s' table", tag) 

126 return data 

127 

128 def __delitem__(self, tag: str | bytes) -> None: 

129 del self.tables[Tag(tag)] 

130 

131 def close(self) -> None: 

132 self.file.close() 

133 

134 # We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able 

135 # and deepcopy-able. When a TTFont is loaded as lazy=True, SFNTReader holds a 

136 # reference to an external file object which is not pickleable. So in __getstate__ 

137 # we store the file name and current position, and in __setstate__ we reopen the 

138 # same named file after unpickling. 

139 

140 def __getstate__(self): 

141 if isinstance(self.file, BytesIO): 

142 # BytesIO is already pickleable, return the state unmodified 

143 return self.__dict__ 

144 

145 # remove unpickleable file attribute, and only store its name and pos 

146 state = self.__dict__.copy() 

147 del state["file"] 

148 state["_filename"] = self.file.name 

149 state["_filepos"] = self.file.tell() 

150 return state 

151 

152 def __setstate__(self, state): 

153 if "file" not in state: 

154 self.file = open(state.pop("_filename"), "rb") 

155 self.file.seek(state.pop("_filepos")) 

156 self.__dict__.update(state) 

157 

158 

159# default compression level for WOFF 1.0 tables and metadata 

160ZLIB_COMPRESSION_LEVEL = 6 

161 

162# if set to True, use zopfli instead of zlib for compressing WOFF 1.0. 

163# The Python bindings are available at https://pypi.python.org/pypi/zopfli 

164USE_ZOPFLI = False 

165 

166# mapping between zlib's compression levels and zopfli's 'numiterations'. 

167# Use lower values for files over several MB in size or it will be too slow 

168ZOPFLI_LEVELS = { 

169 # 0: 0, # can't do 0 iterations... 

170 1: 1, 

171 2: 3, 

172 3: 5, 

173 4: 8, 

174 5: 10, 

175 6: 15, 

176 7: 25, 

177 8: 50, 

178 9: 100, 

179} 

180 

181 

182def compress(data, level=ZLIB_COMPRESSION_LEVEL): 

183 """Compress 'data' to Zlib format. If 'USE_ZOPFLI' variable is True, 

184 zopfli is used instead of the zlib module. 

185 The compression 'level' must be between 0 and 9. 1 gives best speed, 

186 9 gives best compression (0 gives no compression at all). 

187 The default value is a compromise between speed and compression (6). 

188 """ 

189 if not (0 <= level <= 9): 

190 raise ValueError("Bad compression level: %s" % level) 

191 if not USE_ZOPFLI or level == 0: 

192 from zlib import compress 

193 

194 return compress(data, level) 

195 else: 

196 from zopfli.zlib import compress 

197 

198 return compress(data, numiterations=ZOPFLI_LEVELS[level]) 

199 

200 

201class SFNTWriter(object): 

202 def __new__(cls, *args, **kwargs): 

203 """Return an instance of the SFNTWriter sub-class which is compatible 

204 with the specified 'flavor'. 

205 """ 

206 flavor = None 

207 if kwargs and "flavor" in kwargs: 

208 flavor = kwargs["flavor"] 

209 elif args and len(args) > 3: 

210 flavor = args[3] 

211 if cls is SFNTWriter: 

212 if flavor == "woff2": 

213 # return new WOFF2Writer object 

214 from fontTools.ttLib.woff2 import WOFF2Writer 

215 

216 return object.__new__(WOFF2Writer) 

217 # return default object 

218 return object.__new__(cls) 

219 

220 def __init__( 

221 self, 

222 file, 

223 numTables, 

224 sfntVersion="\000\001\000\000", 

225 flavor=None, 

226 flavorData=None, 

227 ): 

228 self.file = file 

229 self.numTables = numTables 

230 self.sfntVersion = Tag(sfntVersion) 

231 self.flavor = flavor 

232 self.flavorData = flavorData 

233 

234 if self.flavor == "woff": 

235 self.directoryFormat = woffDirectoryFormat 

236 self.directorySize = woffDirectorySize 

237 self.DirectoryEntry = WOFFDirectoryEntry 

238 

239 self.signature = "wOFF" 

240 

241 # to calculate WOFF checksum adjustment, we also need the original SFNT offsets 

242 self.origNextTableOffset = ( 

243 sfntDirectorySize + numTables * sfntDirectoryEntrySize 

244 ) 

245 else: 

246 assert not self.flavor, "Unknown flavor '%s'" % self.flavor 

247 self.directoryFormat = sfntDirectoryFormat 

248 self.directorySize = sfntDirectorySize 

249 self.DirectoryEntry = SFNTDirectoryEntry 

250 

251 from fontTools.ttLib import getSearchRange 

252 

253 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( 

254 numTables, 16 

255 ) 

256 

257 self.directoryOffset = self.file.tell() 

258 self.nextTableOffset = ( 

259 self.directoryOffset 

260 + self.directorySize 

261 + numTables * self.DirectoryEntry.formatSize 

262 ) 

263 # clear out directory area 

264 self.file.seek(self.nextTableOffset) 

265 # make sure we're actually where we want to be. (old cStringIO bug) 

266 self.file.write(b"\0" * (self.nextTableOffset - self.file.tell())) 

267 self.tables = OrderedDict() 

268 

269 def setEntry(self, tag, entry): 

270 if tag in self.tables: 

271 raise TTLibError("cannot rewrite '%s' table" % tag) 

272 

273 self.tables[tag] = entry 

274 

275 def __setitem__(self, tag, data): 

276 """Write raw table data to disk.""" 

277 if tag in self.tables: 

278 raise TTLibError("cannot rewrite '%s' table" % tag) 

279 

280 entry = self.DirectoryEntry() 

281 entry.tag = tag 

282 entry.offset = self.nextTableOffset 

283 if tag == "head": 

284 entry.checkSum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:]) 

285 self.headTable = data 

286 entry.uncompressed = True 

287 else: 

288 entry.checkSum = calcChecksum(data) 

289 entry.saveData(self.file, data) 

290 

291 if self.flavor == "woff": 

292 entry.origOffset = self.origNextTableOffset 

293 self.origNextTableOffset += (entry.origLength + 3) & ~3 

294 

295 self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3) 

296 # Add NUL bytes to pad the table data to a 4-byte boundary. 

297 # Don't depend on f.seek() as we need to add the padding even if no 

298 # subsequent write follows (seek is lazy), ie. after the final table 

299 # in the font. 

300 self.file.write(b"\0" * (self.nextTableOffset - self.file.tell())) 

301 assert self.nextTableOffset == self.file.tell() 

302 

303 self.setEntry(tag, entry) 

304 

305 def __getitem__(self, tag): 

306 return self.tables[tag] 

307 

308 def close(self): 

309 """All tables must have been written to disk. Now write the 

310 directory. 

311 """ 

312 tables = sorted(self.tables.items()) 

313 if len(tables) != self.numTables: 

314 raise TTLibError( 

315 "wrong number of tables; expected %d, found %d" 

316 % (self.numTables, len(tables)) 

317 ) 

318 

319 if self.flavor == "woff": 

320 self.signature = b"wOFF" 

321 self.reserved = 0 

322 

323 self.totalSfntSize = 12 

324 self.totalSfntSize += 16 * len(tables) 

325 for tag, entry in tables: 

326 self.totalSfntSize += (entry.origLength + 3) & ~3 

327 

328 data = self.flavorData if self.flavorData else WOFFFlavorData() 

329 if data.majorVersion is not None and data.minorVersion is not None: 

330 self.majorVersion = data.majorVersion 

331 self.minorVersion = data.minorVersion 

332 else: 

333 if hasattr(self, "headTable"): 

334 self.majorVersion, self.minorVersion = struct.unpack( 

335 ">HH", self.headTable[4:8] 

336 ) 

337 else: 

338 self.majorVersion = self.minorVersion = 0 

339 if data.metaData: 

340 self.metaOrigLength = len(data.metaData) 

341 self.file.seek(0, 2) 

342 self.metaOffset = self.file.tell() 

343 compressedMetaData = compress(data.metaData) 

344 self.metaLength = len(compressedMetaData) 

345 self.file.write(compressedMetaData) 

346 else: 

347 self.metaOffset = self.metaLength = self.metaOrigLength = 0 

348 if data.privData: 

349 self.file.seek(0, 2) 

350 off = self.file.tell() 

351 paddedOff = (off + 3) & ~3 

352 self.file.write(b"\0" * (paddedOff - off)) 

353 self.privOffset = self.file.tell() 

354 self.privLength = len(data.privData) 

355 self.file.write(data.privData) 

356 else: 

357 self.privOffset = self.privLength = 0 

358 

359 self.file.seek(0, 2) 

360 self.length = self.file.tell() 

361 

362 else: 

363 assert not self.flavor, "Unknown flavor '%s'" % self.flavor 

364 pass 

365 

366 directory = sstruct.pack(self.directoryFormat, self) 

367 

368 self.file.seek(self.directoryOffset + self.directorySize) 

369 seenHead = 0 

370 for tag, entry in tables: 

371 if tag == "head": 

372 seenHead = 1 

373 directory = directory + entry.toString() 

374 if seenHead: 

375 self.writeMasterChecksum(directory) 

376 self.file.seek(self.directoryOffset) 

377 self.file.write(directory) 

378 

379 def _calcMasterChecksum(self, directory): 

380 # calculate checkSumAdjustment 

381 checksums = [] 

382 for tag in self.tables.keys(): 

383 checksums.append(self.tables[tag].checkSum) 

384 

385 if self.DirectoryEntry != SFNTDirectoryEntry: 

386 # Create a SFNT directory for checksum calculation purposes 

387 from fontTools.ttLib import getSearchRange 

388 

389 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( 

390 self.numTables, 16 

391 ) 

392 directory = sstruct.pack(sfntDirectoryFormat, self) 

393 tables = sorted(self.tables.items()) 

394 for tag, entry in tables: 

395 sfntEntry = SFNTDirectoryEntry() 

396 sfntEntry.tag = entry.tag 

397 sfntEntry.checkSum = entry.checkSum 

398 sfntEntry.offset = entry.origOffset 

399 sfntEntry.length = entry.origLength 

400 directory = directory + sfntEntry.toString() 

401 

402 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize 

403 assert directory_end == len(directory) 

404 

405 checksums.append(calcChecksum(directory)) 

406 checksum = sum(checksums) & 0xFFFFFFFF 

407 # BiboAfba! 

408 checksumadjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF 

409 return checksumadjustment 

410 

411 def writeMasterChecksum(self, directory): 

412 checksumadjustment = self._calcMasterChecksum(directory) 

413 # write the checksum to the file 

414 self.file.seek(self.tables["head"].offset + 8) 

415 self.file.write(struct.pack(">L", checksumadjustment)) 

416 

417 def reordersTables(self): 

418 return False 

419 

420 

421# -- sfnt directory helpers and cruft 

422 

423ttcHeaderFormat = """ 

424 > # big endian 

425 TTCTag: 4s # "ttcf" 

426 Version: L # 0x00010000 or 0x00020000 

427 numFonts: L # number of fonts 

428 # OffsetTable[numFonts]: L # array with offsets from beginning of file 

429 # ulDsigTag: L # version 2.0 only 

430 # ulDsigLength: L # version 2.0 only 

431 # ulDsigOffset: L # version 2.0 only 

432""" 

433 

434ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat) 

435 

436sfntDirectoryFormat = """ 

437 > # big endian 

438 sfntVersion: 4s 

439 numTables: H # number of tables 

440 searchRange: H # (max2 <= numTables)*16 

441 entrySelector: H # log2(max2 <= numTables) 

442 rangeShift: H # numTables*16-searchRange 

443""" 

444 

445sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat) 

446 

447sfntDirectoryEntryFormat = """ 

448 > # big endian 

449 tag: 4s 

450 checkSum: L 

451 offset: L 

452 length: L 

453""" 

454 

455sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat) 

456 

457woffDirectoryFormat = """ 

458 > # big endian 

459 signature: 4s # "wOFF" 

460 sfntVersion: 4s 

461 length: L # total woff file size 

462 numTables: H # number of tables 

463 reserved: H # set to 0 

464 totalSfntSize: L # uncompressed size 

465 majorVersion: H # major version of WOFF file 

466 minorVersion: H # minor version of WOFF file 

467 metaOffset: L # offset to metadata block 

468 metaLength: L # length of compressed metadata 

469 metaOrigLength: L # length of uncompressed metadata 

470 privOffset: L # offset to private data block 

471 privLength: L # length of private data block 

472""" 

473 

474woffDirectorySize = sstruct.calcsize(woffDirectoryFormat) 

475 

476woffDirectoryEntryFormat = """ 

477 > # big endian 

478 tag: 4s 

479 offset: L 

480 length: L # compressed length 

481 origLength: L # original length 

482 checkSum: L # original checksum 

483""" 

484 

485woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat) 

486 

487 

488class DirectoryEntry(object): 

489 def __init__(self): 

490 self.uncompressed = False # if True, always embed entry raw 

491 

492 def fromFile(self, file): 

493 sstruct.unpack(self.format, file.read(self.formatSize), self) 

494 

495 def fromString(self, str): 

496 sstruct.unpack(self.format, str, self) 

497 

498 def toString(self): 

499 return sstruct.pack(self.format, self) 

500 

501 def __repr__(self): 

502 if hasattr(self, "tag"): 

503 return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self)) 

504 else: 

505 return "<%s at %x>" % (self.__class__.__name__, id(self)) 

506 

507 def loadData(self, file): 

508 file.seek(self.offset) 

509 data = file.read(self.length) 

510 assert len(data) == self.length 

511 if hasattr(self.__class__, "decodeData"): 

512 data = self.decodeData(data) 

513 return data 

514 

515 def saveData(self, file, data): 

516 if hasattr(self.__class__, "encodeData"): 

517 data = self.encodeData(data) 

518 self.length = len(data) 

519 file.seek(self.offset) 

520 file.write(data) 

521 

522 def decodeData(self, rawData): 

523 return rawData 

524 

525 def encodeData(self, data): 

526 return data 

527 

528 

529class SFNTDirectoryEntry(DirectoryEntry): 

530 format = sfntDirectoryEntryFormat 

531 formatSize = sfntDirectoryEntrySize 

532 

533 

534class WOFFDirectoryEntry(DirectoryEntry): 

535 format = woffDirectoryEntryFormat 

536 formatSize = woffDirectoryEntrySize 

537 

538 def __init__(self): 

539 super(WOFFDirectoryEntry, self).__init__() 

540 # With fonttools<=3.1.2, the only way to set a different zlib 

541 # compression level for WOFF directory entries was to set the class 

542 # attribute 'zlibCompressionLevel'. This is now replaced by a globally 

543 # defined `ZLIB_COMPRESSION_LEVEL`, which is also applied when 

544 # compressing the metadata. For backward compatibility, we still 

545 # use the class attribute if it was already set. 

546 if not hasattr(WOFFDirectoryEntry, "zlibCompressionLevel"): 

547 self.zlibCompressionLevel = ZLIB_COMPRESSION_LEVEL 

548 

549 def decodeData(self, rawData): 

550 import zlib 

551 

552 if self.length == self.origLength: 

553 data = rawData 

554 else: 

555 assert self.length < self.origLength 

556 data = zlib.decompress(rawData) 

557 assert len(data) == self.origLength 

558 return data 

559 

560 def encodeData(self, data): 

561 self.origLength = len(data) 

562 if not self.uncompressed: 

563 compressedData = compress(data, self.zlibCompressionLevel) 

564 if self.uncompressed or len(compressedData) >= self.origLength: 

565 # Encode uncompressed 

566 rawData = data 

567 self.length = self.origLength 

568 else: 

569 rawData = compressedData 

570 self.length = len(rawData) 

571 return rawData 

572 

573 

574class WOFFFlavorData: 

575 Flavor = "woff" 

576 

577 def __init__(self, reader=None): 

578 self.majorVersion = None 

579 self.minorVersion = None 

580 self.metaData = None 

581 self.privData = None 

582 if reader: 

583 self.majorVersion = reader.majorVersion 

584 self.minorVersion = reader.minorVersion 

585 if reader.metaLength: 

586 reader.file.seek(reader.metaOffset) 

587 rawData = reader.file.read(reader.metaLength) 

588 assert len(rawData) == reader.metaLength 

589 data = self._decompress(rawData) 

590 assert len(data) == reader.metaOrigLength 

591 self.metaData = data 

592 if reader.privLength: 

593 reader.file.seek(reader.privOffset) 

594 data = reader.file.read(reader.privLength) 

595 assert len(data) == reader.privLength 

596 self.privData = data 

597 

598 def _decompress(self, rawData): 

599 import zlib 

600 

601 return zlib.decompress(rawData) 

602 

603 

604def calcChecksum(data): 

605 """Calculate the checksum for an arbitrary block of data. 

606 

607 If the data length is not a multiple of four, it assumes 

608 it is to be padded with null byte. 

609 

610 >>> print(calcChecksum(b"abcd")) 

611 1633837924 

612 >>> print(calcChecksum(b"abcdxyz")) 

613 3655064932 

614 """ 

615 remainder = len(data) % 4 

616 if remainder: 

617 data += b"\0" * (4 - remainder) 

618 value = 0 

619 blockSize = 4096 

620 assert blockSize % 4 == 0 

621 for i in range(0, len(data), blockSize): 

622 block = data[i : i + blockSize] 

623 longs = struct.unpack(">%dL" % (len(block) // 4), block) 

624 value = (value + sum(longs)) & 0xFFFFFFFF 

625 return value 

626 

627 

628def readTTCHeader(file): 

629 file.seek(0) 

630 data = file.read(ttcHeaderSize) 

631 if len(data) != ttcHeaderSize: 

632 raise TTLibError("Not a Font Collection (not enough data)") 

633 self = SimpleNamespace() 

634 sstruct.unpack(ttcHeaderFormat, data, self) 

635 if self.TTCTag != "ttcf": 

636 raise TTLibError("Not a Font Collection") 

637 assert self.Version == 0x00010000 or self.Version == 0x00020000, ( 

638 "unrecognized TTC version 0x%08x" % self.Version 

639 ) 

640 self.offsetTable = struct.unpack( 

641 ">%dL" % self.numFonts, file.read(self.numFonts * 4) 

642 ) 

643 if self.Version == 0x00020000: 

644 pass # ignoring version 2.0 signatures 

645 return self 

646 

647 

648def writeTTCHeader(file, numFonts): 

649 self = SimpleNamespace() 

650 self.TTCTag = "ttcf" 

651 self.Version = 0x00010000 

652 self.numFonts = numFonts 

653 file.seek(0) 

654 file.write(sstruct.pack(ttcHeaderFormat, self)) 

655 offset = file.tell() 

656 file.write(struct.pack(">%dL" % self.numFonts, *([0] * self.numFonts))) 

657 return offset 

658 

659 

660if __name__ == "__main__": 

661 import sys 

662 import doctest 

663 

664 sys.exit(doctest.testmod().failed)