sfnt.py revision b0e5f299ffc5794f1960e694a855a05fc9cea01e
1"""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
21	def __init__(self, file, checkChecksums=1):
22		self.file = file
23		self.checkChecksums = checkChecksums
24		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()
35			entry.fromFile(self.file)
36			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
44
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)
56		if self.checkChecksums:
57			if tag == 'head':
58				# Beh: we have to special-case the 'head' table.
59				checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
60			else:
61				checksum = calcChecksum(data)
62			if self.checkChecksums > 1:
63				# 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
70	def __delitem__(self, tag):
71		del self.tables[tag]
72
73	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
83		self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
84		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':
113			entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
114		else:
115			entry.checkSum = calcChecksum(data)
116		self.tables[tag] = entry
117
118	def close(self, closeStream=1):
119		"""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:
132			directory = directory + entry.toString()
133		self.calcMasterChecksum(directory)
134		self.file.seek(0)
135		self.file.write(directory)
136		if closeStream:
137			self.file.close()
138
139	def calcMasterChecksum(self, directory):
140		# 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
149		checksums[-1] = calcChecksum(directory)
150		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
183	def fromFile(self, file):
184		sstruct.unpack(sfntDirectoryEntryFormat,
185				file.read(sfntDirectoryEntrySize), self)
186
187	def fromString(self, str):
188		sstruct.unpack(sfntDirectoryEntryFormat, str, self)
189
190	def toString(self):
191		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
200def calcChecksum(data, start=0):
201	"""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
219def maxPowerOfTwo(x):
220	"""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
227	return max(exponent - 1, 0)
228
229
230def getSearchRange(n):
231	"""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
236	exponent = maxPowerOfTwo(n)
237	searchRange = (2 ** exponent) * 16
238	entrySelector = exponent
239	rangeShift = n * 16 - searchRange
240	return searchRange, entrySelector, rangeShift
241
242