blob: 93f70b62027d1dbfb232a7137473d8c0817be0c6 [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)
132 for tag, entry in tables:
jvrea9dfa92002-05-12 17:14:50 +0000133 directory = directory + entry.toString()
134 self.calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000135 self.file.seek(0)
136 self.file.write(directory)
Just0f675862000-10-02 07:51:42 +0000137 if closeStream:
138 self.file.close()
Just7842e561999-12-16 21:34:53 +0000139
jvrea9dfa92002-05-12 17:14:50 +0000140 def calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000141 # calculate checkSumAdjustment
142 tags = self.tables.keys()
143 checksums = Numeric.zeros(len(tags)+1)
144 for i in range(len(tags)):
145 checksums[i] = self.tables[tags[i]].checkSum
146
147 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
148 assert directory_end == len(directory)
149
jvrea9dfa92002-05-12 17:14:50 +0000150 checksums[-1] = calcChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000151 checksum = Numeric.add.reduce(checksums)
152 # BiboAfba!
153 checksumadjustment = Numeric.array(0xb1b0afba) - checksum
154 # write the checksum to the file
155 self.file.seek(self.tables['head'].offset + 8)
jvr58629632002-07-21 20:05:52 +0000156 self.file.write(struct.pack(">l", checksumadjustment))
Just7842e561999-12-16 21:34:53 +0000157
158
159# -- sfnt directory helpers and cruft
160
161sfntDirectoryFormat = """
162 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000163 sfntVersion: 4s
164 numTables: H # number of tables
165 searchRange: H # (max2 <= numTables)*16
166 entrySelector: H # log2(max2 <= numTables)
167 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000168"""
169
170sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
171
172sfntDirectoryEntryFormat = """
173 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000174 tag: 4s
175 checkSum: l
176 offset: l
177 length: l
Just7842e561999-12-16 21:34:53 +0000178"""
179
180sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
181
182class SFNTDirectoryEntry:
183
jvrea9dfa92002-05-12 17:14:50 +0000184 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000185 sstruct.unpack(sfntDirectoryEntryFormat,
186 file.read(sfntDirectoryEntrySize), self)
187
jvrea9dfa92002-05-12 17:14:50 +0000188 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000189 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
190
jvrea9dfa92002-05-12 17:14:50 +0000191 def toString(self):
Just7842e561999-12-16 21:34:53 +0000192 return sstruct.pack(sfntDirectoryEntryFormat, self)
193
194 def __repr__(self):
195 if hasattr(self, "tag"):
196 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
197 else:
198 return "<SFNTDirectoryEntry at %x>" % id(self)
199
200
jvrea9dfa92002-05-12 17:14:50 +0000201def calcChecksum(data, start=0):
Just7842e561999-12-16 21:34:53 +0000202 """Calculate the checksum for an arbitrary block of data.
203 Optionally takes a 'start' argument, which allows you to
204 calculate a checksum in chunks by feeding it a previous
205 result.
206
207 If the data length is not a multiple of four, it assumes
208 it is to be padded with null byte.
209 """
210 from fontTools import ttLib
211 remainder = len(data) % 4
212 if remainder:
213 data = data + '\0' * (4-remainder)
214 a = Numeric.fromstring(struct.pack(">l", start) + data, Numeric.Int32)
215 if ttLib.endian <> "big":
216 a = a.byteswapped()
217 return Numeric.add.reduce(a)
218
219
jvrea9dfa92002-05-12 17:14:50 +0000220def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000221 """Return the highest exponent of two, so that
222 (2 ** exponent) <= x
223 """
224 exponent = 0
225 while x:
226 x = x >> 1
227 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000228 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000229
230
jvrea9dfa92002-05-12 17:14:50 +0000231def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000232 """Calculate searchRange, entrySelector, rangeShift for the
233 sfnt directory. 'n' is the number of tables.
234 """
235 # This stuff needs to be stored in the file, because?
236 import math
jvrea9dfa92002-05-12 17:14:50 +0000237 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000238 searchRange = (2 ** exponent) * 16
239 entrySelector = exponent
240 rangeShift = n * 16 - searchRange
241 return searchRange, entrySelector, rangeShift
242