blob: 7943e1ef5aa9b13d1024187fe14457a09687bdfe [file] [log] [blame]
Just7842e561999-12-16 21:34:53 +00001"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format.
2
3Defines two public classes:
4 SFNTReader
5 SFNTWriter
6
7(Normally you don't have to use these classes explicitly; they are
8used automatically by ttLib.TTFont.)
9
10The reading and writing of sfnt files is separated in two distinct
11classes, since whenever to number of tables changes or whenever
12a table's length chages you need to rewrite the whole file anyway.
13"""
14
jvr9be387c2008-03-01 11:43:01 +000015import sys
Just7842e561999-12-16 21:34:53 +000016import struct, sstruct
Just7842e561999-12-16 21:34:53 +000017import os
18
jvr04b32042002-05-14 12:09:10 +000019
Just7842e561999-12-16 21:34:53 +000020class SFNTReader:
21
pabs37e91e772009-02-22 08:55:00 +000022 def __init__(self, file, checkChecksums=1, fontNumber=-1):
Just7842e561999-12-16 21:34:53 +000023 self.file = file
jvrea9dfa92002-05-12 17:14:50 +000024 self.checkChecksums = checkChecksums
Behdad Esfahbod58d74162013-08-15 15:30:55 -040025
26 self.flavor = None
27 self.flavorData = None
28 self.DirectoryEntry = SFNTDirectoryEntry
29 self.sfntVersion = self.file.read(4)
30 self.file.seek(0)
pabs37e91e772009-02-22 08:55:00 +000031 if self.sfntVersion == "ttcf":
Behdad Esfahbod58d74162013-08-15 15:30:55 -040032 sstruct.unpack(ttcHeaderFormat, self.file.read(ttcHeaderSize), self)
pabs37e91e772009-02-22 08:55:00 +000033 assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version
34 if not 0 <= fontNumber < self.numFonts:
35 from fontTools import ttLib
36 raise ttLib.TTLibError, "specify a font number between 0 and %d (inclusive)" % (self.numFonts - 1)
37 offsetTable = struct.unpack(">%dL" % self.numFonts, self.file.read(self.numFonts * 4))
38 if self.Version == 0x00020000:
39 pass # ignoring version 2.0 signatures
40 self.file.seek(offsetTable[fontNumber])
Behdad Esfahbod58d74162013-08-15 15:30:55 -040041 sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self)
42 elif self.sfntVersion == "wOFF":
43 self.flavor = "woff"
44 self.DirectoryEntry = WOFFDirectoryEntry
45 sstruct.unpack(woffDirectoryFormat, self.file.read(woffDirectorySize), self)
46 else:
47 sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self)
48
Just7842e561999-12-16 21:34:53 +000049 if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
50 from fontTools import ttLib
51 raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
52 self.tables = {}
53 for i in range(self.numTables):
Behdad Esfahbod58d74162013-08-15 15:30:55 -040054 entry = self.DirectoryEntry()
jvrea9dfa92002-05-12 17:14:50 +000055 entry.fromFile(self.file)
jvrce1d50a2002-05-12 17:02:50 +000056 if entry.length > 0:
57 self.tables[entry.tag] = entry
58 else:
59 # Ignore zero-length tables. This doesn't seem to be documented,
60 # yet it's apparently how the Windows TT rasterizer behaves.
61 # Besides, at least one font has been sighted which actually
62 # *has* a zero-length table.
63 pass
Behdad Esfahbod58d74162013-08-15 15:30:55 -040064
65 # Load flavor data if any
66 if self.flavor == "woff":
67 self.flavorData = WOFFFlavorData(self)
68
Just7842e561999-12-16 21:34:53 +000069 def has_key(self, tag):
70 return self.tables.has_key(tag)
71
72 def keys(self):
73 return self.tables.keys()
74
75 def __getitem__(self, tag):
76 """Fetch the raw table data."""
77 entry = self.tables[tag]
Behdad Esfahbod58d74162013-08-15 15:30:55 -040078 data = entry.loadData (self.file)
jvrea9dfa92002-05-12 17:14:50 +000079 if self.checkChecksums:
Just7842e561999-12-16 21:34:53 +000080 if tag == 'head':
81 # Beh: we have to special-case the 'head' table.
jvrea9dfa92002-05-12 17:14:50 +000082 checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +000083 else:
jvrea9dfa92002-05-12 17:14:50 +000084 checksum = calcChecksum(data)
85 if self.checkChecksums > 1:
Just7842e561999-12-16 21:34:53 +000086 # Be obnoxious, and barf when it's wrong
87 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
88 elif checksum <> entry.checkSum:
89 # Be friendly, and just print a warning.
90 print "bad checksum for '%s' table" % tag
91 return data
92
jvrf7074632002-05-04 22:04:02 +000093 def __delitem__(self, tag):
94 del self.tables[tag]
95
Just7842e561999-12-16 21:34:53 +000096 def close(self):
97 self.file.close()
98
99
100class SFNTWriter:
101
102 def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
103 self.file = file
104 self.numTables = numTables
105 self.sfntVersion = sfntVersion
jvrea9dfa92002-05-12 17:14:50 +0000106 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
Just7842e561999-12-16 21:34:53 +0000107 self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
108 # clear out directory area
109 self.file.seek(self.nextTableOffset)
110 # make sure we're actually where we want to be. (XXX old cStringIO bug)
111 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
112 self.tables = {}
113
114 def __setitem__(self, tag, data):
115 """Write raw table data to disk."""
116 if self.tables.has_key(tag):
117 # We've written this table to file before. If the length
jvr04b32042002-05-14 12:09:10 +0000118 # of the data is still the same, we allow overwriting it.
Just7842e561999-12-16 21:34:53 +0000119 entry = self.tables[tag]
120 if len(data) <> entry.length:
121 from fontTools import ttLib
122 raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
123 else:
124 entry = SFNTDirectoryEntry()
125 entry.tag = tag
126 entry.offset = self.nextTableOffset
127 entry.length = len(data)
128 self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
129 self.file.seek(entry.offset)
130 self.file.write(data)
jvrc63ac642008-06-17 20:41:15 +0000131 # Add NUL bytes to pad the table data to a 4-byte boundary.
132 # Don't depend on f.seek() as we need to add the padding even if no
133 # subsequent write follows (seek is lazy), ie. after the final table
134 # in the font.
Just7842e561999-12-16 21:34:53 +0000135 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
jvrc63ac642008-06-17 20:41:15 +0000136 assert self.nextTableOffset == self.file.tell()
Just7842e561999-12-16 21:34:53 +0000137
138 if tag == 'head':
jvrea9dfa92002-05-12 17:14:50 +0000139 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +0000140 else:
jvrea9dfa92002-05-12 17:14:50 +0000141 entry.checkSum = calcChecksum(data)
Just7842e561999-12-16 21:34:53 +0000142 self.tables[tag] = entry
143
jvr28ae1962004-11-16 10:37:59 +0000144 def close(self):
Just7842e561999-12-16 21:34:53 +0000145 """All tables must have been written to disk. Now write the
146 directory.
147 """
148 tables = self.tables.items()
149 tables.sort()
150 if len(tables) <> self.numTables:
151 from fontTools import ttLib
152 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
153
154 directory = sstruct.pack(sfntDirectoryFormat, self)
155
156 self.file.seek(sfntDirectorySize)
jvrf509c0f2003-08-22 19:38:37 +0000157 seenHead = 0
Just7842e561999-12-16 21:34:53 +0000158 for tag, entry in tables:
jvrf509c0f2003-08-22 19:38:37 +0000159 if tag == "head":
160 seenHead = 1
jvrea9dfa92002-05-12 17:14:50 +0000161 directory = directory + entry.toString()
jvrf509c0f2003-08-22 19:38:37 +0000162 if seenHead:
jvr91bca422012-10-18 12:49:22 +0000163 self.writeMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000164 self.file.seek(0)
165 self.file.write(directory)
jvr91bca422012-10-18 12:49:22 +0000166
167 def _calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000168 # calculate checkSumAdjustment
169 tags = self.tables.keys()
jvr91bca422012-10-18 12:49:22 +0000170 checksums = []
Just7842e561999-12-16 21:34:53 +0000171 for i in range(len(tags)):
jvr91bca422012-10-18 12:49:22 +0000172 checksums.append(self.tables[tags[i]].checkSum)
173
Just7842e561999-12-16 21:34:53 +0000174 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
175 assert directory_end == len(directory)
jvr91bca422012-10-18 12:49:22 +0000176
177 checksums.append(calcChecksum(directory))
178 checksum = sum(checksums) & 0xffffffff
Just7842e561999-12-16 21:34:53 +0000179 # BiboAfba!
jvr91bca422012-10-18 12:49:22 +0000180 checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff
181 return checksumadjustment
182
183 def writeMasterChecksum(self, directory):
184 checksumadjustment = self._calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000185 # write the checksum to the file
186 self.file.seek(self.tables['head'].offset + 8)
pabs30e2aece2009-03-24 09:42:15 +0000187 self.file.write(struct.pack(">L", checksumadjustment))
jvr1ebda672008-03-08 20:29:30 +0000188
Just7842e561999-12-16 21:34:53 +0000189
190# -- sfnt directory helpers and cruft
191
pabs37e91e772009-02-22 08:55:00 +0000192ttcHeaderFormat = """
193 > # big endian
194 TTCTag: 4s # "ttcf"
195 Version: L # 0x00010000 or 0x00020000
196 numFonts: L # number of fonts
197 # OffsetTable[numFonts]: L # array with offsets from beginning of file
198 # ulDsigTag: L # version 2.0 only
199 # ulDsigLength: L # version 2.0 only
200 # ulDsigOffset: L # version 2.0 only
201"""
202
203ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat)
204
Just7842e561999-12-16 21:34:53 +0000205sfntDirectoryFormat = """
206 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000207 sfntVersion: 4s
208 numTables: H # number of tables
209 searchRange: H # (max2 <= numTables)*16
210 entrySelector: H # log2(max2 <= numTables)
211 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000212"""
213
214sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
215
216sfntDirectoryEntryFormat = """
217 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000218 tag: 4s
pabs30e2aece2009-03-24 09:42:15 +0000219 checkSum: L
220 offset: L
221 length: L
Just7842e561999-12-16 21:34:53 +0000222"""
223
224sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
225
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400226woffDirectoryFormat = """
227 > # big endian
228 signature: 4s # "wOFF"
229 sfntVersion: 4s
230 length: L # total woff file size
231 numTables: H # number of tables
232 reserved: H # set to 0
233 totalSfntSize: L # uncompressed size
234 majorVersion: H # major version of WOFF file
235 minorVersion: H # minor version of WOFF file
236 metaOffset: L # offset to metadata block
237 metaLength: L # length of compressed metadata
238 metaOrigLength: L # length of uncompressed metadata
239 privOffset: L # offset to private data block
240 privLength: L # length of private data block
241"""
242
243woffDirectorySize = sstruct.calcsize(woffDirectoryFormat)
244
245woffDirectoryEntryFormat = """
246 > # big endian
247 tag: 4s
248 offset: L
249 length: L # compressed length
250 origLength: L # original length
251 checksum: L # original checksum
252"""
253
254woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat)
255
256
257class DirectoryEntry:
Just7842e561999-12-16 21:34:53 +0000258
jvrea9dfa92002-05-12 17:14:50 +0000259 def fromFile(self, file):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400260 sstruct.unpack(self.format, file.read(self.formatSize), self)
Just7842e561999-12-16 21:34:53 +0000261
jvrea9dfa92002-05-12 17:14:50 +0000262 def fromString(self, str):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400263 sstruct.unpack(self.format, str, self)
Just7842e561999-12-16 21:34:53 +0000264
jvrea9dfa92002-05-12 17:14:50 +0000265 def toString(self):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400266 return sstruct.pack(self.format, self)
Just7842e561999-12-16 21:34:53 +0000267
268 def __repr__(self):
269 if hasattr(self, "tag"):
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400270 return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self))
Just7842e561999-12-16 21:34:53 +0000271 else:
Behdad Esfahbod58d74162013-08-15 15:30:55 -0400272 return "<%s at %x>" % (self.__class__.__name__, id(self))
273
274 def loadData(self, file):
275 file.seek(self.offset)
276 data = file.read(self.length)
277 assert len(data) == self.length
278 return self.decodeData (data)
279
280 def decodeData(self, rawData):
281 return rawData
282
283class SFNTDirectoryEntry(DirectoryEntry):
284
285 format = sfntDirectoryEntryFormat
286 formatSize = sfntDirectoryEntrySize
287
288class WOFFDirectoryEntry(DirectoryEntry):
289
290 format = woffDirectoryEntryFormat
291 formatSize = woffDirectoryEntrySize
292
293 def decodeData(self, rawData):
294 import zlib
295 if self.length == self.origLength:
296 data = rawData
297 else:
298 assert self.length < self.origLength
299 data = zlib.decompress(rawData)
300 assert len (data) == self.origLength
301 return data
302
303class WOFFFlavorData():
304
305 def __init__(self, reader=None):
306 self.majorVersion = None
307 self.minorVersion = None
308 self.metaData = None
309 self.privData = None
310 if reader:
311 self.majorVersion = reader.majorVersion
312 self.minorVersion = reader.minorVersion
313 if reader.metaLength:
314 reader.file.seek(reader.metaOffset)
315 rawData = read.file.read(reader.metaLength)
316 assert len(rawData) == reader.metaLength
317 data = zlib.decompress(rawData)
318 assert len(data) == reader.metaOrigLength
319 self.metaData = data
320 if reader.privLength:
321 reader.file.seek(reader.privOffset)
322 data = read.file.read(reader.privLength)
323 assert len(data) == reader.privLength
324 self.privData = data
Just7842e561999-12-16 21:34:53 +0000325
326
jvr91bca422012-10-18 12:49:22 +0000327def calcChecksum(data):
Just7842e561999-12-16 21:34:53 +0000328 """Calculate the checksum for an arbitrary block of data.
329 Optionally takes a 'start' argument, which allows you to
330 calculate a checksum in chunks by feeding it a previous
331 result.
332
333 If the data length is not a multiple of four, it assumes
334 it is to be padded with null byte.
jvr91bca422012-10-18 12:49:22 +0000335
336 >>> print calcChecksum("abcd")
337 1633837924
338 >>> print calcChecksum("abcdxyz")
339 3655064932
Just7842e561999-12-16 21:34:53 +0000340 """
Just7842e561999-12-16 21:34:53 +0000341 remainder = len(data) % 4
342 if remainder:
jvr91bca422012-10-18 12:49:22 +0000343 data += "\0" * (4 - remainder)
344 value = 0
345 blockSize = 4096
346 assert blockSize % 4 == 0
347 for i in xrange(0, len(data), blockSize):
348 block = data[i:i+blockSize]
349 longs = struct.unpack(">%dL" % (len(block) // 4), block)
350 value = (value + sum(longs)) & 0xffffffff
351 return value
Just7842e561999-12-16 21:34:53 +0000352
353
jvrea9dfa92002-05-12 17:14:50 +0000354def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000355 """Return the highest exponent of two, so that
356 (2 ** exponent) <= x
357 """
358 exponent = 0
359 while x:
360 x = x >> 1
361 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000362 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000363
364
jvrea9dfa92002-05-12 17:14:50 +0000365def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000366 """Calculate searchRange, entrySelector, rangeShift for the
367 sfnt directory. 'n' is the number of tables.
368 """
369 # This stuff needs to be stored in the file, because?
370 import math
jvrea9dfa92002-05-12 17:14:50 +0000371 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000372 searchRange = (2 ** exponent) * 16
373 entrySelector = exponent
374 rangeShift = n * 16 - searchRange
375 return searchRange, entrySelector, rangeShift
376
jvr91bca422012-10-18 12:49:22 +0000377
378if __name__ == "__main__":
379 import doctest
380 doctest.testmod()