added first version of AFM parser#
[PyX/mjg.git] / pyx / dvifile.py
blob4dc7fe28fb5295f0a6b2aad3df53033159233546
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2005 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2005 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 import cStringIO, exceptions, re, struct, string, sys, warnings, math
26 import unit, epsfile, bbox, canvas, color, trafo, path, pykpathsea, type1font
29 class binfile:
31 def __init__(self, filename, mode="r"):
32 self.file = open(filename, mode)
34 def close(self):
35 self.file.close()
37 def tell(self):
38 return self.file.tell()
40 def eof(self):
41 return self.file.eof()
43 def read(self, bytes):
44 return self.file.read(bytes)
46 def readint(self, bytes=4, signed=0):
47 first = 1
48 result = 0
49 while bytes:
50 value = ord(self.file.read(1))
51 if first and signed and value > 127:
52 value -= 256
53 first = 0
54 result = 256 * result + value
55 bytes -= 1
56 return result
58 def readint32(self):
59 return struct.unpack(">l", self.file.read(4))[0]
61 def readuint32(self):
62 return struct.unpack(">L", self.file.read(4))[0]
64 def readint24(self):
65 # XXX: checkme
66 return struct.unpack(">l", "\0"+self.file.read(3))[0]
68 def readuint24(self):
69 # XXX: checkme
70 return struct.unpack(">L", "\0"+self.file.read(3))[0]
72 def readint16(self):
73 return struct.unpack(">h", self.file.read(2))[0]
75 def readuint16(self):
76 return struct.unpack(">H", self.file.read(2))[0]
78 def readchar(self):
79 return struct.unpack("b", self.file.read(1))[0]
81 def readuchar(self):
82 return struct.unpack("B", self.file.read(1))[0]
84 def readstring(self, bytes):
85 l = self.readuchar()
86 assert l <= bytes-1, "inconsistency in file: string too long"
87 return self.file.read(bytes-1)[:l]
89 class stringbinfile(binfile):
91 def __init__(self, s):
92 self.file = cStringIO.StringIO(s)
97 ##############################################################################
98 # TFM file handling
99 ##############################################################################
101 class TFMError(exceptions.Exception): pass
104 class char_info_word:
105 def __init__(self, word):
106 self.width_index = int((word & 0xFF000000L) >> 24) #make sign-safe
107 self.height_index = (word & 0x00F00000) >> 20
108 self.depth_index = (word & 0x000F0000) >> 16
109 self.italic_index = (word & 0x0000FC00) >> 10
110 self.tag = (word & 0x00000300) >> 8
111 self.remainder = (word & 0x000000FF)
114 class tfmfile:
115 def __init__(self, name, debug=0):
116 self.file = binfile(name, "rb")
119 # read pre header
122 self.lf = self.file.readint16()
123 self.lh = self.file.readint16()
124 self.bc = self.file.readint16()
125 self.ec = self.file.readint16()
126 self.nw = self.file.readint16()
127 self.nh = self.file.readint16()
128 self.nd = self.file.readint16()
129 self.ni = self.file.readint16()
130 self.nl = self.file.readint16()
131 self.nk = self.file.readint16()
132 self.ne = self.file.readint16()
133 self.np = self.file.readint16()
135 if not (self.bc-1 <= self.ec <= 255 and
136 self.ne <= 256 and
137 self.lf == 6+self.lh+(self.ec-self.bc+1)+self.nw+self.nh+self.nd
138 +self.ni+self.nl+self.nk+self.ne+self.np):
139 raise TFMError, "error in TFM pre-header"
141 if debug:
142 print "lh=%d" % self.lh
145 # read header
148 self.checksum = self.file.readint32()
149 self.designsize = self.file.readint32()
150 assert self.designsize > 0, "invald design size"
151 if self.lh > 2:
152 assert self.lh > 11, "inconsistency in TFM file: incomplete field"
153 self.charcoding = self.file.readstring(40)
154 else:
155 self.charcoding = None
157 if self.lh > 12:
158 assert self.lh > 16, "inconsistency in TFM file: incomplete field"
159 self.fontfamily = self.file.readstring(20)
160 else:
161 self.fontfamily = None
163 if debug:
164 print "(FAMILY %s)" % self.fontfamily
165 print "(CODINGSCHEME %s)" % self.charcoding
166 print "(DESINGSIZE R %f)" % 16.0*self.designsize/16777216L
168 if self.lh > 17:
169 self.sevenbitsave = self.file.readuchar()
170 # ignore the following two bytes
171 self.file.readint16()
172 facechar = self.file.readuchar()
173 # decode ugly face specification into the Knuth suggested string
174 if facechar < 18:
175 if facechar >= 12:
176 self.face = "E"
177 facechar -= 12
178 elif facechar >= 6:
179 self.face = "C"
180 facechar -= 6
181 else:
182 self.face = "R"
184 if facechar >= 4:
185 self.face = "L" + self.face
186 facechar -= 4
187 elif facechar >= 2:
188 self.face = "B" + self.face
189 facechar -= 2
190 else:
191 self.face = "M" + self.face
193 if facechar == 1:
194 self.face = self.face[0] + "I" + self.face[1]
195 else:
196 self.face = self.face[0] + "R" + self.face[1]
198 else:
199 self.face = None
200 else:
201 self.sevenbitsave = self.face = None
203 if self.lh > 18:
204 # just ignore the rest
205 print self.file.read((self.lh-18)*4)
208 # read char_info
211 self.char_info = [None]*(self.ec+1)
212 for charcode in range(self.bc, self.ec+1):
213 self.char_info[charcode] = char_info_word(self.file.readint32())
214 if self.char_info[charcode].width_index == 0:
215 # disable character if width_index is zero
216 self.char_info[charcode] = None
219 # read widths
222 self.width = [None for width_index in range(self.nw)]
223 for width_index in range(self.nw):
224 self.width[width_index] = self.file.readint32()
227 # read heights
230 self.height = [None for height_index in range(self.nh)]
231 for height_index in range(self.nh):
232 self.height[height_index] = self.file.readint32()
235 # read depths
238 self.depth = [None for depth_index in range(self.nd)]
239 for depth_index in range(self.nd):
240 self.depth[depth_index] = self.file.readint32()
243 # read italic
246 self.italic = [None for italic_index in range(self.ni)]
247 for italic_index in range(self.ni):
248 self.italic[italic_index] = self.file.readint32()
251 # read lig_kern
254 # XXX decode to lig_kern_command
256 self.lig_kern = [None for lig_kern_index in range(self.nl)]
257 for lig_kern_index in range(self.nl):
258 self.lig_kern[lig_kern_index] = self.file.readint32()
261 # read kern
264 self.kern = [None for kern_index in range(self.nk)]
265 for kern_index in range(self.nk):
266 self.kern[kern_index] = self.file.readint32()
269 # read exten
272 # XXX decode to extensible_recipe
274 self.exten = [None for exten_index in range(self.ne)]
275 for exten_index in range(self.ne):
276 self.exten[exten_index] = self.file.readint32()
279 # read param
282 # XXX decode
284 self.param = [None for param_index in range(self.np)]
285 for param_index in range(self.np):
286 self.param[param_index] = self.file.readint32()
288 self.file.close()
292 ##############################################################################
293 # Font handling
294 ##############################################################################
297 # PostScript font selection and output primitives
300 class UnsupportedFontFormat(Exception):
301 pass
303 class UnsupportedPSFragment(Exception):
304 pass
306 class fontmapping:
308 tokenpattern = re.compile(r'"(.*?)("\s+|"$|$)|(.*?)(\s+|$)')
310 def __init__(self, s):
311 """ construct font mapping from line s of font mapping file """
312 self.texname = self.basepsname = self.fontfile = None
314 # standard encoding
315 self.encodingfile = None
317 # supported postscript fragments occuring in psfonts.map
318 self.reencodefont = self.extendfont = self.slantfont = None
320 tokens = []
321 while len(s):
322 match = self.tokenpattern.match(s)
323 if match:
324 if match.groups()[0] is not None:
325 tokens.append('"%s"' % match.groups()[0])
326 else:
327 tokens.append(match.groups()[2])
328 s = s[match.end():]
329 else:
330 raise RuntimeError("wrong syntax")
332 for token in tokens:
333 if token.startswith("<"):
334 if token.startswith("<<"):
335 # XXX: support non-partial download here
336 self.fontfile = token[2:]
337 elif token.startswith("<["):
338 self.encodingfile = token[2:]
339 elif token.endswith(".pfa") or token.endswith(".pfb"):
340 self.fontfile = token[1:]
341 elif token.endswith(".enc"):
342 self.encodingfile = token[1:]
343 elif toke.endswith(".ttf"):
344 raise UnsupportedFontFormat("TrueType font")
345 else:
346 raise RuntimeError("wrong syntax")
347 elif token.startswith('"'):
348 pscode = token[1:-1].split()
349 # parse standard postscript code fragments
350 while pscode:
351 try:
352 arg, cmd = pscode[:2]
353 except:
354 raise UnsupportedPSFragment("Unsupported Postscript fragment '%s'" % pscode)
355 pscode = pscode[2:]
356 if cmd == "ReEncodeFont":
357 self.reencodefont = arg
358 elif cmd == "ExtendFont":
359 self.extendfont = arg
360 elif cmd == "SlantFont":
361 self.slantfont = arg
362 else:
363 raise UnsupportedPSFragment("Unsupported Postscript fragment '%s %s'" % (arg, cmd))
364 else:
365 if self.texname is None:
366 self.texname = token
367 else:
368 self.basepsname = token
369 if self.basepsname is None:
370 self.basepsname = self.texname
372 def __str__(self):
373 return ("'%s' is '%s' read from '%s' encoded as '%s'" %
374 (self.texname, self.basepsname, self.fontfile, repr(self.encodingfile)))
376 # generate fontmap
378 def readfontmap(filenames):
379 """ read font map from filename (without path) """
380 fontmap = {}
381 for filename in filenames:
382 mappath = pykpathsea.find_file(filename, pykpathsea.kpse_fontmap_format)
383 # try also the oft-used registration as dvips config file
384 if not mappath:
385 mappath = pykpathsea.find_file(filename, pykpathsea.kpse_dvips_config_format)
386 if not mappath:
387 raise RuntimeError("cannot find font mapping file '%s'" % filename)
388 mapfile = open(mappath, "r")
389 lineno = 0
390 for line in mapfile.readlines():
391 lineno += 1
392 line = line.rstrip()
393 if not (line=="" or line[0] in (" ", "%", "*", ";" , "#")):
394 try:
395 fm = fontmapping(line)
396 except (RuntimeError, UnsupportedPSFragment), e:
397 warnings.warn("Ignoring line %i in mapping file '%s': %s" % (lineno, filename, e))
398 except UnsupportedFontFormat, e:
399 pass
400 else:
401 fontmap[fm.texname] = fm
402 mapfile.close()
403 return fontmap
406 class font:
408 def __init__(self, name, c, q, d, tfmconv, pyxconv, fontmap, debug=0):
409 self.name = name
410 self.q = q # desired size of font (fix_word) in TeX points
411 self.d = d # design size of font (fix_word) in TeX points
412 self.tfmconv = tfmconv # conversion factor from tfm units to dvi units
413 self.pyxconv = pyxconv # conversion factor from dvi units to PostScript points
414 self.fontmap = fontmap
415 tfmpath = pykpathsea.find_file("%s.tfm" % self.name, pykpathsea.kpse_tfm_format)
416 if not tfmpath:
417 raise TFMError("cannot find %s.tfm" % self.name)
418 self.tfmfile = tfmfile(tfmpath, debug)
420 # We only check for equality of font checksums if none of them
421 # is zero. The case c == 0 happend in some VF files and
422 # according to the VFtoVP documentation, paragraph 40, a check
423 # is only performed if tfmfile.checksum > 0. Anyhow, being
424 # more generous here seems to be reasonable
425 if self.tfmfile.checksum != c and self.tfmfile.checksum != 0 and c != 0:
426 raise DVIError("check sums do not agree: %d vs. %d" %
427 (self.tfmfile.checksum, c))
429 # Check whether the given design size matches the one defined in the tfm file
430 if abs(self.tfmfile.designsize - d) > 2:
431 raise DVIError("design sizes do not agree: %d vs. %d" % (self.tfmfile.designsize, d))
432 #if q < 0 or q > 134217728:
433 # raise DVIError("font '%s' not loaded: bad scale" % self.name)
434 if d < 0 or d > 134217728:
435 raise DVIError("font '%s' not loaded: bad design size" % self.name)
437 self.scale = 1.0*q/d
439 def fontinfo(self):
440 class fontinfo:
441 pass
443 # The following code is a very crude way to obtain the information
444 # required for the PDF font descritor. (TODO: The correct way would
445 # be to read the information from the AFM file.)
446 fontinfo = fontinfo()
447 fontinfo.fontbbox = (0,
448 -self.getdepth_ds(ord("y")),
449 self.getwidth_ds(ord("W")),
450 self.getheight_ds(ord("H")))
451 try:
452 fontinfo.italicangle = -180/math.pi*math.atan(self.tfmfile.param[0]/65536.0)
453 except IndexError:
454 fontinfo.italicangle = 0
455 fontinfo.ascent = fontinfo.fontbbox[3]
456 fontinfo.descent = fontinfo.fontbbox[1]
457 fontinfo.capheight = self.getheight_ds(ord("h"))
458 fontinfo.vstem = self.getwidth_ds(ord("."))/3
459 return fontinfo
461 def __str__(self):
462 return "font %s designed at %g TeX pts used at %g TeX pts" % (self.name,
463 16.0*self.d/16777216L,
464 16.0*self.q/16777216L)
465 __repr__ = __str__
467 def getsize_pt(self):
468 """ return size of font in (PS) points """
469 # The factor 16L/16777216L=2**(-20) converts a fix_word (here self.q)
470 # to the corresponding float. Furthermore, we have to convert from TeX
471 # points to points, hence the factor 72/72.27.
472 return 16L*self.q/16777216L*72/72.27
474 def _convert_tfm_to_dvi(self, length):
475 # doing the integer math with long integers will lead to different roundings
476 # return 16*length*int(round(self.q*self.tfmconv))/16777216
478 # Knuth instead suggests the following algorithm based on 4 byte integer logic only
479 # z = int(round(self.q*self.tfmconv))
480 # b0, b1, b2, b3 = [ord(c) for c in struct.pack(">L", length)]
481 # assert b0 == 0 or b0 == 255
482 # shift = 4
483 # while z >= 8388608:
484 # z >>= 1
485 # shift -= 1
486 # assert shift >= 0
487 # result = ( ( ( ( ( b3 * z ) >> 8 ) + ( b2 * z ) ) >> 8 ) + ( b1 * z ) ) >> shift
488 # if b0 == 255:
489 # result = result - (z << (8-shift))
491 # however, we can simplify this using a single long integer multiplication,
492 # but take into account the transformation of z
493 z = int(round(self.q*self.tfmconv))
494 assert -16777216 <= length < 16777216 # -(1 << 24) <= length < (1 << 24)
495 assert z < 134217728 # 1 << 27
496 shift = 20 # 1 << 20
497 while z >= 8388608: # 1 << 23
498 z >>= 1
499 shift -= 1
500 # length*z is a long integer, but the result will be a regular integer
501 return int(length*long(z) >> shift)
503 def _convert_tfm_to_ds(self, length):
504 return (16*long(round(length*float(self.q)*self.tfmconv))/16777216) * self.pyxconv * 1000 / self.getsize_pt()
506 def _convert_tfm_to_pt(self, length):
507 return (16*long(round(length*float(self.q)*self.tfmconv))/16777216) * self.pyxconv
509 # routines returning lengths as integers in dvi units
511 def getwidth_dvi(self, charcode):
512 return self._convert_tfm_to_dvi(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])
514 def getheight_dvi(self, charcode):
515 return self._convert_tfm_to_dvi(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])
517 def getdepth_dvi(self, charcode):
518 return self._convert_tfm_to_dvi(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])
520 def getitalic_dvi(self, charcode):
521 return self._convert_tfm_to_dvi(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])
523 # routines returning lengths as integers in design size (AFM) units
525 def getwidth_ds(self, charcode):
526 return self._convert_tfm_to_ds(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])
528 def getheight_ds(self, charcode):
529 return self._convert_tfm_to_ds(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])
531 def getdepth_ds(self, charcode):
532 return self._convert_tfm_to_ds(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])
534 def getitalic_ds(self, charcode):
535 return self._convert_tfm_to_ds(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])
537 # routines returning lengths as floats in PostScript points
539 def getwidth_pt(self, charcode):
540 return self._convert_tfm_to_pt(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])
542 def getheight_pt(self, charcode):
543 return self._convert_tfm_to_pt(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])
545 def getdepth_pt(self, charcode):
546 return self._convert_tfm_to_pt(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])
548 def getitalic_pt(self, charcode):
549 return self._convert_tfm_to_pt(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])
552 class virtualfont(font):
553 def __init__(self, name, c, q, d, tfmconv, pyxconv, fontmap, debug=0):
554 fontpath = pykpathsea.find_file(name, pykpathsea.kpse_vf_format)
555 if fontpath is None or not len(fontpath):
556 raise RuntimeError
557 font.__init__(self, name, c, q, d, tfmconv, pyxconv, fontmap, debug)
558 self.vffile = vffile(fontpath, self.scale, tfmconv, pyxconv, fontmap, debug > 1)
560 def getfonts(self):
561 """ return fonts used in virtual font itself """
562 return self.vffile.getfonts()
564 def getchar(self, cc):
565 """ return dvi chunk corresponding to char code cc """
566 return self.vffile.getchar(cc)
569 ##############################################################################
570 # DVI file handling
571 ##############################################################################
573 _DVI_CHARMIN = 0 # typeset a character and move right (range min)
574 _DVI_CHARMAX = 127 # typeset a character and move right (range max)
575 _DVI_SET1234 = 128 # typeset a character and move right
576 _DVI_SETRULE = 132 # typeset a rule and move right
577 _DVI_PUT1234 = 133 # typeset a character
578 _DVI_PUTRULE = 137 # typeset a rule
579 _DVI_NOP = 138 # no operation
580 _DVI_BOP = 139 # beginning of page
581 _DVI_EOP = 140 # ending of page
582 _DVI_PUSH = 141 # save the current positions (h, v, w, x, y, z)
583 _DVI_POP = 142 # restore positions (h, v, w, x, y, z)
584 _DVI_RIGHT1234 = 143 # move right
585 _DVI_W0 = 147 # move right by w
586 _DVI_W1234 = 148 # move right and set w
587 _DVI_X0 = 152 # move right by x
588 _DVI_X1234 = 153 # move right and set x
589 _DVI_DOWN1234 = 157 # move down
590 _DVI_Y0 = 161 # move down by y
591 _DVI_Y1234 = 162 # move down and set y
592 _DVI_Z0 = 166 # move down by z
593 _DVI_Z1234 = 167 # move down and set z
594 _DVI_FNTNUMMIN = 171 # set current font (range min)
595 _DVI_FNTNUMMAX = 234 # set current font (range max)
596 _DVI_FNT1234 = 235 # set current font
597 _DVI_SPECIAL1234 = 239 # special (dvi extention)
598 _DVI_FNTDEF1234 = 243 # define the meaning of a font number
599 _DVI_PRE = 247 # preamble
600 _DVI_POST = 248 # postamble beginning
601 _DVI_POSTPOST = 249 # postamble ending
603 _DVI_VERSION = 2 # dvi version
605 # position variable indices
606 _POS_H = 0
607 _POS_V = 1
608 _POS_W = 2
609 _POS_X = 3
610 _POS_Y = 4
611 _POS_Z = 5
613 # reader states
614 _READ_PRE = 1
615 _READ_NOPAGE = 2
616 _READ_PAGE = 3
617 _READ_POST = 4 # XXX not used
618 _READ_POSTPOST = 5 # XXX not used
619 _READ_DONE = 6
622 class DVIError(exceptions.Exception): pass
624 # save and restore colors
626 class _savecolor(canvas.canvasitem):
627 def outputPS(self, file, writer, context):
628 file.write("currentcolor currentcolorspace\n")
630 def outputPDF(self, file, writer, context):
631 file.write("q\n")
634 class _restorecolor(canvas.canvasitem):
635 def outputPS(self, file, writer, context):
636 file.write("setcolorspace setcolor\n")
638 def outputPDF(self, file, writer, context):
639 file.write("Q\n")
641 class _savetrafo(canvas.canvasitem):
642 def outputPS(self, file, writer, context):
643 file.write("matrix currentmatrix\n")
645 def outputPDF(self, file, writer, context):
646 file.write("q\n")
649 class _restoretrafo(canvas.canvasitem):
650 def outputPS(self, file, writer, context):
651 file.write("setmatrix\n")
653 def outputPDF(self, file, writer, context):
654 file.write("Q\n")
657 class dvifile:
659 def __init__(self, filename, fontmap, debug=0, debugfile=sys.stdout):
660 """ opens the dvi file and reads the preamble """
661 self.filename = filename
662 self.fontmap = fontmap
663 self.debug = debug
664 self.debugfile = debugfile
665 self.debugstack = []
667 self.fonts = {}
668 self.activefont = None
670 # stack of fonts and fontscale currently used (used for VFs)
671 self.fontstack = []
672 self.stack = []
674 # pointer to currently active page
675 self.actpage = None
677 # stack for self.file, self.fonts and self.stack, needed for VF inclusion
678 self.statestack = []
680 self.file = binfile(self.filename, "rb")
682 # currently read byte in file (for debugging output)
683 self.filepos = None
685 self._read_pre()
687 # helper routines
689 def flushtext(self):
690 """ finish currently active text object """
691 if self.debug and self.activetext:
692 self.debugfile.write("[%s]\n" % "".join([chr(char) for char in self.activetext.chars]))
693 self.activetext = None
695 def putrule(self, height, width, advancepos=1):
696 self.flushtext()
697 x1 = self.pos[_POS_H] * self.pyxconv
698 y1 = -self.pos[_POS_V] * self.pyxconv
699 w = width * self.pyxconv
700 h = height * self.pyxconv
702 if height > 0 and width > 0:
703 if self.debug:
704 self.debugfile.write("%d: %srule height %d, width %d (???x??? pixels)\n" %
705 (self.filepos, advancepos and "set" or "put", height, width))
706 self.actpage.fill(path.rect_pt(x1, y1, w, h))
707 else:
708 if self.debug:
709 self.debugfile.write("%d: %srule height %d, width %d (invisible)\n" %
710 (self.filepos, advancepos and "set" or "put", height, width))
712 if advancepos:
713 if self.debug:
714 self.debugfile.write(" h:=%d+%d=%d, hh:=???\n" %
715 (self.pos[_POS_H], width, self.pos[_POS_H]+width))
716 self.pos[_POS_H] += width
718 def putchar(self, char, advancepos=1, id1234=0):
719 dx = advancepos and self.activefont.getwidth_dvi(char) or 0
721 if self.debug:
722 self.debugfile.write("%d: %s%s%d h:=%d+%d=%d, hh:=???\n" %
723 (self.filepos,
724 advancepos and "set" or "put",
725 id1234 and "%i " % id1234 or "char",
726 char,
727 self.pos[_POS_H], dx, self.pos[_POS_H]+dx))
729 if isinstance(self.activefont, virtualfont):
730 # virtual font handling
731 afterpos = list(self.pos)
732 afterpos[_POS_H] += dx
733 self._push_dvistring(self.activefont.getchar(char), self.activefont.getfonts(), afterpos,
734 self.activefont.getsize_pt())
735 else:
736 if self.activetext is None:
737 if not self.fontmap.has_key(self.activefont.name):
738 raise RuntimeError("missing font information for '%s'; check fontmapping file(s)" % self.activefont.name)
739 fontmapinfo = self.fontmap[self.activefont.name]
741 encodingname = fontmapinfo.reencodefont
742 if encodingname is not None:
743 encodingfilename = pykpathsea.find_file(fontmapinfo.encodingfile, pykpathsea.kpse_tex_ps_header_format)
744 if not encodingfilename:
745 raise RuntimeError("cannot find font encoding file %s" % fontmapinfo.encodingfile)
746 fontencoding = type1font.encoding(encodingname, encodingfilename)
747 else:
748 fontencoding = None
750 fontbasefontname = fontmapinfo.basepsname
751 if fontmapinfo.fontfile is not None:
752 fontfilename = pykpathsea.find_file(fontmapinfo.fontfile, pykpathsea.kpse_type1_format)
753 if not fontfilename:
754 raise RuntimeError("cannot find type 1 font %s" % fontmapinfo.fontfile)
755 else:
756 fontfilename = None
758 # XXX we currently misuse use self.activefont as metric
759 font = type1font.font(fontbasefontname, fontfilename, fontencoding, self.activefont)
761 self.activetext = type1font.text_pt(self.pos[_POS_H] * self.pyxconv, -self.pos[_POS_V] * self.pyxconv, font)
762 self.actpage.insert(self.activetext)
763 self.activetext.addchar(char)
764 self.pos[_POS_H] += dx
766 if not advancepos:
767 self.flushtext()
769 def usefont(self, fontnum, id1234=0):
770 self.flushtext()
771 self.activefont = self.fonts[fontnum]
772 if self.debug:
773 self.debugfile.write("%d: fnt%s%i current font is %s\n" %
774 (self.filepos,
775 id1234 and "%i " % id1234 or "num",
776 fontnum,
777 self.fonts[fontnum].name))
780 def definefont(self, cmdnr, num, c, q, d, fontname):
781 # cmdnr: type of fontdef command (only used for debugging output)
782 # c: checksum
783 # q: scaling factor (fix_word)
784 # Note that q is actually s in large parts of the documentation.
785 # d: design size (fix_word)
787 try:
788 afont = virtualfont(fontname, c, q/self.tfmconv, d/self.tfmconv, self.tfmconv, self.pyxconv, self.fontmap, self.debug > 1)
789 except (TypeError, RuntimeError):
790 afont = font(fontname, c, q/self.tfmconv, d/self.tfmconv, self.tfmconv, self.pyxconv, self.fontmap, self.debug > 1)
792 self.fonts[num] = afont
794 if self.debug:
795 self.debugfile.write("%d: fntdef%d %i: %s\n" % (self.filepos, cmdnr, num, fontname))
797 # scale = round((1000.0*self.conv*q)/(self.trueconv*d))
798 # m = 1.0*q/d
799 # scalestring = scale!=1000 and " scaled %d" % scale or ""
800 # print ("Font %i: %s%s---loaded at size %d DVI units" %
801 # (num, fontname, scalestring, q))
802 # if scale!=1000:
803 # print " (this font is magnified %d%%)" % round(scale/10)
805 def special(self, s):
806 x = self.pos[_POS_H] * self.pyxconv
807 y = -self.pos[_POS_V] * self.pyxconv
808 if self.debug:
809 self.debugfile.write("%d: xxx '%s'\n" % (self.filepos, s))
810 if not s.startswith("PyX:"):
811 warnings.warn("ignoring special '%s'" % s)
812 return
814 # it is in general not safe to continue using the currently active font because
815 # the specials may involve some gsave/grestore operations
816 self.flushtext()
818 command, args = s[4:].split()[0], s[4:].split()[1:]
819 if command == "color_begin":
820 if args[0] == "cmyk":
821 c = color.cmyk(float(args[1]), float(args[2]), float(args[3]), float(args[4]))
822 elif args[0] == "gray":
823 c = color.gray(float(args[1]))
824 elif args[0] == "hsb":
825 c = color.hsb(float(args[1]), float(args[2]), float(args[3]))
826 elif args[0] == "rgb":
827 c = color.rgb(float(args[1]), float(args[2]), float(args[3]))
828 elif args[0] == "RGB":
829 c = color.rgb(int(args[1])/255.0, int(args[2])/255.0, int(args[3])/255.0)
830 elif args[0] == "texnamed":
831 try:
832 c = getattr(color.cmyk, args[1])
833 except AttributeError:
834 raise RuntimeError("unknown TeX color '%s', aborting" % args[1])
835 else:
836 raise RuntimeError("color model '%s' cannot be handled by PyX, aborting" % args[0])
837 self.actpage.insert(_savecolor())
838 self.actpage.insert(c)
839 elif command == "color_end":
840 self.actpage.insert(_restorecolor())
841 elif command == "rotate_begin":
842 self.actpage.insert(_savetrafo())
843 self.actpage.insert(trafo.rotate_pt(float(args[0]), x, y))
844 elif command == "rotate_end":
845 self.actpage.insert(_restoretrafo())
846 elif command == "scale_begin":
847 self.actpage.insert(_savetrafo())
848 self.actpage.insert(trafo.scale_pt(float(args[0]), float(args[1]), x, y))
849 elif command == "scale_end":
850 self.actpage.insert(_restoretrafo())
851 elif command == "epsinclude":
852 # parse arguments
853 argdict = {}
854 for arg in args:
855 name, value = arg.split("=")
856 argdict[name] = value
858 # construct kwargs for epsfile constructor
859 epskwargs = {}
860 epskwargs["filename"] = argdict["file"]
861 epskwargs["bbox"] = bbox.bbox_pt(float(argdict["llx"]), float(argdict["lly"]),
862 float(argdict["urx"]), float(argdict["ury"]))
863 if argdict.has_key("width"):
864 epskwargs["width"] = float(argdict["width"]) * unit.t_pt
865 if argdict.has_key("height"):
866 epskwargs["height"] = float(argdict["height"]) * unit.t_pt
867 if argdict.has_key("clip"):
868 epskwargs["clip"] = int(argdict["clip"])
869 self.actpage.insert(epsfile.epsfile(x * unit.t_pt, y * unit.t_pt, **epskwargs))
870 elif command == "marker":
871 if len(args) != 1:
872 raise RuntimeError("marker contains spaces")
873 for c in args[0]:
874 if c not in string.digits + string.letters + "@":
875 raise RuntimeError("marker contains invalid characters")
876 if self.actpage.markers.has_key(args[0]):
877 raise RuntimeError("marker name occurred several times")
878 self.actpage.markers[args[0]] = x * unit.t_pt, y * unit.t_pt
879 else:
880 raise RuntimeError("unknown PyX special '%s', aborting" % command)
882 # routines for pushing and popping different dvi chunks on the reader
884 def _push_dvistring(self, dvi, fonts, afterpos, fontsize):
885 """ push dvi string with defined fonts on top of reader
886 stack. Every positions gets scaled relatively by the factor
887 scale. After the interpreting of the dvi chunk has been finished,
888 continue with self.pos=afterpos. The designsize of the virtual
889 font is passed as a fix_word
893 #if self.debug:
894 # self.debugfile.write("executing new dvi chunk\n")
895 self.debugstack.append(self.debug)
896 self.debug = 0
898 self.statestack.append((self.file, self.fonts, self.activefont, afterpos, self.stack, self.pyxconv, self.tfmconv))
900 # units in vf files are relative to the size of the font and given as fix_words
901 # which can be converted to floats by diving by 2**20
902 oldpyxconv = self.pyxconv
903 self.pyxconv = fontsize/2**20
904 rescale = self.pyxconv/oldpyxconv
906 self.file = stringbinfile(dvi)
907 self.fonts = fonts
908 self.stack = []
909 self.filepos = 0
911 # rescale self.pos in order to be consistent with the new scaling
912 self.pos = map(lambda x, rescale=rescale:1.0*x/rescale, self.pos)
914 # since tfmconv converts from tfm units to dvi units, rescale it as well
915 self.tfmconv /= rescale
917 self.usefont(0)
919 def _pop_dvistring(self):
920 self.flushtext()
921 #if self.debug:
922 # self.debugfile.write("finished executing dvi chunk\n")
923 self.debug = self.debugstack.pop()
925 self.file.close()
926 self.file, self.fonts, self.activefont, self.pos, self.stack, self.pyxconv, self.tfmconv = self.statestack.pop()
928 # routines corresponding to the different reader states of the dvi maschine
930 def _read_pre(self):
931 afile = self.file
932 while 1:
933 self.filepos = afile.tell()
934 cmd = afile.readuchar()
935 if cmd == _DVI_NOP:
936 pass
937 elif cmd == _DVI_PRE:
938 if afile.readuchar() != _DVI_VERSION: raise DVIError
939 num = afile.readuint32()
940 den = afile.readuint32()
941 self.mag = afile.readuint32()
943 # For the interpretation of the lengths in dvi and tfm files,
944 # three conversion factors are relevant:
945 # - self.tfmconv: tfm units -> dvi units
946 # - self.pyxconv: dvi units -> (PostScript) points
947 # - self.conv: dvi units -> pixels
948 self.tfmconv = (25400000.0/num)*(den/473628672.0)/16.0
950 # calculate conv as described in the DVIType docu using
951 # a given resolution in dpi
952 self.resolution = 300.0
953 self.conv = (num/254000.0)*(self.resolution/den)
955 # self.pyxconv is the conversion factor from the dvi units
956 # to (PostScript) points. It consists of
957 # - self.mag/1000.0: magstep scaling
958 # - self.conv: conversion from dvi units to pixels
959 # - 1/self.resolution: conversion from pixels to inch
960 # - 72 : conversion from inch to points
961 self.pyxconv = self.mag/1000.0*self.conv/self.resolution*72
963 comment = afile.read(afile.readuchar())
964 return
965 else:
966 raise DVIError
968 def readpage(self, pageid=None):
969 """ reads a page from the dvi file
971 This routine reads a page from the dvi file which is
972 returned as a canvas. When there is no page left in the
973 dvifile, None is returned and the file is closed properly."""
975 while 1:
976 self.filepos = self.file.tell()
977 cmd = self.file.readuchar()
978 if cmd == _DVI_NOP:
979 pass
980 elif cmd == _DVI_BOP:
981 ispageid = [self.file.readuint32() for i in range(10)]
982 if pageid is not None and ispageid != pageid:
983 raise DVIError("invalid pageid")
984 if self.debug:
985 self.debugfile.write("%d: beginning of page %i\n" % (self.filepos, ispageid[0]))
986 self.file.readuint32()
987 break
988 elif cmd == _DVI_POST:
989 self.file.close()
990 return None # nothing left
991 else:
992 raise DVIError
994 self.actpage = canvas.canvas()
995 self.actpage.markers = {}
996 self.pos = [0, 0, 0, 0, 0, 0]
998 # currently active output: text instance currently used
999 self.activetext = None
1001 while 1:
1002 afile = self.file
1003 self.filepos = afile.tell()
1004 try:
1005 cmd = afile.readuchar()
1006 except struct.error:
1007 # we most probably (if the dvi file is not corrupt) hit the end of a dvi chunk,
1008 # so we have to continue with the rest of the dvi file
1009 self._pop_dvistring()
1010 continue
1011 if cmd == _DVI_NOP:
1012 pass
1013 if cmd >= _DVI_CHARMIN and cmd <= _DVI_CHARMAX:
1014 self.putchar(cmd)
1015 elif cmd >= _DVI_SET1234 and cmd < _DVI_SET1234 + 4:
1016 self.putchar(afile.readint(cmd - _DVI_SET1234 + 1), id1234=cmd-_DVI_SET1234+1)
1017 elif cmd == _DVI_SETRULE:
1018 self.putrule(afile.readint32(), afile.readint32())
1019 elif cmd >= _DVI_PUT1234 and cmd < _DVI_PUT1234 + 4:
1020 self.putchar(afile.readint(cmd - _DVI_PUT1234 + 1), advancepos=0, id1234=cmd-_DVI_SET1234+1)
1021 elif cmd == _DVI_PUTRULE:
1022 self.putrule(afile.readint32(), afile.readint32(), 0)
1023 elif cmd == _DVI_EOP:
1024 self.flushtext()
1025 if self.debug:
1026 self.debugfile.write("%d: eop\n \n" % self.filepos)
1027 return self.actpage
1028 elif cmd == _DVI_PUSH:
1029 self.stack.append(list(self.pos))
1030 if self.debug:
1031 self.debugfile.write("%s: push\n"
1032 "level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=???,vv=???)\n" %
1033 ((self.filepos, len(self.stack)-1) + tuple(self.pos)))
1034 elif cmd == _DVI_POP:
1035 self.flushtext()
1036 self.pos = self.stack.pop()
1037 if self.debug:
1038 self.debugfile.write("%s: pop\n"
1039 "level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=???,vv=???)\n" %
1040 ((self.filepos, len(self.stack)) + tuple(self.pos)))
1041 elif cmd >= _DVI_RIGHT1234 and cmd < _DVI_RIGHT1234 + 4:
1042 self.flushtext()
1043 dh = afile.readint(cmd - _DVI_RIGHT1234 + 1, 1)
1044 if self.debug:
1045 self.debugfile.write("%d: right%d %d h:=%d%+d=%d, hh:=???\n" %
1046 (self.filepos,
1047 cmd - _DVI_RIGHT1234 + 1,
1049 self.pos[_POS_H],
1051 self.pos[_POS_H]+dh))
1052 self.pos[_POS_H] += dh
1053 elif cmd == _DVI_W0:
1054 self.flushtext()
1055 if self.debug:
1056 self.debugfile.write("%d: w0 %d h:=%d%+d=%d, hh:=???\n" %
1057 (self.filepos,
1058 self.pos[_POS_W],
1059 self.pos[_POS_H],
1060 self.pos[_POS_W],
1061 self.pos[_POS_H]+self.pos[_POS_W]))
1062 self.pos[_POS_H] += self.pos[_POS_W]
1063 elif cmd >= _DVI_W1234 and cmd < _DVI_W1234 + 4:
1064 self.flushtext()
1065 self.pos[_POS_W] = afile.readint(cmd - _DVI_W1234 + 1, 1)
1066 if self.debug:
1067 self.debugfile.write("%d: w%d %d h:=%d%+d=%d, hh:=???\n" %
1068 (self.filepos,
1069 cmd - _DVI_W1234 + 1,
1070 self.pos[_POS_W],
1071 self.pos[_POS_H],
1072 self.pos[_POS_W],
1073 self.pos[_POS_H]+self.pos[_POS_W]))
1074 self.pos[_POS_H] += self.pos[_POS_W]
1075 elif cmd == _DVI_X0:
1076 self.flushtext()
1077 if self.debug:
1078 self.debugfile.write("%d: x0 %d h:=%d%+d=%d, hh:=???\n" %
1079 (self.filepos,
1080 self.pos[_POS_X],
1081 self.pos[_POS_H],
1082 self.pos[_POS_X],
1083 self.pos[_POS_H]+self.pos[_POS_X]))
1084 self.pos[_POS_H] += self.pos[_POS_X]
1085 elif cmd >= _DVI_X1234 and cmd < _DVI_X1234 + 4:
1086 self.flushtext()
1087 self.pos[_POS_X] = afile.readint(cmd - _DVI_X1234 + 1, 1)
1088 if self.debug:
1089 self.debugfile.write("%d: x%d %d h:=%d%+d=%d, hh:=???\n" %
1090 (self.filepos,
1091 cmd - _DVI_X1234 + 1,
1092 self.pos[_POS_X],
1093 self.pos[_POS_H],
1094 self.pos[_POS_X],
1095 self.pos[_POS_H]+self.pos[_POS_X]))
1096 self.pos[_POS_H] += self.pos[_POS_X]
1097 elif cmd >= _DVI_DOWN1234 and cmd < _DVI_DOWN1234 + 4:
1098 self.flushtext()
1099 dv = afile.readint(cmd - _DVI_DOWN1234 + 1, 1)
1100 if self.debug:
1101 self.debugfile.write("%d: down%d %d v:=%d%+d=%d, vv:=???\n" %
1102 (self.filepos,
1103 cmd - _DVI_DOWN1234 + 1,
1105 self.pos[_POS_V],
1107 self.pos[_POS_V]+dv))
1108 self.pos[_POS_V] += dv
1109 elif cmd == _DVI_Y0:
1110 self.flushtext()
1111 if self.debug:
1112 self.debugfile.write("%d: y0 %d v:=%d%+d=%d, vv:=???\n" %
1113 (self.filepos,
1114 self.pos[_POS_Y],
1115 self.pos[_POS_V],
1116 self.pos[_POS_Y],
1117 self.pos[_POS_V]+self.pos[_POS_Y]))
1118 self.pos[_POS_V] += self.pos[_POS_Y]
1119 elif cmd >= _DVI_Y1234 and cmd < _DVI_Y1234 + 4:
1120 self.flushtext()
1121 self.pos[_POS_Y] = afile.readint(cmd - _DVI_Y1234 + 1, 1)
1122 if self.debug:
1123 self.debugfile.write("%d: y%d %d v:=%d%+d=%d, vv:=???\n" %
1124 (self.filepos,
1125 cmd - _DVI_Y1234 + 1,
1126 self.pos[_POS_Y],
1127 self.pos[_POS_V],
1128 self.pos[_POS_Y],
1129 self.pos[_POS_V]+self.pos[_POS_Y]))
1130 self.pos[_POS_V] += self.pos[_POS_Y]
1131 elif cmd == _DVI_Z0:
1132 self.flushtext()
1133 if self.debug:
1134 self.debugfile.write("%d: z0 %d v:=%d%+d=%d, vv:=???\n" %
1135 (self.filepos,
1136 self.pos[_POS_Z],
1137 self.pos[_POS_V],
1138 self.pos[_POS_Z],
1139 self.pos[_POS_V]+self.pos[_POS_Z]))
1140 self.pos[_POS_V] += self.pos[_POS_Z]
1141 elif cmd >= _DVI_Z1234 and cmd < _DVI_Z1234 + 4:
1142 self.flushtext()
1143 self.pos[_POS_Z] = afile.readint(cmd - _DVI_Z1234 + 1, 1)
1144 if self.debug:
1145 self.debugfile.write("%d: z%d %d v:=%d%+d=%d, vv:=???\n" %
1146 (self.filepos,
1147 cmd - _DVI_Z1234 + 1,
1148 self.pos[_POS_Z],
1149 self.pos[_POS_V],
1150 self.pos[_POS_Z],
1151 self.pos[_POS_V]+self.pos[_POS_Z]))
1152 self.pos[_POS_V] += self.pos[_POS_Z]
1153 elif cmd >= _DVI_FNTNUMMIN and cmd <= _DVI_FNTNUMMAX:
1154 self.usefont(cmd - _DVI_FNTNUMMIN, 0)
1155 elif cmd >= _DVI_FNT1234 and cmd < _DVI_FNT1234 + 4:
1156 # note that according to the DVI docs, for four byte font numbers,
1157 # the font number is signed. Don't ask why!
1158 fntnum = afile.readint(cmd - _DVI_FNT1234 + 1, cmd == _DVI_FNT1234 + 3)
1159 self.usefont(fntnum, id1234=cmd-_DVI_FNT1234+1)
1160 elif cmd >= _DVI_SPECIAL1234 and cmd < _DVI_SPECIAL1234 + 4:
1161 self.special(afile.read(afile.readint(cmd - _DVI_SPECIAL1234 + 1)))
1162 elif cmd >= _DVI_FNTDEF1234 and cmd < _DVI_FNTDEF1234 + 4:
1163 if cmd == _DVI_FNTDEF1234:
1164 num = afile.readuchar()
1165 elif cmd == _DVI_FNTDEF1234+1:
1166 num = afile.readuint16()
1167 elif cmd == _DVI_FNTDEF1234+2:
1168 num = afile.readuint24()
1169 elif cmd == _DVI_FNTDEF1234+3:
1170 # Cool, here we have according to docu a signed int. Why?
1171 num = afile.readint32()
1172 self.definefont(cmd-_DVI_FNTDEF1234+1,
1173 num,
1174 afile.readint32(),
1175 afile.readint32(),
1176 afile.readint32(),
1177 afile.read(afile.readuchar()+afile.readuchar()))
1178 else:
1179 raise DVIError
1182 ##############################################################################
1183 # VF file handling
1184 ##############################################################################
1186 _VF_LONG_CHAR = 242 # character packet (long version)
1187 _VF_FNTDEF1234 = _DVI_FNTDEF1234 # font definition
1188 _VF_PRE = _DVI_PRE # preamble
1189 _VF_POST = _DVI_POST # postamble
1191 _VF_ID = 202 # VF id byte
1193 class VFError(exceptions.Exception): pass
1195 class vffile:
1196 def __init__(self, filename, scale, tfmconv, pyxconv, fontmap, debug=0):
1197 self.filename = filename
1198 self.scale = scale
1199 self.tfmconv = tfmconv
1200 self.pyxconv = pyxconv
1201 self.fontmap = fontmap
1202 self.debug = debug
1203 self.fonts = {} # used fonts
1204 self.widths = {} # widths of defined chars
1205 self.chardefs = {} # dvi chunks for defined chars
1207 afile = binfile(self.filename, "rb")
1209 cmd = afile.readuchar()
1210 if cmd == _VF_PRE:
1211 if afile.readuchar() != _VF_ID: raise VFError
1212 comment = afile.read(afile.readuchar())
1213 self.cs = afile.readuint32()
1214 self.ds = afile.readuint32()
1215 else:
1216 raise VFError
1218 while 1:
1219 cmd = afile.readuchar()
1220 if cmd >= _VF_FNTDEF1234 and cmd < _VF_FNTDEF1234 + 4:
1221 # font definition
1222 if cmd == _VF_FNTDEF1234:
1223 num = afile.readuchar()
1224 elif cmd == _VF_FNTDEF1234+1:
1225 num = afile.readuint16()
1226 elif cmd == _VF_FNTDEF1234+2:
1227 num = afile.readuint24()
1228 elif cmd == _VF_FNTDEF1234+3:
1229 num = afile.readint32()
1230 c = afile.readint32()
1231 s = afile.readint32() # relative scaling used for font (fix_word)
1232 d = afile.readint32() # design size of font
1233 fontname = afile.read(afile.readuchar()+afile.readuchar())
1235 # rescaled size of font: s is relative to the scaling
1236 # of the virtual font itself. Note that realscale has
1237 # to be a fix_word (like s)
1238 # XXX: check rounding
1239 reals = int(round(self.scale * (16*self.ds/16777216L) * s))
1241 # print ("defining font %s -- VF scale: %g, VF design size: %d, relative font size: %d => real size: %d" %
1242 # (fontname, self.scale, self.ds, s, reals)
1245 # XXX allow for virtual fonts here too
1246 self.fonts[num] = font(fontname, c, reals, d, self.tfmconv, self.pyxconv, self.fontmap, self.debug > 1)
1247 elif cmd == _VF_LONG_CHAR:
1248 # character packet (long form)
1249 pl = afile.readuint32() # packet length
1250 cc = afile.readuint32() # char code (assumed unsigned, but anyhow only 0 <= cc < 255 is actually used)
1251 tfm = afile.readuint24() # character width
1252 dvi = afile.read(pl) # dvi code of character
1253 self.widths[cc] = tfm
1254 self.chardefs[cc] = dvi
1255 elif cmd < _VF_LONG_CHAR:
1256 # character packet (short form)
1257 cc = afile.readuchar() # char code
1258 tfm = afile.readuint24() # character width
1259 dvi = afile.read(cmd)
1260 self.widths[cc] = tfm
1261 self.chardefs[cc] = dvi
1262 elif cmd == _VF_POST:
1263 break
1264 else:
1265 raise VFError
1267 afile.close()
1269 def getfonts(self):
1270 return self.fonts
1272 def getchar(self, cc):
1273 return self.chardefs[cc]