blob: 7ba40f4e8f62fc0363dd02c42bad464b217be057 [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
Just7842e561999-12-16 21:34:53 +000025 data = self.file.read(sfntDirectorySize)
26 if len(data) <> sfntDirectorySize:
27 from fontTools import ttLib
28 raise ttLib.TTLibError, "Not a TrueType or OpenType font (not enough data)"
29 sstruct.unpack(sfntDirectoryFormat, data, self)
pabs37e91e772009-02-22 08:55:00 +000030 if self.sfntVersion == "ttcf":
31 assert ttcHeaderSize == sfntDirectorySize
32 sstruct.unpack(ttcHeaderFormat, data, self)
33 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])
41 data = self.file.read(sfntDirectorySize)
42 sstruct.unpack(sfntDirectoryFormat, data, self)
Just7842e561999-12-16 21:34:53 +000043 if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
44 from fontTools import ttLib
45 raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
46 self.tables = {}
47 for i in range(self.numTables):
48 entry = SFNTDirectoryEntry()
jvrea9dfa92002-05-12 17:14:50 +000049 entry.fromFile(self.file)
jvrce1d50a2002-05-12 17:02:50 +000050 if entry.length > 0:
51 self.tables[entry.tag] = entry
52 else:
53 # Ignore zero-length tables. This doesn't seem to be documented,
54 # yet it's apparently how the Windows TT rasterizer behaves.
55 # Besides, at least one font has been sighted which actually
56 # *has* a zero-length table.
57 pass
Just7842e561999-12-16 21:34:53 +000058
59 def has_key(self, tag):
60 return self.tables.has_key(tag)
61
62 def keys(self):
63 return self.tables.keys()
64
65 def __getitem__(self, tag):
66 """Fetch the raw table data."""
67 entry = self.tables[tag]
68 self.file.seek(entry.offset)
69 data = self.file.read(entry.length)
jvrea9dfa92002-05-12 17:14:50 +000070 if self.checkChecksums:
Just7842e561999-12-16 21:34:53 +000071 if tag == 'head':
72 # Beh: we have to special-case the 'head' table.
jvrea9dfa92002-05-12 17:14:50 +000073 checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +000074 else:
jvrea9dfa92002-05-12 17:14:50 +000075 checksum = calcChecksum(data)
76 if self.checkChecksums > 1:
Just7842e561999-12-16 21:34:53 +000077 # Be obnoxious, and barf when it's wrong
78 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
79 elif checksum <> entry.checkSum:
80 # Be friendly, and just print a warning.
81 print "bad checksum for '%s' table" % tag
82 return data
83
jvrf7074632002-05-04 22:04:02 +000084 def __delitem__(self, tag):
85 del self.tables[tag]
86
Just7842e561999-12-16 21:34:53 +000087 def close(self):
88 self.file.close()
89
90
91class SFNTWriter:
92
93 def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
94 self.file = file
95 self.numTables = numTables
96 self.sfntVersion = sfntVersion
jvrea9dfa92002-05-12 17:14:50 +000097 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
Just7842e561999-12-16 21:34:53 +000098 self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
99 # clear out directory area
100 self.file.seek(self.nextTableOffset)
101 # make sure we're actually where we want to be. (XXX old cStringIO bug)
102 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
103 self.tables = {}
104
105 def __setitem__(self, tag, data):
106 """Write raw table data to disk."""
107 if self.tables.has_key(tag):
108 # We've written this table to file before. If the length
jvr04b32042002-05-14 12:09:10 +0000109 # of the data is still the same, we allow overwriting it.
Just7842e561999-12-16 21:34:53 +0000110 entry = self.tables[tag]
111 if len(data) <> entry.length:
112 from fontTools import ttLib
113 raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
114 else:
115 entry = SFNTDirectoryEntry()
116 entry.tag = tag
117 entry.offset = self.nextTableOffset
118 entry.length = len(data)
119 self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
120 self.file.seek(entry.offset)
121 self.file.write(data)
jvrc63ac642008-06-17 20:41:15 +0000122 # Add NUL bytes to pad the table data to a 4-byte boundary.
123 # Don't depend on f.seek() as we need to add the padding even if no
124 # subsequent write follows (seek is lazy), ie. after the final table
125 # in the font.
Just7842e561999-12-16 21:34:53 +0000126 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
jvrc63ac642008-06-17 20:41:15 +0000127 assert self.nextTableOffset == self.file.tell()
Just7842e561999-12-16 21:34:53 +0000128
129 if tag == 'head':
jvrea9dfa92002-05-12 17:14:50 +0000130 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +0000131 else:
jvrea9dfa92002-05-12 17:14:50 +0000132 entry.checkSum = calcChecksum(data)
Just7842e561999-12-16 21:34:53 +0000133 self.tables[tag] = entry
134
jvr28ae1962004-11-16 10:37:59 +0000135 def close(self):
Just7842e561999-12-16 21:34:53 +0000136 """All tables must have been written to disk. Now write the
137 directory.
138 """
139 tables = self.tables.items()
140 tables.sort()
141 if len(tables) <> self.numTables:
142 from fontTools import ttLib
143 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
144
145 directory = sstruct.pack(sfntDirectoryFormat, self)
146
147 self.file.seek(sfntDirectorySize)
jvrf509c0f2003-08-22 19:38:37 +0000148 seenHead = 0
Just7842e561999-12-16 21:34:53 +0000149 for tag, entry in tables:
jvrf509c0f2003-08-22 19:38:37 +0000150 if tag == "head":
151 seenHead = 1
jvrea9dfa92002-05-12 17:14:50 +0000152 directory = directory + entry.toString()
jvrf509c0f2003-08-22 19:38:37 +0000153 if seenHead:
jvr91bca422012-10-18 12:49:22 +0000154 self.writeMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000155 self.file.seek(0)
156 self.file.write(directory)
jvr91bca422012-10-18 12:49:22 +0000157
158 def _calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000159 # calculate checkSumAdjustment
160 tags = self.tables.keys()
jvr91bca422012-10-18 12:49:22 +0000161 checksums = []
Just7842e561999-12-16 21:34:53 +0000162 for i in range(len(tags)):
jvr91bca422012-10-18 12:49:22 +0000163 checksums.append(self.tables[tags[i]].checkSum)
164
Just7842e561999-12-16 21:34:53 +0000165 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
166 assert directory_end == len(directory)
jvr91bca422012-10-18 12:49:22 +0000167
168 checksums.append(calcChecksum(directory))
169 checksum = sum(checksums) & 0xffffffff
Just7842e561999-12-16 21:34:53 +0000170 # BiboAfba!
jvr91bca422012-10-18 12:49:22 +0000171 checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff
172 return checksumadjustment
173
174 def writeMasterChecksum(self, directory):
175 checksumadjustment = self._calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000176 # write the checksum to the file
177 self.file.seek(self.tables['head'].offset + 8)
pabs30e2aece2009-03-24 09:42:15 +0000178 self.file.write(struct.pack(">L", checksumadjustment))
jvr1ebda672008-03-08 20:29:30 +0000179
Just7842e561999-12-16 21:34:53 +0000180
181# -- sfnt directory helpers and cruft
182
pabs37e91e772009-02-22 08:55:00 +0000183ttcHeaderFormat = """
184 > # big endian
185 TTCTag: 4s # "ttcf"
186 Version: L # 0x00010000 or 0x00020000
187 numFonts: L # number of fonts
188 # OffsetTable[numFonts]: L # array with offsets from beginning of file
189 # ulDsigTag: L # version 2.0 only
190 # ulDsigLength: L # version 2.0 only
191 # ulDsigOffset: L # version 2.0 only
192"""
193
194ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat)
195
Just7842e561999-12-16 21:34:53 +0000196sfntDirectoryFormat = """
197 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000198 sfntVersion: 4s
199 numTables: H # number of tables
200 searchRange: H # (max2 <= numTables)*16
201 entrySelector: H # log2(max2 <= numTables)
202 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000203"""
204
205sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
206
207sfntDirectoryEntryFormat = """
208 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000209 tag: 4s
pabs30e2aece2009-03-24 09:42:15 +0000210 checkSum: L
211 offset: L
212 length: L
Just7842e561999-12-16 21:34:53 +0000213"""
214
215sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
216
217class SFNTDirectoryEntry:
218
jvrea9dfa92002-05-12 17:14:50 +0000219 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000220 sstruct.unpack(sfntDirectoryEntryFormat,
221 file.read(sfntDirectoryEntrySize), self)
222
jvrea9dfa92002-05-12 17:14:50 +0000223 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000224 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
225
jvrea9dfa92002-05-12 17:14:50 +0000226 def toString(self):
Just7842e561999-12-16 21:34:53 +0000227 return sstruct.pack(sfntDirectoryEntryFormat, self)
228
229 def __repr__(self):
230 if hasattr(self, "tag"):
231 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
232 else:
233 return "<SFNTDirectoryEntry at %x>" % id(self)
234
235
jvr91bca422012-10-18 12:49:22 +0000236def calcChecksum(data):
Just7842e561999-12-16 21:34:53 +0000237 """Calculate the checksum for an arbitrary block of data.
238 Optionally takes a 'start' argument, which allows you to
239 calculate a checksum in chunks by feeding it a previous
240 result.
241
242 If the data length is not a multiple of four, it assumes
243 it is to be padded with null byte.
jvr91bca422012-10-18 12:49:22 +0000244
245 >>> print calcChecksum("abcd")
246 1633837924
247 >>> print calcChecksum("abcdxyz")
248 3655064932
Just7842e561999-12-16 21:34:53 +0000249 """
Just7842e561999-12-16 21:34:53 +0000250 remainder = len(data) % 4
251 if remainder:
jvr91bca422012-10-18 12:49:22 +0000252 data += "\0" * (4 - remainder)
253 value = 0
254 blockSize = 4096
255 assert blockSize % 4 == 0
256 for i in xrange(0, len(data), blockSize):
257 block = data[i:i+blockSize]
258 longs = struct.unpack(">%dL" % (len(block) // 4), block)
259 value = (value + sum(longs)) & 0xffffffff
260 return value
Just7842e561999-12-16 21:34:53 +0000261
262
jvrea9dfa92002-05-12 17:14:50 +0000263def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000264 """Return the highest exponent of two, so that
265 (2 ** exponent) <= x
266 """
267 exponent = 0
268 while x:
269 x = x >> 1
270 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000271 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000272
273
jvrea9dfa92002-05-12 17:14:50 +0000274def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000275 """Calculate searchRange, entrySelector, rangeShift for the
276 sfnt directory. 'n' is the number of tables.
277 """
278 # This stuff needs to be stored in the file, because?
279 import math
jvrea9dfa92002-05-12 17:14:50 +0000280 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000281 searchRange = (2 ** exponent) * 16
282 entrySelector = exponent
283 rangeShift = n * 16 - searchRange
284 return searchRange, entrySelector, rangeShift
285
jvr91bca422012-10-18 12:49:22 +0000286
287if __name__ == "__main__":
288 import doctest
289 doctest.testmod()