blob: 66f47532ca76dde2c278c6f010e37c5ef32544f1 [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
jvrea9dfa92002-05-12 17:14:50 +000021 def __init__(self, file, checkChecksums=1):
Just7842e561999-12-16 21:34:53 +000022 self.file = file
jvrea9dfa92002-05-12 17:14:50 +000023 self.checkChecksums = checkChecksums
Just7842e561999-12-16 21:34:53 +000024 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()
jvrea9dfa92002-05-12 17:14:50 +000035 entry.fromFile(self.file)
jvrce1d50a2002-05-12 17:02:50 +000036 if entry.length > 0:
37 self.tables[entry.tag] = entry
38 else:
39 # Ignore zero-length tables. This doesn't seem to be documented,
40 # yet it's apparently how the Windows TT rasterizer behaves.
41 # Besides, at least one font has been sighted which actually
42 # *has* a zero-length table.
43 pass
Just7842e561999-12-16 21:34:53 +000044
45 def has_key(self, tag):
46 return self.tables.has_key(tag)
47
48 def keys(self):
49 return self.tables.keys()
50
51 def __getitem__(self, tag):
52 """Fetch the raw table data."""
53 entry = self.tables[tag]
54 self.file.seek(entry.offset)
55 data = self.file.read(entry.length)
jvrea9dfa92002-05-12 17:14:50 +000056 if self.checkChecksums:
Just7842e561999-12-16 21:34:53 +000057 if tag == 'head':
58 # Beh: we have to special-case the 'head' table.
jvrea9dfa92002-05-12 17:14:50 +000059 checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +000060 else:
jvrea9dfa92002-05-12 17:14:50 +000061 checksum = calcChecksum(data)
62 if self.checkChecksums > 1:
Just7842e561999-12-16 21:34:53 +000063 # Be obnoxious, and barf when it's wrong
64 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
65 elif checksum <> entry.checkSum:
66 # Be friendly, and just print a warning.
67 print "bad checksum for '%s' table" % tag
68 return data
69
jvrf7074632002-05-04 22:04:02 +000070 def __delitem__(self, tag):
71 del self.tables[tag]
72
Just7842e561999-12-16 21:34:53 +000073 def close(self):
74 self.file.close()
75
76
77class SFNTWriter:
78
79 def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
80 self.file = file
81 self.numTables = numTables
82 self.sfntVersion = sfntVersion
jvrea9dfa92002-05-12 17:14:50 +000083 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
Just7842e561999-12-16 21:34:53 +000084 self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
85 # clear out directory area
86 self.file.seek(self.nextTableOffset)
87 # make sure we're actually where we want to be. (XXX old cStringIO bug)
88 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
89 self.tables = {}
90
91 def __setitem__(self, tag, data):
92 """Write raw table data to disk."""
93 if self.tables.has_key(tag):
94 # We've written this table to file before. If the length
95 # of the data is still the same, we allow overwritng it.
96 entry = self.tables[tag]
97 if len(data) <> entry.length:
98 from fontTools import ttLib
99 raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
100 else:
101 entry = SFNTDirectoryEntry()
102 entry.tag = tag
103 entry.offset = self.nextTableOffset
104 entry.length = len(data)
105 self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
106 self.file.seek(entry.offset)
107 self.file.write(data)
108 self.file.seek(self.nextTableOffset)
109 # make sure we're actually where we want to be. (XXX old cStringIO bug)
110 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
111
112 if tag == 'head':
jvrea9dfa92002-05-12 17:14:50 +0000113 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +0000114 else:
jvrea9dfa92002-05-12 17:14:50 +0000115 entry.checkSum = calcChecksum(data)
Just7842e561999-12-16 21:34:53 +0000116 self.tables[tag] = entry
117
Just0f675862000-10-02 07:51:42 +0000118 def close(self, closeStream=1):
Just7842e561999-12-16 21:34:53 +0000119 """All tables must have been written to disk. Now write the
120 directory.
121 """
122 tables = self.tables.items()
123 tables.sort()
124 if len(tables) <> self.numTables:
125 from fontTools import ttLib
126 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
127
128 directory = sstruct.pack(sfntDirectoryFormat, self)
129
130 self.file.seek(sfntDirectorySize)
131 for tag, entry in tables:
jvrea9dfa92002-05-12 17:14:50 +0000132 directory = directory + entry.toString()
133 self.calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000134 self.file.seek(0)
135 self.file.write(directory)
Just0f675862000-10-02 07:51:42 +0000136 if closeStream:
137 self.file.close()
Just7842e561999-12-16 21:34:53 +0000138
jvrea9dfa92002-05-12 17:14:50 +0000139 def calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000140 # calculate checkSumAdjustment
141 tags = self.tables.keys()
142 checksums = Numeric.zeros(len(tags)+1)
143 for i in range(len(tags)):
144 checksums[i] = self.tables[tags[i]].checkSum
145
146 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
147 assert directory_end == len(directory)
148
jvrea9dfa92002-05-12 17:14:50 +0000149 checksums[-1] = calcChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000150 checksum = Numeric.add.reduce(checksums)
151 # BiboAfba!
152 checksumadjustment = Numeric.array(0xb1b0afba) - checksum
153 # write the checksum to the file
154 self.file.seek(self.tables['head'].offset + 8)
155 self.file.write(struct.pack("l", checksumadjustment))
156
157
158# -- sfnt directory helpers and cruft
159
160sfntDirectoryFormat = """
161 > # big endian
162 sfntVersion: 4s
163 numTables: H # number of tables
164 searchRange: H # (max2 <= numTables)*16
165 entrySelector: H # log2(max2 <= numTables)
166 rangeShift: H # numTables*16-searchRange
167"""
168
169sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
170
171sfntDirectoryEntryFormat = """
172 > # big endian
173 tag: 4s
174 checkSum: l
175 offset: l
176 length: l
177"""
178
179sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
180
181class SFNTDirectoryEntry:
182
jvrea9dfa92002-05-12 17:14:50 +0000183 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000184 sstruct.unpack(sfntDirectoryEntryFormat,
185 file.read(sfntDirectoryEntrySize), self)
186
jvrea9dfa92002-05-12 17:14:50 +0000187 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000188 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
189
jvrea9dfa92002-05-12 17:14:50 +0000190 def toString(self):
Just7842e561999-12-16 21:34:53 +0000191 return sstruct.pack(sfntDirectoryEntryFormat, self)
192
193 def __repr__(self):
194 if hasattr(self, "tag"):
195 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
196 else:
197 return "<SFNTDirectoryEntry at %x>" % id(self)
198
199
jvrea9dfa92002-05-12 17:14:50 +0000200def calcChecksum(data, start=0):
Just7842e561999-12-16 21:34:53 +0000201 """Calculate the checksum for an arbitrary block of data.
202 Optionally takes a 'start' argument, which allows you to
203 calculate a checksum in chunks by feeding it a previous
204 result.
205
206 If the data length is not a multiple of four, it assumes
207 it is to be padded with null byte.
208 """
209 from fontTools import ttLib
210 remainder = len(data) % 4
211 if remainder:
212 data = data + '\0' * (4-remainder)
213 a = Numeric.fromstring(struct.pack(">l", start) + data, Numeric.Int32)
214 if ttLib.endian <> "big":
215 a = a.byteswapped()
216 return Numeric.add.reduce(a)
217
218
jvrea9dfa92002-05-12 17:14:50 +0000219def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000220 """Return the highest exponent of two, so that
221 (2 ** exponent) <= x
222 """
223 exponent = 0
224 while x:
225 x = x >> 1
226 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000227 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000228
229
jvrea9dfa92002-05-12 17:14:50 +0000230def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000231 """Calculate searchRange, entrySelector, rangeShift for the
232 sfnt directory. 'n' is the number of tables.
233 """
234 # This stuff needs to be stored in the file, because?
235 import math
jvrea9dfa92002-05-12 17:14:50 +0000236 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000237 searchRange = (2 ** exponent) * 16
238 entrySelector = exponent
239 rangeShift = n * 16 - searchRange
240 return searchRange, entrySelector, rangeShift
241