blob: 0094ecbcf8d2459401e383ab78f6f2e150cc5d71 [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)
110 self.file.seek(self.nextTableOffset)
111 # make sure we're actually where we want to be. (XXX old cStringIO bug)
112 self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
113
114 if tag == 'head':
jvrea9dfa92002-05-12 17:14:50 +0000115 entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
Just7842e561999-12-16 21:34:53 +0000116 else:
jvrea9dfa92002-05-12 17:14:50 +0000117 entry.checkSum = calcChecksum(data)
Just7842e561999-12-16 21:34:53 +0000118 self.tables[tag] = entry
119
jvr28ae1962004-11-16 10:37:59 +0000120 def close(self):
Just7842e561999-12-16 21:34:53 +0000121 """All tables must have been written to disk. Now write the
122 directory.
123 """
124 tables = self.tables.items()
125 tables.sort()
126 if len(tables) <> self.numTables:
127 from fontTools import ttLib
128 raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
129
130 directory = sstruct.pack(sfntDirectoryFormat, self)
131
132 self.file.seek(sfntDirectorySize)
jvrf509c0f2003-08-22 19:38:37 +0000133 seenHead = 0
Just7842e561999-12-16 21:34:53 +0000134 for tag, entry in tables:
jvrf509c0f2003-08-22 19:38:37 +0000135 if tag == "head":
136 seenHead = 1
jvrea9dfa92002-05-12 17:14:50 +0000137 directory = directory + entry.toString()
jvrf509c0f2003-08-22 19:38:37 +0000138 if seenHead:
139 self.calcMasterChecksum(directory)
Just7842e561999-12-16 21:34:53 +0000140 self.file.seek(0)
141 self.file.write(directory)
Just7842e561999-12-16 21:34:53 +0000142
jvrea9dfa92002-05-12 17:14:50 +0000143 def calcMasterChecksum(self, directory):
Just7842e561999-12-16 21:34:53 +0000144 # calculate checkSumAdjustment
145 tags = self.tables.keys()
jvr1b7d54f2008-03-04 15:25:27 +0000146 checksums = numpy.zeros(len(tags)+1)
Just7842e561999-12-16 21:34:53 +0000147 for i in range(len(tags)):
148 checksums[i] = self.tables[tags[i]].checkSum
149
150 directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
151 assert directory_end == len(directory)
152
jvrea9dfa92002-05-12 17:14:50 +0000153 checksums[-1] = calcChecksum(directory)
jvr1b7d54f2008-03-04 15:25:27 +0000154 checksum = numpy.add.reduce(checksums)
Just7842e561999-12-16 21:34:53 +0000155 # BiboAfba!
jvr1b7d54f2008-03-04 15:25:27 +0000156 checksumadjustment = numpy.array(0xb1b0afbaL - 0x100000000L,
157 numpy.int32) - checksum
Just7842e561999-12-16 21:34:53 +0000158 # write the checksum to the file
159 self.file.seek(self.tables['head'].offset + 8)
jvr58629632002-07-21 20:05:52 +0000160 self.file.write(struct.pack(">l", checksumadjustment))
Just7842e561999-12-16 21:34:53 +0000161
162
163# -- sfnt directory helpers and cruft
164
165sfntDirectoryFormat = """
166 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000167 sfntVersion: 4s
168 numTables: H # number of tables
169 searchRange: H # (max2 <= numTables)*16
170 entrySelector: H # log2(max2 <= numTables)
171 rangeShift: H # numTables*16-searchRange
Just7842e561999-12-16 21:34:53 +0000172"""
173
174sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
175
176sfntDirectoryEntryFormat = """
177 > # big endian
jvrb0e5f292002-05-13 11:21:48 +0000178 tag: 4s
179 checkSum: l
180 offset: l
181 length: l
Just7842e561999-12-16 21:34:53 +0000182"""
183
184sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
185
186class SFNTDirectoryEntry:
187
jvrea9dfa92002-05-12 17:14:50 +0000188 def fromFile(self, file):
Just7842e561999-12-16 21:34:53 +0000189 sstruct.unpack(sfntDirectoryEntryFormat,
190 file.read(sfntDirectoryEntrySize), self)
191
jvrea9dfa92002-05-12 17:14:50 +0000192 def fromString(self, str):
Just7842e561999-12-16 21:34:53 +0000193 sstruct.unpack(sfntDirectoryEntryFormat, str, self)
194
jvrea9dfa92002-05-12 17:14:50 +0000195 def toString(self):
Just7842e561999-12-16 21:34:53 +0000196 return sstruct.pack(sfntDirectoryEntryFormat, self)
197
198 def __repr__(self):
199 if hasattr(self, "tag"):
200 return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
201 else:
202 return "<SFNTDirectoryEntry at %x>" % id(self)
203
204
jvrea9dfa92002-05-12 17:14:50 +0000205def calcChecksum(data, start=0):
Just7842e561999-12-16 21:34:53 +0000206 """Calculate the checksum for an arbitrary block of data.
207 Optionally takes a 'start' argument, which allows you to
208 calculate a checksum in chunks by feeding it a previous
209 result.
210
211 If the data length is not a multiple of four, it assumes
212 it is to be padded with null byte.
213 """
214 from fontTools import ttLib
215 remainder = len(data) % 4
216 if remainder:
217 data = data + '\0' * (4-remainder)
jvr1b7d54f2008-03-04 15:25:27 +0000218 a = numpy.fromstring(struct.pack(">l", start) + data, numpy.int32)
jvr9be387c2008-03-01 11:43:01 +0000219 if sys.byteorder <> "big":
jvr1b7d54f2008-03-04 15:25:27 +0000220 a = a.byteswap()
221 return numpy.add.reduce(a)
Just7842e561999-12-16 21:34:53 +0000222
223
jvrea9dfa92002-05-12 17:14:50 +0000224def maxPowerOfTwo(x):
Just7842e561999-12-16 21:34:53 +0000225 """Return the highest exponent of two, so that
226 (2 ** exponent) <= x
227 """
228 exponent = 0
229 while x:
230 x = x >> 1
231 exponent = exponent + 1
Justfdea99d2000-08-23 12:34:44 +0000232 return max(exponent - 1, 0)
Just7842e561999-12-16 21:34:53 +0000233
234
jvrea9dfa92002-05-12 17:14:50 +0000235def getSearchRange(n):
Just7842e561999-12-16 21:34:53 +0000236 """Calculate searchRange, entrySelector, rangeShift for the
237 sfnt directory. 'n' is the number of tables.
238 """
239 # This stuff needs to be stored in the file, because?
240 import math
jvrea9dfa92002-05-12 17:14:50 +0000241 exponent = maxPowerOfTwo(n)
Just7842e561999-12-16 21:34:53 +0000242 searchRange = (2 ** exponent) * 16
243 entrySelector = exponent
244 rangeShift = n * 16 - searchRange
245 return searchRange, entrySelector, rangeShift
246