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
71ae29591efbb29492ce05378909ccf4028d7c1eeBehdad Esfahbodfrom __future__ import print_function, division, absolute_import
87ed91eca1eaa96b79eae780778e89bb9ec44c1eeBehdad Esfahbodfrom fontTools.misc.py23 import *
930e691edd056ba22fa8970280e986747817bec3dBehdad Esfahbodimport re
107842e56b97ce677b83bdab09cda48bc2d89ac75aJust
117842e56b97ce677b83bdab09cda48bc2d89ac75aJust# every single line starts with a "word"
127842e56b97ce677b83bdab09cda48bc2d89ac75aJustidentifierRE = re.compile("^([A-Za-z]+).*")
137842e56b97ce677b83bdab09cda48bc2d89ac75aJust
147842e56b97ce677b83bdab09cda48bc2d89ac75aJust# regular expression to parse char lines
157842e56b97ce677b83bdab09cda48bc2d89ac75aJustcharRE = re.compile(
167842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# charnum
177842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*;\s*WX\s+"		# ; WX
1831ad351b75c56d7bf0d2d059e156baff14eafe68jvr		"(-?\d+)"			# width
197842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*;\s*N\s+"		# ; N
203c3c32c5a4db928ab702d54adddb32e9a3cf2beajvr		"([.A-Za-z0-9_]+)"	# charname
217842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*;\s*B\s+"		# ; B
227842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# left
237842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
247842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# bottom
257842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
267842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# right
277842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
287842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# top
297842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*;\s*"			# ;
307842e56b97ce677b83bdab09cda48bc2d89ac75aJust		)
317842e56b97ce677b83bdab09cda48bc2d89ac75aJust
327842e56b97ce677b83bdab09cda48bc2d89ac75aJust# regular expression to parse kerning lines
337842e56b97ce677b83bdab09cda48bc2d89ac75aJustkernRE = re.compile(
347842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"([.A-Za-z0-9_]+)"	# leftchar
357842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
367842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"([.A-Za-z0-9_]+)"	# rightchar
377842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s+"				#
387842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"(-?\d+)"			# value
397842e56b97ce677b83bdab09cda48bc2d89ac75aJust		"\s*"				#
407842e56b97ce677b83bdab09cda48bc2d89ac75aJust		)
417842e56b97ce677b83bdab09cda48bc2d89ac75aJust
4232f868492ce3f4a06aaf97fac1e4655c281c233aJust# regular expressions to parse composite info lines of the form:
4332f868492ce3f4a06aaf97fac1e4655c281c233aJust# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
4432f868492ce3f4a06aaf97fac1e4655c281c233aJustcompositeRE = re.compile(
4532f868492ce3f4a06aaf97fac1e4655c281c233aJust		"([.A-Za-z0-9_]+)"	# char name
4632f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s+"				#
4732f868492ce3f4a06aaf97fac1e4655c281c233aJust		"(\d+)"				# number of parts
4832f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s*;\s*"			#
4932f868492ce3f4a06aaf97fac1e4655c281c233aJust		)
5032f868492ce3f4a06aaf97fac1e4655c281c233aJustcomponentRE = re.compile(
5132f868492ce3f4a06aaf97fac1e4655c281c233aJust		"PCC\s+"			# PPC
5232f868492ce3f4a06aaf97fac1e4655c281c233aJust		"([.A-Za-z0-9_]+)"	# base char name
5332f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s+"				#
5432f868492ce3f4a06aaf97fac1e4655c281c233aJust		"(-?\d+)"			# x offset
5532f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s+"				#
5632f868492ce3f4a06aaf97fac1e4655c281c233aJust		"(-?\d+)"			# y offset
5732f868492ce3f4a06aaf97fac1e4655c281c233aJust		"\s*;\s*"			#
5832f868492ce3f4a06aaf97fac1e4655c281c233aJust		)
5932f868492ce3f4a06aaf97fac1e4655c281c233aJust
6032f868492ce3f4a06aaf97fac1e4655c281c233aJustpreferredAttributeOrder = [
6132f868492ce3f4a06aaf97fac1e4655c281c233aJust		"FontName",
6232f868492ce3f4a06aaf97fac1e4655c281c233aJust		"FullName",
6332f868492ce3f4a06aaf97fac1e4655c281c233aJust		"FamilyName",
6432f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Weight",
6532f868492ce3f4a06aaf97fac1e4655c281c233aJust		"ItalicAngle",
6632f868492ce3f4a06aaf97fac1e4655c281c233aJust		"IsFixedPitch",
6732f868492ce3f4a06aaf97fac1e4655c281c233aJust		"FontBBox",
6832f868492ce3f4a06aaf97fac1e4655c281c233aJust		"UnderlinePosition",
6932f868492ce3f4a06aaf97fac1e4655c281c233aJust		"UnderlineThickness",
7032f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Version",
7132f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Notice",
7232f868492ce3f4a06aaf97fac1e4655c281c233aJust		"EncodingScheme",
7332f868492ce3f4a06aaf97fac1e4655c281c233aJust		"CapHeight",
7432f868492ce3f4a06aaf97fac1e4655c281c233aJust		"XHeight",
7532f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Ascender",
7632f868492ce3f4a06aaf97fac1e4655c281c233aJust		"Descender",
7732f868492ce3f4a06aaf97fac1e4655c281c233aJust]
7832f868492ce3f4a06aaf97fac1e4655c281c233aJust
7932f868492ce3f4a06aaf97fac1e4655c281c233aJust
8032f868492ce3f4a06aaf97fac1e4655c281c233aJustclass error(Exception): pass
8132f868492ce3f4a06aaf97fac1e4655c281c233aJust
827842e56b97ce677b83bdab09cda48bc2d89ac75aJust
83e388db566b9ba42669c7e353db4293cf27bc2a5bBehdad Esfahbodclass AFM(object):
847842e56b97ce677b83bdab09cda48bc2d89ac75aJust
855910f971461cd3df60be89acd73672a22b62cba1jvr	_attrs = None
865910f971461cd3df60be89acd73672a22b62cba1jvr
877842e56b97ce677b83bdab09cda48bc2d89ac75aJust	_keywords = ['StartFontMetrics',
887842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'EndFontMetrics',
897842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'StartCharMetrics',
907842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'EndCharMetrics',
917842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'StartKernData',
927842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'StartKernPairs',
937842e56b97ce677b83bdab09cda48bc2d89ac75aJust			'EndKernPairs',
9432f868492ce3f4a06aaf97fac1e4655c281c233aJust			'EndKernData',
9532f868492ce3f4a06aaf97fac1e4655c281c233aJust			'StartComposites',
9632f868492ce3f4a06aaf97fac1e4655c281c233aJust			'EndComposites',
9732f868492ce3f4a06aaf97fac1e4655c281c233aJust			]
987842e56b97ce677b83bdab09cda48bc2d89ac75aJust
9932f868492ce3f4a06aaf97fac1e4655c281c233aJust	def __init__(self, path=None):
1007842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._attrs = {}
1017842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._chars = {}
1027842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._kerning = {}
1037842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._index = {}
1047842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._comments = []
10532f868492ce3f4a06aaf97fac1e4655c281c233aJust		self._composites = {}
1067842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if path is not None:
1077842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self.read(path)
1087842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1097842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def read(self, path):
1107842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines = readlines(path)
1117842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for line in lines:
11214fb031125b773f0a15eb19be4f02ed8540b2db6Behdad Esfahbod			if not line.strip():
1137842e56b97ce677b83bdab09cda48bc2d89ac75aJust				continue
1147842e56b97ce677b83bdab09cda48bc2d89ac75aJust			m = identifierRE.match(line)
1157842e56b97ce677b83bdab09cda48bc2d89ac75aJust			if m is None:
116dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod				raise error("syntax error in AFM file: " + repr(line))
1177842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1187842e56b97ce677b83bdab09cda48bc2d89ac75aJust			pos = m.regs[1][1]
1197842e56b97ce677b83bdab09cda48bc2d89ac75aJust			word = line[:pos]
12014fb031125b773f0a15eb19be4f02ed8540b2db6Behdad Esfahbod			rest = line[pos:].strip()
1217842e56b97ce677b83bdab09cda48bc2d89ac75aJust			if word in self._keywords:
1227842e56b97ce677b83bdab09cda48bc2d89ac75aJust				continue
12332f868492ce3f4a06aaf97fac1e4655c281c233aJust			if word == "C":
1247842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self.parsechar(rest)
1257842e56b97ce677b83bdab09cda48bc2d89ac75aJust			elif word == "KPX":
1267842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self.parsekernpair(rest)
12732f868492ce3f4a06aaf97fac1e4655c281c233aJust			elif word == "CC":
12832f868492ce3f4a06aaf97fac1e4655c281c233aJust				self.parsecomposite(rest)
1297842e56b97ce677b83bdab09cda48bc2d89ac75aJust			else:
1307842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self.parseattr(word, rest)
1317842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1327842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def parsechar(self, rest):
1337842e56b97ce677b83bdab09cda48bc2d89ac75aJust		m = charRE.match(rest)
1347842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if m is None:
135dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod			raise error("syntax error in AFM file: " + repr(rest))
1367842e56b97ce677b83bdab09cda48bc2d89ac75aJust		things = []
1377842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for fr, to in m.regs[1:]:
1387842e56b97ce677b83bdab09cda48bc2d89ac75aJust			things.append(rest[fr:to])
1397842e56b97ce677b83bdab09cda48bc2d89ac75aJust		charname = things[2]
1407842e56b97ce677b83bdab09cda48bc2d89ac75aJust		del things[2]
14114fb031125b773f0a15eb19be4f02ed8540b2db6Behdad Esfahbod		charnum, width, l, b, r, t = (int(thing) for thing in things)
1427842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._chars[charname] = charnum, width, (l, b, r, t)
1437842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1447842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def parsekernpair(self, rest):
1457842e56b97ce677b83bdab09cda48bc2d89ac75aJust		m = kernRE.match(rest)
1467842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if m is None:
147dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod			raise error("syntax error in AFM file: " + repr(rest))
1487842e56b97ce677b83bdab09cda48bc2d89ac75aJust		things = []
1497842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for fr, to in m.regs[1:]:
1507842e56b97ce677b83bdab09cda48bc2d89ac75aJust			things.append(rest[fr:to])
1517842e56b97ce677b83bdab09cda48bc2d89ac75aJust		leftchar, rightchar, value = things
15214fb031125b773f0a15eb19be4f02ed8540b2db6Behdad Esfahbod		value = int(value)
1537842e56b97ce677b83bdab09cda48bc2d89ac75aJust		self._kerning[(leftchar, rightchar)] = value
1547842e56b97ce677b83bdab09cda48bc2d89ac75aJust
1557842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def parseattr(self, word, rest):
1567842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if word == "FontBBox":
15714fb031125b773f0a15eb19be4f02ed8540b2db6Behdad Esfahbod			l, b, r, t = [int(thing) for thing in rest.split()]
1587842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self._attrs[word] = l, b, r, t
1597842e56b97ce677b83bdab09cda48bc2d89ac75aJust		elif word == "Comment":
1607842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self._comments.append(rest)
1617842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
1627842e56b97ce677b83bdab09cda48bc2d89ac75aJust			try:
16314fb031125b773f0a15eb19be4f02ed8540b2db6Behdad Esfahbod				value = int(rest)
1647842e56b97ce677b83bdab09cda48bc2d89ac75aJust			except (ValueError, OverflowError):
1657842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self._attrs[word] = rest
1667842e56b97ce677b83bdab09cda48bc2d89ac75aJust			else:
1677842e56b97ce677b83bdab09cda48bc2d89ac75aJust				self._attrs[word] = value
1687842e56b97ce677b83bdab09cda48bc2d89ac75aJust
16932f868492ce3f4a06aaf97fac1e4655c281c233aJust	def parsecomposite(self, rest):
17032f868492ce3f4a06aaf97fac1e4655c281c233aJust		m = compositeRE.match(rest)
17132f868492ce3f4a06aaf97fac1e4655c281c233aJust		if m is None:
172dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod			raise error("syntax error in AFM file: " + repr(rest))
17332f868492ce3f4a06aaf97fac1e4655c281c233aJust		charname = m.group(1)
17432f868492ce3f4a06aaf97fac1e4655c281c233aJust		ncomponents = int(m.group(2))
17532f868492ce3f4a06aaf97fac1e4655c281c233aJust		rest = rest[m.regs[0][1]:]
17632f868492ce3f4a06aaf97fac1e4655c281c233aJust		components = []
177ac1b4359467ca3deab03186a15eae1d55eb35567Behdad Esfahbod		while True:
17832f868492ce3f4a06aaf97fac1e4655c281c233aJust			m = componentRE.match(rest)
17932f868492ce3f4a06aaf97fac1e4655c281c233aJust			if m is None:
180dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod				raise error("syntax error in AFM file: " + repr(rest))
18132f868492ce3f4a06aaf97fac1e4655c281c233aJust			basechar = m.group(1)
18232f868492ce3f4a06aaf97fac1e4655c281c233aJust			xoffset = int(m.group(2))
18332f868492ce3f4a06aaf97fac1e4655c281c233aJust			yoffset = int(m.group(3))
18432f868492ce3f4a06aaf97fac1e4655c281c233aJust			components.append((basechar, xoffset, yoffset))
18532f868492ce3f4a06aaf97fac1e4655c281c233aJust			rest = rest[m.regs[0][1]:]
18632f868492ce3f4a06aaf97fac1e4655c281c233aJust			if not rest:
18732f868492ce3f4a06aaf97fac1e4655c281c233aJust				break
18832f868492ce3f4a06aaf97fac1e4655c281c233aJust		assert len(components) == ncomponents
18932f868492ce3f4a06aaf97fac1e4655c281c233aJust		self._composites[charname] = components
19032f868492ce3f4a06aaf97fac1e4655c281c233aJust
1916175debd674004b3e88a4fea1338a090fc4b9da8Just	def write(self, path, sep='\r'):
1927842e56b97ce677b83bdab09cda48bc2d89ac75aJust		import time
1937842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines = [	"StartFontMetrics 2.0",
194ea48dba61d3c36a2cb2a7e0df0ce81093b8bd405Khaled Hosny				"Comment Generated by afmLib; at %s" % (
1957842e56b97ce677b83bdab09cda48bc2d89ac75aJust						time.strftime("%m/%d/%Y %H:%M:%S",
1967842e56b97ce677b83bdab09cda48bc2d89ac75aJust						time.localtime(time.time())))]
1977842e56b97ce677b83bdab09cda48bc2d89ac75aJust
19832f868492ce3f4a06aaf97fac1e4655c281c233aJust		# write comments, assuming (possibly wrongly!) they should
19932f868492ce3f4a06aaf97fac1e4655c281c233aJust		# all appear at the top
20032f868492ce3f4a06aaf97fac1e4655c281c233aJust		for comment in self._comments:
20132f868492ce3f4a06aaf97fac1e4655c281c233aJust			lines.append("Comment " + comment)
20232f868492ce3f4a06aaf97fac1e4655c281c233aJust
20332f868492ce3f4a06aaf97fac1e4655c281c233aJust		# write attributes, first the ones we know about, in
20432f868492ce3f4a06aaf97fac1e4655c281c233aJust		# a preferred order
20532f868492ce3f4a06aaf97fac1e4655c281c233aJust		attrs = self._attrs
20632f868492ce3f4a06aaf97fac1e4655c281c233aJust		for attr in preferredAttributeOrder:
207bc5e1cb195c0bfa1c8e7507326d5a9ad05aecb4bBehdad Esfahbod			if attr in attrs:
20832f868492ce3f4a06aaf97fac1e4655c281c233aJust				value = attrs[attr]
20932f868492ce3f4a06aaf97fac1e4655c281c233aJust				if attr == "FontBBox":
21032f868492ce3f4a06aaf97fac1e4655c281c233aJust					value = "%s %s %s %s" % value
21132f868492ce3f4a06aaf97fac1e4655c281c233aJust				lines.append(attr + " " + str(value))
21232f868492ce3f4a06aaf97fac1e4655c281c233aJust		# then write the attributes we don't know about,
21332f868492ce3f4a06aaf97fac1e4655c281c233aJust		# in alphabetical order
214ac1b4359467ca3deab03186a15eae1d55eb35567Behdad Esfahbod		items = sorted(attrs.items())
2157842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for attr, value in items:
21632f868492ce3f4a06aaf97fac1e4655c281c233aJust			if attr in preferredAttributeOrder:
21732f868492ce3f4a06aaf97fac1e4655c281c233aJust				continue
2187842e56b97ce677b83bdab09cda48bc2d89ac75aJust			lines.append(attr + " " + str(value))
2197842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2207842e56b97ce677b83bdab09cda48bc2d89ac75aJust		# write char metrics
221dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod		lines.append("StartCharMetrics " + repr(len(self._chars)))
222e5ca79699d00fdf7ac6eaceaed372aea8d6bc1fdBehdad Esfahbod		items = [(charnum, (charname, width, box)) for charname, (charnum, width, box) in self._chars.items()]
2237842e56b97ce677b83bdab09cda48bc2d89ac75aJust
224b7fd2e19138b177403689bdd6989cfd2402aa2b3Behdad Esfahbod		def myKey(a):
225b7fd2e19138b177403689bdd6989cfd2402aa2b3Behdad Esfahbod			"""Custom key function to make sure unencoded chars (-1)
2267842e56b97ce677b83bdab09cda48bc2d89ac75aJust			end up at the end of the list after sorting."""
2277842e56b97ce677b83bdab09cda48bc2d89ac75aJust			if a[0] == -1:
2287842e56b97ce677b83bdab09cda48bc2d89ac75aJust				a = (0xffff,) + a[1:]  # 0xffff is an arbitrary large number
229b7fd2e19138b177403689bdd6989cfd2402aa2b3Behdad Esfahbod			return a
230b7fd2e19138b177403689bdd6989cfd2402aa2b3Behdad Esfahbod		items.sort(key=myKey)
2317842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2327842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for charnum, (charname, width, (l, b, r, t)) in items:
2337842e56b97ce677b83bdab09cda48bc2d89ac75aJust			lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" %
2347842e56b97ce677b83bdab09cda48bc2d89ac75aJust					(charnum, width, charname, l, b, r, t))
2357842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("EndCharMetrics")
2367842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2377842e56b97ce677b83bdab09cda48bc2d89ac75aJust		# write kerning info
2387842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("StartKernData")
239dc7e6f3e5563a853477ebe26166b002c158dbe8bBehdad Esfahbod		lines.append("StartKernPairs " + repr(len(self._kerning)))
240c2297cd41d6c00b95f857b65bc9fd4b57559ac5eBehdad Esfahbod		items = sorted(self._kerning.items())
2417842e56b97ce677b83bdab09cda48bc2d89ac75aJust		for (leftchar, rightchar), value in items:
2427842e56b97ce677b83bdab09cda48bc2d89ac75aJust			lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
2437842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("EndKernPairs")
2447842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("EndKernData")
24532f868492ce3f4a06aaf97fac1e4655c281c233aJust
24632f868492ce3f4a06aaf97fac1e4655c281c233aJust		if self._composites:
247ac1b4359467ca3deab03186a15eae1d55eb35567Behdad Esfahbod			composites = sorted(self._composites.items())
24832f868492ce3f4a06aaf97fac1e4655c281c233aJust			lines.append("StartComposites %s" % len(self._composites))
24932f868492ce3f4a06aaf97fac1e4655c281c233aJust			for charname, components in composites:
25032f868492ce3f4a06aaf97fac1e4655c281c233aJust				line = "CC %s %s ;" % (charname, len(components))
25132f868492ce3f4a06aaf97fac1e4655c281c233aJust				for basechar, xoffset, yoffset in components:
25232f868492ce3f4a06aaf97fac1e4655c281c233aJust					line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
25332f868492ce3f4a06aaf97fac1e4655c281c233aJust				lines.append(line)
25432f868492ce3f4a06aaf97fac1e4655c281c233aJust			lines.append("EndComposites")
25532f868492ce3f4a06aaf97fac1e4655c281c233aJust
2567842e56b97ce677b83bdab09cda48bc2d89ac75aJust		lines.append("EndFontMetrics")
2577842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2587842e56b97ce677b83bdab09cda48bc2d89ac75aJust		writelines(path, lines, sep)
2597842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2607842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def has_kernpair(self, pair):
261bc5e1cb195c0bfa1c8e7507326d5a9ad05aecb4bBehdad Esfahbod		return pair in self._kerning
2627842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2637842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def kernpairs(self):
264c2297cd41d6c00b95f857b65bc9fd4b57559ac5eBehdad Esfahbod		return list(self._kerning.keys())
2657842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2667842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def has_char(self, char):
267bc5e1cb195c0bfa1c8e7507326d5a9ad05aecb4bBehdad Esfahbod		return char in self._chars
2687842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2697842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def chars(self):
270c2297cd41d6c00b95f857b65bc9fd4b57559ac5eBehdad Esfahbod		return list(self._chars.keys())
2717842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2727842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def comments(self):
2737842e56b97ce677b83bdab09cda48bc2d89ac75aJust		return self._comments
2747842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2756175debd674004b3e88a4fea1338a090fc4b9da8Just	def addComment(self, comment):
2766175debd674004b3e88a4fea1338a090fc4b9da8Just		self._comments.append(comment)
2776175debd674004b3e88a4fea1338a090fc4b9da8Just
2786175debd674004b3e88a4fea1338a090fc4b9da8Just	def addComposite(self, glyphName, components):
2796175debd674004b3e88a4fea1338a090fc4b9da8Just		self._composites[glyphName] = components
2806175debd674004b3e88a4fea1338a090fc4b9da8Just
2817842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def __getattr__(self, attr):
282bc5e1cb195c0bfa1c8e7507326d5a9ad05aecb4bBehdad Esfahbod		if attr in self._attrs:
2837842e56b97ce677b83bdab09cda48bc2d89ac75aJust			return self._attrs[attr]
2847842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
285cd5aad92f23737ff93a110d5c73d624658a28da8Behdad Esfahbod			raise AttributeError(attr)
2867842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2877842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def __setattr__(self, attr, value):
2887842e56b97ce677b83bdab09cda48bc2d89ac75aJust		# all attrs *not* starting with "_" are consider to be AFM keywords
2897842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if attr[:1] == "_":
2907842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self.__dict__[attr] = value
2917842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
2927842e56b97ce677b83bdab09cda48bc2d89ac75aJust			self._attrs[attr] = value
2937842e56b97ce677b83bdab09cda48bc2d89ac75aJust
2946175debd674004b3e88a4fea1338a090fc4b9da8Just	def __delattr__(self, attr):
2956175debd674004b3e88a4fea1338a090fc4b9da8Just		# all attrs *not* starting with "_" are consider to be AFM keywords
2966175debd674004b3e88a4fea1338a090fc4b9da8Just		if attr[:1] == "_":
2976175debd674004b3e88a4fea1338a090fc4b9da8Just			try:
2986175debd674004b3e88a4fea1338a090fc4b9da8Just				del self.__dict__[attr]
2996175debd674004b3e88a4fea1338a090fc4b9da8Just			except KeyError:
300cd5aad92f23737ff93a110d5c73d624658a28da8Behdad Esfahbod				raise AttributeError(attr)
3016175debd674004b3e88a4fea1338a090fc4b9da8Just		else:
3026175debd674004b3e88a4fea1338a090fc4b9da8Just			try:
3036175debd674004b3e88a4fea1338a090fc4b9da8Just				del self._attrs[attr]
3046175debd674004b3e88a4fea1338a090fc4b9da8Just			except KeyError:
305cd5aad92f23737ff93a110d5c73d624658a28da8Behdad Esfahbod				raise AttributeError(attr)
3066175debd674004b3e88a4fea1338a090fc4b9da8Just
3077842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def __getitem__(self, key):
308002c32fd0d869e280783777ec57916a9267aaea5Behdad Esfahbod		if isinstance(key, tuple):
3097842e56b97ce677b83bdab09cda48bc2d89ac75aJust			# key is a tuple, return the kernpair
3106175debd674004b3e88a4fea1338a090fc4b9da8Just			return self._kerning[key]
3117842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
3127842e56b97ce677b83bdab09cda48bc2d89ac75aJust			# return the metrics instead
3136175debd674004b3e88a4fea1338a090fc4b9da8Just			return self._chars[key]
3146175debd674004b3e88a4fea1338a090fc4b9da8Just
3156175debd674004b3e88a4fea1338a090fc4b9da8Just	def __setitem__(self, key, value):
316002c32fd0d869e280783777ec57916a9267aaea5Behdad Esfahbod		if isinstance(key, tuple):
3176175debd674004b3e88a4fea1338a090fc4b9da8Just			# key is a tuple, set kernpair
3186175debd674004b3e88a4fea1338a090fc4b9da8Just			self._kerning[key] = value
3196175debd674004b3e88a4fea1338a090fc4b9da8Just		else:
3206175debd674004b3e88a4fea1338a090fc4b9da8Just			# set char metrics
3216175debd674004b3e88a4fea1338a090fc4b9da8Just			self._chars[key] = value
3226175debd674004b3e88a4fea1338a090fc4b9da8Just
3236175debd674004b3e88a4fea1338a090fc4b9da8Just	def __delitem__(self, key):
324002c32fd0d869e280783777ec57916a9267aaea5Behdad Esfahbod		if isinstance(key, tuple):
3256175debd674004b3e88a4fea1338a090fc4b9da8Just			# key is a tuple, del kernpair
3266175debd674004b3e88a4fea1338a090fc4b9da8Just			del self._kerning[key]
3276175debd674004b3e88a4fea1338a090fc4b9da8Just		else:
3286175debd674004b3e88a4fea1338a090fc4b9da8Just			# del char metrics
3296175debd674004b3e88a4fea1338a090fc4b9da8Just			del self._chars[key]
3307842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3317842e56b97ce677b83bdab09cda48bc2d89ac75aJust	def __repr__(self):
3327842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if hasattr(self, "FullName"):
3337842e56b97ce677b83bdab09cda48bc2d89ac75aJust			return '<AFM object for %s>' % self.FullName
3347842e56b97ce677b83bdab09cda48bc2d89ac75aJust		else:
3357842e56b97ce677b83bdab09cda48bc2d89ac75aJust			return '<AFM object at %x>' % id(self)
3367842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3377842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3387842e56b97ce677b83bdab09cda48bc2d89ac75aJustdef readlines(path):
3397842e56b97ce677b83bdab09cda48bc2d89ac75aJust	f = open(path, 'rb')
3407842e56b97ce677b83bdab09cda48bc2d89ac75aJust	data = f.read()
3417842e56b97ce677b83bdab09cda48bc2d89ac75aJust	f.close()
3427842e56b97ce677b83bdab09cda48bc2d89ac75aJust	# read any text file, regardless whether it's formatted for Mac, Unix or Dos
3437842e56b97ce677b83bdab09cda48bc2d89ac75aJust	sep = ""
3447842e56b97ce677b83bdab09cda48bc2d89ac75aJust	if '\r' in data:
3457842e56b97ce677b83bdab09cda48bc2d89ac75aJust		sep = sep + '\r'	# mac or dos
3467842e56b97ce677b83bdab09cda48bc2d89ac75aJust	if '\n' in data:
3477842e56b97ce677b83bdab09cda48bc2d89ac75aJust		sep = sep + '\n'	# unix or dos
34814fb031125b773f0a15eb19be4f02ed8540b2db6Behdad Esfahbod	return data.split(sep)
3497842e56b97ce677b83bdab09cda48bc2d89ac75aJust
35032f868492ce3f4a06aaf97fac1e4655c281c233aJustdef writelines(path, lines, sep='\r'):
3517842e56b97ce677b83bdab09cda48bc2d89ac75aJust	f = open(path, 'wb')
3527842e56b97ce677b83bdab09cda48bc2d89ac75aJust	for line in lines:
3537842e56b97ce677b83bdab09cda48bc2d89ac75aJust		f.write(line + sep)
3547842e56b97ce677b83bdab09cda48bc2d89ac75aJust	f.close()
3557842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3567842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3577842e56b97ce677b83bdab09cda48bc2d89ac75aJust
3587842e56b97ce677b83bdab09cda48bc2d89ac75aJustif __name__ == "__main__":
35991bca4244286fb519c93fe92329da96b0e6f32eejvr	import EasyDialogs
36091bca4244286fb519c93fe92329da96b0e6f32eejvr	path = EasyDialogs.AskFileForOpen()
36191bca4244286fb519c93fe92329da96b0e6f32eejvr	if path:
3627842e56b97ce677b83bdab09cda48bc2d89ac75aJust		afm = AFM(path)
3637842e56b97ce677b83bdab09cda48bc2d89ac75aJust		char = 'A'
3647842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if afm.has_char(char):
3653ec6a258238b6068e4eef3fe579f1f5c0a06bbbaBehdad Esfahbod			print(afm[char])	# print charnum, width and boundingbox
3667842e56b97ce677b83bdab09cda48bc2d89ac75aJust		pair = ('A', 'V')
3677842e56b97ce677b83bdab09cda48bc2d89ac75aJust		if afm.has_kernpair(pair):
3683ec6a258238b6068e4eef3fe579f1f5c0a06bbbaBehdad Esfahbod			print(afm[pair])	# print kerning value for pair
3693ec6a258238b6068e4eef3fe579f1f5c0a06bbbaBehdad Esfahbod		print(afm.Version)	# various other afm entries have become attributes
3703ec6a258238b6068e4eef3fe579f1f5c0a06bbbaBehdad Esfahbod		print(afm.Weight)
3717842e56b97ce677b83bdab09cda48bc2d89ac75aJust		# afm.comments() returns a list of all Comment lines found in the AFM
3723ec6a258238b6068e4eef3fe579f1f5c0a06bbbaBehdad Esfahbod		print(afm.comments())
3737842e56b97ce677b83bdab09cda48bc2d89ac75aJust		#print afm.chars()
3747842e56b97ce677b83bdab09cda48bc2d89ac75aJust		#print afm.kernpairs()
3753ec6a258238b6068e4eef3fe579f1f5c0a06bbbaBehdad Esfahbod		print(afm)
37632f868492ce3f4a06aaf97fac1e4655c281c233aJust		afm.write(path + ".muck")
3777842e56b97ce677b83bdab09cda48bc2d89ac75aJust
378