make bitmap functional after pdf object reorganization
[PyX/mjg.git] / pyx / font / afm.py
blobe62a051d737f121975a8103e130ae51aca6e2f3f
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 # reader states
29 _READ_START = 0
30 _READ_MAIN = 1
31 _READ_DIRECTION = 2
32 _READ_CHARMETRICS = 3
33 _READ_KERNDATA = 4
34 _READ_TRACKKERN = 5
35 _READ_KERNPAIRS = 6
36 _READ_COMPOSITES = 7
37 _READ_END = 8
39 # various parsing functions
40 def _parseint(s):
41 try:
42 return int(s)
43 except:
44 raise AFMError("Expecting int, got '%s'" % s)
46 def _parsehex(s):
47 try:
48 if s[0] != "<" or s[-1] != ">":
49 raise AFMError()
50 return int(s[1:-1], 16)
51 except:
52 raise AFMError("Expecting hexadecimal int, got '%s'" % s)
54 def _parsefloat(s):
55 try:
56 return float(s)
57 except:
58 raise AFMError("Expecting float, got '%s'" % s)
60 def _parsefloats(s, nos):
61 try:
62 numbers = s.split()
63 result = map(float, numbers)
64 if len(result) != nos:
65 raise AFMError()
66 except:
67 raise AFMError("Expecting list of %d numbers, got '%s'" % (s, nos))
68 return result
70 def _parsestr(s):
71 # XXX: check for invalid characters in s
72 return s
74 def _parsebool(s):
75 s = s.rstrip()
76 if s == "true":
77 return 1
78 elif s == "false":
79 return 0
80 else:
81 raise AFMError("Expecting boolean, got '%s'" % s)
84 class AFMcharmetrics:
85 def __init__(self, code, widths=None, vvector=None, name=None, bbox=None, ligatures=None):
86 self.code = code
87 if widths is None:
88 self.widths = [None] * 2
89 else:
90 self.widths = widths
91 self.vvector = vvector
92 self.name = name
93 self.bbox = bbox
94 if ligatures is None:
95 self.ligatures = []
96 else:
97 self.ligatures = ligatures
100 class AFMtrackkern:
101 def __init__(self, degree, min_ptsize, min_kern, max_ptsize, max_kern):
102 self.degree = degree
103 self.min_ptsize = min_ptsize
104 self.min_kern = min_kern
105 self.max_ptsize = max_ptsize
106 self.max_kern = max_kern
109 class AFMkernpair:
110 def __init__(self, name1, name2, x, y):
111 self.name1 = name1
112 self.name2 = name2
113 self.x = x
114 self.y = y
117 class AFMcomposite:
118 def __init__(self, name, parts):
119 self.name = name
120 self.parts = parts
123 class AFMfile:
125 def __init__(self, filename):
126 self.filename = filename
127 self.metricssets = 0 # int, optional
128 self.fontname = None # str, required
129 self.fullname = None # str, optional
130 self.familyname = None # str, optional
131 self.weight = None # str, optional
132 self.fontbbox = None # 4 floats, required
133 self.version = None # str, optional
134 self.notice = None # str, optional
135 self.encodingscheme = None # str, optional
136 self.mappingscheme = None # int, optional (not present in base font programs)
137 self.escchar = None # int, required if mappingscheme == 3
138 self.characterset = None # str, optional
139 self.characters = None # int, optional
140 self.isbasefont = 1 # bool, optional
141 self.vvector = None # 2 floats, required if metricssets == 2
142 self.isfixedv = None # bool, default: true if vvector present, false otherwise
143 self.capheight = None # float, optional
144 self.xheight = None # float, optional
145 self.ascender = None # float, optional
146 self.descender = None # float, optional
147 self.underlinepositions = [None] * 2 # int, optional (for each direction)
148 self.underlinethicknesses = [None] * 2 # float, optional (for each direction)
149 self.italicangles = [None] * 2 # float, optional (for each direction)
150 self.charwidths = [None] * 2 # 2 floats, optional (for each direction)
151 self.isfixedpitchs = [None] * 2 # bool, optional (for each direction)
152 self.charmetrics = None # list of 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 return _READ_CHARMETRICS, 0
233 elif key == "StartKernData":
234 return _READ_KERNDATA, None
235 elif key == "StartComposites":
236 if self.composites is not None:
237 raise AFMError("Multiple composite character data sections")
238 self.composites = [None] * _parseint(args)
239 return _READ_COMPOSITES, 0
240 elif key == "EndFontMetrics":
241 return _READ_END, None
242 elif key[0] in string.lowercase:
243 # ignoring private commands
244 pass
245 return _READ_MAIN, None
247 def _processline_direction(self, line, direction):
248 try:
249 key, args = line.split(None, 1)
250 except ValueError:
251 key = line.rstrip()
252 if key == "UnderLinePosition":
253 self.underlinepositions[direction] = _parseint(args)
254 elif key == "UnderlineThickness":
255 self.underlinethicknesses[direction] = _parsefloat(args)
256 elif key == "ItalicAngle":
257 self.italicangles[direction] = _parsefloat(args)
258 elif key == "Charwidth":
259 self.charwidths[direction] = _parsefloats(args, 2)
260 elif key == "IsFixedPitch":
261 self.isfixedpitchs[direction] = _parsebool(args)
262 elif key == "EndDirection":
263 return _READ_MAIN, None
264 else:
265 # we assume that we are implicitly leaving the direction section again,
266 # so try to reprocess the line in the header reader state
267 return self._processline_main(line)
268 return _READ_DIRECTION, direction
270 def _processline_charmetrics(self, line, charno):
271 if line.rstrip() == "EndCharMetrics":
272 if charno != len(self.charmetrics):
273 raise AFMError("Fewer character metrics than expected")
274 return _READ_MAIN, None
275 if charno >= len(self.charmetrics):
276 raise AFMError("More character metrics than expected")
278 char = None
279 for s in line.split(";"):
280 s = s.strip()
281 if not s:
282 continue
283 key, args = s.split(None, 1)
284 if key == "C":
285 if char is not None:
286 raise AFMError("Cannot define char code twice")
287 char = AFMcharmetrics(_parseint(args))
288 elif key == "CH":
289 if char is not None:
290 raise AFMError("Cannot define char code twice")
291 char = AFMcharmetrics(_parsehex(args))
292 elif key == "WX" or key == "W0X":
293 char.widths[0] = _parsefloat(args), 0
294 elif key == "W1X":
295 char.widths[1] = _parsefloat(args), 0
296 elif key == "WY" or key == "W0Y":
297 char.widths[0] = 0, _parsefloat(args)
298 elif key == "W1Y":
299 char.widths[1] = 0, _parsefloat(args)
300 elif key == "W" or key == "W0":
301 char.widths[0] = _parsefloats(args, 2)
302 elif key == "W1":
303 char.widths[1] = _parsefloats(args, 2)
304 elif key == "VV":
305 char.vvector = _parsefloats(args, 2)
306 elif key == "N":
307 # XXX: we should check that name is valid (no whitespcae, etc.)
308 char.name = _parsestr(args)
309 elif key == "B":
310 char.bbox = _parsefloats(args, 4)
311 elif key == "L":
312 successor, ligature = args.split(None, 1)
313 char.ligatures.append((_parsestr(successor), ligature))
314 else:
315 raise AFMError("Undefined command in character widths specification: '%s'", s)
316 if char is None:
317 raise AFMError("Character metrics not defined")
319 self.charmetrics[charno] = char
320 return _READ_CHARMETRICS, charno+1
322 def _processline_kerndata(self, line):
323 try:
324 key, args = line.split(None, 1)
325 except ValueError:
326 key = line.rstrip()
327 if key == "Comment":
328 return _READ_KERNDATA, None
329 if key == "StartTrackKern":
330 if self.trackkerns is not None:
331 raise AFMError("Multiple track kernings data sections")
332 self.trackkerns = [None] * _parseint(args)
333 return _READ_TRACKKERN, 0
334 elif key == "StartKernPairs" or key == "StartKernPairs0":
335 if self.kernpairs[0] is not None:
336 raise AFMError("Multiple kerning pairs data sections for direction 0")
337 self.kernpairs[0] = [None] * _parseint(args)
338 return _READ_KERNPAIRS, (0, 0)
339 elif key == "StartKernPairs1":
340 if self.kernpairs[1] is not None:
341 raise AFMError("Multiple kerning pairs data sections for direction 0")
342 self.kernpairs[1] = [None] * _parseint(args)
343 return _READ_KERNPAIRS, (1, 0)
344 elif key == "EndKernData":
345 return _READ_MAIN, None
346 else:
347 raise AFMError("Unsupported key %s in kerning data section" % key)
349 def _processline_trackkern(self, line, i):
350 try:
351 key, args = line.split(None, 1)
352 except ValueError:
353 key = line.rstrip()
354 if key == "Comment":
355 return _READ_TRACKKERN, i
356 elif key == "TrackKern":
357 if i >= len(self.trackkerns):
358 raise AFMError("More track kerning data sets than expected")
359 degrees, args = args.split(None, 1)
360 self.trackkerns[i] = AFMtrackkern(int(degrees), *_parsefloats(args, 4))
361 return _READ_TRACKKERN, i+1
362 elif key == "EndTrackKern":
363 if i < len(self.trackkerns):
364 raise AFMError("Fewer track kerning data sets than expected")
365 return _READ_KERNDATA, None
366 else:
367 raise AFMError("Unsupported key %s in kerning data section" % key)
369 def _processline_kernpairs(self, line, (direction, i)):
370 try:
371 key, args = line.split(None, 1)
372 except ValueError:
373 key = line.rstrip()
374 if key == "Comment":
375 return _READ_KERNPAIRS, (direction, i)
376 elif key == "EndKernPairs":
377 if i < len(self.kernpairs[direction]):
378 raise AFMError("Fewer kerning pairs than expected")
379 return _READ_KERNDATA, None
380 else:
381 if i >= len(self.kernpairs[direction]):
382 raise AFMError("More kerning pairs than expected")
383 if key == "KP":
384 try:
385 name1, name2, x, y = args.split()
386 except:
387 raise AFMError("Expecting name1, name2, x, y, got '%s'" % args)
388 self.kernpairs[direction][i] = AFMkernpair(name1, name2, _parsefloat(x), _parsefloat(y))
389 elif key == "KPH":
390 try:
391 hex1, hex2, x, y = args.split()
392 except:
393 raise AFMError("Expecting <hex1>, <hex2>, x, y, got '%s'" % args)
394 self.kernpairs[direction][i] = AFMkernpair(_parsehex(hex1), _parsehex(hex2),
395 _parsefloat(x), _parsefloat(y))
396 elif key == "KPX":
397 try:
398 name1, name2, x = args.split()
399 except:
400 raise AFMError("Expecting name1, name2, x, got '%s'" % args)
401 self.kernpairs[direction][i] = AFMkernpair(name1, name2, _parsefloat(x), 0)
402 elif key == "KPY":
403 try:
404 name1, name2, y = args.split()
405 except:
406 raise AFMError("Expecting name1, name2, x, got '%s'" % args)
407 self.kernpairs[direction][i] = AFMkernpair(name1, name2, 0, _parsefloat(y))
408 else:
409 raise AFMError("Unknown key '%s' in kern pair section" % key)
410 return _READ_KERNPAIRS, (direction, i+1)
412 def _processline_composites(self, line, i):
413 if line.rstrip() == "EndComposites":
414 if i < len(self.composites):
415 raise AFMError("Fewer composite character data sets than expected")
416 return _READ_MAIN, None
417 if i >= len(self.composites):
418 raise AFMError("More composite character data sets than expected")
420 name = None
421 no = None
422 parts = []
423 for s in line.split(";"):
424 s = s.strip()
425 if not s:
426 continue
427 key, args = s.split(None, 1)
428 if key == "CC":
429 try:
430 name, no = args.split()
431 except:
432 raise AFMError("Expecting name number, got '%s'" % args)
433 no = _parseint(no)
434 elif key == "PCC":
435 try:
436 name1, x, y = args.split()
437 except:
438 raise AFMError("Expecting name x y, got '%s'" % args)
439 parts.append((name1, _parsefloat(x), _parsefloat(y)))
440 else:
441 raise AFMError("Unknown key '%s' in composite character data section" % key)
442 if len(parts) != no:
443 raise AFMError("Wrong number of composite characters")
444 self.composites[i] = AFMcomposite(name, parts)
445 return _READ_COMPOSITES, i+1
447 def parse(self):
448 f = open(self.filename, "r")
449 try:
450 # state of the reader, consisting of
451 # - the main state, i.e. the type of the section
452 # - a parameter sstate
453 state = _READ_START, None
454 # Note that we do a line by line processing here, since one
455 # of the states (_READ_DIRECTION) can be entered implicitly, i.e.
456 # without a corresponding StartDirection section and we thus
457 # may need to reprocess a line in the context of the new state
458 for line in f:
459 line = line[:-1]
460 mstate, sstate = state
461 if mstate == _READ_START:
462 state = self._processline_start(line)
463 else:
464 # except for the first line, any empty will be ignored
465 if not line.strip():
466 continue
467 if mstate == _READ_MAIN:
468 state = self._processline_main(line)
469 elif mstate == _READ_DIRECTION:
470 state = self._processline_direction(line, sstate)
471 elif mstate == _READ_CHARMETRICS:
472 state = self._processline_charmetrics(line, sstate)
473 elif mstate == _READ_KERNDATA:
474 state = self._processline_kerndata(line)
475 elif mstate == _READ_TRACKKERN:
476 state = self._processline_trackkern(line, sstate)
477 elif mstate == _READ_KERNPAIRS:
478 state = self._processline_kernpairs(line, sstate)
479 elif mstate == _READ_COMPOSITES:
480 state = self._processline_composites(line, sstate)
481 else:
482 raise RuntimeError("Undefined state in AFM reader")
483 finally:
484 f.close()
486 if __name__ == "__main__":
487 a = AFMfile("/opt/local/share/texmf-dist/fonts/afm/yandy/lucida/lbc.afm")
488 print a.charmetrics[0].name
489 a = AFMfile("/usr/share/enscript/hv.afm")
490 print a.charmetrics[32].name