Created a new library directory called "FreeLib". All OpenSource RFMKII components will reside there, fontTools being the flagship.


git-svn-id: svn://svn.code.sf.net/p/fonttools/code/trunk@2 4cde692c-a291-49d1-8350-778aa11640f8
diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py
new file mode 100644
index 0000000..2fdeb99
--- /dev/null
+++ b/Lib/fontTools/ttLib/sfnt.py
@@ -0,0 +1,230 @@
+"""ttLib/sfnt.py -- low-level module to deal with the sfnt file format.
+
+Defines two public classes:
+	SFNTReader
+	SFNTWriter
+
+(Normally you don't have to use these classes explicitly; they are 
+used automatically by ttLib.TTFont.)
+
+The reading and writing of sfnt files is separated in two distinct 
+classes, since whenever to number of tables changes or whenever
+a table's length chages you need to rewrite the whole file anyway.
+"""
+
+import struct, sstruct
+import Numeric
+import os
+
+class SFNTReader:
+	
+	def __init__(self, file, checkchecksums=1):
+		self.file = file
+		self.checkchecksums = checkchecksums
+		data = self.file.read(sfntDirectorySize)
+		if len(data) <> sfntDirectorySize:
+			from fontTools import ttLib
+			raise ttLib.TTLibError, "Not a TrueType or OpenType font (not enough data)"
+		sstruct.unpack(sfntDirectoryFormat, data, self)
+		if self.sfntVersion not in ("\000\001\000\000", "OTTO", "true"):
+			from fontTools import ttLib
+			raise ttLib.TTLibError, "Not a TrueType or OpenType font (bad sfntVersion)"
+		self.tables = {}
+		for i in range(self.numTables):
+			entry = SFNTDirectoryEntry()
+			entry.fromfile(self.file)
+			self.tables[entry.tag] = entry
+	
+	def has_key(self, tag):
+		return self.tables.has_key(tag)
+	
+	def keys(self):
+		return self.tables.keys()
+	
+	def __getitem__(self, tag):
+		"""Fetch the raw table data."""
+		entry = self.tables[tag]
+		self.file.seek(entry.offset)
+		data = self.file.read(entry.length)
+		if self.checkchecksums:
+			if tag == 'head':
+				# Beh: we have to special-case the 'head' table.
+				checksum = calcchecksum(data[:8] + '\0\0\0\0' + data[12:])
+			else:
+				checksum = calcchecksum(data)
+			if self.checkchecksums > 1:
+				# Be obnoxious, and barf when it's wrong
+				assert checksum == entry.checksum, "bad checksum for '%s' table" % tag
+			elif checksum <> entry.checkSum:
+				# Be friendly, and just print a warning.
+				print "bad checksum for '%s' table" % tag
+		return data
+	
+	def close(self):
+		self.file.close()
+
+
+class SFNTWriter:
+	
+	def __init__(self, file, numTables, sfntVersion="\000\001\000\000"):
+		self.file = file
+		self.numTables = numTables
+		self.sfntVersion = sfntVersion
+		self.searchRange, self.entrySelector, self.rangeShift = getsearchrange(numTables)
+		self.nextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize
+		# clear out directory area
+		self.file.seek(self.nextTableOffset)
+		# make sure we're actually where we want to be. (XXX old cStringIO bug)
+		self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
+		self.tables = {}
+	
+	def __setitem__(self, tag, data):
+		"""Write raw table data to disk."""
+		if self.tables.has_key(tag):
+			# We've written this table to file before. If the length
+			# of the data is still the same, we allow overwritng it.
+			entry = self.tables[tag]
+			if len(data) <> entry.length:
+				from fontTools import ttLib
+				raise ttLib.TTLibError, "cannot rewrite '%s' table: length does not match directory entry" % tag
+		else:
+			entry = SFNTDirectoryEntry()
+			entry.tag = tag
+			entry.offset = self.nextTableOffset
+			entry.length = len(data)
+			self.nextTableOffset = self.nextTableOffset + ((len(data) + 3) & ~3)
+		self.file.seek(entry.offset)
+		self.file.write(data)
+		self.file.seek(self.nextTableOffset)
+		# make sure we're actually where we want to be. (XXX old cStringIO bug)
+		self.file.write('\0' * (self.nextTableOffset - self.file.tell()))
+		
+		if tag == 'head':
+			entry.checkSum = calcchecksum(data[:8] + '\0\0\0\0' + data[12:])
+		else:
+			entry.checkSum = calcchecksum(data)
+		self.tables[tag] = entry
+	
+	def close(self):
+		"""All tables must have been written to disk. Now write the
+		directory.
+		"""
+		tables = self.tables.items()
+		tables.sort()
+		if len(tables) <> self.numTables:
+			from fontTools import ttLib
+			raise ttLib.TTLibError, "wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))
+		
+		directory = sstruct.pack(sfntDirectoryFormat, self)
+		
+		self.file.seek(sfntDirectorySize)
+		for tag, entry in tables:
+			directory = directory + entry.tostring()
+		self.calcmasterchecksum(directory)
+		self.file.seek(0)
+		self.file.write(directory)
+		self.file.close()
+	
+	def calcmasterchecksum(self, directory):
+		# calculate checkSumAdjustment
+		tags = self.tables.keys()
+		checksums = Numeric.zeros(len(tags)+1)
+		for i in range(len(tags)):
+			checksums[i] = self.tables[tags[i]].checkSum
+		
+		directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize
+		assert directory_end == len(directory)
+		
+		checksums[-1] = calcchecksum(directory)
+		checksum = Numeric.add.reduce(checksums)
+		# BiboAfba!
+		checksumadjustment = Numeric.array(0xb1b0afba) - checksum
+		# write the checksum to the file
+		self.file.seek(self.tables['head'].offset + 8)
+		self.file.write(struct.pack("l", checksumadjustment))
+		
+
+# -- sfnt directory helpers and cruft
+
+sfntDirectoryFormat = """
+		> # big endian
+		sfntVersion: 	4s
+		numTables:		H		# number of tables
+		searchRange:	H		# (max2 <= numTables)*16
+		entrySelector:	H		# log2(max2 <= numTables)
+		rangeShift:		H		# numTables*16-searchRange
+"""
+
+sfntDirectorySize = sstruct.calcsize(sfntDirectoryFormat)
+
+sfntDirectoryEntryFormat = """
+		> # big endian
+		tag:		4s
+		checkSum:	l
+		offset:		l
+		length:		l
+"""
+
+sfntDirectoryEntrySize = sstruct.calcsize(sfntDirectoryEntryFormat)
+
+class SFNTDirectoryEntry:
+	
+	def fromfile(self, file):
+		sstruct.unpack(sfntDirectoryEntryFormat, 
+				file.read(sfntDirectoryEntrySize), self)
+	
+	def fromstring(self, str):
+		sstruct.unpack(sfntDirectoryEntryFormat, str, self)
+	
+	def tostring(self):
+		return sstruct.pack(sfntDirectoryEntryFormat, self)
+	
+	def __repr__(self):
+		if hasattr(self, "tag"):
+			return "<SFNTDirectoryEntry '%s' at %x>" % (self.tag, id(self))
+		else:
+			return "<SFNTDirectoryEntry at %x>" % id(self)
+
+
+def calcchecksum(data, start=0):
+	"""Calculate the checksum for an arbitrary block of data.
+	Optionally takes a 'start' argument, which allows you to
+	calculate a checksum in chunks by feeding it a previous
+	result.
+	
+	If the data length is not a multiple of four, it assumes
+	it is to be padded with null byte. 
+	"""
+	from fontTools import ttLib
+	remainder = len(data) % 4
+	if remainder:
+		data = data + '\0' * (4-remainder)
+	a = Numeric.fromstring(struct.pack(">l", start) + data, Numeric.Int32)
+	if ttLib.endian <> "big":
+		a = a.byteswapped()
+	return Numeric.add.reduce(a)
+
+
+def maxpoweroftwo(x):
+	"""Return the highest exponent of two, so that
+	(2 ** exponent) <= x
+	"""
+	exponent = 0
+	while x:
+		x = x >> 1
+		exponent = exponent + 1
+	return exponent - 1
+
+
+def getsearchrange(n):
+	"""Calculate searchRange, entrySelector, rangeShift for the
+	sfnt directory. 'n' is the number of tables.
+	"""
+	# This stuff needs to be stored in the file, because?
+	import math
+	exponent = maxpoweroftwo(n)
+	searchRange = (2 ** exponent) * 16
+	entrySelector = exponent
+	rangeShift = n * 16 - searchRange
+	return searchRange, entrySelector, rangeShift
+