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
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 __future__ import annotations
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
29log = logging.getLogger(__name__)
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
46 return object.__new__(WOFF2Reader)
47 # return default object
48 return object.__new__(cls)
50 def __init__(self, file, checkChecksums=0, fontNumber=-1):
51 self.file = file
52 self.checkChecksums = checkChecksums
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)
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))
98 # Load flavor data if any
99 if self.flavor == "woff":
100 self.flavorData = WOFFFlavorData(self)
102 def has_key(self, tag: str | bytes) -> bool:
103 return tag in self.tables
105 __contains__ = has_key
107 def keys(self) -> KeysView[Tag]:
108 return self.tables.keys()
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
128 def __delitem__(self, tag: str | bytes) -> None:
129 del self.tables[Tag(tag)]
131 def close(self) -> None:
132 self.file.close()
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.
140 def __getstate__(self):
141 if isinstance(self.file, BytesIO):
142 # BytesIO is already pickleable, return the state unmodified
143 return self.__dict__
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
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)
159# default compression level for WOFF 1.0 tables and metadata
160ZLIB_COMPRESSION_LEVEL = 6
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
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}
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
194 return compress(data, level)
195 else:
196 from zopfli.zlib import compress
198 return compress(data, numiterations=ZOPFLI_LEVELS[level])
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
216 return object.__new__(WOFF2Writer)
217 # return default object
218 return object.__new__(cls)
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
234 if self.flavor == "woff":
235 self.directoryFormat = woffDirectoryFormat
236 self.directorySize = woffDirectorySize
237 self.DirectoryEntry = WOFFDirectoryEntry
239 self.signature = "wOFF"
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
251 from fontTools.ttLib import getSearchRange
253 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(
254 numTables, 16
255 )
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()
269 def setEntry(self, tag, entry):
270 if tag in self.tables:
271 raise TTLibError("cannot rewrite '%s' table" % tag)
273 self.tables[tag] = entry
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)
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)
291 if self.flavor == "woff":
292 entry.origOffset = self.origNextTableOffset
293 self.origNextTableOffset += (entry.origLength + 3) & ~3
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()
303 self.setEntry(tag, entry)
305 def __getitem__(self, tag):
306 return self.tables[tag]
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 )
319 if self.flavor == "woff":
320 self.signature = b"wOFF"
321 self.reserved = 0
323 self.totalSfntSize = 12
324 self.totalSfntSize += 16 * len(tables)
325 for tag, entry in tables:
326 self.totalSfntSize += (entry.origLength + 3) & ~3
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
359 self.file.seek(0, 2)
360 self.length = self.file.tell()
362 else:
363 assert not self.flavor, "Unknown flavor '%s'" % self.flavor
364 pass
366 directory = sstruct.pack(self.directoryFormat, self)
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)
379 def _calcMasterChecksum(self, directory):
380 # calculate checkSumAdjustment
381 checksums = []
382 for tag in self.tables.keys():
383 checksums.append(self.tables[tag].checkSum)
385 if self.DirectoryEntry != SFNTDirectoryEntry:
386 # Create a SFNT directory for checksum calculation purposes
387 from fontTools.ttLib import getSearchRange
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()
402 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
403 assert directory_end == len(directory)
405 checksums.append(calcChecksum(directory))
406 checksum = sum(checksums) & 0xFFFFFFFF
407 # BiboAfba!
408 checksumadjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF
409 return checksumadjustment
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))
417 def reordersTables(self):
418 return False
421# -- sfnt directory helpers and cruft
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"""
434ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat)
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"""
445sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
447sfntDirectoryEntryFormat = """
448 > # big endian
449 tag: 4s
450 checkSum: L
451 offset: L
452 length: L
453"""
455sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
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"""
474woffDirectorySize = sstruct.calcsize(woffDirectoryFormat)
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"""
485woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat)
488class DirectoryEntry(object):
489 def __init__(self):
490 self.uncompressed = False # if True, always embed entry raw
492 def fromFile(self, file):
493 sstruct.unpack(self.format, file.read(self.formatSize), self)
495 def fromString(self, str):
496 sstruct.unpack(self.format, str, self)
498 def toString(self):
499 return sstruct.pack(self.format, self)
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))
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
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)
522 def decodeData(self, rawData):
523 return rawData
525 def encodeData(self, data):
526 return data
529class SFNTDirectoryEntry(DirectoryEntry):
530 format = sfntDirectoryEntryFormat
531 formatSize = sfntDirectoryEntrySize
534class WOFFDirectoryEntry(DirectoryEntry):
535 format = woffDirectoryEntryFormat
536 formatSize = woffDirectoryEntrySize
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
549 def decodeData(self, rawData):
550 import zlib
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
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
574class WOFFFlavorData:
575 Flavor = "woff"
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
598 def _decompress(self, rawData):
599 import zlib
601 return zlib.decompress(rawData)
604def calcChecksum(data):
605 """Calculate the checksum for an arbitrary block of data.
607 If the data length is not a multiple of four, it assumes
608 it is to be padded with null byte.
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
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
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
660if __name__ == "__main__":
661 import sys
662 import doctest
664 sys.exit(doctest.testmod().failed)