blob: 0ce0ca039588c6c3096d8044f4bad8c9a83999b4 [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
15import struct, sstruct
16import Numeric
17import os
18
jvr04b32042002-05-14 12:09:10 +000019
Just7842e561999-12-16 21:34:53 +000020class SFNTReader:
21
jvrea9dfa92002-05-12 17:14:50 +000022 def __init__(self, file, checkChecksums=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)
30 if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
31 from fontTools import ttLib
32 raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
33 self.tables = {}
34 for i in range(self.numTables):
35 entry = SFNTDirectoryEntry()
jvrea9dfa92002-05-12 17:14:50 +000036 entry.fromFile(self.file)
jvrce1d50a2002-05-12 17:02:50 +000037 if entry.length > 0:
38 self.tables[entry.tag] = entry
39 else:
40 # Ignore zero-length tables. This doesn't seem to be documented,
41 # yet it's apparently how the Windows TT rasterizer behaves.
42 # Besides, at least one font has been sighted which actually
43 # *has* a zero-length table.
44 pass
Just7842e561999-12-16 21:34:53 +000045
46 def has_key(self, tag):
47 return self.tables.has_key(tag)
48
49 def keys(self):
50 return self.tables.keys()
51
52 def __getitem__(self, tag):
53 """Fetch the raw table data."""
54 entry = self.tables[tag]
55 self.file.seek(entry.offset)
56 data = self.file.read(entry.length)
jvrea9dfa92002-05-12 17:14:50 +000057 if self.checkChecksums:
Just7842e561999-12-16 21:34:53 +000058 if tag == 'head':
59 # Beh: we have to special-case the 'head' table.
jvrea9dfa92002-05-12 17:14:50 +000060 checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +000061 else:
jvrea9dfa92002-05-12 17:14:50 +000062 checksum = calcChecksum(data)
63 if self.checkChecksums > 1:
Just7842e561999-12-16 21:34:53 +000064 # Be obnoxious, and barf when it's wrong
65 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
66 elif checksum <> entry.checkSum:
67 # Be friendly, and just print a warning.
68 print "bad checksum for '%s' table" % tag
69 return data
70
jvrf7074632002-05-04 22:04:02 +000071 def __delitem__(self, tag):
72 del self.tables[tag]
73
Just7842e561999-12-16 21:34:53 +000074 def close(self):
75 self.file.close()
76
77
78class SFNTWriter:
79
80 def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
81 self.file = file
82 self.numTables = numTables
83 self.sfntVersion = sfntVersion
jvrea9dfa92002-05-12 17:14:50 +000084 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
Just7842e561999-12-16 21:34:53 +000085 self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
86 # clear out directory area
87 self.file.seek(self.nextTableOffset)
88 # make sure we're actually where we want to be. (XXX old cStringIO bug)
89 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
90 self.tables = {}
91
92 def __setitem__(self, tag, data):
93 """Write raw table data to disk."""
94 if self.tables.has_key(tag):
95 # We've written this table to file before. If the length
jvr04b32042002-05-14 12:09:10 +000096 # of the data is still the same, we allow overwriting it.
Just7842e561999-12-16 21:34:53 +000097 entry = self.tables[tag]
98 if len(data) <> entry.length:
99 from fontTools import ttLib
100 raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
101 else:
102 entry = SFNTDirectoryEntry()
103 entry.tag = tag
104 entry.offset = self.nextTableOffset
105 entry.length = len(data)
106 self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
107 self.file.seek(entry.offset)
108 self.file.write(data)
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
113 if tag == 'head':
jvrea9dfa92002-05-12 17:14:50 +0000114 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +0000115 else:
jvrea9dfa92002-05-12 17:14:50 +0000116 entry.checkSum = calcChecksum(data)
Just7842e561999-12-16 21:34:53 +0000117 self.tables[tag] = entry
118
Just0f675862000-10-02 07:51:42 +0000119 def close(self, closeStream=1):
Just7842e561999-12-16 21:34:53 +0000120 """All tables must have been written to disk. Now write the
121 directory.
122 """
123 tables = self.tables.items()
124 tables.sort()
125 if len(tables) <> self.numTables:
126 from fontTools import ttLib
127 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
128
129 directory = sstruct.pack(sfntDirectoryFormat, self)
130
131 self.file.seek(sfntDirectorySize)
jvrf509c0f2003-08-22 19:38:37 +0000132 seenHead = 0
Just7842e561999-12-16 21:34:53 +0000133 for tag, entry in tables:
jvrf509c0f2003-08-22 19:38:37 +0000134 if tag == "head":
135 seenHead = 1
jvrea9dfa92002-05-12 17:14:50 +0000136 directory = directory + entry.toString()
jvrf509c0f2003-08-22 19:38:37 +0000137 if seenHead:
138 self.calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000139 self.file.seek(0)
140 self.file.write(directory)
Just0f675862000-10-02 07:51:42 +0000141 if closeStream:
142 self.file.close()
Just7842e561999-12-16 21:34:53 +0000143
jvrea9dfa92002-05-12 17:14:50 +0000144 def calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000145 # calculate checkSumAdjustment
146 tags = self.tables.keys()
147 checksums = Numeric.zeros(len(tags)+1)
148 for i in range(len(tags)):
149 checksums[i] = self.tables[tags[i]].checkSum
150
151 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
152 assert directory_end == len(directory)
153
jvrea9dfa92002-05-12 17:14:50 +0000154 checksums[-1] = calcChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000155 checksum = Numeric.add.reduce(checksums)
156 # BiboAfba!
jvr02e76e92003-01-03 20:57:04 +0000157 checksumadjustment = Numeric.array(0xb1b0afbaL - 0x100000000L,
158 Numeric.Int32) - checksum
Just7842e561999-12-16 21:34:53 +0000159 # write the checksum to the file
160 self.file.seek(self.tables['head'].offset + 8)
jvr58629632002-07-21 20:05:52 +0000161 self.file.write(struct.pack(">l", checksumadjustment))
Just7842e561999-12-16 21:34:53 +0000162
163
164# -- sfnt directory helpers and cruft
165
166sfntDirectoryFormat = """
167 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000168 sfntVersion: 4s
169 numTables: H # number of tables
170 searchRange: H # (max2 <= numTables)*16
171 entrySelector: H # log2(max2 <= numTables)
172 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000173"""
174
175sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
176
177sfntDirectoryEntryFormat = """
178 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000179 tag: 4s
180 checkSum: l
181 offset: l
182 length: l
Just7842e561999-12-16 21:34:53 +0000183"""
184
185sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
186
187class SFNTDirectoryEntry:
188
jvrea9dfa92002-05-12 17:14:50 +0000189 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000190 sstruct.unpack(sfntDirectoryEntryFormat,
191 file.read(sfntDirectoryEntrySize), self)
192
jvrea9dfa92002-05-12 17:14:50 +0000193 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000194 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
195
jvrea9dfa92002-05-12 17:14:50 +0000196 def toString(self):
Just7842e561999-12-16 21:34:53 +0000197 return sstruct.pack(sfntDirectoryEntryFormat, self)
198
199 def __repr__(self):
200 if hasattr(self, "tag"):
201 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
202 else:
203 return "<SFNTDirectoryEntry at %x>" % id(self)
204
205
jvrea9dfa92002-05-12 17:14:50 +0000206def calcChecksum(data, start=0):
Just7842e561999-12-16 21:34:53 +0000207 """Calculate the checksum for an arbitrary block of data.
208 Optionally takes a 'start' argument, which allows you to
209 calculate a checksum in chunks by feeding it a previous
210 result.
211
212 If the data length is not a multiple of four, it assumes
213 it is to be padded with null byte.
214 """
215 from fontTools import ttLib
216 remainder = len(data) % 4
217 if remainder:
218 data = data + '\0' * (4-remainder)
219 a = Numeric.fromstring(struct.pack(">l", start) + data, Numeric.Int32)
220 if ttLib.endian <> "big":
221 a = a.byteswapped()
222 return Numeric.add.reduce(a)
223
224
jvrea9dfa92002-05-12 17:14:50 +0000225def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000226 """Return the highest exponent of two, so that
227 (2 ** exponent) <= x
228 """
229 exponent = 0
230 while x:
231 x = x >> 1
232 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000233 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000234
235
jvrea9dfa92002-05-12 17:14:50 +0000236def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000237 """Calculate searchRange, entrySelector, rangeShift for the
238 sfnt directory. 'n' is the number of tables.
239 """
240 # This stuff needs to be stored in the file, because?
241 import math
jvrea9dfa92002-05-12 17:14:50 +0000242 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000243 searchRange = (2 ** exponent) * 16
244 entrySelector = exponent
245 rangeShift = n * 16 - searchRange
246 return searchRange, entrySelector, rangeShift
247