added first version of AFM parser#
[PyX/mjg.git] / pyx / font / afm.py
blob0caac565ea47f82bd805d1e95897345b8adff866
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2006 Jörg Lehmann <joergl@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 import string
25 class AFMError(Exception):
26 pass
28 _READ_START = 0
29 _READ_HEADER = 1
30 _READ_DIRECTION = 2
31 _READ_CHARMETRICS = 3
32 _READ_KERNDATA = 4
33 _READ_TRACKKERN = 5
34 _READ_KERNPAIRS = 6
35 _READ_COMPOSITES = 7
36 _READ_END = 8
38 def parseint(s):
39 try:
40 return int(s)
41 except:
42 raise AFMError("Expecting int, got '%s'" % s)
44 def parsehex(s):
45 try:
46 if s[0] != "<" or s[-1] != ">":
47 raise AFMError()
48 return int(s[1:-1], 16)
49 except:
50 raise AFMError("Expecting hexadecimal int, got '%s'" % s)
52 def parsefloat(s):
53 try:
54 return float(s)
55 except:
56 raise AFMError("Expecting float, got '%s'" % s)
58 def parsefloats(s, nos):
59 try:
60 numbers = s.split()
61 result = map(float, numbers)
62 if len(result) != nos:
63 raise AFMError()
64 except:
65 raise AFMError("Expecting list of %d numbers, got '%s'" % (s, nos))
66 return result
68 def parsestr(s):
69 # XXX: check for invalid characters in s
70 return s
72 def parsebool(s):
73 s = s.rstrip()
74 if s == "true":
75 return 1
76 elif s == "false":
77 return 0
78 else:
79 raise AFMError("Expecting boolean, got '%s'" % s)
81 class AFMcharmetrics:
82 def __init__(self, code, widths=None, vvector=None, name=None, bbox=None, ligatures=None):
83 self.code = code
84 if widths is None:
85 self.widths = [None] * 2
86 else:
87 self.widths = widths
88 self.vvector = vvector
89 self.name = name
90 self.bbox = bbox
91 if ligatures is None:
92 self.ligatures = []
93 else:
94 self.ligatures = ligatures
97 class AFMtrackkern:
98 def __init__(self, degree, min_ptsize, min_kern, max_ptsize, max_kern):
99 self.degree = degree
100 self.min_ptsize = min_ptsize
101 self.min_kern = min_kern
102 self.max_ptsize = max_ptsize
103 self.max_kern = max_kern
106 class AFMkernpair:
107 def __init__(self, name1, name2, x, y):
108 self.name1 = name1
109 self.name2 = name2
110 self.x = x
111 self.y = y
114 class AFMcomposite:
115 def __init__(self, name, parts):
116 self.name = name
117 self.parts = parts
120 class AFMfile:
122 def __init__(self, filename):
123 self.filename = filename
124 self.metricssets = 0 # int, optional
125 self.fontname = None # str, required
126 self.fullname = None # str, optional
127 self.familyname = None # str, optional
128 self.weight = None # str, optional
129 self.fontbbox = None # 4 floats, required
130 self.version = None # str, optional
131 self.notice = None # str, optional
132 self.encodingscheme = None # str, optional
133 self.mappingscheme = None # int, optional (not present in base font programs)
134 self.escchar = None # int, required if mappingscheme == 3
135 self.characterset = None # str, optional
136 self.characters = None # int, optional
137 self.isbasefont = 1 # bool, optional
138 self.vvector = None # 2 floats, required if metricssets == 2
139 self.isfixedv = None # bool, default: true if vvector present, false otherwise
140 self.capheight = None # float, optional
141 self.xheight = None # float, optional
142 self.ascender = None # float, optional
143 self.descender = None # float, optional
144 self.underlinepositions = [None] * 2 # int, optional (for each direction)
145 self.underlinethicknesses = [None] * 2 # float, optional (for each direction)
146 self.italicangles = [None] * 2 # float, optional (for each direction)
147 self.charwidths = [None] * 2 # 2 floats, optional (for each direction)
148 self.isfixedpitchs = [None] * 2 # bool, optional (for each direction)
149 self.charmetrics = None # list of character metrics information, optional
150 self.trackkerns = None # list of track kernings, optional
151 self.kernpairs = [None] * 2 # list of list of kerning pairs (for each direction), optional
152 self.composites = None # list of composite character data sets, optional
153 self.parse()
155 # the following methods process a line when the reader is in the corresponding
156 # state and return the new state
157 def _processline_start(self, line):
158 key, args = line.split(None, 1)
159 if key != "StartFontMetrics":
160 raise AFMError("Expecting StartFontMetrics, no found")
161 return _READ_HEADER, None
163 def _processline_header(self, line):
164 try:
165 key, args = line.split(None, 1)
166 except ValueError:
167 key = line.rstrip()
168 if key == "Comment":
169 return _READ_HEADER, None
170 elif key == "MetricsSets":
171 self.metricssets = parseint(args)
172 if direction is not None:
173 raise AFMError("MetricsSets not allowed after first (implicit) StartDirection")
174 elif key == "FontName":
175 self.fontname = parsestr(args)
176 elif key == "FullName":
177 self.fullname = parsestr(args)
178 elif key == "FamilyName":
179 self.familyname = parsestr(args)
180 elif key == "Weight":
181 self.weight = parsestr(args)
182 elif key == "FontBBox":
183 self.fontbbox = parsefloats(args, 4)
184 elif key == "Version":
185 self.version = parsestr(args)
186 elif key == "Notice":
187 self.notice = parsestr(args)
188 elif key == "EncodingScheme":
189 self.encodingscheme = parsestr(args)
190 elif key == "MappingScheme":
191 self.mappingscheme = parseint(args)
192 elif key == "EscChar":
193 self.escchar = parseint(args)
194 elif key == "CharacterSet":
195 self.characterset = parsestr(args)
196 elif key == "Characters":
197 self.characters = parseint(args)
198 elif key == "IsBaseFont":
199 self.isbasefont = parsebool(args)
200 elif key == "VVector":
201 self.vvector = parsefloats(args, 2)
202 elif key == "IsFixedV":
203 self.isfixedv = parsebool(args)
204 elif key == "CapHeight":
205 self.capheight = parsefloat(args)
206 elif key == "XHeight":
207 self.xheight = parsefloat(args)
208 elif key == "Ascender":
209 self.ascender = parsefloat(args)
210 elif key == "Descender":
211 self.descender = parsefloat(args)
212 elif key == "StartDirection":
213 direction = parseint(args)
214 if 0 <= direction <= 2:
215 return _READ_DIRECTION, direction
216 else:
217 raise AFMError("Wrong direction number %d" % direction)
218 elif (key == "UnderLinePosition" or key == "UnderlineThickness" or key == "ItalicAngle" or
219 key == "Charwidth" or key == "IsFixedPitch"):
220 # we implicitly entered a direction section, so we should process the line again
221 return self._processline_direction(line, 0)
222 elif key == "StartCharMetrics":
223 if self.charmetrics is not None:
224 raise AFMError("Multiple character metrics sections")
225 self.charmetrics = [None] * parseint(args)
226 return _READ_CHARMETRICS, 0
227 elif key == "StartKernData":
228 return _READ_KERNDATA, None
229 elif key == "StartComposites":
230 if self.composites is not None:
231 raise AFMError("Multiple composite character data sections")
232 self.composites = [None] * parseint(args)
233 return _READ_COMPOSITES, 0
234 elif key == "EndFontMetrics":
235 return _READ_END, None
236 elif key[0] in string.lowercase:
237 # ignoring private commands
238 pass
239 return _READ_HEADER, None
241 def _processline_direction(self, line, direction):
242 try:
243 key, args = line.split(None, 1)
244 except ValueError:
245 key = line.rstrip()
246 if key == "UnderLinePosition":
247 self.underlinepositions[direction] = parseint(args)
248 elif key == "UnderlineThickness":
249 self.underlinethicknesses[direction] = parsefloat(args)
250 elif key == "ItalicAngle":
251 self.italicangles[direction] = parsefloat(args)
252 elif key == "Charwidth":
253 self.charwidths[direction] = parsefloats(args, 2)
254 elif key == "IsFixedPitch":
255 self.isfixedpitchs[direction] = parsebool(args)
256 elif key == "EndDirection":
257 return _READ_HEADER, None
258 else:
259 # we assume that we are implicitly leaving the direction section again,
260 # so try to reprocess the line in the header reader state
261 return self._processline_header(line)
262 return _READ_DIRECTION, direction
264 def _processline_charmetrics(self, line, charno):
265 if line.rstrip() == "EndCharMetrics":
266 if charno != len(self.charmetrics):
267 raise AFMError("Fewer character metrics than expected")
268 return _READ_HEADER, None
269 if charno >= len(self.charmetrics):
270 raise AFMError("More character metrics than expected")
272 char = None
273 for s in line.split(";"):
274 s = s.strip()
275 if not s:
276 continue
277 key, args = s.split(None, 1)
278 if key == "C":
279 if char is not None:
280 raise AFMError("Cannot define char code twice")
281 char = AFMcharmetrics(parseint(args))
282 elif key == "CH":
283 if char is not None:
284 raise AFMError("Cannot define char code twice")
285 char = AFMcharmetrics(parsehex(args))
286 elif key == "WX" or key == "W0X":
287 char.widths[0] = parsefloat(args), 0
288 elif key == "W1X":
289 char.widths[1] = parsefloat(args), 0
290 elif key == "WY" or key == "W0Y":
291 char.widths[0] = 0, parsefloat(args)
292 elif key == "W1Y":
293 char.widths[1] = 0, parsefloat(args)
294 elif key == "W" or key == "W0":
295 char.widths[0] = parsefloats(args, 2)
296 elif key == "W1":
297 char.widths[1] = parsefloats(args, 2)
298 elif key == "VV":
299 char.vvector = parsefloats(args, 2)
300 elif key == "N":
301 # XXX: we should check that name is valid (no whitespcae, etc.)
302 char.name = parsestr(args)
303 elif key == "B":
304 char.bbox = parsefloats(args, 4)
305 elif key == "L":
306 successor, ligature = args.split(None, 1)
307 char.ligatures.append((parsestr(successor), ligature))
308 else:
309 raise AFMError("Undefined command in character widths specification: '%s'", s)
310 if char is None:
311 raise AFMError("Character metrics not defined")
313 self.charmetrics[charno] = char
314 return _READ_CHARMETRICS, charno+1
316 def _processline_kerndata(self, line):
317 try:
318 key, args = line.split(None, 1)
319 except ValueError:
320 key = line.rstrip()
321 if key == "Comment":
322 return _READ_KERNDATA, None
323 if key == "StartTrackKern":
324 if self.trackkerns is not None:
325 raise AFMError("Multiple track kernings data sections")
326 self.trackkerns = [None] * parseint(args)
327 return _READ_TRACKKERN, 0
328 elif key == "StartKernPairs" or key == "StartKernPairs0":
329 if self.kernpairs[0] is not None:
330 raise AFMError("Multiple kerning pairs data sections for direction 0")
331 self.kernpairs[0] = [None] * parseint(args)
332 return _READ_KERNPAIRS, 0
333 elif key == "StartKernPairs1":
334 if self.kernpairs[1] is not None:
335 raise AFMError("Multiple kerning pairs data sections for direction 0")
336 self.kernpairs[1] = [None] * parseint(args)
337 return _READ_KERNPAIRS, 1
338 elif key == "EndKernData":
339 return _READ_HEADER, None
340 else:
341 raise AFMError("Unsupported key %s in kerning data section" % key)
343 def _processline_trackkern(self, line, i):
344 try:
345 key, args = line.split(None, 1)
346 except ValueError:
347 key = line.rstrip()
348 if key == "Comment":
349 return _READ_TRACKKERN, i
350 elif key == "TrackKern":
351 if i >= len(self.trackkerns):
352 raise AFMError("More track kerning data sets than expected")
353 degrees, args = args.split(None, 1)
354 self.trackkerns[i] = AFMtrackkern(int(degrees), *parsefloats(args, 4))
355 return _READ_TRACKKERN, i+1
356 elif key == "EndTrackKern":
357 if i < len(self.trackkerns):
358 raise AFMError("Fewer track kerning data sets than expected")
359 return _READ_KERNDATA, None
360 else:
361 raise AFMError("Unsupported key %s in kerning data section" % key)
363 def _processline_kernpairs(self, line, (direction, i)):
364 try:
365 key, args = line.split(None, 1)
366 except ValueError:
367 key = line.rstrip()
368 if key == "Comment":
369 return _READ_KERNPAIRS, (direction, i)
370 else:
371 if i >= len(self.kernpairs):
372 raise AFMError("More track kerning data sets than expected")
373 if key == "KP":
374 try:
375 name1, name2, x, y = args.split()
376 except:
377 raise AFMError("Expecting name1, name2, x, y, got '%s'" % args)
378 self.kernpairs[direction][i] = AFMkernpair(name1, name2, parsefloat(x), parsefloat(y))
379 elif key == "KPH":
380 try:
381 hex1, hex2, x, y = args.split()
382 except:
383 raise AFMError("Expecting <hex1>, <hex2>, x, y, got '%s'" % args)
384 self.kernpairs[direction][i] = AFMkernpair(parsehex(hex1), parsehex(hex2),
385 parsefloat(x), parsefloat(y))
386 elif key == "KPX":
387 try:
388 name1, name2, x = args.split()
389 except:
390 raise AFMError("Expecting name1, name2, x, got '%s'" % args)
391 self.kernpairs[direction][i] = AFMkernpair(name1, name2, parsefloat(x), 0)
392 elif key == "KPY":
393 try:
394 name1, name2, y = args.split()
395 except:
396 raise AFMError("Expecting name1, name2, x, got '%s'" % args)
397 self.kernpairs[direction][i] = AFMkernpair(name1, name2, 0, parsefloat(y))
398 elif key == "EndKernPairs":
399 if i < len(self.kernpairs[direction]):
400 raise AFMError("Fewer kerning pairs than expected")
401 else:
402 raise AFMError("Unknown key '%s' in kern pair section" % key)
403 return _READ_KERNPAIRS, (direction, i+1)
405 def _processline_composites(self, line, i):
406 if line.rstrip() == "EndComposites":
407 if i < len(self.composites):
408 raise AFMError("Fewer composite character data sets than expected")
409 return _READ_HEADER, None
410 if i >= len(self.composites):
411 raise AFMError("More composite character data sets than expected")
413 name = None
414 no = None
415 parts = []
416 for s in line.split(";"):
417 s = s.strip()
418 if not s:
419 continue
420 key, args = s.split(None, 1)
421 if key == "CC":
422 try:
423 name, no = args.split()
424 except:
425 raise AFMError("Expecting name number, got '%s'" % args)
426 no = parseint(no)
427 elif key == "PCC":
428 try:
429 name1, x, y = args.split()
430 except:
431 raise AFMError("Expecting name x y, got '%s'" % args)
432 parts.append((name1, parsefloat(x), parsefloat(y)))
433 else:
434 raise AFMError("Unknown key '%s' in composite character data section" % key)
435 if len(parts) != no:
436 raise AFMError("Wrong number of composite characters")
437 self.composites[i] = AFMcomposite(name, parts)
438 return _READ_COMPOSITES, i+1
440 def parse(self):
441 f = open(self.filename, "r")
442 try:
443 # state of the reader, consisting of
444 # - the main state, i.e. the type of the section
445 # - optionally a substate, which defines the section, if it can occur more than once
446 state = _READ_START, None
447 for line in f:
448 line = line[:-1]
449 mstate, sstate = state
450 if mstate == _READ_START:
451 state = self._processline_start(line)
452 else:
453 # except for the first line, any empty will be ignored
454 if not line.strip():
455 continue
456 if mstate == _READ_HEADER:
457 state = self._processline_header(line)
458 elif mstate == _READ_DIRECTION:
459 state = self._processline_direction(line, sstate)
460 elif mstate == _READ_CHARMETRICS:
461 state = self._processline_charmetrics(line, sstate)
462 elif mstate == _READ_KERNDATA:
463 state = self._processline_kerndata(line, sstate)
464 elif mstate == _READ_TRACKKERN:
465 state = self._processline_trackkern(line, sstate)
466 elif mstate == _READ_KERNPAIRS:
467 state = self._processline_kernpairs(line, sstate)
468 elif mstate == _READ_COMPOSITES:
469 state = self._processline_composites(line, sstate)
470 else:
471 raise RuntimeError("Undefined state in AFM reader")
472 finally:
473 f.close()
475 if __name__ == "__main__":
476 a = AFMfile("/private/opt/local/share/texmf-dist/fonts/afm/yandy/lucida/lbc.afm")
477 print a.charmetrics[0].name