blob: bd5fe7a31b73b7b93cb06c0662a697f22555e3c8 [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
Just0f675862000-10-02 07:51:42 +0000119 def close(self, closeStream=1):
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)
132 for tag, entry in tables:
jvrea9dfa92002-05-12 17:14:50 +0000133 directory = directory + entry.toString()
134 self.calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000135 self.file.seek(0)
136 self.file.write(directory)
Just0f675862000-10-02 07:51:42 +0000137 if closeStream:
138 self.file.close()
Just7842e561999-12-16 21:34:53 +0000139
jvrea9dfa92002-05-12 17:14:50 +0000140 def calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000141 # calculate checkSumAdjustment
142 tags = self.tables.keys()
143 checksums = Numeric.zeros(len(tags)+1)
144 for i in range(len(tags)):
145 checksums[i] = self.tables[tags[i]].checkSum
146
147 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
148 assert directory_end == len(directory)
149
jvrea9dfa92002-05-12 17:14:50 +0000150 checksums[-1] = calcChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000151 checksum = Numeric.add.reduce(checksums)
152 # BiboAfba!
jvr02e76e92003-01-03 20:57:04 +0000153 checksumadjustment = Numeric.array(0xb1b0afbaL - 0x100000000L,
154 Numeric.Int32) - checksum
Just7842e561999-12-16 21:34:53 +0000155 # write the checksum to the file
156 self.file.seek(self.tables['head'].offset + 8)
jvr58629632002-07-21 20:05:52 +0000157 self.file.write(struct.pack(">l", checksumadjustment))
Just7842e561999-12-16 21:34:53 +0000158
159
160# -- sfnt directory helpers and cruft
161
162sfntDirectoryFormat = """
163 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000164 sfntVersion: 4s
165 numTables: H # number of tables
166 searchRange: H # (max2 <= numTables)*16
167 entrySelector: H # log2(max2 <= numTables)
168 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000169"""
170
171sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
172
173sfntDirectoryEntryFormat = """
174 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000175 tag: 4s
176 checkSum: l
177 offset: l
178 length: l
Just7842e561999-12-16 21:34:53 +0000179"""
180
181sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
182
183class SFNTDirectoryEntry:
184
jvrea9dfa92002-05-12 17:14:50 +0000185 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000186 sstruct.unpack(sfntDirectoryEntryFormat,
187 file.read(sfntDirectoryEntrySize), self)
188
jvrea9dfa92002-05-12 17:14:50 +0000189 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000190 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
191
jvrea9dfa92002-05-12 17:14:50 +0000192 def toString(self):
Just7842e561999-12-16 21:34:53 +0000193 return sstruct.pack(sfntDirectoryEntryFormat, self)
194
195 def __repr__(self):
196 if hasattr(self, "tag"):
197 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
198 else:
199 return "<SFNTDirectoryEntry at %x>" % id(self)
200
201
jvrea9dfa92002-05-12 17:14:50 +0000202def calcChecksum(data, start=0):
Just7842e561999-12-16 21:34:53 +0000203 """Calculate the checksum for an arbitrary block of data.
204 Optionally takes a 'start' argument, which allows you to
205 calculate a checksum in chunks by feeding it a previous
206 result.
207
208 If the data length is not a multiple of four, it assumes
209 it is to be padded with null byte.
210 """
211 from fontTools import ttLib
212 remainder = len(data) % 4
213 if remainder:
214 data = data + '\0' * (4-remainder)
215 a = Numeric.fromstring(struct.pack(">l", start) + data, Numeric.Int32)
216 if ttLib.endian <> "big":
217 a = a.byteswapped()
218 return Numeric.add.reduce(a)
219
220
jvrea9dfa92002-05-12 17:14:50 +0000221def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000222 """Return the highest exponent of two, so that
223 (2 ** exponent) <= x
224 """
225 exponent = 0
226 while x:
227 x = x >> 1
228 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000229 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000230
231
jvrea9dfa92002-05-12 17:14:50 +0000232def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000233 """Calculate searchRange, entrySelector, rangeShift for the
234 sfnt directory. 'n' is the number of tables.
235 """
236 # This stuff needs to be stored in the file, because?
237 import math
jvrea9dfa92002-05-12 17:14:50 +0000238 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000239 searchRange = (2 ** exponent) * 16
240 entrySelector = exponent
241 rangeShift = n * 16 - searchRange
242 return searchRange, entrySelector, rangeShift
243