sfnt.py revision 0e2aecec53da493c44d6a5c253910a9475da218a
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 sys
16import struct, sstruct
17import numpy
18import os
19
20
21class SFNTReader:
22
23	def __init__(self, file, checkChecksums=1, fontNumber=-1):
24		self.file = file
25		self.checkChecksums = checkChecksums
26		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 == "ttcf":
32			assert ttcHeaderSize == sfntDirectorySize
33			sstruct.unpack(ttcHeaderFormat, data, self)
34			assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version
35			if not 0 <= fontNumber < self.numFonts:
36				from fontTools import ttLib
37				raise ttLib.TTLibError, "specify a font number between 0 and %d (inclusive)" % (self.numFonts - 1)
38			offsetTable = struct.unpack(">%dL" % self.numFonts, self.file.read(self.numFonts * 4))
39			if self.Version == 0x00020000:
40				pass # ignoring version 2.0 signatures
41			self.file.seek(offsetTable[fontNumber])
42			data = self.file.read(sfntDirectorySize)
43			sstruct.unpack(sfntDirectoryFormat, data, self)
44		if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
45			from fontTools import ttLib
46			raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
47		self.tables = {}
48		for i in range(self.numTables):
49			entry = SFNTDirectoryEntry()
50			entry.fromFile(self.file)
51			if entry.length > 0:
52				self.tables[entry.tag] = entry
53			else:
54				# Ignore zero-length tables. This doesn't seem to be documented,
55				# yet it's apparently how the Windows TT rasterizer behaves.
56				# Besides, at least one font has been sighted which actually
57				# *has* a zero-length table.
58				pass
59
60	def has_key(self, tag):
61		return self.tables.has_key(tag)
62
63	def keys(self):
64		return self.tables.keys()
65
66	def __getitem__(self, tag):
67		"""Fetch the raw table data."""
68		entry = self.tables[tag]
69		self.file.seek(entry.offset)
70		data = self.file.read(entry.length)
71		if self.checkChecksums:
72			if tag == 'head':
73				# Beh: we have to special-case the 'head' table.
74				checksum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
75			else:
76				checksum = calcChecksum(data)
77			if self.checkChecksums > 1:
78				# Be obnoxious, and barf when it's wrong
79				assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
80			elif checksum <> entry.checkSum:
81				# Be friendly, and just print a warning.
82				print "bad checksum for '%s' table" % tag
83		return data
84
85	def __delitem__(self, tag):
86		del self.tables[tag]
87
88	def close(self):
89		self.file.close()
90
91
92class SFNTWriter:
93
94	def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
95		self.file = file
96		self.numTables = numTables
97		self.sfntVersion = sfntVersion
98		self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables)
99		self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
100		# clear out directory area
101		self.file.seek(self.nextTableOffset)
102		# make sure we're actually where we want to be. (XXX old cStringIO bug)
103		self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
104		self.tables = {}
105
106	def __setitem__(self, tag, data):
107		"""Write raw table data to disk."""
108		if self.tables.has_key(tag):
109			# We've written this table to file before. If the length
110			# of the data is still the same, we allow overwriting it.
111			entry = self.tables[tag]
112			if len(data) <> entry.length:
113				from fontTools import ttLib
114				raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
115		else:
116			entry = SFNTDirectoryEntry()
117			entry.tag = tag
118			entry.offset = self.nextTableOffset
119			entry.length = len(data)
120			self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
121		self.file.seek(entry.offset)
122		self.file.write(data)
123		# Add NUL bytes to pad the table data to a 4-byte boundary.
124		# Don't depend on f.seek() as we need to add the padding even if no
125		# subsequent write follows (seek is lazy), ie. after the final table
126		# in the font.
127		self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
128		assert self.nextTableOffset == self.file.tell()
129
130		if tag == 'head':
131			entry.checkSum = calcChecksum(data[:8] + '\0\0\0\0' + data[12:])
132		else:
133			entry.checkSum = calcChecksum(data)
134		self.tables[tag] = entry
135
136	def close(self):
137		"""All tables must have been written to disk. Now write the
138		directory.
139		"""
140		tables = self.tables.items()
141		tables.sort()
142		if len(tables) <> self.numTables:
143			from fontTools import ttLib
144			raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
145
146		directory = sstruct.pack(sfntDirectoryFormat, self)
147
148		self.file.seek(sfntDirectorySize)
149		seenHead = 0
150		for tag, entry in tables:
151			if tag == "head":
152				seenHead = 1
153			directory = directory + entry.toString()
154		if seenHead:
155			self.calcMasterChecksum(directory)
156		self.file.seek(0)
157		self.file.write(directory)
158
159	def calcMasterChecksum(self, directory):
160		# calculate checkSumAdjustment
161		tags = self.tables.keys()
162		checksums = numpy.zeros(len(tags)+1, numpy.uint32)
163		for i in range(len(tags)):
164			checksums[i] = self.tables[tags[i]].checkSum
165
166		directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
167		assert directory_end == len(directory)
168
169		checksums[-1] = calcChecksum(directory)
170		checksum = numpy.add.reduce(checksums,dtype=numpy.uint32)
171		# BiboAfba!
172		checksumadjustment = int(numpy.subtract.reduce(numpy.array([0xB1B0AFBA, checksum], numpy.uint32)))
173		# write the checksum to the file
174		self.file.seek(self.tables['head'].offset + 8)
175		self.file.write(struct.pack(">L", checksumadjustment))
176
177
178# -- sfnt directory helpers and cruft
179
180ttcHeaderFormat = """
181		> # big endian
182		TTCTag:                  4s # "ttcf"
183		Version:                 L  # 0x00010000 or 0x00020000
184		numFonts:                L  # number of fonts
185		# OffsetTable[numFonts]: L  # array with offsets from beginning of file
186		# ulDsigTag:             L  # version 2.0 only
187		# ulDsigLength:          L  # version 2.0 only
188		# ulDsigOffset:          L  # version 2.0 only
189"""
190
191ttcHeaderSize = sstruct.calcsize(ttcHeaderFormat)
192
193sfntDirectoryFormat = """
194		> # big endian
195		sfntVersion:    4s
196		numTables:      H    # number of tables
197		searchRange:    H    # (max2 <= numTables)*16
198		entrySelector:  H    # log2(max2 <= numTables)
199		rangeShift:     H    # numTables*16-searchRange
200"""
201
202sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
203
204sfntDirectoryEntryFormat = """
205		> # big endian
206		tag:            4s
207		checkSum:       L
208		offset:         L
209		length:         L
210"""
211
212sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
213
214class SFNTDirectoryEntry:
215
216	def fromFile(self, file):
217		sstruct.unpack(sfntDirectoryEntryFormat,
218				file.read(sfntDirectoryEntrySize), self)
219
220	def fromString(self, str):
221		sstruct.unpack(sfntDirectoryEntryFormat, str, self)
222
223	def toString(self):
224		return sstruct.pack(sfntDirectoryEntryFormat, self)
225
226	def __repr__(self):
227		if hasattr(self, "tag"):
228			return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
229		else:
230			return "<SFNTDirectoryEntry at %x>" % id(self)
231
232
233def calcChecksum(data, start=0):
234	"""Calculate the checksum for an arbitrary block of data.
235	Optionally takes a 'start' argument, which allows you to
236	calculate a checksum in chunks by feeding it a previous
237	result.
238
239	If the data length is not a multiple of four, it assumes
240	it is to be padded with null byte.
241	"""
242	from fontTools import ttLib
243	remainder = len(data) % 4
244	if remainder:
245		data = data + '\0' * (4-remainder)
246	data = struct.unpack(">%dL"%(len(data)/4), data)
247	a = numpy.array((start,)+data, numpy.uint32)
248	return int(numpy.sum(a,dtype=numpy.uint32))
249
250
251def maxPowerOfTwo(x):
252	"""Return the highest exponent of two, so that
253	(2 ** exponent) <= x
254	"""
255	exponent = 0
256	while x:
257		x = x >> 1
258		exponent = exponent + 1
259	return max(exponent - 1, 0)
260
261
262def getSearchRange(n):
263	"""Calculate searchRange, entrySelector, rangeShift for the
264	sfnt directory. 'n' is the number of tables.
265	"""
266	# This stuff needs to be stored in the file, because?
267	import math
268	exponent = maxPowerOfTwo(n)
269	searchRange = (2 ** exponent) * 16
270	entrySelector = exponent
271	rangeShift = n * 16 - searchRange
272	return searchRange, entrySelector, rangeShift
273
274