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