sfnt.py revision 28ae1962292b66ad67117aef2a99d5735a70b779
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
19
20class SFNTReader:
21
22	def __init__(self, file, checkChecksums=1):
23		self.file = file
24		self.checkChecksums = checkChecksums
25		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()
36			entry.fromFile(self.file)
37			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
45
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)
57		if self.checkChecksums:
58			if tag == 'head':
59				# Beh: we have to special-case the 'head' table.
60				checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
61			else:
62				checksum = calcChecksum(data)
63			if self.checkChecksums > 1:
64				# 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
71	def __delitem__(self, tag):
72		del self.tables[tag]
73
74	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
84		self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
85		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
96			# of the data is still the same, we allow overwriting it.
97			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':
114			entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
115		else:
116			entry.checkSum = calcChecksum(data)
117		self.tables[tag] = entry
118
119	def close(self):
120		"""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		seenHead = 0
133		for tag, entry in tables:
134			if tag == "head":
135				seenHead = 1
136			directory = directory + entry.toString()
137		if seenHead:
138			self.calcMasterChecksum(directory)
139		self.file.seek(0)
140		self.file.write(directory)
141
142	def calcMasterChecksum(self, directory):
143		# 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
152		checksums[-1] = calcChecksum(directory)
153		checksum = Numeric.add.reduce(checksums)
154		# BiboAfba!
155		checksumadjustment = Numeric.array(0xb1b0afbaL - 0x100000000L,
156				Numeric.Int32) - checksum
157		# write the checksum to the file
158		self.file.seek(self.tables['head'].offset + 8)
159		self.file.write(struct.pack(">l", checksumadjustment))
160
161
162# -- sfnt directory helpers and cruft
163
164sfntDirectoryFormat = """
165		> # big endian
166		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
171"""
172
173sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
174
175sfntDirectoryEntryFormat = """
176		> # big endian
177		tag:            4s
178		checkSum:       l
179		offset:         l
180		length:         l
181"""
182
183sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
184
185class SFNTDirectoryEntry:
186
187	def fromFile(self, file):
188		sstruct.unpack(sfntDirectoryEntryFormat,
189				file.read(sfntDirectoryEntrySize), self)
190
191	def fromString(self, str):
192		sstruct.unpack(sfntDirectoryEntryFormat, str, self)
193
194	def toString(self):
195		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
204def calcChecksum(data, start=0):
205	"""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
223def maxPowerOfTwo(x):
224	"""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
231	return max(exponent - 1, 0)
232
233
234def getSearchRange(n):
235	"""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
240	exponent = maxPowerOfTwo(n)
241	searchRange = (2 ** exponent) * 16
242	entrySelector = exponent
243	rangeShift = n * 16 - searchRange
244	return searchRange, entrySelector, rangeShift
245
246