Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/fontTools/ttLib/sfnt.py: 40%
378 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:33 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:33 +0000
1"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format.
3Defines two public classes:
4 SFNTReader
5 SFNTWriter
7(Normally you don't have to use these classes explicitly; they are
8used automatically by ttLib.TTFont.)
10The reading and writing of sfnt files is separated in two distinct
11classes, since whenever the number of tables changes or whenever
12a table's length changes you need to rewrite the whole file anyway.
13"""
15from io import BytesIO
16from types import SimpleNamespace
17from fontTools.misc.textTools import Tag
18from fontTools.misc import sstruct
19from fontTools.ttLib import TTLibError, TTLibFileIsCollectionError
20import struct
21from collections import OrderedDict
22import logging
25log = logging.getLogger(__name__)
28class SFNTReader(object):
29 def __new__(cls, *args, **kwargs):
30 """Return an instance of the SFNTReader sub-class which is compatible
31 with the input file type.
32 """
33 if args and cls is SFNTReader:
34 infile = args[0]
35 infile.seek(0)
36 sfntVersion = Tag(infile.read(4))
37 infile.seek(0)
38 if sfntVersion == "wOF2":
39 # return new WOFF2Reader object
40 from fontTools.ttLib.woff2 import WOFF2Reader
42 return object.__new__(WOFF2Reader)
43 # return default object
44 return object.__new__(cls)
46 def __init__(self, file, checkChecksums=0, fontNumber=-1):
47 self.file = file
48 self.checkChecksums = checkChecksums
50 self.flavor = None
51 self.flavorData = None
52 self.DirectoryEntry = SFNTDirectoryEntry
53 self.file.seek(0)
54 self.sfntVersion = self.file.read(4)
55 self.file.seek(0)
56 if self.sfntVersion == b"ttcf":
57 header = readTTCHeader(self.file)
58 numFonts = header.numFonts
59 if not 0 <= fontNumber < numFonts:
60 raise TTLibFileIsCollectionError(
61 "specify a font number between 0 and %d (inclusive)"
62 % (numFonts - 1)
63 )
64 self.numFonts = numFonts
65 self.file.seek(header.offsetTable[fontNumber])
66 data = self.file.read(sfntDirectorySize)
67 if len(data) != sfntDirectorySize:
68 raise TTLibError("Not a Font Collection (not enough data)")
69 sstruct.unpack(sfntDirectoryFormat, data, self)
70 elif self.sfntVersion == b"wOFF":
71 self.flavor = "woff"
72 self.DirectoryEntry = WOFFDirectoryEntry
73 data = self.file.read(woffDirectorySize)
74 if len(data) != woffDirectorySize:
75 raise TTLibError("Not a WOFF font (not enough data)")
76 sstruct.unpack(woffDirectoryFormat, data, self)
77 else:
78 data = self.file.read(sfntDirectorySize)
79 if len(data) != sfntDirectorySize:
80 raise TTLibError("Not a TrueType or OpenType font (not enough data)")
81 sstruct.unpack(sfntDirectoryFormat, data, self)
82 self.sfntVersion = Tag(self.sfntVersion)
84 if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"):
85 raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
86 tables = {}
87 for i in range(self.numTables):
88 entry = self.DirectoryEntry()
89 entry.fromFile(self.file)
90 tag = Tag(entry.tag)
91 tables[tag] = entry
92 self.tables = OrderedDict(sorted(tables.items(), key=lambda i: i[1].offset))
94 # Load flavor data if any
95 if self.flavor == "woff":
96 self.flavorData = WOFFFlavorData(self)
98 def has_key(self, tag):
99 return tag in self.tables
101 __contains__ = has_key
103 def keys(self):
104 return self.tables.keys()
106 def __getitem__(self, tag):
107 """Fetch the raw table data."""
108 entry = self.tables[Tag(tag)]
109 data = entry.loadData(self.file)
110 if self.checkChecksums:
111 if tag == "head":
112 # Beh: we have to special-case the 'head' table.
113 checksum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:])
114 else:
115 checksum = calcChecksum(data)
116 if self.checkChecksums > 1:
117 # Be obnoxious, and barf when it's wrong
118 assert checksum == entry.checkSum, "bad checksum for '%s' table" % tag
119 elif checksum != entry.checkSum:
120 # Be friendly, and just log a warning.
121 log.warning("bad checksum for '%s' table", tag)
122 return data
124 def __delitem__(self, tag):
125 del self.tables[Tag(tag)]
127 def close(self):
128 self.file.close()
130 # We define custom __getstate__ and __setstate__ to make SFNTReader pickle-able
131 # and deepcopy-able. When a TTFont is loaded as lazy=True, SFNTReader holds a
132 # reference to an external file object which is not pickleable. So in __getstate__
133 # we store the file name and current position, and in __setstate__ we reopen the
134 # same named file after unpickling.
136 def __getstate__(self):
137 if isinstance(self.file, BytesIO):
138 # BytesIO is already pickleable, return the state unmodified
139 return self.__dict__
141 # remove unpickleable file attribute, and only store its name and pos
142 state = self.__dict__.copy()
143 del state["file"]
144 state["_filename"] = self.file.name
145 state["_filepos"] = self.file.tell()
146 return state
148 def __setstate__(self, state):
149 if "file" not in state:
150 self.file = open(state.pop("_filename"), "rb")
151 self.file.seek(state.pop("_filepos"))
152 self.__dict__.update(state)
155# default compression level for WOFF 1.0 tables and metadata
156ZLIB_COMPRESSION_LEVEL = 6
158# if set to True, use zopfli instead of zlib for compressing WOFF 1.0.
159# The Python bindings are available at https://pypi.python.org/pypi/zopfli
160USE_ZOPFLI = False
162# mapping between zlib's compression levels and zopfli's 'numiterations'.
163# Use lower values for files over several MB in size or it will be too slow
164ZOPFLI_LEVELS = {
165 # 0: 0, # can't do 0 iterations...
166 1: 1,
167 2: 3,
168 3: 5,
169 4: 8,
170 5: 10,
171 6: 15,
172 7: 25,
173 8: 50,
174 9: 100,
175}
178def compress(data, level=ZLIB_COMPRESSION_LEVEL):
179 """Compress 'data' to Zlib format. If 'USE_ZOPFLI' variable is True,
180 zopfli is used instead of the zlib module.
181 The compression 'level' must be between 0 and 9. 1 gives best speed,
182 9 gives best compression (0 gives no compression at all).
183 The default value is a compromise between speed and compression (6).
184 """
185 if not (0 <= level <= 9):
186 raise ValueError("Bad compression level: %s" % level)
187 if not USE_ZOPFLI or level == 0:
188 from zlib import compress
190 return compress(data, level)
191 else:
192 from zopfli.zlib import compress
194 return compress(data, numiterations=ZOPFLI_LEVELS[level])
197class SFNTWriter(object):
198 def __new__(cls, *args, **kwargs):
199 """Return an instance of the SFNTWriter sub-class which is compatible
200 with the specified 'flavor'.
201 """
202 flavor = None
203 if kwargs and "flavor" in kwargs:
204 flavor = kwargs["flavor"]
205 elif args and len(args) > 3:
206 flavor = args[3]
207 if cls is SFNTWriter:
208 if flavor == "woff2":
209 # return new WOFF2Writer object
210 from fontTools.ttLib.woff2 import WOFF2Writer
212 return object.__new__(WOFF2Writer)
213 # return default object
214 return object.__new__(cls)
216 def __init__(
217 self,
218 file,
219 numTables,
220 sfntVersion="\000\001\000\000",
221 flavor=None,
222 flavorData=None,
223 ):
224 self.file = file
225 self.numTables = numTables
226 self.sfntVersion = Tag(sfntVersion)
227 self.flavor = flavor
228 self.flavorData = flavorData
230 if self.flavor == "woff":
231 self.directoryFormat = woffDirectoryFormat
232 self.directorySize = woffDirectorySize
233 self.DirectoryEntry = WOFFDirectoryEntry
235 self.signature = "wOFF"
237 # to calculate WOFF checksum adjustment, we also need the original SFNT offsets
238 self.origNextTableOffset = (
239 sfntDirectorySize + numTables * sfntDirectoryEntrySize
240 )
241 else:
242 assert not self.flavor, "Unknown flavor '%s'" % self.flavor
243 self.directoryFormat = sfntDirectoryFormat
244 self.directorySize = sfntDirectorySize
245 self.DirectoryEntry = SFNTDirectoryEntry
247 from fontTools.ttLib import getSearchRange
249 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(
250 numTables, 16
251 )
253 self.directoryOffset = self.file.tell()
254 self.nextTableOffset = (
255 self.directoryOffset
256 + self.directorySize
257 + numTables * self.DirectoryEntry.formatSize
258 )
259 # clear out directory area
260 self.file.seek(self.nextTableOffset)
261 # make sure we're actually where we want to be. (old cStringIO bug)
262 self.file.write(b"\0" * (self.nextTableOffset - self.file.tell()))
263 self.tables = OrderedDict()
265 def setEntry(self, tag, entry):
266 if tag in self.tables:
267 raise TTLibError("cannot rewrite '%s' table" % tag)
269 self.tables[tag] = entry
271 def __setitem__(self, tag, data):
272 """Write raw table data to disk."""
273 if tag in self.tables:
274 raise TTLibError("cannot rewrite '%s' table" % tag)
276 entry = self.DirectoryEntry()
277 entry.tag = tag
278 entry.offset = self.nextTableOffset
279 if tag == "head":
280 entry.checkSum = calcChecksum(data[:8] + b"\0\0\0\0" + data[12:])
281 self.headTable = data
282 entry.uncompressed = True
283 else:
284 entry.checkSum = calcChecksum(data)
285 entry.saveData(self.file, data)
287 if self.flavor == "woff":
288 entry.origOffset = self.origNextTableOffset
289 self.origNextTableOffset += (entry.origLength + 3) & ~3
291 self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3)
292 # Add NUL bytes to pad the table data to a 4-byte boundary.
293 # Don't depend on f.seek() as we need to add the padding even if no
294 # subsequent write follows (seek is lazy), ie. after the final table
295 # in the font.
296 self.file.write(b"\0" * (self.nextTableOffset - self.file.tell()))
297 assert self.nextTableOffset == self.file.tell()
299 self.setEntry(tag, entry)
301 def __getitem__(self, tag):
302 return self.tables[tag]
304 def close(self):
305 """All tables must have been written to disk. Now write the
306 directory.
307 """
308 tables = sorted(self.tables.items())
309 if len(tables) != self.numTables:
310 raise TTLibError(
311 "wrong number of tables; expected %d, found %d"
312 % (self.numTables, len(tables))
313 )
315 if self.flavor == "woff":
316 self.signature = b"wOFF"
317 self.reserved = 0
319 self.totalSfntSize = 12
320 self.totalSfntSize += 16 * len(tables)
321 for tag, entry in tables:
322 self.totalSfntSize += (entry.origLength + 3) & ~3
324 data = self.flavorData if self.flavorData else WOFFFlavorData()
325 if data.majorVersion is not None and data.minorVersion is not None:
326 self.majorVersion = data.majorVersion
327 self.minorVersion = data.minorVersion
328 else:
329 if hasattr(self, "headTable"):
330 self.majorVersion, self.minorVersion = struct.unpack(
331 ">HH", self.headTable[4:8]
332 )
333 else:
334 self.majorVersion = self.minorVersion = 0
335 if data.metaData:
336 self.metaOrigLength = len(data.metaData)
337 self.file.seek(0, 2)
338 self.metaOffset = self.file.tell()
339 compressedMetaData = compress(data.metaData)
340 self.metaLength = len(compressedMetaData)
341 self.file.write(compressedMetaData)
342 else:
343 self.metaOffset = self.metaLength = self.metaOrigLength = 0
344 if data.privData:
345 self.file.seek(0, 2)
346 off = self.file.tell()
347 paddedOff = (off + 3) & ~3
348 self.file.write(b"\0" * (paddedOff - off))
349 self.privOffset = self.file.tell()
350 self.privLength = len(data.privData)
351 self.file.write(data.privData)
352 else:
353 self.privOffset = self.privLength = 0
355 self.file.seek(0, 2)
356 self.length = self.file.tell()
358 else:
359 assert not self.flavor, "Unknown flavor '%s'" % self.flavor
360 pass
362 directory = sstruct.pack(self.directoryFormat, self)
364 self.file.seek(self.directoryOffset + self.directorySize)
365 seenHead = 0
366 for tag, entry in tables:
367 if tag == "head":
368 seenHead = 1
369 directory = directory + entry.toString()
370 if seenHead:
371 self.writeMasterChecksum(directory)
372 self.file.seek(self.directoryOffset)
373 self.file.write(directory)
375 def _calcMasterChecksum(self, directory):
376 # calculate checkSumAdjustment
377 tags = list(self.tables.keys())
378 checksums = []
379 for i in range(len(tags)):
380 checksums.append(self.tables[tags[i]].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):
528 format = sfntDirectoryEntryFormat
529 formatSize = sfntDirectoryEntrySize
532class WOFFDirectoryEntry(DirectoryEntry):
534 format = woffDirectoryEntryFormat
535 formatSize = woffDirectoryEntrySize
537 def __init__(self):
538 super(WOFFDirectoryEntry, self).__init__()
539 # With fonttools<=3.1.2, the only way to set a different zlib
540 # compression level for WOFF directory entries was to set the class
541 # attribute 'zlibCompressionLevel'. This is now replaced by a globally
542 # defined `ZLIB_COMPRESSION_LEVEL`, which is also applied when
543 # compressing the metadata. For backward compatibility, we still
544 # use the class attribute if it was already set.
545 if not hasattr(WOFFDirectoryEntry, "zlibCompressionLevel"):
546 self.zlibCompressionLevel = ZLIB_COMPRESSION_LEVEL
548 def decodeData(self, rawData):
549 import zlib
551 if self.length == self.origLength:
552 data = rawData
553 else:
554 assert self.length < self.origLength
555 data = zlib.decompress(rawData)
556 assert len(data) == self.origLength
557 return data
559 def encodeData(self, data):
560 self.origLength = len(data)
561 if not self.uncompressed:
562 compressedData = compress(data, self.zlibCompressionLevel)
563 if self.uncompressed or len(compressedData) >= self.origLength:
564 # Encode uncompressed
565 rawData = data
566 self.length = self.origLength
567 else:
568 rawData = compressedData
569 self.length = len(rawData)
570 return rawData
573class 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)