blob: e98a59fec3187faf6a5802c53ac0f6c69411220d [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
jvr5910f972003-05-24 12:50:47 +000011__version__ = "$Id: afmLib.py,v 1.6 2003-05-24 12:50:47 jvr 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
jvr31ad3512002-11-26 14:09:52 +000021 "(-?\d+)" # width
Just7842e561999-12-16 21:34:53 +000022 "\s*;\s*N\s+" # ; N
jvr3c3c32c2002-03-12 14:34:43 +000023 "([.A-Za-z0-9_]+)" # charname
Just7842e561999-12-16 21:34:53 +000024 "\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
jvr5910f972003-05-24 12:50:47 +000088 _attrs = None
89
Just7842e561999-12-16 21:34:53 +000090 _keywords = ['StartFontMetrics',
91 'EndFontMetrics',
92 'StartCharMetrics',
93 'EndCharMetrics',
94 'StartKernData',
95 'StartKernPairs',
96 'EndKernPairs',
Just32f86842001-04-30 14:40:17 +000097 'EndKernData',
98 'StartComposites',
99 'EndComposites',
100 ]
Just7842e561999-12-16 21:34:53 +0000101
Just32f86842001-04-30 14:40:17 +0000102 def __init__(self, path=None):
Just7842e561999-12-16 21:34:53 +0000103 self._attrs = {}
104 self._chars = {}
105 self._kerning = {}
106 self._index = {}
107 self._comments = []
Just32f86842001-04-30 14:40:17 +0000108 self._composites = {}
Just7842e561999-12-16 21:34:53 +0000109 if path is not None:
110 self.read(path)
111
112 def read(self, path):
113 lines = readlines(path)
114 for line in lines:
115 if not string.strip(line):
116 continue
117 m = identifierRE.match(line)
118 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500119 raise error("syntax error in AFM file: " + repr(line))
Just7842e561999-12-16 21:34:53 +0000120
121 pos = m.regs[1][1]
122 word = line[:pos]
123 rest = string.strip(line[pos:])
124 if word in self._keywords:
125 continue
Just32f86842001-04-30 14:40:17 +0000126 if word == "C":
Just7842e561999-12-16 21:34:53 +0000127 self.parsechar(rest)
128 elif word == "KPX":
129 self.parsekernpair(rest)
Just32f86842001-04-30 14:40:17 +0000130 elif word == "CC":
131 self.parsecomposite(rest)
Just7842e561999-12-16 21:34:53 +0000132 else:
133 self.parseattr(word, rest)
134
135 def parsechar(self, rest):
136 m = charRE.match(rest)
137 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500138 raise error("syntax error in AFM file: " + repr(rest))
Just7842e561999-12-16 21:34:53 +0000139 things = []
140 for fr, to in m.regs[1:]:
141 things.append(rest[fr:to])
142 charname = things[2]
143 del things[2]
Behdad Esfahbode5ca7962013-11-27 04:38:16 -0500144 charnum, width, l, b, r, t = [string.atoi(thing) for thing in things]
Just7842e561999-12-16 21:34:53 +0000145 self._chars[charname] = charnum, width, (l, b, r, t)
146
147 def parsekernpair(self, rest):
148 m = kernRE.match(rest)
149 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500150 raise error("syntax error in AFM file: " + repr(rest))
Just7842e561999-12-16 21:34:53 +0000151 things = []
152 for fr, to in m.regs[1:]:
153 things.append(rest[fr:to])
154 leftchar, rightchar, value = things
155 value = string.atoi(value)
156 self._kerning[(leftchar, rightchar)] = value
157
158 def parseattr(self, word, rest):
159 if word == "FontBBox":
Behdad Esfahbode5ca7962013-11-27 04:38:16 -0500160 l, b, r, t = [string.atoi(thing) for thing in string.split(rest)]
Just7842e561999-12-16 21:34:53 +0000161 self._attrs[word] = l, b, r, t
162 elif word == "Comment":
163 self._comments.append(rest)
164 else:
165 try:
166 value = string.atoi(rest)
167 except (ValueError, OverflowError):
168 self._attrs[word] = rest
169 else:
170 self._attrs[word] = value
171
Just32f86842001-04-30 14:40:17 +0000172 def parsecomposite(self, rest):
173 m = compositeRE.match(rest)
174 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500175 raise error("syntax error in AFM file: " + repr(rest))
Just32f86842001-04-30 14:40:17 +0000176 charname = m.group(1)
177 ncomponents = int(m.group(2))
178 rest = rest[m.regs[0][1]:]
179 components = []
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500180 while True:
Just32f86842001-04-30 14:40:17 +0000181 m = componentRE.match(rest)
182 if m is None:
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500183 raise error("syntax error in AFM file: " + repr(rest))
Just32f86842001-04-30 14:40:17 +0000184 basechar = m.group(1)
185 xoffset = int(m.group(2))
186 yoffset = int(m.group(3))
187 components.append((basechar, xoffset, yoffset))
188 rest = rest[m.regs[0][1]:]
189 if not rest:
190 break
191 assert len(components) == ncomponents
192 self._composites[charname] = components
193
Just6175deb2001-06-24 15:11:31 +0000194 def write(self, path, sep='\r'):
Just7842e561999-12-16 21:34:53 +0000195 import time
196 lines = [ "StartFontMetrics 2.0",
197 "Comment Generated by afmLib, version %s; at %s" %
198 (string.split(__version__)[2],
199 time.strftime("%m/%d/%Y %H:%M:%S",
200 time.localtime(time.time())))]
201
Just32f86842001-04-30 14:40:17 +0000202 # write comments, assuming (possibly wrongly!) they should
203 # all appear at the top
204 for comment in self._comments:
205 lines.append("Comment " + comment)
206
207 # write attributes, first the ones we know about, in
208 # a preferred order
209 attrs = self._attrs
210 for attr in preferredAttributeOrder:
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500211 if attr in attrs:
Just32f86842001-04-30 14:40:17 +0000212 value = attrs[attr]
213 if attr == "FontBBox":
214 value = "%s %s %s %s" % value
215 lines.append(attr + " " + str(value))
216 # then write the attributes we don't know about,
217 # in alphabetical order
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500218 items = sorted(attrs.items())
Just7842e561999-12-16 21:34:53 +0000219 for attr, value in items:
Just32f86842001-04-30 14:40:17 +0000220 if attr in preferredAttributeOrder:
221 continue
Just7842e561999-12-16 21:34:53 +0000222 lines.append(attr + " " + str(value))
223
224 # write char metrics
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500225 lines.append("StartCharMetrics " + repr(len(self._chars)))
Behdad Esfahbode5ca7962013-11-27 04:38:16 -0500226 items = [(charnum, (charname, width, box)) for charname, (charnum, width, box) in self._chars.items()]
Just7842e561999-12-16 21:34:53 +0000227
228 def myCmp(a, b):
229 """Custom compare function to make sure unencoded chars (-1)
230 end up at the end of the list after sorting."""
231 if a[0] == -1:
232 a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number
233 if b[0] == -1:
234 b = (0xffff,) + b[1:]
235 return cmp(a, b)
236 items.sort(myCmp)
237
238 for charnum, (charname, width, (l, b, r, t)) in items:
239 lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" %
240 (charnum, width, charname, l, b, r, t))
241 lines.append("EndCharMetrics")
242
243 # write kerning info
244 lines.append("StartKernData")
Behdad Esfahboddc7e6f32013-11-27 02:44:56 -0500245 lines.append("StartKernPairs " + repr(len(self._kerning)))
Just7842e561999-12-16 21:34:53 +0000246 items = self._kerning.items()
247 items.sort() # XXX is order important?
248 for (leftchar, rightchar), value in items:
249 lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
Just7842e561999-12-16 21:34:53 +0000250 lines.append("EndKernPairs")
251 lines.append("EndKernData")
Just32f86842001-04-30 14:40:17 +0000252
253 if self._composites:
Behdad Esfahbodac1b4352013-11-27 04:15:34 -0500254 composites = sorted(self._composites.items())
Just32f86842001-04-30 14:40:17 +0000255 lines.append("StartComposites %s" % len(self._composites))
256 for charname, components in composites:
257 line = "CC %s %s ;" % (charname, len(components))
258 for basechar, xoffset, yoffset in components:
259 line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
260 lines.append(line)
261 lines.append("EndComposites")
262
Just7842e561999-12-16 21:34:53 +0000263 lines.append("EndFontMetrics")
264
265 writelines(path, lines, sep)
266
267 def has_kernpair(self, pair):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500268 return pair in self._kerning
Just7842e561999-12-16 21:34:53 +0000269
270 def kernpairs(self):
271 return self._kerning.keys()
272
273 def has_char(self, char):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500274 return char in self._chars
Just7842e561999-12-16 21:34:53 +0000275
276 def chars(self):
277 return self._chars.keys()
278
279 def comments(self):
280 return self._comments
281
Just6175deb2001-06-24 15:11:31 +0000282 def addComment(self, comment):
283 self._comments.append(comment)
284
285 def addComposite(self, glyphName, components):
286 self._composites[glyphName] = components
287
Just7842e561999-12-16 21:34:53 +0000288 def __getattr__(self, attr):
Behdad Esfahbodbc5e1cb2013-11-27 02:33:03 -0500289 if attr in self._attrs:
Just7842e561999-12-16 21:34:53 +0000290 return self._attrs[attr]
291 else:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500292 raise AttributeError(attr)
Just7842e561999-12-16 21:34:53 +0000293
294 def __setattr__(self, attr, value):
295 # all attrs *not* starting with "_" are consider to be AFM keywords
296 if attr[:1] == "_":
297 self.__dict__[attr] = value
298 else:
299 self._attrs[attr] = value
300
Just6175deb2001-06-24 15:11:31 +0000301 def __delattr__(self, attr):
302 # all attrs *not* starting with "_" are consider to be AFM keywords
303 if attr[:1] == "_":
304 try:
305 del self.__dict__[attr]
306 except KeyError:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500307 raise AttributeError(attr)
Just6175deb2001-06-24 15:11:31 +0000308 else:
309 try:
310 del self._attrs[attr]
311 except KeyError:
Behdad Esfahbodcd5aad92013-11-27 02:42:28 -0500312 raise AttributeError(attr)
Just6175deb2001-06-24 15:11:31 +0000313
Just7842e561999-12-16 21:34:53 +0000314 def __getitem__(self, key):
Behdad Esfahbod002c32f2013-11-27 04:48:20 -0500315 if isinstance(key, tuple):
Just7842e561999-12-16 21:34:53 +0000316 # key is a tuple, return the kernpair
Just6175deb2001-06-24 15:11:31 +0000317 return self._kerning[key]
Just7842e561999-12-16 21:34:53 +0000318 else:
319 # return the metrics instead
Just6175deb2001-06-24 15:11:31 +0000320 return self._chars[key]
321
322 def __setitem__(self, key, value):
Behdad Esfahbod002c32f2013-11-27 04:48:20 -0500323 if isinstance(key, tuple):
Just6175deb2001-06-24 15:11:31 +0000324 # key is a tuple, set kernpair
325 self._kerning[key] = value
326 else:
327 # set char metrics
328 self._chars[key] = value
329
330 def __delitem__(self, key):
Behdad Esfahbod002c32f2013-11-27 04:48:20 -0500331 if isinstance(key, tuple):
Just6175deb2001-06-24 15:11:31 +0000332 # key is a tuple, del kernpair
333 del self._kerning[key]
334 else:
335 # del char metrics
336 del self._chars[key]
Just7842e561999-12-16 21:34:53 +0000337
338 def __repr__(self):
339 if hasattr(self, "FullName"):
340 return '<AFM object for %s>' % self.FullName
341 else:
342 return '<AFM object at %x>' % id(self)
343
344
345def readlines(path):
346 f = open(path, 'rb')
347 data = f.read()
348 f.close()
349 # read any text file, regardless whether it's formatted for Mac, Unix or Dos
350 sep = ""
351 if '\r' in data:
352 sep = sep + '\r' # mac or dos
353 if '\n' in data:
354 sep = sep + '\n' # unix or dos
355 return string.split(data, sep)
356
Just32f86842001-04-30 14:40:17 +0000357def writelines(path, lines, sep='\r'):
Just7842e561999-12-16 21:34:53 +0000358 f = open(path, 'wb')
359 for line in lines:
360 f.write(line + sep)
361 f.close()
362
363
364
365if __name__ == "__main__":
jvr91bca422012-10-18 12:49:22 +0000366 import EasyDialogs
367 path = EasyDialogs.AskFileForOpen()
368 if path:
Just7842e561999-12-16 21:34:53 +0000369 afm = AFM(path)
370 char = 'A'
371 if afm.has_char(char):
372 print afm[char] # print charnum, width and boundingbox
373 pair = ('A', 'V')
374 if afm.has_kernpair(pair):
375 print afm[pair] # print kerning value for pair
376 print afm.Version # various other afm entries have become attributes
377 print afm.Weight
378 # afm.comments() returns a list of all Comment lines found in the AFM
379 print afm.comments()
380 #print afm.chars()
381 #print afm.kernpairs()
382 print afm
Just32f86842001-04-30 14:40:17 +0000383 afm.write(path + ".muck")
Just7842e561999-12-16 21:34:53 +0000384