git-svn-id: svn://svn.code.sf.net/p/fonttools/code/trunk@2 4cde692c-a291-49d1-8350-778aa11640f8
266 lines
6.6 KiB
Python
266 lines
6.6 KiB
Python
"""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")
|
|
|