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