use AFM exception
[PyX.git] / pyx / font / afmfile.py
blobec9ba1836ed1deb0ffd6cea8a5ef0f404442f082
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.glyphs = None # dictionary mapping glyph names to character metrics information, optional
153 self.trackkerns = None # list of track kernings, optional
154 self.kernpairs = [None] * 2 # list of list of kerning pairs (for each direction), optional
155 self.composites = None # list of composite character data sets, optional
156 self.parse()
157 if self.isfixedv is None:
158 self.isfixedv = self.vvector is not None
159 # XXX we should check the constraints on some parameters
161 # the following methods process a line when the reader is in the corresponding
162 # state and return the new state
163 def _processline_start(self, line):
164 key, args = line.split(None, 1)
165 if key != "StartFontMetrics":
166 raise AFMError("Expecting StartFontMetrics, no found")
167 return _READ_MAIN, None
169 def _processline_main(self, line):
170 try:
171 key, args = line.split(None, 1)
172 except ValueError:
173 key = line.rstrip()
174 if key == "Comment":
175 return _READ_MAIN, None
176 elif key == "MetricsSets":
177 self.metricssets = _parseint(args)
178 if direction is not None:
179 raise AFMError("MetricsSets not allowed after first (implicit) StartDirection")
180 elif key == "FontName":
181 self.fontname = _parsestr(args)
182 elif key == "FullName":
183 self.fullname = _parsestr(args)
184 elif key == "FamilyName":
185 self.familyname = _parsestr(args)
186 elif key == "Weight":
187 self.weight = _parsestr(args)
188 elif key == "FontBBox":
189 self.fontbbox = _parsefloats(args, 4)
190 elif key == "Version":
191 self.version = _parsestr(args)
192 elif key == "Notice":
193 self.notice = _parsestr(args)
194 elif key == "EncodingScheme":
195 self.encodingscheme = _parsestr(args)
196 elif key == "MappingScheme":
197 self.mappingscheme = _parseint(args)
198 elif key == "EscChar":
199 self.escchar = _parseint(args)
200 elif key == "CharacterSet":
201 self.characterset = _parsestr(args)
202 elif key == "Characters":
203 self.characters = _parseint(args)
204 elif key == "IsBaseFont":
205 self.isbasefont = _parsebool(args)
206 elif key == "VVector":
207 self.vvector = _parsefloats(args, 2)
208 elif key == "IsFixedV":
209 self.isfixedv = _parsebool(args)
210 elif key == "CapHeight":
211 self.capheight = _parsefloat(args)
212 elif key == "XHeight":
213 self.xheight = _parsefloat(args)
214 elif key == "Ascender":
215 self.ascender = _parsefloat(args)
216 elif key == "Descender":
217 self.descender = _parsefloat(args)
218 elif key == "StartDirection":
219 direction = _parseint(args)
220 if 0 <= direction <= 2:
221 return _READ_DIRECTION, direction
222 else:
223 raise AFMError("Wrong direction number %d" % direction)
224 elif (key == "UnderLinePosition" or key == "UnderlineThickness" or key == "ItalicAngle" or
225 key == "Charwidth" or key == "IsFixedPitch"):
226 # we implicitly entered a direction section, so we should process the line again
227 return self._processline_direction(line, 0)
228 elif key == "StartCharMetrics":
229 if self.charmetrics is not None:
230 raise AFMError("Multiple character metrics sections")
231 self.charmetrics = [None] * _parseint(args)
232 self.glyphs = {}
233 return _READ_CHARMETRICS, 0
234 elif key == "StartKernData":
235 return _READ_KERNDATA, None
236 elif key == "StartComposites":
237 if self.composites is not None:
238 raise AFMError("Multiple composite character data sections")
239 self.composites = [None] * _parseint(args)
240 return _READ_COMPOSITES, 0
241 elif key == "EndFontMetrics":
242 return _READ_END, None
243 elif key[0] in string.lowercase:
244 # ignoring private commands
245 pass
246 return _READ_MAIN, None
248 def _processline_direction(self, line, direction):
249 try:
250 key, args = line.split(None, 1)
251 except ValueError:
252 key = line.rstrip()
253 if key == "UnderLinePosition":
254 self.underlinepositions[direction] = _parseint(args)
255 elif key == "UnderlineThickness":
256 self.underlinethicknesses[direction] = _parsefloat(args)
257 elif key == "ItalicAngle":
258 self.italicangles[direction] = _parsefloat(args)
259 elif key == "Charwidth":
260 self.charwidths[direction] = _parsefloats(args, 2)
261 elif key == "IsFixedPitch":
262 self.isfixedpitchs[direction] = _parsebool(args)
263 elif key == "EndDirection":
264 return _READ_MAIN, None
265 else:
266 # we assume that we are implicitly leaving the direction section again,
267 # so try to reprocess the line in the header reader state
268 return self._processline_main(line)
269 return _READ_DIRECTION, direction
271 def _processline_charmetrics(self, line, charno):
272 if line.rstrip() == "EndCharMetrics":
273 if charno != len(self.charmetrics):
274 raise AFMError("Fewer character metrics than expected")
275 return _READ_MAIN, None
276 if charno >= len(self.charmetrics):
277 raise AFMError("More character metrics than expected")
279 has_name = False
280 char = None
281 for s in line.split(";"):
282 s = s.strip()
283 if not s:
284 continue
285 key, args = s.split(None, 1)
286 if key == "C":
287 if char is not None:
288 raise AFMError("Cannot define char code twice")
289 char = AFMcharmetrics(_parseint(args))
290 elif key == "CH":
291 if char is not None:
292 raise AFMError("Cannot define char code twice")
293 char = AFMcharmetrics(_parsehex(args))
294 elif key == "WX" or key == "W0X":
295 char.widths[0] = _parsefloat(args), 0
296 elif key == "W1X":
297 char.widths[1] = _parsefloat(args), 0
298 elif key == "WY" or key == "W0Y":
299 char.widths[0] = 0, _parsefloat(args)
300 elif key == "W1Y":
301 char.widths[1] = 0, _parsefloat(args)
302 elif key == "W" or key == "W0":
303 char.widths[0] = _parsefloats(args, 2)
304 elif key == "W1":
305 char.widths[1] = _parsefloats(args, 2)
306 elif key == "VV":
307 char.vvector = _parsefloats(args, 2)
308 elif key == "N":
309 # XXX: we should check that name is valid (no whitespcae, etc.)
310 has_name = True
311 char.name = _parsestr(args)
312 elif key == "B":
313 char.bbox = _parsefloats(args, 4)
314 elif key == "L":
315 successor, ligature = args.split(None, 1)
316 char.ligatures.append((_parsestr(successor), ligature))
317 else:
318 raise AFMError("Undefined command in character widths specification: '%s'", s)
319 if char is None:
320 raise AFMError("Character metrics not defined")
322 self.charmetrics[charno] = char
323 if has_name:
324 self.glyphs[char.name] = char
325 return _READ_CHARMETRICS, charno+1
327 def _processline_kerndata(self, line):
328 try:
329 key, args = line.split(None, 1)
330 except ValueError:
331 key = line.rstrip()
332 if key == "Comment":
333 return _READ_KERNDATA, None
334 if key == "StartTrackKern":
335 if self.trackkerns is not None:
336 raise AFMError("Multiple track kernings data sections")
337 self.trackkerns = [None] * _parseint(args)
338 return _READ_TRACKKERN, 0
339 elif key == "StartKernPairs" or key == "StartKernPairs0":
340 if self.kernpairs[0] is not None:
341 raise AFMError("Multiple kerning pairs data sections for direction 0")
342 self.kernpairs[0] = [None] * _parseint(args)
343 return _READ_KERNPAIRS, (0, 0)
344 elif key == "StartKernPairs1":
345 if self.kernpairs[1] is not None:
346 raise AFMError("Multiple kerning pairs data sections for direction 0")
347 self.kernpairs[1] = [None] * _parseint(args)
348 return _READ_KERNPAIRS, (1, 0)
349 elif key == "EndKernData":
350 return _READ_MAIN, None
351 else:
352 raise AFMError("Unsupported key %s in kerning data section" % key)
354 def _processline_trackkern(self, line, i):
355 try:
356 key, args = line.split(None, 1)
357 except ValueError:
358 key = line.rstrip()
359 if key == "Comment":
360 return _READ_TRACKKERN, i
361 elif key == "TrackKern":
362 if i >= len(self.trackkerns):
363 raise AFMError("More track kerning data sets than expected")
364 degrees, args = args.split(None, 1)
365 self.trackkerns[i] = AFMtrackkern(int(degrees), *_parsefloats(args, 4))
366 return _READ_TRACKKERN, i+1
367 elif key == "EndTrackKern":
368 if i < len(self.trackkerns):
369 raise AFMError("Fewer track kerning data sets than expected")
370 return _READ_KERNDATA, None
371 else:
372 raise AFMError("Unsupported key %s in kerning data section" % key)
374 def _processline_kernpairs(self, line, (direction, i)):
375 try:
376 key, args = line.split(None, 1)
377 except ValueError:
378 key = line.rstrip()
379 if key == "Comment":
380 return _READ_KERNPAIRS, (direction, i)
381 elif key == "EndKernPairs":
382 if i < len(self.kernpairs[direction]):
383 raise AFMError("Fewer kerning pairs than expected")
384 return _READ_KERNDATA, None
385 else:
386 if i >= len(self.kernpairs[direction]):
387 raise AFMError("More kerning pairs than expected")
388 if key == "KP":
389 try:
390 name1, name2, x, y = args.split()
391 except:
392 raise AFMError("Expecting name1, name2, x, y, got '%s'" % args)
393 self.kernpairs[direction][i] = AFMkernpair(name1, name2, _parsefloat(x), _parsefloat(y))
394 elif key == "KPH":
395 try:
396 hex1, hex2, x, y = args.split()
397 except:
398 raise AFMError("Expecting <hex1>, <hex2>, x, y, got '%s'" % args)
399 self.kernpairs[direction][i] = AFMkernpair(_parsehex(hex1), _parsehex(hex2),
400 _parsefloat(x), _parsefloat(y))
401 elif key == "KPX":
402 try:
403 name1, name2, x = args.split()
404 except:
405 raise AFMError("Expecting name1, name2, x, got '%s'" % args)
406 self.kernpairs[direction][i] = AFMkernpair(name1, name2, _parsefloat(x), 0)
407 elif key == "KPY":
408 try:
409 name1, name2, y = args.split()
410 except:
411 raise AFMError("Expecting name1, name2, x, got '%s'" % args)
412 self.kernpairs[direction][i] = AFMkernpair(name1, name2, 0, _parsefloat(y))
413 else:
414 raise AFMError("Unknown key '%s' in kern pair section" % key)
415 return _READ_KERNPAIRS, (direction, i+1)
417 def _processline_composites(self, line, i):
418 if line.rstrip() == "EndComposites":
419 if i < len(self.composites):
420 raise AFMError("Fewer composite character data sets than expected")
421 return _READ_MAIN, None
422 if i >= len(self.composites):
423 raise AFMError("More composite character data sets than expected")
425 name = None
426 no = None
427 parts = []
428 for s in line.split(";"):
429 s = s.strip()
430 if not s:
431 continue
432 key, args = s.split(None, 1)
433 if key == "CC":
434 try:
435 name, no = args.split()
436 except:
437 raise AFMError("Expecting name number, got '%s'" % args)
438 no = _parseint(no)
439 elif key == "PCC":
440 try:
441 name1, x, y = args.split()
442 except:
443 raise AFMError("Expecting name x y, got '%s'" % args)
444 parts.append((name1, _parsefloat(x), _parsefloat(y)))
445 else:
446 raise AFMError("Unknown key '%s' in composite character data section" % key)
447 if len(parts) != no:
448 raise AFMError("Wrong number of composite characters")
449 self.composites[i] = AFMcomposite(name, parts)
450 return _READ_COMPOSITES, i+1
452 def parse(self):
453 f = open(self.filename, "r")
454 try:
455 # state of the reader, consisting of
456 # - the main state, i.e. the type of the section
457 # - a parameter sstate
458 state = _READ_START, None
459 # Note that we do a line by line processing here, since one
460 # of the states (_READ_DIRECTION) can be entered implicitly, i.e.
461 # without a corresponding StartDirection section and we thus
462 # may need to reprocess a line in the context of the new state
463 for line in f:
464 line = line[:-1]
465 mstate, sstate = state
466 if mstate == _READ_START:
467 state = self._processline_start(line)
468 else:
469 # except for the first line, any empty will be ignored
470 if not line.strip():
471 continue
472 if mstate == _READ_MAIN:
473 state = self._processline_main(line)
474 elif mstate == _READ_DIRECTION:
475 state = self._processline_direction(line, sstate)
476 elif mstate == _READ_CHARMETRICS:
477 state = self._processline_charmetrics(line, sstate)
478 elif mstate == _READ_KERNDATA:
479 state = self._processline_kerndata(line)
480 elif mstate == _READ_TRACKKERN:
481 state = self._processline_trackkern(line, sstate)
482 elif mstate == _READ_KERNPAIRS:
483 state = self._processline_kernpairs(line, sstate)
484 elif mstate == _READ_COMPOSITES:
485 state = self._processline_composites(line, sstate)
486 else:
487 raise AFMError("Undefined state in AFM reader")
488 finally:
489 f.close()
491 def width_ds(self, glyphname):
492 return self.glyphs[glyphname].widths[0][0]
494 def width_pt(self, glyphnames, size):
495 return sum([self.glyphs[glyphname].widths[0][0] for glyphname in glyphnames])*size/1000.0
497 def height_pt(self, glyphnames, size):
498 return max([self.glyphs[glyphname].bbox[3] for glyphname in glyphnames])*size/1000.0
500 def depth_pt(self, glyphnames, size):
501 return min([self.glyphs[glyphname].bbox[1] for glyphname in glyphnames])*size/1000.0
503 def writePDFfontinfo(self, file):
504 file.write("/Flags 4\n") # any better???
505 if self.fontbbox is not None:
506 file.write("/FontBBox [%d %d %d %d]\n" % tuple(self.fontbbox))
507 if self.italicangles is not None:
508 file.write("/ItalicAngles %d\n" % self.italicangles[0])
509 if self.fontbbox is not None:
510 file.write("/Ascent %d\n" % self.fontbbox[3]) # TODO: any better???
511 if self.fontbbox is not None:
512 file.write("/Descent %d\n" % self.fontbbox[1]) # TODO: any better???
513 if self.capheight is not None:
514 file.write("/CapHeight %d\n" % self.capheight)
515 file.write("/StemV %d\n" % 100) # TODO: any better???
518 if __name__ == "__main__":
519 a = AFMfile("/opt/local/share/texmf-dist/fonts/afm/yandy/lucida/lbc.afm")
520 print a.charmetrics[0].name
521 a = AFMfile("/usr/share/enscript/hv.afm")
522 print a.charmetrics[32].name