blob: 4a4a7e73102639b5534f18622357f63944aee0ab [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
19class SFNTReader:
20
21 def __init__(self, file, checkchecksums=1):
22 self.file = file
23 self.checkchecksums = checkchecksums
24 data = self.file.read(sfntDirectorySize)
25 if len(data) <> sfntDirectorySize:
26 from fontTools import ttLib
27 raise ttLib.TTLibError, "Not a TrueType or OpenType font (not enough data)"
28 sstruct.unpack(sfntDirectoryFormat, data, self)
29 if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
30 from fontTools import ttLib
31 raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
32 self.tables = {}
33 for i in range(self.numTables):
34 entry = SFNTDirectoryEntry()
35 entry.fromfile(self.file)
36 self.tables[entry.tag] = entry
37
38 def has_key(self, tag):
39 return self.tables.has_key(tag)
40
41 def keys(self):
42 return self.tables.keys()
43
44 def __getitem__(self, tag):
45 """Fetch the raw table data."""
46 entry = self.tables[tag]
47 self.file.seek(entry.offset)
48 data = self.file.read(entry.length)
49 if self.checkchecksums:
50 if tag == 'head':
51 # Beh: we have to special-case the 'head' table.
52 checksum = calcchecksum(data[:8] + '\0\0\0\0' + data[12:])
53 else:
54 checksum = calcchecksum(data)
55 if self.checkchecksums > 1:
56 # Be obnoxious, and barf when it's wrong
57 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
58 elif checksum <> entry.checkSum:
59 # Be friendly, and just print a warning.
60 print "bad checksum for '%s' table" % tag
61 return data
62
63 def close(self):
64 self.file.close()
65
66
67class SFNTWriter:
68
69 def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
70 self.file = file
71 self.numTables = numTables
72 self.sfntVersion = sfntVersion
73 self.searchRange, self.entrySelector, self.rangeShift = getsearchrange(numTables)
74 self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
75 # clear out directory area
76 self.file.seek(self.nextTableOffset)
77 # make sure we're actually where we want to be. (XXX old cStringIO bug)
78 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
79 self.tables = {}
80
81 def __setitem__(self, tag, data):
82 """Write raw table data to disk."""
83 if self.tables.has_key(tag):
84 # We've written this table to file before. If the length
85 # of the data is still the same, we allow overwritng it.
86 entry = self.tables[tag]
87 if len(data) <> entry.length:
88 from fontTools import ttLib
89 raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
90 else:
91 entry = SFNTDirectoryEntry()
92 entry.tag = tag
93 entry.offset = self.nextTableOffset
94 entry.length = len(data)
95 self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
96 self.file.seek(entry.offset)
97 self.file.write(data)
98 self.file.seek(self.nextTableOffset)
99 # make sure we're actually where we want to be. (XXX old cStringIO bug)
100 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
101
102 if tag == 'head':
103 entry.checkSum = calcchecksum(data[:8] + '\0\0\0\0' + data[12:])
104 else:
105 entry.checkSum = calcchecksum(data)
106 self.tables[tag] = entry
107
108 def close(self):
109 """All tables must have been written to disk. Now write the
110 directory.
111 """
112 tables = self.tables.items()
113 tables.sort()
114 if len(tables) <> self.numTables:
115 from fontTools import ttLib
116 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
117
118 directory = sstruct.pack(sfntDirectoryFormat, self)
119
120 self.file.seek(sfntDirectorySize)
121 for tag, entry in tables:
122 directory = directory + entry.tostring()
123 self.calcmasterchecksum(directory)
124 self.file.seek(0)
125 self.file.write(directory)
126 self.file.close()
127
128 def calcmasterchecksum(self, directory):
129 # calculate checkSumAdjustment
130 tags = self.tables.keys()
131 checksums = Numeric.zeros(len(tags)+1)
132 for i in range(len(tags)):
133 checksums[i] = self.tables[tags[i]].checkSum
134
135 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
136 assert directory_end == len(directory)
137
138 checksums[-1] = calcchecksum(directory)
139 checksum = Numeric.add.reduce(checksums)
140 # BiboAfba!
141 checksumadjustment = Numeric.array(0xb1b0afba) - checksum
142 # write the checksum to the file
143 self.file.seek(self.tables['head'].offset + 8)
144 self.file.write(struct.pack("l", checksumadjustment))
145
146
147# -- sfnt directory helpers and cruft
148
149sfntDirectoryFormat = """
150 > # big endian
151 sfntVersion: 4s
152 numTables: H # number of tables
153 searchRange: H # (max2 <= numTables)*16
154 entrySelector: H # log2(max2 <= numTables)
155 rangeShift: H # numTables*16-searchRange
156"""
157
158sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
159
160sfntDirectoryEntryFormat = """
161 > # big endian
162 tag: 4s
163 checkSum: l
164 offset: l
165 length: l
166"""
167
168sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
169
170class SFNTDirectoryEntry:
171
172 def fromfile(self, file):
173 sstruct.unpack(sfntDirectoryEntryFormat,
174 file.read(sfntDirectoryEntrySize), self)
175
176 def fromstring(self, str):
177 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
178
179 def tostring(self):
180 return sstruct.pack(sfntDirectoryEntryFormat, self)
181
182 def __repr__(self):
183 if hasattr(self, "tag"):
184 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
185 else:
186 return "<SFNTDirectoryEntry at %x>" % id(self)
187
188
189def calcchecksum(data, start=0):
190 """Calculate the checksum for an arbitrary block of data.
191 Optionally takes a 'start' argument, which allows you to
192 calculate a checksum in chunks by feeding it a previous
193 result.
194
195 If the data length is not a multiple of four, it assumes
196 it is to be padded with null byte.
197 """
198 from fontTools import ttLib
199 remainder = len(data) % 4
200 if remainder:
201 data = data + '\0' * (4-remainder)
202 a = Numeric.fromstring(struct.pack(">l", start) + data, Numeric.Int32)
203 if ttLib.endian <> "big":
204 a = a.byteswapped()
205 return Numeric.add.reduce(a)
206
207
208def maxpoweroftwo(x):
209 """Return the highest exponent of two, so that
210 (2 ** exponent) <= x
211 """
212 exponent = 0
213 while x:
214 x = x >> 1
215 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000216 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000217
218
219def getsearchrange(n):
220 """Calculate searchRange, entrySelector, rangeShift for the
221 sfnt directory. 'n' is the number of tables.
222 """
223 # This stuff needs to be stored in the file, because?
224 import math
225 exponent = maxpoweroftwo(n)
226 searchRange = (2 ** exponent) * 16
227 entrySelector = exponent
228 rangeShift = n * 16 - searchRange
229 return searchRange, entrySelector, rangeShift
230