blob: db90c7a4cb895deecefcc94c371c5764a9020c7b [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
jvr9be387c2008-03-01 11:43:01 +000015import sys
Just7842e561999-12-16 21:34:53 +000016import struct, sstruct
jvr1b7d54f2008-03-04 15:25:27 +000017import numpy
Just7842e561999-12-16 21:34:53 +000018import os
19
jvr04b32042002-05-14 12:09:10 +000020
Just7842e561999-12-16 21:34:53 +000021class SFNTReader:
22
jvrea9dfa92002-05-12 17:14:50 +000023 def __init__(self, file, checkChecksums=1):
Just7842e561999-12-16 21:34:53 +000024 self.file = file
jvrea9dfa92002-05-12 17:14:50 +000025 self.checkChecksums = checkChecksums
Just7842e561999-12-16 21:34:53 +000026 data = self.file.read(sfntDirectorySize)
27 if len(data) <> sfntDirectorySize:
28 from fontTools import ttLib
29 raise ttLib.TTLibError, "Not a TrueType or OpenType font (not enough data)"
30 sstruct.unpack(sfntDirectoryFormat, data, self)
31 if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
32 from fontTools import ttLib
33 raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
34 self.tables = {}
35 for i in range(self.numTables):
36 entry = SFNTDirectoryEntry()
jvrea9dfa92002-05-12 17:14:50 +000037 entry.fromFile(self.file)
jvrce1d50a2002-05-12 17:02:50 +000038 if entry.length > 0:
39 self.tables[entry.tag] = entry
40 else:
41 # Ignore zero-length tables. This doesn't seem to be documented,
42 # yet it's apparently how the Windows TT rasterizer behaves.
43 # Besides, at least one font has been sighted which actually
44 # *has* a zero-length table.
45 pass
Just7842e561999-12-16 21:34:53 +000046
47 def has_key(self, tag):
48 return self.tables.has_key(tag)
49
50 def keys(self):
51 return self.tables.keys()
52
53 def __getitem__(self, tag):
54 """Fetch the raw table data."""
55 entry = self.tables[tag]
56 self.file.seek(entry.offset)
57 data = self.file.read(entry.length)
jvrea9dfa92002-05-12 17:14:50 +000058 if self.checkChecksums:
Just7842e561999-12-16 21:34:53 +000059 if tag == 'head':
60 # Beh: we have to special-case the 'head' table.
jvrea9dfa92002-05-12 17:14:50 +000061 checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +000062 else:
jvrea9dfa92002-05-12 17:14:50 +000063 checksum = calcChecksum(data)
64 if self.checkChecksums > 1:
Just7842e561999-12-16 21:34:53 +000065 # Be obnoxious, and barf when it's wrong
66 assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
67 elif checksum <> entry.checkSum:
68 # Be friendly, and just print a warning.
69 print "bad checksum for '%s' table" % tag
70 return data
71
jvrf7074632002-05-04 22:04:02 +000072 def __delitem__(self, tag):
73 del self.tables[tag]
74
Just7842e561999-12-16 21:34:53 +000075 def close(self):
76 self.file.close()
77
78
79class SFNTWriter:
80
81 def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
82 self.file = file
83 self.numTables = numTables
84 self.sfntVersion = sfntVersion
jvrea9dfa92002-05-12 17:14:50 +000085 self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
Just7842e561999-12-16 21:34:53 +000086 self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
87 # clear out directory area
88 self.file.seek(self.nextTableOffset)
89 # make sure we're actually where we want to be. (XXX old cStringIO bug)
90 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
91 self.tables = {}
92
93 def __setitem__(self, tag, data):
94 """Write raw table data to disk."""
95 if self.tables.has_key(tag):
96 # We've written this table to file before. If the length
jvr04b32042002-05-14 12:09:10 +000097 # of the data is still the same, we allow overwriting it.
Just7842e561999-12-16 21:34:53 +000098 entry = self.tables[tag]
99 if len(data) <> entry.length:
100 from fontTools import ttLib
101 raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
102 else:
103 entry = SFNTDirectoryEntry()
104 entry.tag = tag
105 entry.offset = self.nextTableOffset
106 entry.length = len(data)
107 self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
108 self.file.seek(entry.offset)
109 self.file.write(data)
jvrc63ac642008-06-17 20:41:15 +0000110 # Add NUL bytes to pad the table data to a 4-byte boundary.
111 # Don't depend on f.seek() as we need to add the padding even if no
112 # subsequent write follows (seek is lazy), ie. after the final table
113 # in the font.
Just7842e561999-12-16 21:34:53 +0000114 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
jvrc63ac642008-06-17 20:41:15 +0000115 assert self.nextTableOffset == self.file.tell()
Just7842e561999-12-16 21:34:53 +0000116
117 if tag == 'head':
jvrea9dfa92002-05-12 17:14:50 +0000118 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +0000119 else:
jvrea9dfa92002-05-12 17:14:50 +0000120 entry.checkSum = calcChecksum(data)
Just7842e561999-12-16 21:34:53 +0000121 self.tables[tag] = entry
122
jvr28ae1962004-11-16 10:37:59 +0000123 def close(self):
Just7842e561999-12-16 21:34:53 +0000124 """All tables must have been written to disk. Now write the
125 directory.
126 """
127 tables = self.tables.items()
128 tables.sort()
129 if len(tables) <> self.numTables:
130 from fontTools import ttLib
131 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
132
133 directory = sstruct.pack(sfntDirectoryFormat, self)
134
135 self.file.seek(sfntDirectorySize)
jvrf509c0f2003-08-22 19:38:37 +0000136 seenHead = 0
Just7842e561999-12-16 21:34:53 +0000137 for tag, entry in tables:
jvrf509c0f2003-08-22 19:38:37 +0000138 if tag == "head":
139 seenHead = 1
jvrea9dfa92002-05-12 17:14:50 +0000140 directory = directory + entry.toString()
jvrf509c0f2003-08-22 19:38:37 +0000141 if seenHead:
142 self.calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000143 self.file.seek(0)
144 self.file.write(directory)
Just7842e561999-12-16 21:34:53 +0000145
jvrea9dfa92002-05-12 17:14:50 +0000146 def calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000147 # calculate checkSumAdjustment
148 tags = self.tables.keys()
jvr1ebda672008-03-08 20:29:30 +0000149 checksums = numpy.zeros(len(tags)+1, numpy.int32)
Just7842e561999-12-16 21:34:53 +0000150 for i in range(len(tags)):
151 checksums[i] = self.tables[tags[i]].checkSum
152
153 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
154 assert directory_end == len(directory)
155
jvrea9dfa92002-05-12 17:14:50 +0000156 checksums[-1] = calcChecksum(directory)
jvr1b7d54f2008-03-04 15:25:27 +0000157 checksum = numpy.add.reduce(checksums)
Just7842e561999-12-16 21:34:53 +0000158 # BiboAfba!
jvr1b7d54f2008-03-04 15:25:27 +0000159 checksumadjustment = numpy.array(0xb1b0afbaL - 0x100000000L,
160 numpy.int32) - checksum
Just7842e561999-12-16 21:34:53 +0000161 # write the checksum to the file
162 self.file.seek(self.tables['head'].offset + 8)
jvr58629632002-07-21 20:05:52 +0000163 self.file.write(struct.pack(">l", checksumadjustment))
jvr1ebda672008-03-08 20:29:30 +0000164
Just7842e561999-12-16 21:34:53 +0000165
166# -- sfnt directory helpers and cruft
167
168sfntDirectoryFormat = """
169 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000170 sfntVersion: 4s
171 numTables: H # number of tables
172 searchRange: H # (max2 <= numTables)*16
173 entrySelector: H # log2(max2 <= numTables)
174 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000175"""
176
177sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
178
179sfntDirectoryEntryFormat = """
180 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000181 tag: 4s
182 checkSum: l
183 offset: l
184 length: l
Just7842e561999-12-16 21:34:53 +0000185"""
186
187sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
188
189class SFNTDirectoryEntry:
190
jvrea9dfa92002-05-12 17:14:50 +0000191 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000192 sstruct.unpack(sfntDirectoryEntryFormat,
193 file.read(sfntDirectoryEntrySize), self)
194
jvrea9dfa92002-05-12 17:14:50 +0000195 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000196 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
197
jvrea9dfa92002-05-12 17:14:50 +0000198 def toString(self):
Just7842e561999-12-16 21:34:53 +0000199 return sstruct.pack(sfntDirectoryEntryFormat, self)
200
201 def __repr__(self):
202 if hasattr(self, "tag"):
203 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
204 else:
205 return "<SFNTDirectoryEntry at %x>" % id(self)
206
207
jvrea9dfa92002-05-12 17:14:50 +0000208def calcChecksum(data, start=0):
Just7842e561999-12-16 21:34:53 +0000209 """Calculate the checksum for an arbitrary block of data.
210 Optionally takes a 'start' argument, which allows you to
211 calculate a checksum in chunks by feeding it a previous
212 result.
213
214 If the data length is not a multiple of four, it assumes
215 it is to be padded with null byte.
216 """
217 from fontTools import ttLib
218 remainder = len(data) % 4
219 if remainder:
220 data = data + '\0' * (4-remainder)
jvr1b7d54f2008-03-04 15:25:27 +0000221 a = numpy.fromstring(struct.pack(">l", start) + data, numpy.int32)
jvr9be387c2008-03-01 11:43:01 +0000222 if sys.byteorder <> "big":
jvr1b7d54f2008-03-04 15:25:27 +0000223 a = a.byteswap()
224 return numpy.add.reduce(a)
Just7842e561999-12-16 21:34:53 +0000225
226
jvrea9dfa92002-05-12 17:14:50 +0000227def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000228 """Return the highest exponent of two, so that
229 (2 ** exponent) <= x
230 """
231 exponent = 0
232 while x:
233 x = x >> 1
234 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000235 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000236
237
jvrea9dfa92002-05-12 17:14:50 +0000238def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000239 """Calculate searchRange, entrySelector, rangeShift for the
240 sfnt directory. 'n' is the number of tables.
241 """
242 # This stuff needs to be stored in the file, because?
243 import math
jvrea9dfa92002-05-12 17:14:50 +0000244 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000245 searchRange = (2 ** exponent) * 16
246 entrySelector = exponent
247 rangeShift = n * 16 - searchRange
248 return searchRange, entrySelector, rangeShift
249