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