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")
+