blob: a725e83265aebed25722789a5ffae9fb57d508f3 [file] [log] [blame]
Just7842e561999-12-16 21:34:53 +00001"""Module for reading and writing AFM files."""
2
3# XXX reads AFM's generated by Fog, not tested with much else.
4# It does not implement the full spec (Adobe Technote 5004, Adobe Font Metrics
5# File Format Specification). Still, it should read most "common" AFM files.
6
7import re
8import string
9import types
10
11__version__ = "$Id: afmLib.py,v 1.1 1999-12-16 21:34:51 Just Exp $"
12
13
14# every single line starts with a "word"
15identifierRE = re.compile("^([A-Za-z]+).*")
16
17# regular expression to parse char lines
18charRE = re.compile(
19 "(-?\d+)" # charnum
20 "\s*;\s*WX\s+" # ; WX
21 "(\d+)" # width
22 "\s*;\s*N\s+" # ; N
23 "(\.?[A-Za-z0-9_]+)" # charname
24 "\s*;\s*B\s+" # ; B
25 "(-?\d+)" # left
26 "\s+" #
27 "(-?\d+)" # bottom
28 "\s+" #
29 "(-?\d+)" # right
30 "\s+" #
31 "(-?\d+)" # top
32 "\s*;\s*" # ;
33 )
34
35# regular expression to parse kerning lines
36kernRE = re.compile(
37 "([.A-Za-z0-9_]+)" # leftchar
38 "\s+" #
39 "([.A-Za-z0-9_]+)" # rightchar
40 "\s+" #
41 "(-?\d+)" # value
42 "\s*" #
43 )
44
45error = "AFM.error"
46
47class AFM:
48
49 _keywords = ['StartFontMetrics',
50 'EndFontMetrics',
51 'StartCharMetrics',
52 'EndCharMetrics',
53 'StartKernData',
54 'StartKernPairs',
55 'EndKernPairs',
56 'EndKernData', ]
57
58 def __init__(self, path = None):
59 self._attrs = {}
60 self._chars = {}
61 self._kerning = {}
62 self._index = {}
63 self._comments = []
64 if path is not None:
65 self.read(path)
66
67 def read(self, path):
68 lines = readlines(path)
69 for line in lines:
70 if not string.strip(line):
71 continue
72 m = identifierRE.match(line)
73 if m is None:
74 raise error, "syntax error in AFM file: " + `line`
75
76 pos = m.regs[1][1]
77 word = line[:pos]
78 rest = string.strip(line[pos:])
79 if word in self._keywords:
80 continue
81 if word == 'C':
82 self.parsechar(rest)
83 elif word == "KPX":
84 self.parsekernpair(rest)
85 else:
86 self.parseattr(word, rest)
87
88 def parsechar(self, rest):
89 m = charRE.match(rest)
90 if m is None:
91 raise error, "syntax error in AFM file: " + `rest`
92 things = []
93 for fr, to in m.regs[1:]:
94 things.append(rest[fr:to])
95 charname = things[2]
96 del things[2]
97 charnum, width, l, b, r, t = map(string.atoi, things)
98 self._chars[charname] = charnum, width, (l, b, r, t)
99
100 def parsekernpair(self, rest):
101 m = kernRE.match(rest)
102 if m is None:
103 raise error, "syntax error in AFM file: " + `rest`
104 things = []
105 for fr, to in m.regs[1:]:
106 things.append(rest[fr:to])
107 leftchar, rightchar, value = things
108 value = string.atoi(value)
109 self._kerning[(leftchar, rightchar)] = value
110
111 def parseattr(self, word, rest):
112 if word == "FontBBox":
113 l, b, r, t = map(string.atoi, string.split(rest))
114 self._attrs[word] = l, b, r, t
115 elif word == "Comment":
116 self._comments.append(rest)
117 else:
118 try:
119 value = string.atoi(rest)
120 except (ValueError, OverflowError):
121 self._attrs[word] = rest
122 else:
123 self._attrs[word] = value
124
125 def write(self, path, sep = '\r'):
126 import time
127 lines = [ "StartFontMetrics 2.0",
128 "Comment Generated by afmLib, version %s; at %s" %
129 (string.split(__version__)[2],
130 time.strftime("%m/%d/%Y %H:%M:%S",
131 time.localtime(time.time())))]
132
133 # write attributes
134 items = self._attrs.items()
135 items.sort() # XXX proper ordering???
136 for attr, value in items:
137 if attr == "FontBBox":
138 value = string.join(map(str, value), " ")
139 lines.append(attr + " " + str(value))
140
141 # write char metrics
142 lines.append("StartCharMetrics " + `len(self._chars)`)
143 items = map(lambda (charname, (charnum, width, box)):
144 (charnum, (charname, width, box)),
145 self._chars.items())
146
147 def myCmp(a, b):
148 """Custom compare function to make sure unencoded chars (-1)
149 end up at the end of the list after sorting."""
150 if a[0] == -1:
151 a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number
152 if b[0] == -1:
153 b = (0xffff,) + b[1:]
154 return cmp(a, b)
155 items.sort(myCmp)
156
157 for charnum, (charname, width, (l, b, r, t)) in items:
158 lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" %
159 (charnum, width, charname, l, b, r, t))
160 lines.append("EndCharMetrics")
161
162 # write kerning info
163 lines.append("StartKernData")
164 lines.append("StartKernPairs " + `len(self._kerning)`)
165 items = self._kerning.items()
166 items.sort() # XXX is order important?
167 for (leftchar, rightchar), value in items:
168 lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
169
170 lines.append("EndKernPairs")
171 lines.append("EndKernData")
172 lines.append("EndFontMetrics")
173
174 writelines(path, lines, sep)
175
176 def has_kernpair(self, pair):
177 return self._kerning.has_key(pair)
178
179 def kernpairs(self):
180 return self._kerning.keys()
181
182 def has_char(self, char):
183 return self._chars.has_key(char)
184
185 def chars(self):
186 return self._chars.keys()
187
188 def comments(self):
189 return self._comments
190
191 def __getattr__(self, attr):
192 if self._attrs.has_key(attr):
193 return self._attrs[attr]
194 else:
195 raise AttributeError, attr
196
197 def __setattr__(self, attr, value):
198 # all attrs *not* starting with "_" are consider to be AFM keywords
199 if attr[:1] == "_":
200 self.__dict__[attr] = value
201 else:
202 self._attrs[attr] = value
203
204 def __getitem__(self, key):
205 if type(key) == types.TupleType:
206 # key is a tuple, return the kernpair
207 if self._kerning.has_key(key):
208 return self._kerning[key]
209 else:
210 raise KeyError, "no kerning pair: " + str(key)
211 else:
212 # return the metrics instead
213 if self._chars.has_key(key):
214 return self._chars[key]
215 else:
216 raise KeyError, "metrics index " + str(key) + " out of range"
217
218 def __repr__(self):
219 if hasattr(self, "FullName"):
220 return '<AFM object for %s>' % self.FullName
221 else:
222 return '<AFM object at %x>' % id(self)
223
224
225def readlines(path):
226 f = open(path, 'rb')
227 data = f.read()
228 f.close()
229 # read any text file, regardless whether it's formatted for Mac, Unix or Dos
230 sep = ""
231 if '\r' in data:
232 sep = sep + '\r' # mac or dos
233 if '\n' in data:
234 sep = sep + '\n' # unix or dos
235 return string.split(data, sep)
236
237def writelines(path, lines, sep = '\r'):
238 f = open(path, 'wb')
239 for line in lines:
240 f.write(line + sep)
241 f.close()
242
243
244
245if __name__ == "__main__":
246 import macfs
247 fss, ok = macfs.StandardGetFile('TEXT')
248 if ok:
249 path = fss.as_pathname()
250 afm = AFM(path)
251 char = 'A'
252 if afm.has_char(char):
253 print afm[char] # print charnum, width and boundingbox
254 pair = ('A', 'V')
255 if afm.has_kernpair(pair):
256 print afm[pair] # print kerning value for pair
257 print afm.Version # various other afm entries have become attributes
258 print afm.Weight
259 # afm.comments() returns a list of all Comment lines found in the AFM
260 print afm.comments()
261 #print afm.chars()
262 #print afm.kernpairs()
263 print afm
264 afm.write(path + ".xxx")
265