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/afmLib.py b/Lib/fontTools/afmLib.py
new file mode 100644
index 0000000..a725e83
--- /dev/null
+++ b/Lib/fontTools/afmLib.py
@@ -0,0 +1,265 @@
+"""Module for reading and writing AFM files."""
+
+# XXX reads AFM's generated by Fog, not tested with much else.
+# It does not implement the full spec (Adobe Technote 5004, Adobe Font Metrics
+# File Format Specification). Still, it should read most "common" AFM files.
+
+import re
+import string
+import types
+
+__version__ = "$Id: afmLib.py,v 1.1 1999-12-16 21:34:51 Just Exp $"
+
+
+# every single line starts with a "word"
+identifierRE = re.compile("^([A-Za-z]+).*")
+
+# regular expression to parse char lines
+charRE = re.compile(
+		"(-?\d+)"			# charnum
+		"\s*;\s*WX\s+"		# ; WX 
+		"(\d+)"			# width
+		"\s*;\s*N\s+"		# ; N 
+		"(\.?[A-Za-z0-9_]+)"	# charname
+		"\s*;\s*B\s+"		# ; B 
+		"(-?\d+)"			# left
+		"\s+"				# 
+		"(-?\d+)"			# bottom
+		"\s+"				# 
+		"(-?\d+)"			# right
+		"\s+"				# 
+		"(-?\d+)"			# top
+		"\s*;\s*"			# ; 
+		)
+
+# regular expression to parse kerning lines
+kernRE = re.compile(
+		"([.A-Za-z0-9_]+)"	# leftchar
+		"\s+"				# 
+		"([.A-Za-z0-9_]+)"	# rightchar
+		"\s+"				# 
+		"(-?\d+)"			# value
+		"\s*"				# 
+		)
+
+error = "AFM.error"
+
+class AFM:
+	
+	_keywords = ['StartFontMetrics',
+			'EndFontMetrics',
+			'StartCharMetrics',
+			'EndCharMetrics',
+			'StartKernData',
+			'StartKernPairs',
+			'EndKernPairs',
+			'EndKernData', ]
+	
+	def __init__(self, path = None):
+		self._attrs = {}
+		self._chars = {}
+		self._kerning = {}
+		self._index = {}
+		self._comments = []
+		if path is not None:
+			self.read(path)
+	
+	def read(self, path):
+		lines = readlines(path)
+		for line in lines:
+			if not string.strip(line):
+				continue
+			m = identifierRE.match(line)
+			if m is None:
+				raise error, "syntax error in AFM file: " + `line`
+			
+			pos = m.regs[1][1]
+			word = line[:pos]
+			rest = string.strip(line[pos:])
+			if word in self._keywords:
+				continue
+			if word == 'C':
+				self.parsechar(rest)
+			elif word == "KPX":
+				self.parsekernpair(rest)
+			else:
+				self.parseattr(word, rest)
+	
+	def parsechar(self, rest):
+		m = charRE.match(rest)
+		if m is None:
+			raise error, "syntax error in AFM file: " + `rest`
+		things = []
+		for fr, to in m.regs[1:]:
+			things.append(rest[fr:to])
+		charname = things[2]
+		del things[2]
+		charnum, width, l, b, r, t = map(string.atoi, things)
+		self._chars[charname] = charnum, width, (l, b, r, t)
+	
+	def parsekernpair(self, rest):
+		m = kernRE.match(rest)
+		if m is None:
+			raise error, "syntax error in AFM file: " + `rest`
+		things = []
+		for fr, to in m.regs[1:]:
+			things.append(rest[fr:to])
+		leftchar, rightchar, value = things
+		value = string.atoi(value)
+		self._kerning[(leftchar, rightchar)] = value
+	
+	def parseattr(self, word, rest):
+		if word == "FontBBox":
+			l, b, r, t = map(string.atoi, string.split(rest))
+			self._attrs[word] = l, b, r, t
+		elif word == "Comment":
+			self._comments.append(rest)
+		else:
+			try:
+				value = string.atoi(rest)
+			except (ValueError, OverflowError):
+				self._attrs[word] = rest
+			else:
+				self._attrs[word] = value
+	
+	def write(self, path, sep = '\r'):
+		import time
+		lines = [	"StartFontMetrics 2.0",
+				"Comment Generated by afmLib, version %s; at %s" % 
+						(string.split(__version__)[2],
+						time.strftime("%m/%d/%Y %H:%M:%S", 
+						time.localtime(time.time())))]
+		
+		# write attributes
+		items = self._attrs.items()
+		items.sort()		# XXX proper ordering???
+		for attr, value in items:
+			if attr == "FontBBox":
+				value = string.join(map(str, value), " ")
+			lines.append(attr + " " + str(value))
+		
+		# write char metrics
+		lines.append("StartCharMetrics " + `len(self._chars)`)
+		items = map(lambda (charname, (charnum, width, box)):
+			(charnum, (charname, width, box)),
+			self._chars.items())
+		
+		def myCmp(a, b):
+			"""Custom compare function to make sure unencoded chars (-1) 
+			end up at the end of the list after sorting."""
+			if a[0] == -1:
+				a = (0xffff,) + a[1:]  # 0xffff is an arbitrary large number
+			if b[0] == -1:
+				b = (0xffff,) + b[1:]
+			return cmp(a, b)
+		items.sort(myCmp)
+		
+		for charnum, (charname, width, (l, b, r, t)) in items:
+			lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" %
+					(charnum, width, charname, l, b, r, t))
+		lines.append("EndCharMetrics")
+		
+		# write kerning info
+		lines.append("StartKernData")
+		lines.append("StartKernPairs " + `len(self._kerning)`)
+		items = self._kerning.items()
+		items.sort()		# XXX is order important?
+		for (leftchar, rightchar), value in items:
+			lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
+		
+		lines.append("EndKernPairs")
+		lines.append("EndKernData")
+		lines.append("EndFontMetrics")
+		
+		writelines(path, lines, sep)
+	
+	def has_kernpair(self, pair):
+		return self._kerning.has_key(pair)
+	
+	def kernpairs(self):
+		return self._kerning.keys()
+	
+	def has_char(self, char):
+		return self._chars.has_key(char)
+	
+	def chars(self):
+		return self._chars.keys()
+	
+	def comments(self):
+		return self._comments
+	
+	def __getattr__(self, attr):
+		if self._attrs.has_key(attr):
+			return self._attrs[attr]
+		else:
+			raise AttributeError, attr
+	
+	def __setattr__(self, attr, value):
+		# all attrs *not* starting with "_" are consider to be AFM keywords
+		if attr[:1] == "_":
+			self.__dict__[attr] = value
+		else:
+			self._attrs[attr] = value
+	
+	def __getitem__(self, key):
+		if type(key) == types.TupleType:
+			# key is a tuple, return the kernpair
+			if self._kerning.has_key(key):
+				return self._kerning[key]
+			else:
+				raise KeyError, "no kerning pair: " + str(key)
+		else:
+			# return the metrics instead
+			if self._chars.has_key(key):
+				return self._chars[key]
+			else:
+				raise KeyError, "metrics index " + str(key) + " out of range"
+	
+	def __repr__(self):
+		if hasattr(self, "FullName"):
+			return '<AFM object for %s>' % self.FullName
+		else:
+			return '<AFM object at %x>' % id(self)
+
+
+def readlines(path):
+	f = open(path, 'rb')
+	data = f.read()
+	f.close()
+	# read any text file, regardless whether it's formatted for Mac, Unix or Dos
+	sep = ""
+	if '\r' in data:
+		sep = sep + '\r'	# mac or dos
+	if '\n' in data:
+		sep = sep + '\n'	# unix or dos
+	return string.split(data, sep)
+
+def writelines(path, lines, sep = '\r'):
+	f = open(path, 'wb')
+	for line in lines:
+		f.write(line + sep)
+	f.close()
+	
+	
+
+if __name__ == "__main__":
+	import macfs
+	fss, ok = macfs.StandardGetFile('TEXT')
+	if ok:
+		path = fss.as_pathname()
+		afm = AFM(path)
+		char = 'A'
+		if afm.has_char(char):
+			print afm[char]	# print charnum, width and boundingbox
+		pair = ('A', 'V')
+		if afm.has_kernpair(pair):
+			print afm[pair]	# print kerning value for pair
+		print afm.Version	# various other afm entries have become attributes
+		print afm.Weight
+		# afm.comments() returns a list of all Comment lines found in the AFM
+		print afm.comments()
+		#print afm.chars()
+		#print afm.kernpairs()
+		print afm
+		afm.write(path + ".xxx")
+