blob: da3f37e2ac138ac2ce51b871d8fb63a124dd7329 [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
Just0f675862000-10-02 07:51:42 +0000108 def close(self, closeStream=1):
Just7842e561999-12-16 21:34:53 +0000109 """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)
Just0f675862000-10-02 07:51:42 +0000126 if closeStream:
127 self.file.close()
Just7842e561999-12-16 21:34:53 +0000128
129 def calcmasterchecksum(self, directory):
130 # calculate checkSumAdjustment
131 tags = self.tables.keys()
132 checksums = Numeric.zeros(len(tags)+1)
133 for i in range(len(tags)):
134 checksums[i] = self.tables[tags[i]].checkSum
135
136 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
137 assert directory_end == len(directory)
138
139 checksums[-1] = calcchecksum(directory)
140 checksum = Numeric.add.reduce(checksums)
141 # BiboAfba!
142 checksumadjustment = Numeric.array(0xb1b0afba) - checksum
143 # write the checksum to the file
144 self.file.seek(self.tables['head'].offset + 8)
145 self.file.write(struct.pack("l", checksumadjustment))
146
147
148# -- sfnt directory helpers and cruft
149
150sfntDirectoryFormat = """
151 > # big endian
152 sfntVersion: 4s
153 numTables: H # number of tables
154 searchRange: H # (max2 <= numTables)*16
155 entrySelector: H # log2(max2 <= numTables)
156 rangeShift: H # numTables*16-searchRange
157"""
158
159sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
160
161sfntDirectoryEntryFormat = """
162 > # big endian
163 tag: 4s
164 checkSum: l
165 offset: l
166 length: l
167"""
168
169sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
170
171class SFNTDirectoryEntry:
172
173 def fromfile(self, file):
174 sstruct.unpack(sfntDirectoryEntryFormat,
175 file.read(sfntDirectoryEntrySize), self)
176
177 def fromstring(self, str):
178 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
179
180 def tostring(self):
181 return sstruct.pack(sfntDirectoryEntryFormat, self)
182
183 def __repr__(self):
184 if hasattr(self, "tag"):
185 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
186 else:
187 return "<SFNTDirectoryEntry at %x>" % id(self)
188
189
190def calcchecksum(data, start=0):
191 """Calculate the checksum for an arbitrary block of data.
192 Optionally takes a 'start' argument, which allows you to
193 calculate a checksum in chunks by feeding it a previous
194 result.
195
196 If the data length is not a multiple of four, it assumes
197 it is to be padded with null byte.
198 """
199 from fontTools import ttLib
200 remainder = len(data) % 4
201 if remainder:
202 data = data + '\0' * (4-remainder)
203 a = Numeric.fromstring(struct.pack(">l", start) + data, Numeric.Int32)
204 if ttLib.endian <> "big":
205 a = a.byteswapped()
206 return Numeric.add.reduce(a)
207
208
209def maxpoweroftwo(x):
210 """Return the highest exponent of two, so that
211 (2 ** exponent) <= x
212 """
213 exponent = 0
214 while x:
215 x = x >> 1
216 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000217 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000218
219
220def getsearchrange(n):
221 """Calculate searchRange, entrySelector, rangeShift for the
222 sfnt directory. 'n' is the number of tables.
223 """
224 # This stuff needs to be stored in the file, because?
225 import math
226 exponent = maxpoweroftwo(n)
227 searchRange = (2 ** exponent) * 16
228 entrySelector = exponent
229 rangeShift = n * 16 - searchRange
230 return searchRange, entrySelector, rangeShift
231