blob: e94a5165baa0b048ef419792cbae865f6acd95be [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
jvr28ae1962004-11-16 10:37:59 +0000119 def close(self):
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)
jvrf509c0f2003-08-22 19:38:37 +0000132 seenHead = 0
Just7842e561999-12-16 21:34:53 +0000133 for tag, entry in tables:
jvrf509c0f2003-08-22 19:38:37 +0000134 if tag == "head":
135 seenHead = 1
jvrea9dfa92002-05-12 17:14:50 +0000136 directory = directory + entry.toString()
jvrf509c0f2003-08-22 19:38:37 +0000137 if seenHead:
138 self.calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000139 self.file.seek(0)
140 self.file.write(directory)
Just7842e561999-12-16 21:34:53 +0000141
jvrea9dfa92002-05-12 17:14:50 +0000142 def calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000143 # calculate checkSumAdjustment
144 tags = self.tables.keys()
145 checksums = Numeric.zeros(len(tags)+1)
146 for i in range(len(tags)):
147 checksums[i] = self.tables[tags[i]].checkSum
148
149 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
150 assert directory_end == len(directory)
151
jvrea9dfa92002-05-12 17:14:50 +0000152 checksums[-1] = calcChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000153 checksum = Numeric.add.reduce(checksums)
154 # BiboAfba!
jvr02e76e92003-01-03 20:57:04 +0000155 checksumadjustment = Numeric.array(0xb1b0afbaL - 0x100000000L,
156 Numeric.Int32) - checksum
Just7842e561999-12-16 21:34:53 +0000157 # write the checksum to the file
158 self.file.seek(self.tables['head'].offset + 8)
jvr58629632002-07-21 20:05:52 +0000159 self.file.write(struct.pack(">l", checksumadjustment))
Just7842e561999-12-16 21:34:53 +0000160
161
162# -- sfnt directory helpers and cruft
163
164sfntDirectoryFormat = """
165 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000166 sfntVersion: 4s
167 numTables: H # number of tables
168 searchRange: H # (max2 <= numTables)*16
169 entrySelector: H # log2(max2 <= numTables)
170 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000171"""
172
173sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
174
175sfntDirectoryEntryFormat = """
176 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000177 tag: 4s
178 checkSum: l
179 offset: l
180 length: l
Just7842e561999-12-16 21:34:53 +0000181"""
182
183sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
184
185class SFNTDirectoryEntry:
186
jvrea9dfa92002-05-12 17:14:50 +0000187 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000188 sstruct.unpack(sfntDirectoryEntryFormat,
189 file.read(sfntDirectoryEntrySize), self)
190
jvrea9dfa92002-05-12 17:14:50 +0000191 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000192 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
193
jvrea9dfa92002-05-12 17:14:50 +0000194 def toString(self):
Just7842e561999-12-16 21:34:53 +0000195 return sstruct.pack(sfntDirectoryEntryFormat, self)
196
197 def __repr__(self):
198 if hasattr(self, "tag"):
199 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
200 else:
201 return "<SFNTDirectoryEntry at %x>" % id(self)
202
203
jvrea9dfa92002-05-12 17:14:50 +0000204def calcChecksum(data, start=0):
Just7842e561999-12-16 21:34:53 +0000205 """Calculate the checksum for an arbitrary block of data.
206 Optionally takes a 'start' argument, which allows you to
207 calculate a checksum in chunks by feeding it a previous
208 result.
209
210 If the data length is not a multiple of four, it assumes
211 it is to be padded with null byte.
212 """
213 from fontTools import ttLib
214 remainder = len(data) % 4
215 if remainder:
216 data = data + '\0' * (4-remainder)
217 a = Numeric.fromstring(struct.pack(">l", start) + data, Numeric.Int32)
218 if ttLib.endian <> "big":
219 a = a.byteswapped()
220 return Numeric.add.reduce(a)
221
222
jvrea9dfa92002-05-12 17:14:50 +0000223def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000224 """Return the highest exponent of two, so that
225 (2 ** exponent) <= x
226 """
227 exponent = 0
228 while x:
229 x = x >> 1
230 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000231 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000232
233
jvrea9dfa92002-05-12 17:14:50 +0000234def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000235 """Calculate searchRange, entrySelector, rangeShift for the
236 sfnt directory. 'n' is the number of tables.
237 """
238 # This stuff needs to be stored in the file, because?
239 import math
jvrea9dfa92002-05-12 17:14:50 +0000240 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000241 searchRange = (2 ** exponent) * 16
242 entrySelector = exponent
243 rangeShift = n * 16 - searchRange
244 return searchRange, entrySelector, rangeShift
245