blob: e927b6bd444766c2d13c7657aad67c1f763cf1bd [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
Just6175deb2001-06-24 15:11:31 +000011__version__ = "$Id: afmLib.py,v 1.3 2001-06-24 15:11:31 Just Exp $"
Just7842e561999-12-16 21:34:53 +000012
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
Just32f86842001-04-30 14:40:17 +000021 "(\d+)" # width
Just7842e561999-12-16 21:34:53 +000022 "\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
Just32f86842001-04-30 14:40:17 +000045# regular expressions to parse composite info lines of the form:
46# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
47compositeRE = re.compile(
48 "([.A-Za-z0-9_]+)" # char name
49 "\s+" #
50 "(\d+)" # number of parts
51 "\s*;\s*" #
52 )
53componentRE = re.compile(
54 "PCC\s+" # PPC
55 "([.A-Za-z0-9_]+)" # base char name
56 "\s+" #
57 "(-?\d+)" # x offset
58 "\s+" #
59 "(-?\d+)" # y offset
60 "\s*;\s*" #
61 )
62
63preferredAttributeOrder = [
64 "FontName",
65 "FullName",
66 "FamilyName",
67 "Weight",
68 "ItalicAngle",
69 "IsFixedPitch",
70 "FontBBox",
71 "UnderlinePosition",
72 "UnderlineThickness",
73 "Version",
74 "Notice",
75 "EncodingScheme",
76 "CapHeight",
77 "XHeight",
78 "Ascender",
79 "Descender",
80]
81
82
83class error(Exception): pass
84
Just7842e561999-12-16 21:34:53 +000085
86class AFM:
87
88 _keywords = ['StartFontMetrics',
89 'EndFontMetrics',
90 'StartCharMetrics',
91 'EndCharMetrics',
92 'StartKernData',
93 'StartKernPairs',
94 'EndKernPairs',
Just32f86842001-04-30 14:40:17 +000095 'EndKernData',
96 'StartComposites',
97 'EndComposites',
98 ]
Just7842e561999-12-16 21:34:53 +000099
Just32f86842001-04-30 14:40:17 +0000100 def __init__(self, path=None):
Just7842e561999-12-16 21:34:53 +0000101 self._attrs = {}
102 self._chars = {}
103 self._kerning = {}
104 self._index = {}
105 self._comments = []
Just32f86842001-04-30 14:40:17 +0000106 self._composites = {}
Just7842e561999-12-16 21:34:53 +0000107 if path is not None:
108 self.read(path)
109
110 def read(self, path):
111 lines = readlines(path)
112 for line in lines:
113 if not string.strip(line):
114 continue
115 m = identifierRE.match(line)
116 if m is None:
117 raise error, "syntax error in AFM file: " + `line`
118
119 pos = m.regs[1][1]
120 word = line[:pos]
121 rest = string.strip(line[pos:])
122 if word in self._keywords:
123 continue
Just32f86842001-04-30 14:40:17 +0000124 if word == "C":
Just7842e561999-12-16 21:34:53 +0000125 self.parsechar(rest)
126 elif word == "KPX":
127 self.parsekernpair(rest)
Just32f86842001-04-30 14:40:17 +0000128 elif word == "CC":
129 self.parsecomposite(rest)
Just7842e561999-12-16 21:34:53 +0000130 else:
131 self.parseattr(word, rest)
132
133 def parsechar(self, rest):
134 m = charRE.match(rest)
135 if m is None:
136 raise error, "syntax error in AFM file: " + `rest`
137 things = []
138 for fr, to in m.regs[1:]:
139 things.append(rest[fr:to])
140 charname = things[2]
141 del things[2]
142 charnum, width, l, b, r, t = map(string.atoi, things)
143 self._chars[charname] = charnum, width, (l, b, r, t)
144
145 def parsekernpair(self, rest):
146 m = kernRE.match(rest)
147 if m is None:
148 raise error, "syntax error in AFM file: " + `rest`
149 things = []
150 for fr, to in m.regs[1:]:
151 things.append(rest[fr:to])
152 leftchar, rightchar, value = things
153 value = string.atoi(value)
154 self._kerning[(leftchar, rightchar)] = value
155
156 def parseattr(self, word, rest):
157 if word == "FontBBox":
158 l, b, r, t = map(string.atoi, string.split(rest))
159 self._attrs[word] = l, b, r, t
160 elif word == "Comment":
161 self._comments.append(rest)
162 else:
163 try:
164 value = string.atoi(rest)
165 except (ValueError, OverflowError):
166 self._attrs[word] = rest
167 else:
168 self._attrs[word] = value
169
Just32f86842001-04-30 14:40:17 +0000170 def parsecomposite(self, rest):
171 m = compositeRE.match(rest)
172 if m is None:
173 raise error, "syntax error in AFM file: " + `rest`
174 charname = m.group(1)
175 ncomponents = int(m.group(2))
176 rest = rest[m.regs[0][1]:]
177 components = []
178 while 1:
179 m = componentRE.match(rest)
180 if m is None:
181 raise error, "syntax error in AFM file: " + `rest`
182 basechar = m.group(1)
183 xoffset = int(m.group(2))
184 yoffset = int(m.group(3))
185 components.append((basechar, xoffset, yoffset))
186 rest = rest[m.regs[0][1]:]
187 if not rest:
188 break
189 assert len(components) == ncomponents
190 self._composites[charname] = components
191
Just6175deb2001-06-24 15:11:31 +0000192 def write(self, path, sep='\r'):
Just7842e561999-12-16 21:34:53 +0000193 import time
194 lines = [ "StartFontMetrics 2.0",
195 "Comment Generated by afmLib, version %s; at %s" %
196 (string.split(__version__)[2],
197 time.strftime("%m/%d/%Y %H:%M:%S",
198 time.localtime(time.time())))]
199
Just32f86842001-04-30 14:40:17 +0000200 # write comments, assuming (possibly wrongly!) they should
201 # all appear at the top
202 for comment in self._comments:
203 lines.append("Comment " + comment)
204
205 # write attributes, first the ones we know about, in
206 # a preferred order
207 attrs = self._attrs
208 for attr in preferredAttributeOrder:
209 if attrs.has_key(attr):
210 value = attrs[attr]
211 if attr == "FontBBox":
212 value = "%s %s %s %s" % value
213 lines.append(attr + " " + str(value))
214 # then write the attributes we don't know about,
215 # in alphabetical order
216 items = attrs.items()
217 items.sort()
Just7842e561999-12-16 21:34:53 +0000218 for attr, value in items:
Just32f86842001-04-30 14:40:17 +0000219 if attr in preferredAttributeOrder:
220 continue
Just7842e561999-12-16 21:34:53 +0000221 lines.append(attr + " " + str(value))
222
223 # write char metrics
224 lines.append("StartCharMetrics " + `len(self._chars)`)
225 items = map(lambda (charname, (charnum, width, box)):
226 (charnum, (charname, width, box)),
227 self._chars.items())
228
229 def myCmp(a, b):
230 """Custom compare function to make sure unencoded chars (-1)
231 end up at the end of the list after sorting."""
232 if a[0] == -1:
233 a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number
234 if b[0] == -1:
235 b = (0xffff,) + b[1:]
236 return cmp(a, b)
237 items.sort(myCmp)
238
239 for charnum, (charname, width, (l, b, r, t)) in items:
240 lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" %
241 (charnum, width, charname, l, b, r, t))
242 lines.append("EndCharMetrics")
243
244 # write kerning info
245 lines.append("StartKernData")
246 lines.append("StartKernPairs " + `len(self._kerning)`)
247 items = self._kerning.items()
248 items.sort() # XXX is order important?
249 for (leftchar, rightchar), value in items:
250 lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
Just7842e561999-12-16 21:34:53 +0000251 lines.append("EndKernPairs")
252 lines.append("EndKernData")
Just32f86842001-04-30 14:40:17 +0000253
254 if self._composites:
255 composites = self._composites.items()
256 composites.sort()
257 lines.append("StartComposites %s" % len(self._composites))
258 for charname, components in composites:
259 line = "CC %s %s ;" % (charname, len(components))
260 for basechar, xoffset, yoffset in components:
261 line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
262 lines.append(line)
263 lines.append("EndComposites")
264
Just7842e561999-12-16 21:34:53 +0000265 lines.append("EndFontMetrics")
266
267 writelines(path, lines, sep)
268
269 def has_kernpair(self, pair):
270 return self._kerning.has_key(pair)
271
272 def kernpairs(self):
273 return self._kerning.keys()
274
275 def has_char(self, char):
276 return self._chars.has_key(char)
277
278 def chars(self):
279 return self._chars.keys()
280
281 def comments(self):
282 return self._comments
283
Just6175deb2001-06-24 15:11:31 +0000284 def addComment(self, comment):
285 self._comments.append(comment)
286
287 def addComposite(self, glyphName, components):
288 self._composites[glyphName] = components
289
Just7842e561999-12-16 21:34:53 +0000290 def __getattr__(self, attr):
291 if self._attrs.has_key(attr):
292 return self._attrs[attr]
293 else:
294 raise AttributeError, attr
295
296 def __setattr__(self, attr, value):
297 # all attrs *not* starting with "_" are consider to be AFM keywords
298 if attr[:1] == "_":
299 self.__dict__[attr] = value
300 else:
301 self._attrs[attr] = value
302
Just6175deb2001-06-24 15:11:31 +0000303 def __delattr__(self, attr):
304 # all attrs *not* starting with "_" are consider to be AFM keywords
305 if attr[:1] == "_":
306 try:
307 del self.__dict__[attr]
308 except KeyError:
309 raise AttributeError, attr
310 else:
311 try:
312 del self._attrs[attr]
313 except KeyError:
314 raise AttributeError, attr
315
Just7842e561999-12-16 21:34:53 +0000316 def __getitem__(self, key):
317 if type(key) == types.TupleType:
318 # key is a tuple, return the kernpair
Just6175deb2001-06-24 15:11:31 +0000319 return self._kerning[key]
Just7842e561999-12-16 21:34:53 +0000320 else:
321 # return the metrics instead
Just6175deb2001-06-24 15:11:31 +0000322 return self._chars[key]
323
324 def __setitem__(self, key, value):
325 if type(key) == types.TupleType:
326 # key is a tuple, set kernpair
327 self._kerning[key] = value
328 else:
329 # set char metrics
330 self._chars[key] = value
331
332 def __delitem__(self, key):
333 if type(key) == types.TupleType:
334 # key is a tuple, del kernpair
335 del self._kerning[key]
336 else:
337 # del char metrics
338 del self._chars[key]
Just7842e561999-12-16 21:34:53 +0000339
340 def __repr__(self):
341 if hasattr(self, "FullName"):
342 return '<AFM object for %s>' % self.FullName
343 else:
344 return '<AFM object at %x>' % id(self)
345
346
347def readlines(path):
348 f = open(path, 'rb')
349 data = f.read()
350 f.close()
351 # read any text file, regardless whether it's formatted for Mac, Unix or Dos
352 sep = ""
353 if '\r' in data:
354 sep = sep + '\r' # mac or dos
355 if '\n' in data:
356 sep = sep + '\n' # unix or dos
357 return string.split(data, sep)
358
Just32f86842001-04-30 14:40:17 +0000359def writelines(path, lines, sep='\r'):
Just7842e561999-12-16 21:34:53 +0000360 f = open(path, 'wb')
361 for line in lines:
362 f.write(line + sep)
363 f.close()
364
365
366
367if __name__ == "__main__":
368 import macfs
369 fss, ok = macfs.StandardGetFile('TEXT')
370 if ok:
371 path = fss.as_pathname()
372 afm = AFM(path)
373 char = 'A'
374 if afm.has_char(char):
375 print afm[char] # print charnum, width and boundingbox
376 pair = ('A', 'V')
377 if afm.has_kernpair(pair):
378 print afm[pair] # print kerning value for pair
379 print afm.Version # various other afm entries have become attributes
380 print afm.Weight
381 # afm.comments() returns a list of all Comment lines found in the AFM
382 print afm.comments()
383 #print afm.chars()
384 #print afm.kernpairs()
385 print afm
Just32f86842001-04-30 14:40:17 +0000386 afm.write(path + ".muck")
Just7842e561999-12-16 21:34:53 +0000387