sfnt.py 22 KB

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