afmLib.py revision dc7e6f3e5563a853477ebe26166b002c158dbe8b
17842e56b97ce677b83bdab09cda48bc2d89ac75aJust"""Module for reading and writing AFM files."""
27842e56b97ce677b83bdab09cda48bc2d89ac75aJust
37842e56b97ce677b83bdab09cda48bc2d89ac75aJust# XXX reads AFM's generated by Fog, not tested with much else.
47842e56b97ce677b83bdab09cda48bc2d89ac75aJust# It does not implement the full spec (Adobe Technote 5004, Adobe Font Metrics
57842e56b97ce677b83bdab09cda48bc2d89ac75aJust# File Format Specification). Still, it should read most "common" AFM files.
67842e56b97ce677b83bdab09cda48bc2d89ac75aJust
77842e56b97ce677b83bdab09cda48bc2d89ac75aJustimport re
87842e56b97ce677b83bdab09cda48bc2d89ac75aJustimport string
97842e56b97ce677b83bdab09cda48bc2d89ac75aJustimport types
107842e56b97ce677b83bdab09cda48bc2d89ac75aJust
115910f971461cd3df60be89acd73672a22b62cba1jvr__version__ = "$Id: afmLib.py,v 1.6 2003-05-24 12:50:47 jvr Exp $"
127842e56b97ce677b83bdab09cda48bc2d89ac75aJust
137842e56b97ce677b83bdab09cda48bc2d89ac75aJust
147842e56b97ce677b83bdab09cda48bc2d89ac75aJust# every single line starts with a "word"
157842e56b97ce677b83bdab09cda48bc2d89ac75aJustidentifierRE = re.compile("^([A-Za-z]+).*")
167842e56b97ce677b83bdab09cda48bc2d89ac75aJust
177842e56b97ce677b83bdab09cda48bc2d89ac75aJust# regular expression to parse char lines
187842e56b97ce677b83bdab09cda48bc2d89ac75aJustcharRE = re.compile(
197842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# charnum
207842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*;\s*WX\s+"		# ; WX
2131ad351b75c56d7bf0d2d059e156baff14eafe68jvr		"(-?\d+)"			# width
227842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*;\s*N\s+"		# ; N
233c3c32c5a4db928ab702d54adddb32e9a3cf2beajvr		"([.A-Za-z0-9_]+)"	# charname
247842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*;\s*B\s+"		# ; B
257842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# left
267842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
277842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# bottom
287842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
297842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# right
307842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
317842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# top
327842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*;\s*"			# ;
337842e56b97ce677b83bdab09cda48bc2d89ac75aJust		)
347842e56b97ce677b83bdab09cda48bc2d89ac75aJust
357842e56b97ce677b83bdab09cda48bc2d89ac75aJust# regular expression to parse kerning lines
367842e56b97ce677b83bdab09cda48bc2d89ac75aJustkernRE = re.compile(
377842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"([.A-Za-z0-9_]+)"	# leftchar
387842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
397842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"([.A-Za-z0-9_]+)"	# rightchar
407842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
417842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# value
427842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*"				#
437842e56b97ce677b83bdab09cda48bc2d89ac75aJust		)
447842e56b97ce677b83bdab09cda48bc2d89ac75aJust
4532f868492ce3f4a06aaf97fac1e4655c281c233aJust# regular expressions to parse composite info lines of the form:
4632f868492ce3f4a06aaf97fac1e4655c281c233aJust# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
4732f868492ce3f4a06aaf97fac1e4655c281c233aJustcompositeRE = re.compile(
4832f868492ce3f4a06aaf97fac1e4655c281c233aJust		"([.A-Za-z0-9_]+)"	# char name
4932f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s+"				#
5032f868492ce3f4a06aaf97fac1e4655c281c233aJust		"(\d+)"				# number of parts
5132f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s*;\s*"			#
5232f868492ce3f4a06aaf97fac1e4655c281c233aJust		)
5332f868492ce3f4a06aaf97fac1e4655c281c233aJustcomponentRE = re.compile(
5432f868492ce3f4a06aaf97fac1e4655c281c233aJust		"PCC\s+"			# PPC
5532f868492ce3f4a06aaf97fac1e4655c281c233aJust		"([.A-Za-z0-9_]+)"	# base char name
5632f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s+"				#
5732f868492ce3f4a06aaf97fac1e4655c281c233aJust		"(-?\d+)"			# x offset
5832f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s+"				#
5932f868492ce3f4a06aaf97fac1e4655c281c233aJust		"(-?\d+)"			# y offset
6032f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s*;\s*"			#
6132f868492ce3f4a06aaf97fac1e4655c281c233aJust		)
6232f868492ce3f4a06aaf97fac1e4655c281c233aJust
6332f868492ce3f4a06aaf97fac1e4655c281c233aJustpreferredAttributeOrder = [
6432f868492ce3f4a06aaf97fac1e4655c281c233aJust		"FontName",
6532f868492ce3f4a06aaf97fac1e4655c281c233aJust		"FullName",
6632f868492ce3f4a06aaf97fac1e4655c281c233aJust		"FamilyName",
6732f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Weight",
6832f868492ce3f4a06aaf97fac1e4655c281c233aJust		"ItalicAngle",
6932f868492ce3f4a06aaf97fac1e4655c281c233aJust		"IsFixedPitch",
7032f868492ce3f4a06aaf97fac1e4655c281c233aJust		"FontBBox",
7132f868492ce3f4a06aaf97fac1e4655c281c233aJust		"UnderlinePosition",
7232f868492ce3f4a06aaf97fac1e4655c281c233aJust		"UnderlineThickness",
7332f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Version",
7432f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Notice",
7532f868492ce3f4a06aaf97fac1e4655c281c233aJust		"EncodingScheme",
7632f868492ce3f4a06aaf97fac1e4655c281c233aJust		"CapHeight",
7732f868492ce3f4a06aaf97fac1e4655c281c233aJust		"XHeight",
7832f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Ascender",
7932f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Descender",
8032f868492ce3f4a06aaf97fac1e4655c281c233aJust]
8132f868492ce3f4a06aaf97fac1e4655c281c233aJust
8232f868492ce3f4a06aaf97fac1e4655c281c233aJust
8332f868492ce3f4a06aaf97fac1e4655c281c233aJustclass error(Exception): pass
8432f868492ce3f4a06aaf97fac1e4655c281c233aJust
857842e56b97ce677b83bdab09cda48bc2d89ac75aJust
867842e56b97ce677b83bdab09cda48bc2d89ac75aJustclass AFM:
877842e56b97ce677b83bdab09cda48bc2d89ac75aJust
885910f971461cd3df60be89acd73672a22b62cba1jvr	_attrs = None
895910f971461cd3df60be89acd73672a22b62cba1jvr
907842e56b97ce677b83bdab09cda48bc2d89ac75aJust	_keywords = ['StartFontMetrics',
917842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'EndFontMetrics',
927842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'StartCharMetrics',
937842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'EndCharMetrics',
947842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'StartKernData',
957842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'StartKernPairs',
967842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'EndKernPairs',
9732f868492ce3f4a06aaf97fac1e4655c281c233aJust			'EndKernData',
9832f868492ce3f4a06aaf97fac1e4655c281c233aJust			'StartComposites',
9932f868492ce3f4a06aaf97fac1e4655c281c233aJust			'EndComposites',
10032f868492ce3f4a06aaf97fac1e4655c281c233aJust			]
1017842e56b97ce677b83bdab09cda48bc2d89ac75aJust
10232f868492ce3f4a06aaf97fac1e4655c281c233aJust	def __init__(self, path=None):
1037842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._attrs = {}
1047842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._chars = {}
1057842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._kerning = {}
1067842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._index = {}
1077842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._comments = []
10832f868492ce3f4a06aaf97fac1e4655c281c233aJust		self._composites = {}
1097842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if path is not None:
1107842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self.read(path)
1117842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1127842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def read(self, path):
1137842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines = readlines(path)
1147842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for line in lines:
1157842e56b97ce677b83bdab09cda48bc2d89ac75aJust			if not string.strip(line):
1167842e56b97ce677b83bdab09cda48bc2d89ac75aJust				continue
1177842e56b97ce677b83bdab09cda48bc2d89ac75aJust			m = identifierRE.match(line)
1187842e56b97ce677b83bdab09cda48bc2d89ac75aJust			if m is None:
119dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod				raise error("syntax error in AFM file: " + repr(line))
1207842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1217842e56b97ce677b83bdab09cda48bc2d89ac75aJust			pos = m.regs[1][1]
1227842e56b97ce677b83bdab09cda48bc2d89ac75aJust			word = line[:pos]
1237842e56b97ce677b83bdab09cda48bc2d89ac75aJust			rest = string.strip(line[pos:])
1247842e56b97ce677b83bdab09cda48bc2d89ac75aJust			if word in self._keywords:
1257842e56b97ce677b83bdab09cda48bc2d89ac75aJust				continue
12632f868492ce3f4a06aaf97fac1e4655c281c233aJust			if word == "C":
1277842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self.parsechar(rest)
1287842e56b97ce677b83bdab09cda48bc2d89ac75aJust			elif word == "KPX":
1297842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self.parsekernpair(rest)
13032f868492ce3f4a06aaf97fac1e4655c281c233aJust			elif word == "CC":
13132f868492ce3f4a06aaf97fac1e4655c281c233aJust				self.parsecomposite(rest)
1327842e56b97ce677b83bdab09cda48bc2d89ac75aJust			else:
1337842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self.parseattr(word, rest)
1347842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1357842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def parsechar(self, rest):
1367842e56b97ce677b83bdab09cda48bc2d89ac75aJust		m = charRE.match(rest)
1377842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if m is None:
138dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod			raise error("syntax error in AFM file: " + repr(rest))
1397842e56b97ce677b83bdab09cda48bc2d89ac75aJust		things = []
1407842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for fr, to in m.regs[1:]:
1417842e56b97ce677b83bdab09cda48bc2d89ac75aJust			things.append(rest[fr:to])
1427842e56b97ce677b83bdab09cda48bc2d89ac75aJust		charname = things[2]
1437842e56b97ce677b83bdab09cda48bc2d89ac75aJust		del things[2]
1447842e56b97ce677b83bdab09cda48bc2d89ac75aJust		charnum, width, l, b, r, t = map(string.atoi, things)
1457842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._chars[charname] = charnum, width, (l, b, r, t)
1467842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1477842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def parsekernpair(self, rest):
1487842e56b97ce677b83bdab09cda48bc2d89ac75aJust		m = kernRE.match(rest)
1497842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if m is None:
150dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod			raise error("syntax error in AFM file: " + repr(rest))
1517842e56b97ce677b83bdab09cda48bc2d89ac75aJust		things = []
1527842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for fr, to in m.regs[1:]:
1537842e56b97ce677b83bdab09cda48bc2d89ac75aJust			things.append(rest[fr:to])
1547842e56b97ce677b83bdab09cda48bc2d89ac75aJust		leftchar, rightchar, value = things
1557842e56b97ce677b83bdab09cda48bc2d89ac75aJust		value = string.atoi(value)
1567842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._kerning[(leftchar, rightchar)] = value
1577842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1587842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def parseattr(self, word, rest):
1597842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if word == "FontBBox":
1607842e56b97ce677b83bdab09cda48bc2d89ac75aJust			l, b, r, t = map(string.atoi, string.split(rest))
1617842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self._attrs[word] = l, b, r, t
1627842e56b97ce677b83bdab09cda48bc2d89ac75aJust		elif word == "Comment":
1637842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self._comments.append(rest)
1647842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
1657842e56b97ce677b83bdab09cda48bc2d89ac75aJust			try:
1667842e56b97ce677b83bdab09cda48bc2d89ac75aJust				value = string.atoi(rest)
1677842e56b97ce677b83bdab09cda48bc2d89ac75aJust			except (ValueError, OverflowError):
1687842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self._attrs[word] = rest
1697842e56b97ce677b83bdab09cda48bc2d89ac75aJust			else:
1707842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self._attrs[word] = value
1717842e56b97ce677b83bdab09cda48bc2d89ac75aJust
17232f868492ce3f4a06aaf97fac1e4655c281c233aJust	def parsecomposite(self, rest):
17332f868492ce3f4a06aaf97fac1e4655c281c233aJust		m = compositeRE.match(rest)
17432f868492ce3f4a06aaf97fac1e4655c281c233aJust		if m is None:
175dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod			raise error("syntax error in AFM file: " + repr(rest))
17632f868492ce3f4a06aaf97fac1e4655c281c233aJust		charname = m.group(1)
17732f868492ce3f4a06aaf97fac1e4655c281c233aJust		ncomponents = int(m.group(2))
17832f868492ce3f4a06aaf97fac1e4655c281c233aJust		rest = rest[m.regs[0][1]:]
17932f868492ce3f4a06aaf97fac1e4655c281c233aJust		components = []
18032f868492ce3f4a06aaf97fac1e4655c281c233aJust		while 1:
18132f868492ce3f4a06aaf97fac1e4655c281c233aJust			m = componentRE.match(rest)
18232f868492ce3f4a06aaf97fac1e4655c281c233aJust			if m is None:
183dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod				raise error("syntax error in AFM file: " + repr(rest))
18432f868492ce3f4a06aaf97fac1e4655c281c233aJust			basechar = m.group(1)
18532f868492ce3f4a06aaf97fac1e4655c281c233aJust			xoffset = int(m.group(2))
18632f868492ce3f4a06aaf97fac1e4655c281c233aJust			yoffset = int(m.group(3))
18732f868492ce3f4a06aaf97fac1e4655c281c233aJust			components.append((basechar, xoffset, yoffset))
18832f868492ce3f4a06aaf97fac1e4655c281c233aJust			rest = rest[m.regs[0][1]:]
18932f868492ce3f4a06aaf97fac1e4655c281c233aJust			if not rest:
19032f868492ce3f4a06aaf97fac1e4655c281c233aJust				break
19132f868492ce3f4a06aaf97fac1e4655c281c233aJust		assert len(components) == ncomponents
19232f868492ce3f4a06aaf97fac1e4655c281c233aJust		self._composites[charname] = components
19332f868492ce3f4a06aaf97fac1e4655c281c233aJust
1946175debd674004b3e88a4fea1338a090fc4b9da8Just	def write(self, path, sep='\r'):
1957842e56b97ce677b83bdab09cda48bc2d89ac75aJust		import time
1967842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines = [	"StartFontMetrics 2.0",
1977842e56b97ce677b83bdab09cda48bc2d89ac75aJust				"Comment Generated by afmLib, version %s; at %s" %
1987842e56b97ce677b83bdab09cda48bc2d89ac75aJust						(string.split(__version__)[2],
1997842e56b97ce677b83bdab09cda48bc2d89ac75aJust						time.strftime("%m/%d/%Y %H:%M:%S",
2007842e56b97ce677b83bdab09cda48bc2d89ac75aJust						time.localtime(time.time())))]
2017842e56b97ce677b83bdab09cda48bc2d89ac75aJust
20232f868492ce3f4a06aaf97fac1e4655c281c233aJust		# write comments, assuming (possibly wrongly!) they should
20332f868492ce3f4a06aaf97fac1e4655c281c233aJust		# all appear at the top
20432f868492ce3f4a06aaf97fac1e4655c281c233aJust		for comment in self._comments:
20532f868492ce3f4a06aaf97fac1e4655c281c233aJust			lines.append("Comment " + comment)
20632f868492ce3f4a06aaf97fac1e4655c281c233aJust
20732f868492ce3f4a06aaf97fac1e4655c281c233aJust		# write attributes, first the ones we know about, in
20832f868492ce3f4a06aaf97fac1e4655c281c233aJust		# a preferred order
20932f868492ce3f4a06aaf97fac1e4655c281c233aJust		attrs = self._attrs
21032f868492ce3f4a06aaf97fac1e4655c281c233aJust		for attr in preferredAttributeOrder:
211bc5e1cb195c0bfa1c8e7507326d5a9ad05aecb4bBehdad Esfahbod			if attr in attrs:
21232f868492ce3f4a06aaf97fac1e4655c281c233aJust				value = attrs[attr]
21332f868492ce3f4a06aaf97fac1e4655c281c233aJust				if attr == "FontBBox":
21432f868492ce3f4a06aaf97fac1e4655c281c233aJust					value = "%s %s %s %s" % value
21532f868492ce3f4a06aaf97fac1e4655c281c233aJust				lines.append(attr + " " + str(value))
21632f868492ce3f4a06aaf97fac1e4655c281c233aJust		# then write the attributes we don't know about,
21732f868492ce3f4a06aaf97fac1e4655c281c233aJust		# in alphabetical order
21832f868492ce3f4a06aaf97fac1e4655c281c233aJust		items = attrs.items()
21932f868492ce3f4a06aaf97fac1e4655c281c233aJust		items.sort()
2207842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for attr, value in items:
22132f868492ce3f4a06aaf97fac1e4655c281c233aJust			if attr in preferredAttributeOrder:
22232f868492ce3f4a06aaf97fac1e4655c281c233aJust				continue
2237842e56b97ce677b83bdab09cda48bc2d89ac75aJust			lines.append(attr + " " + str(value))
2247842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2257842e56b97ce677b83bdab09cda48bc2d89ac75aJust		# write char metrics
226dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod		lines.append("StartCharMetrics " + repr(len(self._chars)))
2277842e56b97ce677b83bdab09cda48bc2d89ac75aJust		items = map(lambda (charname, (charnum, width, box)):
2287842e56b97ce677b83bdab09cda48bc2d89ac75aJust			(charnum, (charname, width, box)),
2297842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self._chars.items())
2307842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2317842e56b97ce677b83bdab09cda48bc2d89ac75aJust		def myCmp(a, b):
2327842e56b97ce677b83bdab09cda48bc2d89ac75aJust			"""Custom compare function to make sure unencoded chars (-1)
2337842e56b97ce677b83bdab09cda48bc2d89ac75aJust			end up at the end of the list after sorting."""
2347842e56b97ce677b83bdab09cda48bc2d89ac75aJust			if a[0] == -1:
2357842e56b97ce677b83bdab09cda48bc2d89ac75aJust				a = (0xffff,) + a[1:]  # 0xffff is an arbitrary large number
2367842e56b97ce677b83bdab09cda48bc2d89ac75aJust			if b[0] == -1:
2377842e56b97ce677b83bdab09cda48bc2d89ac75aJust				b = (0xffff,) + b[1:]
2387842e56b97ce677b83bdab09cda48bc2d89ac75aJust			return cmp(a, b)
2397842e56b97ce677b83bdab09cda48bc2d89ac75aJust		items.sort(myCmp)
2407842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2417842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for charnum, (charname, width, (l, b, r, t)) in items:
2427842e56b97ce677b83bdab09cda48bc2d89ac75aJust			lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" %
2437842e56b97ce677b83bdab09cda48bc2d89ac75aJust					(charnum, width, charname, l, b, r, t))
2447842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("EndCharMetrics")
2457842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2467842e56b97ce677b83bdab09cda48bc2d89ac75aJust		# write kerning info
2477842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("StartKernData")
248dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod		lines.append("StartKernPairs " + repr(len(self._kerning)))
2497842e56b97ce677b83bdab09cda48bc2d89ac75aJust		items = self._kerning.items()
2507842e56b97ce677b83bdab09cda48bc2d89ac75aJust		items.sort()		# XXX is order important?
2517842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for (leftchar, rightchar), value in items:
2527842e56b97ce677b83bdab09cda48bc2d89ac75aJust			lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
2537842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("EndKernPairs")
2547842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("EndKernData")
25532f868492ce3f4a06aaf97fac1e4655c281c233aJust
25632f868492ce3f4a06aaf97fac1e4655c281c233aJust		if self._composites:
25732f868492ce3f4a06aaf97fac1e4655c281c233aJust			composites = self._composites.items()
25832f868492ce3f4a06aaf97fac1e4655c281c233aJust			composites.sort()
25932f868492ce3f4a06aaf97fac1e4655c281c233aJust			lines.append("StartComposites %s" % len(self._composites))
26032f868492ce3f4a06aaf97fac1e4655c281c233aJust			for charname, components in composites:
26132f868492ce3f4a06aaf97fac1e4655c281c233aJust				line = "CC %s %s ;" % (charname, len(components))
26232f868492ce3f4a06aaf97fac1e4655c281c233aJust				for basechar, xoffset, yoffset in components:
26332f868492ce3f4a06aaf97fac1e4655c281c233aJust					line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
26432f868492ce3f4a06aaf97fac1e4655c281c233aJust				lines.append(line)
26532f868492ce3f4a06aaf97fac1e4655c281c233aJust			lines.append("EndComposites")
26632f868492ce3f4a06aaf97fac1e4655c281c233aJust
2677842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("EndFontMetrics")
2687842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2697842e56b97ce677b83bdab09cda48bc2d89ac75aJust		writelines(path, lines, sep)
2707842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2717842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def has_kernpair(self, pair):
272bc5e1cb195c0bfa1c8e7507326d5a9ad05aecb4bBehdad Esfahbod		return pair in self._kerning
2737842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2747842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def kernpairs(self):
2757842e56b97ce677b83bdab09cda48bc2d89ac75aJust		return self._kerning.keys()
2767842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2777842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def has_char(self, char):
278bc5e1cb195c0bfa1c8e7507326d5a9ad05aecb4bBehdad Esfahbod		return char in self._chars
2797842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2807842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def chars(self):
2817842e56b97ce677b83bdab09cda48bc2d89ac75aJust		return self._chars.keys()
2827842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2837842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def comments(self):
2847842e56b97ce677b83bdab09cda48bc2d89ac75aJust		return self._comments
2857842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2866175debd674004b3e88a4fea1338a090fc4b9da8Just	def addComment(self, comment):
2876175debd674004b3e88a4fea1338a090fc4b9da8Just		self._comments.append(comment)
2886175debd674004b3e88a4fea1338a090fc4b9da8Just
2896175debd674004b3e88a4fea1338a090fc4b9da8Just	def addComposite(self, glyphName, components):
2906175debd674004b3e88a4fea1338a090fc4b9da8Just		self._composites[glyphName] = components
2916175debd674004b3e88a4fea1338a090fc4b9da8Just
2927842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def __getattr__(self, attr):
293bc5e1cb195c0bfa1c8e7507326d5a9ad05aecb4bBehdad Esfahbod		if attr in self._attrs:
2947842e56b97ce677b83bdab09cda48bc2d89ac75aJust			return self._attrs[attr]
2957842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
296cd5aad92f23737ff93a110d5c73d624658a28da8Behdad Esfahbod			raise AttributeError(attr)
2977842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2987842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def __setattr__(self, attr, value):
2997842e56b97ce677b83bdab09cda48bc2d89ac75aJust		# all attrs *not* starting with "_" are consider to be AFM keywords
3007842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if attr[:1] == "_":
3017842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self.__dict__[attr] = value
3027842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
3037842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self._attrs[attr] = value
3047842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3056175debd674004b3e88a4fea1338a090fc4b9da8Just	def __delattr__(self, attr):
3066175debd674004b3e88a4fea1338a090fc4b9da8Just		# all attrs *not* starting with "_" are consider to be AFM keywords
3076175debd674004b3e88a4fea1338a090fc4b9da8Just		if attr[:1] == "_":
3086175debd674004b3e88a4fea1338a090fc4b9da8Just			try:
3096175debd674004b3e88a4fea1338a090fc4b9da8Just				del self.__dict__[attr]
3106175debd674004b3e88a4fea1338a090fc4b9da8Just			except KeyError:
311cd5aad92f23737ff93a110d5c73d624658a28da8Behdad Esfahbod				raise AttributeError(attr)
3126175debd674004b3e88a4fea1338a090fc4b9da8Just		else:
3136175debd674004b3e88a4fea1338a090fc4b9da8Just			try:
3146175debd674004b3e88a4fea1338a090fc4b9da8Just				del self._attrs[attr]
3156175debd674004b3e88a4fea1338a090fc4b9da8Just			except KeyError:
316cd5aad92f23737ff93a110d5c73d624658a28da8Behdad Esfahbod				raise AttributeError(attr)
3176175debd674004b3e88a4fea1338a090fc4b9da8Just
3187842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def __getitem__(self, key):
3197842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if type(key) == types.TupleType:
3207842e56b97ce677b83bdab09cda48bc2d89ac75aJust			# key is a tuple, return the kernpair
3216175debd674004b3e88a4fea1338a090fc4b9da8Just			return self._kerning[key]
3227842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
3237842e56b97ce677b83bdab09cda48bc2d89ac75aJust			# return the metrics instead
3246175debd674004b3e88a4fea1338a090fc4b9da8Just			return self._chars[key]
3256175debd674004b3e88a4fea1338a090fc4b9da8Just
3266175debd674004b3e88a4fea1338a090fc4b9da8Just	def __setitem__(self, key, value):
3276175debd674004b3e88a4fea1338a090fc4b9da8Just		if type(key) == types.TupleType:
3286175debd674004b3e88a4fea1338a090fc4b9da8Just			# key is a tuple, set kernpair
3296175debd674004b3e88a4fea1338a090fc4b9da8Just			self._kerning[key] = value
3306175debd674004b3e88a4fea1338a090fc4b9da8Just		else:
3316175debd674004b3e88a4fea1338a090fc4b9da8Just			# set char metrics
3326175debd674004b3e88a4fea1338a090fc4b9da8Just			self._chars[key] = value
3336175debd674004b3e88a4fea1338a090fc4b9da8Just
3346175debd674004b3e88a4fea1338a090fc4b9da8Just	def __delitem__(self, key):
3356175debd674004b3e88a4fea1338a090fc4b9da8Just		if type(key) == types.TupleType:
3366175debd674004b3e88a4fea1338a090fc4b9da8Just			# key is a tuple, del kernpair
3376175debd674004b3e88a4fea1338a090fc4b9da8Just			del self._kerning[key]
3386175debd674004b3e88a4fea1338a090fc4b9da8Just		else:
3396175debd674004b3e88a4fea1338a090fc4b9da8Just			# del char metrics
3406175debd674004b3e88a4fea1338a090fc4b9da8Just			del self._chars[key]
3417842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3427842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def __repr__(self):
3437842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if hasattr(self, "FullName"):
3447842e56b97ce677b83bdab09cda48bc2d89ac75aJust			return '<AFM object for %s>' % self.FullName
3457842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
3467842e56b97ce677b83bdab09cda48bc2d89ac75aJust			return '<AFM object at %x>' % id(self)
3477842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3487842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3497842e56b97ce677b83bdab09cda48bc2d89ac75aJustdef readlines(path):
3507842e56b97ce677b83bdab09cda48bc2d89ac75aJust	f = open(path, 'rb')
3517842e56b97ce677b83bdab09cda48bc2d89ac75aJust	data = f.read()
3527842e56b97ce677b83bdab09cda48bc2d89ac75aJust	f.close()
3537842e56b97ce677b83bdab09cda48bc2d89ac75aJust	# read any text file, regardless whether it's formatted for Mac, Unix or Dos
3547842e56b97ce677b83bdab09cda48bc2d89ac75aJust	sep = ""
3557842e56b97ce677b83bdab09cda48bc2d89ac75aJust	if '\r' in data:
3567842e56b97ce677b83bdab09cda48bc2d89ac75aJust		sep = sep + '\r'	# mac or dos
3577842e56b97ce677b83bdab09cda48bc2d89ac75aJust	if '\n' in data:
3587842e56b97ce677b83bdab09cda48bc2d89ac75aJust		sep = sep + '\n'	# unix or dos
3597842e56b97ce677b83bdab09cda48bc2d89ac75aJust	return string.split(data, sep)
3607842e56b97ce677b83bdab09cda48bc2d89ac75aJust
36132f868492ce3f4a06aaf97fac1e4655c281c233aJustdef writelines(path, lines, sep='\r'):
3627842e56b97ce677b83bdab09cda48bc2d89ac75aJust	f = open(path, 'wb')
3637842e56b97ce677b83bdab09cda48bc2d89ac75aJust	for line in lines:
3647842e56b97ce677b83bdab09cda48bc2d89ac75aJust		f.write(line + sep)
3657842e56b97ce677b83bdab09cda48bc2d89ac75aJust	f.close()
3667842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3677842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3687842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3697842e56b97ce677b83bdab09cda48bc2d89ac75aJustif __name__ == "__main__":
37091bca4244286fb519c93fe92329da96b0e6f32eejvr	import EasyDialogs
37191bca4244286fb519c93fe92329da96b0e6f32eejvr	path = EasyDialogs.AskFileForOpen()
37291bca4244286fb519c93fe92329da96b0e6f32eejvr	if path:
3737842e56b97ce677b83bdab09cda48bc2d89ac75aJust		afm = AFM(path)
3747842e56b97ce677b83bdab09cda48bc2d89ac75aJust		char = 'A'
3757842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if afm.has_char(char):
3767842e56b97ce677b83bdab09cda48bc2d89ac75aJust			print afm[char]	# print charnum, width and boundingbox
3777842e56b97ce677b83bdab09cda48bc2d89ac75aJust		pair = ('A', 'V')
3787842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if afm.has_kernpair(pair):
3797842e56b97ce677b83bdab09cda48bc2d89ac75aJust			print afm[pair]	# print kerning value for pair
3807842e56b97ce677b83bdab09cda48bc2d89ac75aJust		print afm.Version	# various other afm entries have become attributes
3817842e56b97ce677b83bdab09cda48bc2d89ac75aJust		print afm.Weight
3827842e56b97ce677b83bdab09cda48bc2d89ac75aJust		# afm.comments() returns a list of all Comment lines found in the AFM
3837842e56b97ce677b83bdab09cda48bc2d89ac75aJust		print afm.comments()
3847842e56b97ce677b83bdab09cda48bc2d89ac75aJust		#print afm.chars()
3857842e56b97ce677b83bdab09cda48bc2d89ac75aJust		#print afm.kernpairs()
3867842e56b97ce677b83bdab09cda48bc2d89ac75aJust		print afm
38732f868492ce3f4a06aaf97fac1e4655c281c233aJust		afm.write(path + ".muck")
3887842e56b97ce677b83bdab09cda48bc2d89ac75aJust
389