corrected some weird spacing problems in message handlers
[PyX/mjg.git] / pyx / text.py
blob93d6c1c5452c1f83ee026b8d1e0674a5de1c890c
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2003 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002-2003 André Wobst <wobsta@users.sourceforge.net>
7 # Copyright (C) 2003 Michael Schindler <m-schindler@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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 import exceptions, glob, os, threading, Queue, traceback, re, struct, tempfile, sys, atexit, time, string
26 import config, helper, unit, bbox, box, base, canvas, color, trafo, path, prolog, pykpathsea, version
28 class fix_word:
29 def __init__(self, word):
30 if word >= 0:
31 self.sign = 1
32 else:
33 self.sign = -1
35 self.precomma = abs(word) >> 20
36 self.postcomma = abs(word) & 0xFFFFF
38 def __float__(self):
39 return self.sign * (self.precomma + 1.0*self.postcomma/0xFFFFF)
41 def __mul__(self, other):
42 # hey, it's Q&D
43 result = fix_word(0)
45 result.sign = self.sign*other.sign
46 c = self.postcomma*other.precomma + self.precomma*other.postcomma
47 result.precomma = self.precomma*other.precomma + (c >> 20)
48 result.postcomma = c & 0xFFFFF + ((self.postcomma*other.postcomma) >> 40)
49 return result
52 class binfile:
54 def __init__(self, filename, mode="r"):
55 self.file = open(filename, mode)
57 def __del__(self):
58 self.file.close()
60 def close(self):
61 self.file.close()
63 def tell(self):
64 return self.file.tell()
66 def read(self, bytes):
67 return self.file.read(bytes)
69 def readint(self, bytes=4, signed=0):
70 first = 1
71 result = 0
72 while bytes:
73 value = ord(self.file.read(1))
74 if first and signed and value > 127:
75 value -= 256
76 first = 0
77 result = 256 * result + value
78 bytes -= 1
79 return result
81 def readint32(self):
82 return struct.unpack(">l", self.file.read(4))[0]
84 def readuint32(self):
85 return struct.unpack(">L", self.file.read(4))[0]
87 def readint24(self):
88 # XXX: checkme
89 return struct.unpack(">l", "\0"+self.file.read(3))[0]
91 def readuint24(self):
92 # XXX: checkme
93 return struct.unpack(">L", "\0"+self.file.read(3))[0]
95 def readint16(self):
96 return struct.unpack(">h", self.file.read(2))[0]
98 def readuint16(self):
99 return struct.unpack(">H", self.file.read(2))[0]
101 def readchar(self):
102 return struct.unpack("b", self.file.read(1))[0]
104 def readuchar(self):
105 return struct.unpack("B", self.file.read(1))[0]
107 def readstring(self, bytes):
108 l = self.readuchar()
109 assert l <= bytes-1, "inconsistency in file: string too long"
110 return self.file.read(bytes-1)[:l]
113 # class tokenfile:
114 # """ ascii file containing tokens separated by spaces.
116 # Comments beginning with % are ignored. Strings containing spaces
117 # are not handled correctly
118 # """
120 # def __init__(self, filename):
121 # self.file = open(filename, "r")
122 # self.line = None
124 # def gettoken(self):
125 # """ return next token or None if EOF """
126 # while not self.line:
127 # line = self.file.readline()
128 # if line == "":
129 # return None
130 # self.line = line.split("%")[0].split()
131 # token = self.line[0]
132 # self.line = self.line[1:]
133 # return token
135 # def close(self):
136 # self.file.close()
139 ##############################################################################
140 # TFM file handling
141 ##############################################################################
143 class TFMError(exceptions.Exception): pass
146 class char_info_word:
147 def __init__(self, word):
148 self.width_index = int((word & 0xFF000000L) >> 24) #make sign-safe
149 self.height_index = (word & 0x00F00000) >> 20
150 self.depth_index = (word & 0x000F0000) >> 16
151 self.italic_index = (word & 0x0000FC00) >> 10
152 self.tag = (word & 0x00000300) >> 8
153 self.remainder = (word & 0x000000FF)
156 class tfmfile:
157 def __init__(self, name, debug=0):
158 self.file = binfile(name, "rb")
159 self.debug = debug
162 # read pre header
165 self.lf = self.file.readint16()
166 self.lh = self.file.readint16()
167 self.bc = self.file.readint16()
168 self.ec = self.file.readint16()
169 self.nw = self.file.readint16()
170 self.nh = self.file.readint16()
171 self.nd = self.file.readint16()
172 self.ni = self.file.readint16()
173 self.nl = self.file.readint16()
174 self.nk = self.file.readint16()
175 self.ne = self.file.readint16()
176 self.np = self.file.readint16()
178 if not (self.bc-1 <= self.ec <= 255 and
179 self.ne <= 256 and
180 self.lf == 6+self.lh+(self.ec-self.bc+1)+self.nw+self.nh+self.nd
181 +self.ni+self.nl+self.nk+self.ne+self.np):
182 raise TFMError, "error in TFM pre-header"
184 if debug:
185 print "lh=%d" % self.lh
188 # read header
191 self.checksum = self.file.readint32()
192 self.designsizeraw = self.file.readint32()
193 assert self.designsizeraw > 0, "invald design size"
194 self.designsize = fix_word(self.designsizeraw)
195 if self.lh > 2:
196 assert self.lh > 11, "inconsistency in TFM file: incomplete field"
197 self.charcoding = self.file.readstring(40)
198 else:
199 self.charcoding = None
201 if self.lh > 12:
202 assert self.lh > 16, "inconsistency in TFM file: incomplete field"
203 self.fontfamily = self.file.readstring(20)
204 else:
205 self.fontfamily = None
207 if self.debug:
208 print "(FAMILY %s)" % self.fontfamily
209 print "(CODINGSCHEME %s)" % self.charcoding
210 print "(DESINGSIZE R %f)" % self.designsize
212 if self.lh > 17:
213 self.sevenbitsave = self.file.readuchar()
214 # ignore the following two bytes
215 self.file.readint16()
216 facechar = self.file.readuchar()
217 # decode ugly face specification into the Knuth suggested string
218 if facechar < 18:
219 if facechar >= 12:
220 self.face = "E"
221 facechar -= 12
222 elif facechar >= 6:
223 self.face = "C"
224 facechar -= 6
225 else:
226 self.face = "R"
228 if facechar >= 4:
229 self.face = "L" + self.face
230 facechar -= 4
231 elif facechar >= 2:
232 self.face = "B" + self.face
233 facechar -= 2
234 else:
235 self.face = "M" + self.face
237 if facechar == 1:
238 self.face = self.face[0] + "I" + self.face[1]
239 else:
240 self.face = self.face[0] + "R" + self.face[1]
242 else:
243 self.face = None
244 else:
245 self.sevenbitsave = self.face = None
247 if self.lh > 18:
248 # just ignore the rest
249 print self.file.read((self.lh-18)*4)
252 # read char_info
255 self.char_info = [None]*(self.ec+1)
257 for charcode in range(self.bc, self.ec+1):
258 self.char_info[charcode] = char_info_word(self.file.readint32())
259 if self.char_info[charcode].width_index == 0:
260 # disable character if width_index is zero
261 self.char_info[charcode] = None
264 # read widths
267 self.width = [None for width_index in range(self.nw)]
268 for width_index in range(self.nw):
269 # self.width[width_index] = fix_word(self.file.readint32())
270 self.width[width_index] = self.file.readint32()
273 # read heights
276 self.height = [None for height_index in range(self.nh)]
277 for height_index in range(self.nh):
278 # self.height[height_index] = fix_word(self.file.readint32())
279 self.height[height_index] = self.file.readint32()
282 # read depths
285 self.depth = [None for depth_index in range(self.nd)]
286 for depth_index in range(self.nd):
287 # self.depth[depth_index] = fix_word(self.file.readint32())
288 self.depth[depth_index] = self.file.readint32()
291 # read italic
294 self.italic = [None for italic_index in range(self.ni)]
295 for italic_index in range(self.ni):
296 # self.italic[italic_index] = fix_word(self.file.readint32())
297 self.italic[italic_index] = self.file.readint32()
300 # read lig_kern
303 # XXX decode to lig_kern_command
305 self.lig_kern = [None for lig_kern_index in range(self.nl)]
306 for lig_kern_index in range(self.nl):
307 self.lig_kern[lig_kern_index] = self.file.readint32()
310 # read kern
313 self.kern = [None for kern_index in range(self.nk)]
314 for kern_index in range(self.nk):
315 # self.kern[kern_index] = fix_word(self.file.readint32())
316 self.kern[kern_index] = self.file.readint32()
319 # read exten
322 # XXX decode to extensible_recipe
324 self.exten = [None for exten_index in range(self.ne)]
325 for exten_index in range(self.ne):
326 self.exten[exten_index] = self.file.readint32()
329 # read param
332 # XXX decode
334 self.param = [None for param_index in range(self.np)]
335 for param_index in range(self.np):
336 self.param[param_index] = self.file.readint32()
338 self.file.close()
341 # class FontEncoding:
343 # def __init__(self, filename):
344 # """ font encoding contained in filename """
345 # encpath = pykpathsea.find_file(filename, pykpathsea.kpse_tex_ps_header_format)
346 # encfile = tokenfile(encpath)
348 # # name of encoding
349 # self.encname = encfile.gettoken()
350 # token = encfile.gettoken()
351 # if token != "[":
352 # raise RuntimeError("cannot parse encoding file '%s', expecting '[' got '%s'" % (filename, token))
353 # self.encvector = []
354 # for i in range(256):
355 # token = encfile.gettoken()
356 # if token is None or token=="]":
357 # raise RuntimeError("not enough charcodes in encoding file '%s'" % filename)
358 # self.encvector.append(token)
359 # if encfile.gettoken() != "]":
360 # raise RuntimeError("too many charcodes in encoding file '%s'" % filename)
361 # token = encfile.gettoken()
362 # if token != "def":
363 # raise RuntimeError("cannot parse encoding file '%s', expecting 'def' got '%s'" % (filename, token))
364 # token = encfile.gettoken()
365 # if token != None:
366 # raise RuntimeError("encoding file '%s' too long" % filename)
367 # encfile.close()
369 # def encode(self, charcode):
370 # return self.encvector[charcode]
372 ##############################################################################
373 # Font handling
374 ##############################################################################
376 _ReEncodeFont = prolog.definition("ReEncodeFont", """{
377 5 dict
378 begin
379 /newencoding exch def
380 /newfontname exch def
381 /basefontname exch def
382 /basefontdict basefontname findfont def
383 /newfontdict basefontdict maxlength dict def
384 basefontdict {
385 exch dup dup /FID ne exch /Encoding ne and
386 { exch newfontdict 3 1 roll put }
387 { pop pop }
388 ifelse
389 } forall
390 newfontdict /FontName newfontname put
391 newfontdict /Encoding newencoding put
392 newfontname newfontdict definefont pop
394 }""")
397 # PostScript font selection and output primitives
400 class _selectfont(base.PSOp):
401 def __init__(self, name, size):
402 self.name = name
403 self.size = size
405 def write(self, file):
406 file.write("/%s %f selectfont\n" % (self.name, self.size))
408 # XXX: should we provide a prolog method for the font inclusion
409 # instead of using the coarser logic in dvifile.prolog
412 class _show(base.PSOp):
413 def __init__(self, x, y, s):
414 self.x = x
415 self.y = y
416 self.s = s
418 def write(self, file):
419 file.write("%f %f moveto (%s) show\n" % (self.x, self.y, self.s))
422 class fontmapping:
424 tokenpattern = re.compile(r'"(.*?)("\s+|"$|$)|(.*?)(\s+|$)')
426 def __init__(self, s):
427 """ construct font mapping from line s of font mapping file """
428 self.texname = self.basepsname = self.fontfile = None
430 # standard encoding
431 self.encodingfile = None
433 # supported postscript fragments occuring in psfonts.map
434 self.reencodefont = self.extendfont = self.slantfont = None
436 tokens = []
437 while len(s):
438 match = self.tokenpattern.match(s)
439 if match:
440 if match.groups()[0]:
441 tokens.append('"%s"' % match.groups()[0])
442 else:
443 tokens.append(match.groups()[2])
444 s = s[match.end():]
445 else:
446 raise RuntimeError("wrong syntax")
448 for token in tokens:
449 if token.startswith("<"):
450 if token.startswith("<<"):
451 # XXX: support non-partial download here
452 self.fontfile = token[2:]
453 elif token.startswith("<["):
454 self.encodingfile = token[2:]
455 elif token.endswith(".pfa") or token.endswith(".pfb"):
456 self.fontfile = token[1:]
457 elif token.endswith(".enc"):
458 self.encodingfile = token[1:]
459 else:
460 raise RuntimeError("wrong syntax")
461 elif token.startswith('"'):
462 pscode = token[1:-1].split()
463 # parse standard postscript code fragments
464 while pscode:
465 try:
466 arg, cmd = pscode[:2]
467 except:
468 raise RuntimeError("Unsupported Postscript fragment '%s'" % pscode)
469 pscode = pscode[2:]
470 if cmd == "ReEncodeFont":
471 self.reencodefont = arg
472 elif cmd == "ExtendFont":
473 self.extendfont = arg
474 elif cmd == "SlantFont":
475 self.slantfont = arg
476 else:
477 raise RuntimeError("Unsupported Postscript fragment '%s %s'" % (arg, cmd))
478 else:
479 if self.texname is None:
480 self.texname = token
481 else:
482 self.basepsname = token
483 if self.basepsname is None:
484 self.basepsname = self.texname
486 def __str__(self):
487 return ("'%s' is '%s' read from '%s' encoded as '%s'" %
488 (self.texname, self.basepsname, self.fontfile, repr(self.encodingfile)))
490 # generate fontmap
492 def readfontmap(filenames):
493 """ read font map from filename (without path) """
494 fontmap = {}
495 for filename in filenames:
496 mappath = pykpathsea.find_file(filename, pykpathsea.kpse_dvips_config_format)
497 if not mappath:
498 raise RuntimeError("cannot find font mapping file '%s'" % filename)
499 mapfile = open(mappath, "r")
500 lineno = 0
501 for line in mapfile.readlines():
502 lineno += 1
503 line = line.rstrip()
504 if not (line=="" or line[0] in (" ", "%", "*", ";" , "#")):
505 try:
506 fm = fontmapping(line)
507 except RuntimeError, e:
508 sys.stderr.write("*** PyX Warning: Ignoring line %i in mapping file '%s': %s\n" % (lineno, filename, e))
509 else:
510 fontmap[fm.texname] = fm
511 mapfile.close()
512 return fontmap
515 class font:
516 def __init__(self, name, c, q, d, tfmconv, debug=0):
517 self.name = name
518 self.q = q
519 tfmpath = pykpathsea.find_file("%s.tfm" % self.name, pykpathsea.kpse_tfm_format)
520 if not tfmpath:
521 raise TFMError("cannot find %s.tfm" % self.name)
522 self.tfmfile = tfmfile(tfmpath, debug)
524 # We only check for equality of font checksums if none of them
525 # is zero. The case c == 0 happend in some VF files and
526 # according to the VFtoVP documentation, paragraph 40, a check
527 # is only performed if tfmfile.checksum > 0. Anyhow, begin
528 # more generous here seems to be reasonable
529 if self.tfmfile.checksum != c and self.tfmfile.checksum != 0 and c !=0:
530 raise DVIError("check sums do not agree: %d vs. %d" %
531 (self.tfmfile.checksum, c))
533 tfmdesignsize = round(self.tfmfile.designsizeraw*tfmconv)
534 if abs(tfmdesignsize - d) > 2:
535 raise DVIError("design sizes do not agree: %d vs. %d" % (tfmdesignsize, d))
536 if q < 0 or q > 134217728:
537 raise DVIError("font '%s' not loaded: bad scale" % self.name)
538 if d < 0 or d > 134217728:
539 raise DVIError("font '%s' not loaded: bad design size" % self.name)
541 self.scale = 1.0*q/d
543 # for bookkeeping of used characters
544 self.usedchars = [0] * 256
546 def __str__(self):
547 return "font(%s, %f)" % (self.name, self.scale)
549 __repr__ = __str__
551 def convert(self, width):
552 return 16L*width*self.q/16777216L
554 def getwidth(self, charcode):
555 return self.convert(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])
557 def getheight(self, charcode):
558 return self.convert(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])
560 def getdepth(self, charcode):
561 return self.convert(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])
563 def getitalic(self, charcode):
564 return self.convert(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])
566 def markcharused(self, charcode):
567 self.usedchars[charcode] = 1
569 def mergeusedchars(self, otherfont):
570 for i in range(len(self.usedchars)):
571 self.usedchars[i] = self.usedchars[i] or otherfont.usedchars[i]
574 class type1font(font):
575 def __init__(self, name, c, q, d, tfmconv, fontmap, debug=0):
576 font.__init__(self, name, c, q, d, tfmconv, debug)
577 self.fontmapping = fontmap.get(name)
578 if self.fontmapping is None:
579 raise RuntimeError("no information for font '%s' found in font mapping file, aborting" % name)
581 def getbasepsname(self):
582 return self.fontmapping.basepsname
584 def getpsname(self):
585 if self.fontmapping.reencodefont:
586 return "%s-%s" % (self.fontmapping.basepsname, self.fontmapping.reencodefont)
587 else:
588 return self.fontmapping.basepsname
590 def getfontfile(self):
591 return self.fontmapping.fontfile
593 def getencoding(self):
594 return self.fontmapping.reencodefont
596 def getencodingfile(self):
597 return self.fontmapping.encodingfile
601 ##############################################################################
602 # DVI file handling
603 ##############################################################################
605 _DVI_CHARMIN = 0 # typeset a character and move right (range min)
606 _DVI_CHARMAX = 127 # typeset a character and move right (range max)
607 _DVI_SET1234 = 128 # typeset a character and move right
608 _DVI_SETRULE = 132 # typeset a rule and move right
609 _DVI_PUT1234 = 133 # typeset a character
610 _DVI_PUTRULE = 137 # typeset a rule
611 _DVI_NOP = 138 # no operation
612 _DVI_BOP = 139 # beginning of page
613 _DVI_EOP = 140 # ending of page
614 _DVI_PUSH = 141 # save the current positions (h, v, w, x, y, z)
615 _DVI_POP = 142 # restore positions (h, v, w, x, y, z)
616 _DVI_RIGHT1234 = 143 # move right
617 _DVI_W0 = 147 # move right by w
618 _DVI_W1234 = 148 # move right and set w
619 _DVI_X0 = 152 # move right by x
620 _DVI_X1234 = 153 # move right and set x
621 _DVI_DOWN1234 = 157 # move down
622 _DVI_Y0 = 161 # move down by y
623 _DVI_Y1234 = 162 # move down and set y
624 _DVI_Z0 = 166 # move down by z
625 _DVI_Z1234 = 167 # move down and set z
626 _DVI_FNTNUMMIN = 171 # set current font (range min)
627 _DVI_FNTNUMMAX = 234 # set current font (range max)
628 _DVI_FNT1234 = 235 # set current font
629 _DVI_SPECIAL1234 = 239 # special (dvi extention)
630 _DVI_FNTDEF1234 = 243 # define the meaning of a font number
631 _DVI_PRE = 247 # preamble
632 _DVI_POST = 248 # postamble beginning
633 _DVI_POSTPOST = 249 # postamble ending
635 _DVI_VERSION = 2 # dvi version
637 # position variable indices
638 _POS_H = 0
639 _POS_V = 1
640 _POS_W = 2
641 _POS_X = 3
642 _POS_Y = 4
643 _POS_Z = 5
645 # reader states
646 _READ_PRE = 1
647 _READ_NOPAGE = 2
648 _READ_PAGE = 3
649 _READ_POST = 4 # XXX not used
650 _READ_POSTPOST = 5 # XXX not used
651 _READ_DONE = 6
654 class DVIError(exceptions.Exception): pass
656 # save and restore colors
658 class _savecolor(base.PSOp):
659 def write(self, file):
660 file.write("currentcolor currentcolorspace\n")
663 class _restorecolor(base.PSOp):
664 def write(self, file):
665 file.write("setcolorspace setcolor\n")
667 class _savetrafo(base.PSOp):
668 def write(self, file):
669 file.write("matrix currentmatrix\n")
672 class _restoretrafo(base.PSOp):
673 def write(self, file):
674 file.write("setmatrix\n")
677 class dvifile:
679 def __init__(self, filename, fontmap, debug=0, ipcmode=0):
680 """ initializes the instance
682 Usually, the readfile method should be called once
683 immediately after this constructor. However, if you
684 set ipcmode, you need to call readpages as often as
685 there are pages in the dvi-file plus 1 (for properly
686 closing the dvi-file).
689 self.filename = filename
690 self.fontmap = fontmap
691 self.file = None
692 self.debug = debug
693 self.ipcmode = ipcmode
695 # helper routines
697 def flushout(self):
698 """ flush currently active string """
699 if self.actoutstart:
700 x = unit.t_m(self.actoutstart[0] * self.conv * 0.0254 / self.resolution)
701 y = -unit.t_m(self.actoutstart[1] * self.conv * 0.0254 / self.resolution)
702 if self.debug:
703 print "[%s]" % self.actoutstring
704 self.actpage.insert(_show(unit.topt(x), unit.topt(y), self.actoutstring))
705 self.actoutstart = None
707 def putchar(self, char, inch=1):
708 if self.actoutstart is None:
709 self.actoutstart = self.pos[_POS_H], self.pos[_POS_V]
710 self.actoutstring = ""
711 if char > 32 and char < 127 and chr(char) not in "()[]<>":
712 ascii = "%s" % chr(char)
713 else:
714 ascii = "\\%03o" % char
715 self.actoutstring = self.actoutstring + ascii
716 dx = inch and self.fonts[self.activefont].getwidth(char) or 0
717 self.fonts[self.activefont].markcharused(char)
718 if self.debug:
719 print ("%d: %schar%d h:=%d+%d=%d, hh:=%d" %
720 (self.filepos,
721 inch and "set" or "put",
722 char,
723 self.pos[_POS_H], dx, self.pos[_POS_H]+dx,
725 self.pos[_POS_H] += dx
726 if not inch:
727 # XXX: correct !?
728 self.flushout()
730 def putrule(self, height, width, inch=1):
731 self.flushout()
732 x1 = unit.t_m(self.pos[_POS_H] * self.conv * 0.0254 / self.resolution)
733 y1 = -unit.t_m(self.pos[_POS_V] * self.conv * 0.0254 / self.resolution)
734 w = unit.t_m(width * self.conv * 0.0254 / self.resolution)
735 h = unit.t_m(height * self.conv * 0.0254 / self.resolution)
737 if height > 0 and width > 0:
738 if self.debug:
739 pixelw = int(width*self.conv)
740 if pixelw < width*self.conv: pixelw += 1
741 pixelh = int(height*self.conv)
742 if pixelh < height*self.conv: pixelh += 1
744 print ("%d: %srule height %d, width %d (%dx%d pixels)" %
745 (self.filepos, inch and "set" or "put", height, width, pixelh, pixelw))
746 self.actpage.fill(path.rect(x1, y1, w, h))
747 else:
748 if self.debug:
749 print ("%d: %srule height %d, width %d (invisible)" %
750 (self.filepos, inch and "set" or "put", height, width))
752 if inch:
753 if self.debug:
754 print (" h:=%d+%d=%d, hh:=%d" %
755 (self.pos[_POS_H], width, self.pos[_POS_H]+width, 0))
756 self.pos[_POS_H] += width
759 def usefont(self, fontnum):
760 self.flushout()
761 self.activefont = fontnum
763 fontpsname = self.fonts[self.activefont].getpsname()
764 fontscale = self.fonts[self.activefont].scale
765 fontdesignsize = float(self.fonts[self.activefont].tfmfile.designsize)
766 self.actpage.insert(_selectfont(fontpsname,
767 fontscale*fontdesignsize*72/72.27))
769 if self.debug:
770 print ("%d: fntnum%i current font is %s" %
771 (self.filepos,
772 self.activefont, self.fonts[fontnum].name))
774 def definefont(self, cmdnr, num, c, q, d, fontname):
775 # cmdnr: type of fontdef command (only used for debugging output)
776 # c: checksum
777 # q: scaling factor
778 # Note that q is actually s in large parts of the documentation.
779 # d: design size
781 self.fonts[num] = type1font(fontname, c, q, d, self.tfmconv, self.fontmap, self.debug > 1)
783 if self.debug:
784 print "%d: fntdef%d %i: %s" % (self.filepos, cmdnr, num, fontname)
786 # scale = round((1000.0*self.conv*q)/(self.trueconv*d))
787 # m = 1.0*q/d
788 # scalestring = scale!=1000 and " scaled %d" % scale or ""
789 # print ("Font %i: %s%s---loaded at size %d DVI units" %
790 # (num, fontname, scalestring, q))
791 # if scale!=1000:
792 # print " (this font is magnified %d%%)" % round(scale/10)
794 def special(self, s):
795 self.flushout()
796 x = unit.t_m(self.pos[_POS_H] * self.conv * 0.0254 / self.resolution)
797 y = -unit.t_m(self.pos[_POS_V] * self.conv * 0.0254 / self.resolution)
798 if self.debug:
799 print "%d: xxx '%s'" % (self.filepos, s)
800 if not s.startswith("PyX:"):
801 raise RuntimeError("the special '%s' cannot be handled by PyX, aborting" % s)
802 command, args = s[4:].split()[0], s[4:].split()[1:]
803 if command=="color_begin":
804 if args[0]=="cmyk":
805 c = color.cmyk(float(args[1]), float(args[2]), float(args[3]), float(args[4]))
806 elif args[0]=="gray":
807 c = color.gray(float(args[1]))
808 elif args[0]=="hsb":
809 c = color.hsb(float(args[1]), float(args[2]), float(args[3]))
810 elif args[0]=="rgb":
811 c = color.rgb(float(args[1]), float(args[2]), float(args[3]))
812 elif args[0]=="RGB":
813 c = color.rgb(int(args[1])/255.0, int(args[2])/255.0, int(args[3])/255.0)
814 elif args[0]=="texnamed":
815 try:
816 c = getattr(color.cmyk, args[1])
817 except AttributeError:
818 raise RuntimeError("unknown TeX color '%s', aborting" % args[1])
819 else:
820 raise RuntimeError("color model '%s' cannot be handled by PyX, aborting" % args[0])
821 self.actpage.insert(_savecolor())
822 self.actpage.insert(c)
823 elif command=="color_end":
824 self.actpage.insert(_restorecolor())
825 elif command=="rotate_begin":
826 self.actpage.insert(_savetrafo())
827 self.actpage.insert(trafo.rotate(float(args[0]), x, y))
828 elif command=="rotate_end":
829 self.actpage.insert(_restoretrafo())
830 elif command=="scale_begin":
831 self.actpage.insert(_savetrafo())
832 self.actpage.insert(trafo.scale(float(args[0]), float(args[1]), x, y))
833 elif command=="scale_end":
834 self.actpage.insert(_restoretrafo())
835 elif command=="epsinclude":
836 # XXX: we cannot include epsfile in the header because this would
837 # generate a cyclic import with the canvas and text modules
838 import epsfile
840 # parse arguments
841 argdict = {}
842 for arg in args:
843 name, value = arg.split("=")
844 argdict[name] = value
846 # construct kwargs for epsfile constructor
847 epskwargs = {}
848 epskwargs["filename"] = argdict["file"]
849 epskwargs["bbox"] = bbox._bbox(float(argdict["llx"]), float(argdict["lly"]),
850 float(argdict["urx"]), float(argdict["ury"]))
851 if argdict.has_key("width"):
852 epskwargs["width"] = unit.t_pt(float(argdict["width"]))
853 if argdict.has_key("height"):
854 epskwargs["height"] = unit.t_pt(float(argdict["height"]))
855 if argdict.has_key("clip"):
856 epskwargs["clip"] = int(argdict["clip"])
857 self.actpage.insert(epsfile.epsfile(x, y, **epskwargs))
858 elif command=="marker":
859 if len(args) != 1:
860 raise RuntimeError("marker contains spaces")
861 for c in args[0]:
862 if c not in string.digits + string.letters + "@":
863 raise RuntimeError("marker contains invalid characters")
864 if self.actpage.markers.has_key(args[0]):
865 raise RuntimeError("marker name occurred several times")
866 self.actpage.markers[args[0]] = x, y
867 else:
868 raise RuntimeError("unknown PyX special '%s', aborting" % command)
870 # routines corresponding to the different reader states of the dvi maschine
872 def _read_pre(self):
873 file = self.file
874 while 1:
875 self.filepos = file.tell()
876 cmd = file.readuchar()
877 if cmd == _DVI_NOP:
878 pass
879 elif cmd == _DVI_PRE:
880 if self.file.readuchar() != _DVI_VERSION: raise DVIError
881 num = file.readuint32()
882 den = file.readuint32()
883 mag = file.readuint32()
885 self.tfmconv = (25400000.0/num)*(den/473628672.0)/16.0
886 # resolution in dpi
887 self.resolution = 300.0
888 # self.trueconv = conv in DVIType docu
889 self.trueconv = (num/254000.0)*(self.resolution/den)
890 self.conv = self.trueconv*(mag/1000.0)
892 comment = file.read(file.readuchar())
893 return _READ_NOPAGE
894 else:
895 raise DVIError
897 def _read_nopage(self):
898 file = self.file
899 while 1:
900 self.filepos = file.tell()
901 cmd = file.readuchar()
902 if cmd == _DVI_NOP:
903 pass
904 elif cmd == _DVI_BOP:
905 self.flushout()
906 pagenos = [file.readuint32() for i in range(10)]
907 if pagenos[:3] != [ord("P"), ord("y"), ord("X")] or pagenos[4:] != [0, 0, 0, 0, 0, 0]:
908 raise DVIError("Page in dvi file is not a PyX page.")
909 if self.debug:
910 print "%d: beginning of page %i" % (self.filepos, pagenos[0])
911 file.readuint32()
912 return _READ_PAGE
913 elif cmd == _DVI_POST:
914 return _READ_DONE # we skip the rest
915 else:
916 raise DVIError
918 def _read_page(self):
919 self.pages.append(canvas.canvas())
920 self.actpage = self.pages[-1]
921 self.actpage.markers = {}
922 self.pos = [0, 0, 0, 0, 0, 0]
923 file = self.file
924 while 1:
925 self.filepos = file.tell()
926 cmd = file.readuchar()
927 if cmd == _DVI_NOP:
928 pass
929 if cmd >= _DVI_CHARMIN and cmd <= _DVI_CHARMAX:
930 self.putchar(cmd)
931 elif cmd >= _DVI_SET1234 and cmd < _DVI_SET1234 + 4:
932 self.putchar(file.readint(cmd - _DVI_SET1234 + 1))
933 elif cmd == _DVI_SETRULE:
934 self.putrule(file.readint32(), file.readint32())
935 elif cmd >= _DVI_PUT1234 and cmd < _DVI_PUT1234 + 4:
936 self.putchar(file.readint(cmd - _DVI_PUT1234 + 1), inch=0)
937 elif cmd == _DVI_PUTRULE:
938 self.putrule(file.readint32(), file.readint32(), 0)
939 elif cmd == _DVI_EOP:
940 self.flushout()
941 if self.debug:
942 print "%d: eop" % self.filepos
943 print
944 return _READ_NOPAGE
945 elif cmd == _DVI_PUSH:
946 self.stack.append(tuple(self.pos))
947 if self.debug:
948 print "%d: push" % self.filepos
949 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
950 (( len(self.stack)-1,)+tuple(self.pos)))
951 elif cmd == _DVI_POP:
952 self.flushout()
953 self.pos = list(self.stack[-1])
954 del self.stack[-1]
955 if self.debug:
956 print "%d: pop" % self.filepos
957 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
958 (( len(self.stack),)+tuple(self.pos)))
959 elif cmd >= _DVI_RIGHT1234 and cmd < _DVI_RIGHT1234 + 4:
960 self.flushout()
961 dh = file.readint(cmd - _DVI_RIGHT1234 + 1, 1)
962 if self.debug:
963 print ("%d: right%d %d h:=%d%+d=%d, hh:=" %
964 (self.filepos,
965 cmd - _DVI_RIGHT1234 + 1,
967 self.pos[_POS_H],
969 self.pos[_POS_H]+dh))
970 self.pos[_POS_H] += dh
971 elif cmd == _DVI_W0:
972 self.flushout()
973 if self.debug:
974 print ("%d: w0 %d h:=%d%+d=%d, hh:=" %
975 (self.filepos,
976 self.pos[_POS_W],
977 self.pos[_POS_H],
978 self.pos[_POS_W],
979 self.pos[_POS_H]+self.pos[_POS_W]))
980 self.pos[_POS_H] += self.pos[_POS_W]
981 elif cmd >= _DVI_W1234 and cmd < _DVI_W1234 + 4:
982 self.flushout()
983 self.pos[_POS_W] = file.readint(cmd - _DVI_W1234 + 1, 1)
984 if self.debug:
985 print ("%d: w%d %d h:=%d%+d=%d, hh:=" %
986 (self.filepos,
987 cmd - _DVI_W1234 + 1,
988 self.pos[_POS_W],
989 self.pos[_POS_H],
990 self.pos[_POS_W],
991 self.pos[_POS_H]+self.pos[_POS_W]))
992 self.pos[_POS_H] += self.pos[_POS_W]
993 elif cmd == _DVI_X0:
994 self.flushout()
995 self.pos[_POS_H] += self.pos[_POS_X]
996 elif cmd >= _DVI_X1234 and cmd < _DVI_X1234 + 4:
997 self.flushout()
998 self.pos[_POS_X] = file.readint(cmd - _DVI_X1234 + 1, 1)
999 self.pos[_POS_H] += self.pos[_POS_X]
1000 elif cmd >= _DVI_DOWN1234 and cmd < _DVI_DOWN1234 + 4:
1001 self.flushout()
1002 dv = file.readint(cmd - _DVI_DOWN1234 + 1, 1)
1003 if self.debug:
1004 print ("%d: down%d %d v:=%d%+d=%d, vv:=" %
1005 (self.filepos,
1006 cmd - _DVI_DOWN1234 + 1,
1008 self.pos[_POS_V],
1010 self.pos[_POS_V]+dv))
1011 self.pos[_POS_V] += dv
1012 elif cmd == _DVI_Y0:
1013 self.flushout()
1014 if self.debug:
1015 print ("%d: y0 %d v:=%d%+d=%d, vv:=" %
1016 (self.filepos,
1017 self.pos[_POS_Y],
1018 self.pos[_POS_V],
1019 self.pos[_POS_Y],
1020 self.pos[_POS_V]+self.pos[_POS_Y]))
1021 self.pos[_POS_V] += self.pos[_POS_Y]
1022 elif cmd >= _DVI_Y1234 and cmd < _DVI_Y1234 + 4:
1023 self.flushout()
1024 self.pos[_POS_Y] = file.readint(cmd - _DVI_Y1234 + 1, 1)
1025 if self.debug:
1026 print ("%d: y%d %d v:=%d%+d=%d, vv:=" %
1027 (self.filepos,
1028 cmd - _DVI_Y1234 + 1,
1029 self.pos[_POS_Y],
1030 self.pos[_POS_V],
1031 self.pos[_POS_Y],
1032 self.pos[_POS_V]+self.pos[_POS_Y]))
1033 self.pos[_POS_V] += self.pos[_POS_Y]
1034 elif cmd == _DVI_Z0:
1035 self.flushout()
1036 self.pos[_POS_V] += self.pos[_POS_Z]
1037 elif cmd >= _DVI_Z1234 and cmd < _DVI_Z1234 + 4:
1038 self.flushout()
1039 self.pos[_POS_Z] = file.readint(cmd - _DVI_Z1234 + 1, 1)
1040 self.pos[_POS_V] += self.pos[_POS_Z]
1041 elif cmd >= _DVI_FNTNUMMIN and cmd <= _DVI_FNTNUMMAX:
1042 self.usefont(cmd - _DVI_FNTNUMMIN)
1043 elif cmd >= _DVI_FNT1234 and cmd < _DVI_FNT1234 + 4:
1044 self.usefont(file.readint(cmd - _DVI_FNT1234 + 1, 1))
1045 elif cmd >= _DVI_SPECIAL1234 and cmd < _DVI_SPECIAL1234 + 4:
1046 self.special(file.read(file.readint(cmd - _DVI_SPECIAL1234 + 1)))
1047 elif cmd >= _DVI_FNTDEF1234 and cmd < _DVI_FNTDEF1234 + 4:
1048 if cmd == _DVI_FNTDEF1234:
1049 num = file.readuchar()
1050 elif cmd == _DVI_FNTDEF1234+1:
1051 num = file.readuint16()
1052 elif cmd == _DVI_FNTDEF1234+2:
1053 num = file.readuint24()
1054 elif cmd == _DVI_FNTDEF1234+3:
1055 # Cool, here we have according to docu a signed int. Why?
1056 num = file.readint32()
1057 self.definefont(cmd-_DVI_FNTDEF1234+1,
1058 num,
1059 file.readint32(),
1060 file.readint32(),
1061 file.readint32(),
1062 file.read(file.readuchar()+file.readuchar()))
1063 else: raise DVIError
1065 def readfile(self):
1066 """ reads and parses dvi file
1068 This routine reads the dvi file and generates a list
1069 of pages in self.pages. Each page consists itself of
1070 a list of PSCommands equivalent to the content of
1071 the dvi file. Furthermore, the list of used fonts
1072 can be extracted from the array self.fonts.
1074 Usually, the readfile method should be called once
1075 immediately after constructing the instance. However,
1076 if you set ipcmode, you need to call readpages as
1077 often as there are pages in the dvi-file plus 1 (for
1078 properly closing the dvi-file).
1081 if self.file is None:
1082 self.fonts = {}
1083 self.activefont = None
1085 self.stack = []
1087 # here goes the result, for each page one list.
1088 self.pages = []
1090 # pointer to currently active page
1091 self.actpage = None
1093 # currently active output: position and content
1094 self.actoutstart = None
1095 self.actoutstring = ""
1097 self.file = binfile(self.filename, "rb")
1099 # currently read byte in file (for debugging output)
1100 self.filepos = None
1102 if self._read_pre() != _READ_NOPAGE:
1103 raise DVIError
1105 # start up reading process
1106 if self.ipcmode:
1107 state = self._read_nopage()
1108 if state == _READ_PAGE:
1109 if self._read_page() != _READ_NOPAGE:
1110 raise DVIError
1111 elif state != _READ_DONE:
1112 raise DVIError
1113 else:
1114 state = _READ_NOPAGE
1115 while state != _READ_DONE:
1116 if state == _READ_NOPAGE:
1117 state = self._read_nopage()
1118 elif state == _READ_PAGE:
1119 state = self._read_page()
1120 else:
1121 DVIError
1123 if state == _READ_DONE:
1124 self.file.close()
1126 def marker(self, page, marker):
1127 """return marker from page"""
1128 return self.pages[page-1].markers[marker]
1130 def prolog(self, page): # TODO: AW inserted this page argument -> should return the prolog needed for that page only!
1131 """ return prolog corresponding to contents of dvi file """
1132 # XXX replace this by prolog method in _selectfont
1133 result = [_ReEncodeFont]
1134 for font in self.fonts.values():
1135 result.append(prolog.fontdefinition(font.getbasepsname(),
1136 font.getfontfile(),
1137 font.getencodingfile(),
1138 font.usedchars))
1139 if font.getencoding():
1140 result.append(prolog.fontencoding(font.getencoding(), font.getencodingfile()))
1141 result.append(prolog.fontreencoding(font.getpsname(), font.getbasepsname(), font.getencoding()))
1142 result.extend(self.pages[page-1].prolog())
1143 return result
1145 def write(self, file, page):
1146 """write PostScript output for page into file"""
1147 # XXX: remove this method by return canvas to TexRunner
1148 if self.debug:
1149 print "dvifile(\"%s\").write() for page %s called" % (self.filename, page)
1150 self.pages[page-1].write(file)
1153 _VF_LONG_CHAR = 242 # character packet (long version)
1154 _VF_FNTDEF1234 = _DVI_FNTDEF1234 # font definition
1155 _VF_PRE = _DVI_PRE # preamble
1156 _VF_POST = _DVI_POST # postamble
1158 _VF_ID = 202 # VF id byte
1160 class VFError(exceptions.Exception): pass
1162 class vffile:
1163 def __init__(self, filename, fontmap, debug=0):
1164 self.filename = filename
1165 self.fontmap = fontmap
1166 self.tfmconv = 1
1167 self.debug = debug
1168 self.fonts = {} # used fonts
1169 self.chars = {} # defined chars
1171 file = binfile(self.filename, "rb")
1173 cmd = file.readuchar()
1174 if cmd == _VF_PRE:
1175 if file.readuchar() != _VF_ID: raise VFError
1176 comment = file.read(file.readuchar())
1177 cs = file.readuint32()
1178 ds = file.readuint32()
1179 else:
1180 raise VFError
1182 while 1:
1183 cmd = file.readuchar()
1184 if cmd >= _VF_FNTDEF1234 and cmd < _VF_FNTDEF1234 + 4:
1185 # font definition
1186 if cmd == _VF_FNTDEF1234:
1187 num = file.readuchar()
1188 elif cmd == _VF_FNTDEF1234+1:
1189 num = file.readuint16()
1190 elif cmd == _VF_FNTDEF1234+2:
1191 num = file.readuint24()
1192 elif cmd == _VF_FNTDEF1234+3:
1193 num = file.readint32()
1194 c = file.readint32()
1195 s = file.readint32() # scaling used for font (fix_word)
1196 d = file.readint32() # design size of font
1197 fontname = file.read(file.readuchar()+file.readuchar())
1199 self.fonts[num] = type1font(fontname, c, s, d, self.tfmconv, self.fontmap, self.debug > 1)
1200 elif cmd == _VF_LONG_CHAR:
1201 # character packet (long form)
1202 pl = file.readuint32() # packet length
1203 cc = file.readuint32() # char code (assumed unsigned, but anyhow only 0 <= cc < 255 is actually used)
1204 tfm = file.readuint24() # character width
1205 dvi = file.read(pl) # dvi code of character
1206 self.chars[cc] = (tfm, dvi)
1207 elif cmd < _VF_LONG_CHAR:
1208 # character packet (short form)
1209 cc = file.readuchar() # char code
1210 tfm = file.readuint24() # character width
1211 dvi = file.read(cmd)
1212 self.chars[cc] = (tfm, dvi)
1213 elif cmd == _VF_POST:
1214 break
1215 else:
1216 raise VFError
1218 file.close()
1221 ###############################################################################
1222 # texmessages
1223 # - please don't get confused:
1224 # - there is a texmessage (and a texmessageparsed) attribute within the
1225 # texrunner; it contains TeX/LaTeX response from the last command execution
1226 # - instances of classes derived from the class texmessage are used to
1227 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
1228 # attribute of a texrunner instance
1229 # - the multiple usage of the name texmessage might be removed in the future
1230 # - texmessage instances should implement _Itexmessage
1231 ###############################################################################
1233 class TexResultError(Exception):
1234 """specialized texrunner exception class
1235 - it is raised by texmessage instances, when a texmessage indicates an error
1236 - it is raised by the texrunner itself, whenever there is a texmessage left
1237 after all parsing of this message (by texmessage instances)"""
1239 def __init__(self, description, texrunner):
1240 self.description = description
1241 self.texrunner = texrunner
1243 def __str__(self):
1244 """prints a detailed report about the problem
1245 - the verbose level is controlled by texrunner.errordebug"""
1246 if self.texrunner.errordebug >= 2:
1247 return ("%s\n" % self.description +
1248 "The expression passed to TeX was:\n"
1249 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1250 "The return message from TeX was:\n"
1251 " %s\n" % self.texrunner.texmessage.replace("\n", "\n ").rstrip() +
1252 "After parsing this message, the following was left:\n"
1253 " %s" % self.texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
1254 elif self.texrunner.errordebug == 1:
1255 firstlines = self.texrunner.texmessageparsed.split("\n")
1256 if len(firstlines) > 5:
1257 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
1258 return ("%s\n" % self.description +
1259 "The expression passed to TeX was:\n"
1260 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1261 "After parsing the return message from TeX, the following was left:\n" +
1262 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
1263 else:
1264 return self.description
1267 class TexResultWarning(TexResultError):
1268 """as above, but with different handling of the exception
1269 - when this exception is raised by a texmessage instance,
1270 the information just get reported and the execution continues"""
1271 pass
1274 class _Itexmessage:
1275 """validates/invalidates TeX/LaTeX response"""
1277 def check(self, texrunner):
1278 """check a Tex/LaTeX response and respond appropriate
1279 - read the texrunners texmessageparsed attribute
1280 - if there is an problem found, raise an appropriate
1281 exception (TexResultError or TexResultWarning)
1282 - remove any valid and identified TeX/LaTeX response
1283 from the texrunners texmessageparsed attribute
1284 -> finally, there should be nothing left in there,
1285 otherwise it is interpreted as an error"""
1288 class texmessage: pass
1291 class _texmessagestart(texmessage):
1292 """validates TeX/LaTeX startup"""
1294 __implements__ = _Itexmessage
1296 startpattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
1298 def check(self, texrunner):
1299 m = self.startpattern.search(texrunner.texmessageparsed)
1300 if not m:
1301 raise TexResultError("TeX startup failed", texrunner)
1302 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
1303 try:
1304 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
1305 except (IndexError, ValueError):
1306 raise TexResultError("TeX running startup file failed", texrunner)
1307 try:
1308 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
1309 except (IndexError, ValueError):
1310 raise TexResultError("TeX scrollmode check failed", texrunner)
1313 class _texmessagenoaux(texmessage):
1314 """allows for LaTeXs no-aux-file warning"""
1316 __implements__ = _Itexmessage
1318 def check(self, texrunner):
1319 try:
1320 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
1321 texrunner.texmessageparsed = s1 + s2
1322 except (IndexError, ValueError):
1323 try:
1324 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
1325 os.sep,
1326 texrunner.texfilename), 1)
1327 texrunner.texmessageparsed = s1 + s2
1328 except (IndexError, ValueError):
1329 pass
1332 class _texmessageinputmarker(texmessage):
1333 """validates the PyXInputMarker"""
1335 __implements__ = _Itexmessage
1337 def check(self, texrunner):
1338 try:
1339 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
1340 texrunner.texmessageparsed = s1 + s2
1341 except (IndexError, ValueError):
1342 raise TexResultError("PyXInputMarker expected", texrunner)
1345 class _texmessagepyxbox(texmessage):
1346 """validates the PyXBox output"""
1348 __implements__ = _Itexmessage
1350 pattern = re.compile(r"PyXBox:page=(?P<page>\d+),lt=-?\d*((\d\.?)|(\.?\d))\d*pt,rt=-?\d*((\d\.?)|(\.?\d))\d*pt,ht=-?\d*((\d\.?)|(\.?\d))\d*pt,dp=-?\d*((\d\.?)|(\.?\d))\d*pt:")
1352 def check(self, texrunner):
1353 m = self.pattern.search(texrunner.texmessageparsed)
1354 if m and m.group("page") == str(texrunner.page):
1355 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1356 else:
1357 raise TexResultError("PyXBox expected", texrunner)
1360 class _texmessagepyxpageout(texmessage):
1361 """validates the dvi shipout message (writing a page to the dvi file)"""
1363 __implements__ = _Itexmessage
1365 def check(self, texrunner):
1366 try:
1367 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
1368 texrunner.texmessageparsed = s1 + s2
1369 except (IndexError, ValueError):
1370 raise TexResultError("PyXPageOutMarker expected", texrunner)
1373 class _texmessagetexend(texmessage):
1374 """validates TeX/LaTeX finish"""
1376 __implements__ = _Itexmessage
1378 def check(self, texrunner):
1379 try:
1380 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
1381 texrunner.texmessageparsed = s1 + s2
1382 except (IndexError, ValueError):
1383 try:
1384 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
1385 os.sep,
1386 texrunner.texfilename), 1)
1387 texrunner.texmessageparsed = s1 + s2
1388 except (IndexError, ValueError):
1389 pass
1390 try:
1391 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
1392 texrunner.texmessageparsed = s1 + s2
1393 except (IndexError, ValueError):
1394 pass
1395 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
1396 m = dvipattern.search(texrunner.texmessageparsed)
1397 if texrunner.page:
1398 if not m:
1399 raise TexResultError("TeX dvifile messages expected", texrunner)
1400 if m.group("page") != str(texrunner.page):
1401 raise TexResultError("wrong number of pages reported", texrunner)
1402 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1403 else:
1404 try:
1405 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
1406 texrunner.texmessageparsed = s1 + s2
1407 except (IndexError, ValueError):
1408 raise TexResultError("no dvifile expected")
1409 try:
1410 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
1411 texrunner.texmessageparsed = s1 + s2
1412 except (IndexError, ValueError):
1413 raise TexResultError("TeX logfile message expected")
1416 class _texmessageemptylines(texmessage):
1417 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines"""
1419 __implements__ = _Itexmessage
1421 def check(self, texrunner):
1422 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
1423 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
1426 class _texmessageload(texmessage):
1427 """validates inclusion of arbitrary files
1428 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
1429 <fielname> is a readable file and other stuff can be anything
1430 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
1431 - this is not always wanted, but we just assume that file inclusion is fine"""
1433 __implements__ = _Itexmessage
1435 pattern = re.compile(r" *\((?P<filename>[^()\s\n]+)[^()]*\) *")
1437 def baselevels(self, s, maxlevel=1, brackets="()"):
1438 """strip parts of a string above a given bracket level
1439 - return a modified (some parts might be removed) version of the string s
1440 where all parts inside brackets with level higher than maxlevel are
1441 removed
1442 - if brackets do not match (number of left and right brackets is wrong
1443 or at some points there were more right brackets than left brackets)
1444 just return the unmodified string"""
1445 level = 0
1446 highestlevel = 0
1447 res = ""
1448 for c in s:
1449 if c == brackets[0]:
1450 level += 1
1451 if level > highestlevel:
1452 highestlevel = level
1453 if level <= maxlevel:
1454 res += c
1455 if c == brackets[1]:
1456 level -= 1
1457 if level == 0 and highestlevel > 0:
1458 return res
1460 def check(self, texrunner):
1461 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
1462 if lowestbracketlevel is not None:
1463 m = self.pattern.search(lowestbracketlevel)
1464 while m:
1465 if os.access(m.group("filename"), os.R_OK):
1466 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
1467 else:
1468 break
1469 m = self.pattern.search(lowestbracketlevel)
1470 else:
1471 texrunner.texmessageparsed = lowestbracketlevel
1474 class _texmessageloadfd(_texmessageload):
1475 """validates the inclusion of font description files (fd-files)
1476 - works like _texmessageload
1477 - filename must end with .fd and no further text is allowed"""
1479 pattern = re.compile(r" *\((?P<filename>[^)]+.fd)\) *")
1482 class _texmessagegraphicsload(_texmessageload):
1483 """validates the inclusion of files as the graphics packages writes it
1484 - works like _texmessageload, but using "<" and ">" as delimiters
1485 - filename must end with .eps and no further text is allowed"""
1487 pattern = re.compile(r" *<(?P<filename>[^>]+.eps)> *")
1489 def baselevels(self, s, brackets="<>", **args):
1490 return _texmessageload.baselevels(self, s, brackets=brackets, **args)
1493 #class _texmessagepdfmapload(_texmessageload):
1494 # """validates the inclusion of files as the graphics packages writes it
1495 # - works like _texmessageload, but using "{" and "}" as delimiters
1496 # - filename must end with .map and no further text is allowed"""
1498 # pattern = re.compile(r"{(?P<filename>[^}]+.map)}")
1500 # def baselevels(self, s, brackets="{}", **args):
1501 # return _texmessageload.baselevels(self, s, brackets=brackets, **args)
1504 class _texmessageignore(_texmessageload):
1505 """validates any TeX/LaTeX response
1506 - this might be used, when the expression is ok, but no suitable texmessage
1507 parser is available
1508 - PLEASE: - consider writing suitable tex message parsers
1509 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
1511 __implements__ = _Itexmessage
1513 def check(self, texrunner):
1514 texrunner.texmessageparsed = ""
1517 texmessage.start = _texmessagestart()
1518 texmessage.noaux = _texmessagenoaux()
1519 texmessage.inputmarker = _texmessageinputmarker()
1520 texmessage.pyxbox = _texmessagepyxbox()
1521 texmessage.pyxpageout = _texmessagepyxpageout()
1522 texmessage.texend = _texmessagetexend()
1523 texmessage.emptylines = _texmessageemptylines()
1524 texmessage.load = _texmessageload()
1525 texmessage.loadfd = _texmessageloadfd()
1526 texmessage.graphicsload = _texmessagegraphicsload()
1527 texmessage.ignore = _texmessageignore()
1530 ###############################################################################
1531 # texsettings
1532 # - texsettings are used to modify a TeX/LaTeX expression
1533 # to fit the users need
1534 # - texsettings have an order attribute (id), because the order is usually
1535 # important (e.g. you can not change the fontsize in mathmode in LaTeX)
1536 # - lower id's get applied later (are more outside -> mathmode has a higher
1537 # id than fontsize)
1538 # - order attributes are used to exclude complementary settings (with the
1539 # same id)
1540 # - texsettings might (in rare cases) depend on each other (e.g. parbox and
1541 # valign)
1542 ###############################################################################
1544 class _Itexsetting:
1545 """tex setting
1546 - modifies a TeX/LaTeX expression"""
1548 id = 0
1549 """order attribute for TeX settings
1550 - higher id's will be applied first (most inside)"""
1552 exclusive = 0
1553 """marks exclusive effect of the setting
1554 - when set, settings with this id exclude each other
1555 - when unset, settings with this id do not exclude each other"""
1557 def modifyexpr(self, expr, texsettings, texrunner):
1558 """modifies the TeX/LaTeX expression
1559 - expr is the original expression
1560 - the return value is the modified expression
1561 - texsettings contains a list of all texsettings (in case a tex setting
1562 depends on another texsetting)
1563 - texrunner contains the texrunner in case the texsetting depends
1564 on it"""
1566 def __cmp__(self, other):
1567 """compare texsetting with other
1568 - other is a texsetting as well
1569 - performs an id comparison (NOTE: higher id's come first!!!)"""
1572 # preamble settings for texsetting macros
1573 _texsettingpreamble = ""
1575 class _texsetting:
1577 exclusive = 1
1579 def __cmp__(self, other):
1580 return -cmp(self.id, other.id) # note the sign!!!
1583 class halign(_texsetting):
1584 """horizontal alignment
1585 the left/right splitting is performed within the PyXBox routine"""
1587 __implements__ = _Itexsetting
1589 id = 1000
1591 def __init__(self, hratio):
1592 self.hratio = hratio
1594 def modifyexpr(self, expr, texsettings, texrunner):
1595 return r"\gdef\PyXHAlign{%.5f}%s" % (self.hratio, expr)
1597 halign.left = halign(0)
1598 halign.center = halign(0.5)
1599 halign.right = halign(1)
1602 _texsettingpreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
1604 class valign(_texsetting):
1605 "vertical alignment"
1607 id = 7000
1610 class _valigntop(valign):
1612 __implements__ = _Itexsetting
1614 def modifyexpr(self, expr, texsettings, texrunner):
1615 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1618 class _valignmiddle(valign):
1620 __implements__ = _Itexsetting
1622 def modifyexpr(self, expr, texsettings, texrunner):
1623 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
1626 class _valignbottom(valign):
1628 __implements__ = _Itexsetting
1630 def modifyexpr(self, expr, texsettings, texrunner):
1631 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1634 class _valignbaseline(valign):
1636 __implements__ = _Itexsetting
1638 def modifyexpr(self, expr, texsettings, texrunner):
1639 for texsetting in texsettings:
1640 if isinstance(texsetting, parbox):
1641 raise RuntimeError("valign.baseline: specify top/middle/bottom baseline for parbox")
1642 return expr
1645 class _valignxxxbaseline(valign):
1647 def modifyexpr(self, expr, texsettings, texrunner):
1648 for texsetting in texsettings:
1649 if isinstance(texsetting, parbox):
1650 break
1651 else:
1652 raise RuntimeError(self.noparboxmessage)
1653 return expr
1656 class _valigntopbaseline(_valignxxxbaseline):
1658 __implements__ = _Itexsetting
1660 noparboxmessage = "valign.topbaseline: no parbox defined"
1663 class _valignmiddlebaseline(_valignxxxbaseline):
1665 __implements__ = _Itexsetting
1667 noparboxmessage = "valign.middlebaseline: no parbox defined"
1670 class _valignbottombaseline(_valignxxxbaseline):
1672 __implements__ = _Itexsetting
1674 noparboxmessage = "valign.bottombaseline: no parbox defined"
1677 valign.top = _valigntop()
1678 valign.middle = _valignmiddle()
1679 valign.center = valign.middle
1680 valign.bottom = _valignbottom()
1681 valign.baseline = _valignbaseline()
1682 valign.topbaseline = _valigntopbaseline()
1683 valign.middlebaseline = _valignmiddlebaseline()
1684 valign.centerbaseline = valign.middlebaseline
1685 valign.bottombaseline = _valignbottombaseline()
1688 _texsettingpreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
1691 class _parbox(_texsetting):
1692 "goes into the vertical mode"
1694 __implements__ = _Itexsetting
1696 id = 7100
1698 def __init__(self, width):
1699 self.width = width
1701 def modifyexpr(self, expr, texsettings, texrunner):
1702 boxkind = "vtop"
1703 for texsetting in texsettings:
1704 if isinstance(texsetting, valign):
1705 if (not isinstance(texsetting, _valigntop) and
1706 not isinstance(texsetting, _valignmiddle) and
1707 not isinstance(texsetting, _valignbottom) and
1708 not isinstance(texsetting, _valigntopbaseline)):
1709 if isinstance(texsetting, _valignmiddlebaseline):
1710 boxkind = "vcenter"
1711 elif isinstance(texsetting, _valignbottombaseline):
1712 boxkind = "vbox"
1713 else:
1714 raise RuntimeError("parbox couldn'd identify the valign instance")
1715 if boxkind == "vcenter":
1716 return r"\linewidth%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize\linewidth{%s}}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize\linewidth{%s}}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self.width, expr, expr)
1717 else:
1718 return r"\linewidth%.5ftruept\%s{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, boxkind, expr)
1721 class parbox(_parbox):
1723 def __init__(self, width):
1724 _parbox.__init__(self, unit.topt(width))
1727 class vshift(_texsetting):
1729 exclusive = 0
1731 id = 5000
1734 class _vshiftchar(vshift):
1735 "vertical down shift by a fraction of a character height"
1737 def __init__(self, lowerratio, heightstr="0"):
1738 self.lowerratio = lowerratio
1739 self.heightstr = heightstr
1741 def modifyexpr(self, expr, texsettings, texrunner):
1742 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
1745 class _vshiftmathaxis(vshift):
1746 "vertical down shift by the height of the math axis"
1748 def modifyexpr(self, expr, texsettings, texrunner):
1749 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
1752 vshift.char = _vshiftchar
1753 vshift.bottomzero = vshift.char(0)
1754 vshift.middlezero = vshift.char(0.5)
1755 vshift.centerzero = vshift.middlezero
1756 vshift.topzero = vshift.char(1)
1757 vshift.mathaxis = _vshiftmathaxis()
1760 class _mathmode(_texsetting):
1761 "math mode"
1763 __implements__ = _Itexsetting
1765 id = 9000
1767 def modifyexpr(self, expr, texsettings, texrunner):
1768 return r"$\displaystyle{%s}$" % expr
1770 mathmode = _mathmode()
1773 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
1775 class size(_texsetting):
1776 "font size"
1778 __implements__ = _Itexsetting
1780 id = 3000
1782 def __init__(self, expr, sizelist=defaultsizelist):
1783 if helper.isinteger(expr):
1784 if expr >= 0 and expr < sizelist.index(None):
1785 self.size = sizelist[expr]
1786 elif expr < 0 and expr + len(sizelist) > sizelist.index(None):
1787 self.size = sizelist[expr]
1788 else:
1789 raise IndexError("index out of sizelist range")
1790 else:
1791 self.size = expr
1793 def modifyexpr(self, expr, texsettings, texrunner):
1794 return r"\%s{%s}" % (self.size, expr)
1796 for s in defaultsizelist:
1797 if s is not None:
1798 size.__dict__[s] = size(s)
1801 ###############################################################################
1802 # texrunner
1803 ###############################################################################
1806 class _readpipe(threading.Thread):
1807 """threaded reader of TeX/LaTeX output
1808 - sets an event, when a specific string in the programs output is found
1809 - sets an event, when the terminal ends"""
1811 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
1812 """initialize the reader
1813 - pipe: file to be read from
1814 - expectqueue: keeps the next InputMarker to be wait for
1815 - gotevent: the "got InputMarker" event
1816 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
1817 - quitevent: the "end of terminal" event"""
1818 threading.Thread.__init__(self)
1819 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
1820 self.pipe = pipe
1821 self.expectqueue = expectqueue
1822 self.gotevent = gotevent
1823 self.gotqueue = gotqueue
1824 self.quitevent = quitevent
1825 self.expect = None
1826 self.start()
1828 def run(self):
1829 """thread routine"""
1830 read = self.pipe.readline() # read, what comes in
1831 try:
1832 self.expect = self.expectqueue.get_nowait() # read, what should be expected
1833 except Queue.Empty:
1834 pass
1835 while len(read):
1836 # universal EOL handling (convert everything into unix like EOLs)
1837 read.replace("\r", "")
1838 if not len(read) or read[-1] != "\n":
1839 read += "\n"
1840 self.gotqueue.put(read) # report, whats readed
1841 if self.expect is not None and read.find(self.expect) != -1:
1842 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
1843 read = self.pipe.readline() # read again
1844 try:
1845 self.expect = self.expectqueue.get_nowait()
1846 except Queue.Empty:
1847 pass
1848 # EOF reached
1849 self.pipe.close()
1850 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
1851 raise RuntimeError("TeX/LaTeX finished unexpectedly")
1852 self.quitevent.set()
1856 class _textbox(box._rect, base.PSCmd):
1857 """basically a box.rect, but it contains a text created by the texrunner
1858 - texrunner._text and texrunner.text return such an object
1859 - _textbox instances can be inserted into a canvas
1860 - the output is contained in a page of the dvifile available thru the texrunner"""
1862 def __init__(self, x, y, left, right, height, depth, texrunner, dvinumber, page, *styles):
1863 self.texttrafo = trafo._translate(x, y)
1864 box._rect.__init__(self, x - left, y - depth,
1865 left + right, depth + height,
1866 abscenter = (left, depth))
1867 self.texrunner = texrunner
1868 self.dvinumber = dvinumber
1869 self.page = page
1870 self.styles = styles
1872 def transform(self, *trafos):
1873 box._rect.transform(self, *trafos)
1874 for trafo in trafos:
1875 self.texttrafo = trafo * self.texttrafo
1877 def marker(self, marker):
1878 return self.texttrafo.apply(*self.texrunner.marker(self.dvinumber, self.page, marker))
1880 def prolog(self):
1881 result = []
1882 for cmd in self.styles:
1883 result.extend(cmd.prolog())
1884 return result + self.texrunner.prolog(self.dvinumber, self.page)
1886 def write(self, file):
1887 canvas._gsave().write(file) # XXX: canvas?, constructor call needed?
1888 self.texttrafo.write(file)
1889 for style in self.styles:
1890 style.write(file)
1891 self.texrunner.write(file, self.dvinumber, self.page)
1892 canvas._grestore().write(file)
1896 class textbox(_textbox):
1898 def __init__(self, x, y, left, right, height, depth, texrunner, dvinumber, page, *styles):
1899 _textbox.__init__(self, unit.topt(x), unit.topt(y), unit.topt(left), unit.topt(right),
1900 unit.topt(height), unit.topt(depth), texrunner, dvinumber, page, *styles)
1903 def _cleantmp(texrunner):
1904 """get rid of temporary files
1905 - function to be registered by atexit
1906 - files contained in usefiles are kept"""
1907 if texrunner.texruns: # cleanup while TeX is still running?
1908 texrunner.texruns = 0
1909 texrunner.texdone = 1
1910 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
1911 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
1912 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
1913 else:
1914 texrunner.texinput.write("\n\\end\n")
1915 texrunner.texinput.close() # close the input queue and
1916 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
1917 return # didn't got a quit from TeX -> we can't do much more
1918 for usefile in texrunner.usefiles:
1919 extpos = usefile.rfind(".")
1920 try:
1921 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
1922 except OSError:
1923 pass
1924 for file in glob.glob("%s.*" % texrunner.texfilename):
1925 try:
1926 os.unlink(file)
1927 except OSError:
1928 pass
1929 if texrunner.texdebug is not None:
1930 try:
1931 texrunner.texdebug.close()
1932 texrunner.texdebug = None
1933 except IOError:
1934 pass
1937 # texrunner state exceptions
1938 class TexRunsError(Exception): pass
1939 class TexDoneError(Exception): pass
1940 class TexNotInPreambleModeError(Exception): pass
1943 class texrunner:
1944 """TeX/LaTeX interface
1945 - runs TeX/LaTeX expressions instantly
1946 - checks TeX/LaTeX response
1947 - the instance variable texmessage stores the last TeX
1948 response as a string
1949 - the instance variable texmessageparsed stores a parsed
1950 version of texmessage; it should be empty after
1951 texmessage.check was called, otherwise a TexResultError
1952 is raised
1953 - the instance variable errordebug controls the verbose
1954 level of TexResultError"""
1956 def __init__(self, mode="tex",
1957 lfs="10pt",
1958 docclass="article",
1959 docopt=None,
1960 usefiles=None,
1961 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
1962 waitfortex=config.getint("text", "waitfortex", 60),
1963 showwaitfortex=config.getint("text", "showwaitfortex", 5),
1964 texipc=config.getboolean("text", "texipc", 0),
1965 texdebug=None,
1966 dvidebug=0,
1967 errordebug=1,
1968 dvicopy=0,
1969 pyxgraphics=1,
1970 texmessagestart=texmessage.start,
1971 texmessagedocclass=texmessage.load,
1972 texmessagebegindoc=(texmessage.load, texmessage.noaux),
1973 texmessageend=texmessage.texend,
1974 texmessagedefaultpreamble=texmessage.load,
1975 texmessagedefaultrun=texmessage.loadfd):
1976 mode = mode.lower()
1977 if mode != "tex" and mode != "latex":
1978 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1979 self.mode = mode
1980 self.lfs = lfs
1981 self.docclass = docclass
1982 self.docopt = docopt
1983 self.usefiles = helper.ensurelist(usefiles)
1984 self.fontmap = readfontmap(fontmaps.split())
1985 self.waitfortex = waitfortex
1986 self.showwaitfortex = showwaitfortex
1987 self.texipc = texipc
1988 if texdebug is not None:
1989 if texdebug[-4:] == ".tex":
1990 self.texdebug = open(texdebug, "w")
1991 else:
1992 self.texdebug = open("%s.tex" % texdebug, "w")
1993 else:
1994 self.texdebug = None
1995 self.dvidebug = dvidebug
1996 self.errordebug = errordebug
1997 self.dvicopy = dvicopy
1998 self.pyxgraphics = pyxgraphics
1999 texmessagestart = helper.ensuresequence(texmessagestart)
2000 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
2001 self.texmessagestart = texmessagestart
2002 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
2003 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
2004 self.texmessagedocclass = texmessagedocclass
2005 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
2006 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
2007 self.texmessagebegindoc = texmessagebegindoc
2008 texmessageend = helper.ensuresequence(texmessageend)
2009 helper.checkattr(texmessageend, allowmulti=(texmessage,))
2010 self.texmessageend = texmessageend
2011 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
2012 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
2013 self.texmessagedefaultpreamble = texmessagedefaultpreamble
2014 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
2015 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
2016 self.texmessagedefaultrun = texmessagedefaultrun
2018 self.texruns = 0
2019 self.texdone = 0
2020 self.preamblemode = 1
2021 self.executeid = 0
2022 self.page = 0
2023 self.dvinumber = 0
2024 self.dvifiles = []
2025 self.preambles = []
2026 savetempdir = tempfile.tempdir
2027 tempfile.tempdir = os.curdir
2028 self.texfilename = os.path.basename(tempfile.mktemp())
2029 tempfile.tempdir = savetempdir
2031 def waitforevent(self, event):
2032 """waits verbosely with an timeout for an event
2033 - observes an event while periodly while printing messages
2034 - returns the status of the event (isSet)
2035 - does not clear the event"""
2036 if self.showwaitfortex:
2037 waited = 0
2038 hasevent = 0
2039 while waited < self.waitfortex and not hasevent:
2040 if self.waitfortex - waited > self.showwaitfortex:
2041 event.wait(self.showwaitfortex)
2042 waited += self.showwaitfortex
2043 else:
2044 event.wait(self.waitfortex - waited)
2045 waited += self.waitfortex - waited
2046 hasevent = event.isSet()
2047 if not hasevent:
2048 if waited < self.waitfortex:
2049 sys.stderr.write("*** PyX INFO: still waiting for %s after %i seconds...\n" % (self.mode, waited))
2050 else:
2051 sys.stderr.write("*** PyX ERROR: the timeout of %i seconds expired and %s did not respond.\n" % (waited, self.mode))
2052 return hasevent
2053 else:
2054 event.wait(self.waitfortex)
2055 return event.isSet()
2057 def execute(self, expr, *checks):
2058 """executes expr within TeX/LaTeX
2059 - if self.texruns is not yet set, TeX/LaTeX is initialized,
2060 self.texruns is set and self.preamblemode is set
2061 - the method must not be called, when self.texdone is already set
2062 - expr should be a string or None
2063 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
2064 while self.texdone becomes set
2065 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
2066 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
2068 if not self.texruns:
2069 if self.texdebug is not None:
2070 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
2071 self.texdebug.write("%% mode: %s\n" % self.mode)
2072 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
2073 for usefile in self.usefiles:
2074 extpos = usefile.rfind(".")
2075 try:
2076 os.rename(usefile, self.texfilename + usefile[extpos:])
2077 except OSError:
2078 pass
2079 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
2080 texfile.write("\\relax%\n")
2081 texfile.close()
2082 if self.texipc:
2083 ipcflag = " --ipc"
2084 else:
2085 ipcflag = ""
2086 try:
2087 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
2088 except ValueError:
2089 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
2090 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
2091 atexit.register(_cleantmp, self)
2092 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
2093 self.gotevent = threading.Event() # keeps the got inputmarker event
2094 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
2095 self.quitevent = threading.Event() # keeps for end of terminal event
2096 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
2097 self.texruns = 1
2098 oldpreamblemode = self.preamblemode
2099 self.preamblemode = 1
2100 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
2101 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
2102 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
2103 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
2104 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
2105 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
2106 "\\newdimen\\PyXDimenHAlignRT%\n" +
2107 _texsettingpreamble + # insert preambles for texsetting macros
2108 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
2109 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
2110 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
2111 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
2112 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
2113 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
2114 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
2115 "lt=\\the\\PyXDimenHAlignLT,"
2116 "rt=\\the\\PyXDimenHAlignRT,"
2117 "ht=\\the\\ht\\PyXBox,"
2118 "dp=\\the\\dp\\PyXBox:}%\n"
2119 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
2120 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
2121 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
2122 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
2123 "\\def\\PyXMarker#1{\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
2124 *self.texmessagestart)
2125 os.remove("%s.tex" % self.texfilename)
2126 if self.mode == "tex":
2127 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
2128 lfsname = self.lfs
2129 else:
2130 lfsname = "%s.lfs" % self.lfs
2131 for fulllfsname in [lfsname,
2132 os.path.join(sys.prefix, "share", "pyx", lfsname),
2133 os.path.join(os.path.dirname(__file__), "lfs", lfsname)]:
2134 try:
2135 lfsfile = open(fulllfsname, "r")
2136 lfsdef = lfsfile.read()
2137 lfsfile.close()
2138 break
2139 except IOError:
2140 pass
2141 else:
2142 allfiles = (glob.glob("*.lfs") +
2143 glob.glob(os.path.join(sys.prefix, "share", "pyx", "*.lfs")) +
2144 glob.glob(os.path.join(os.path.dirname(__file__), "lfs", "*.lfs")))
2145 lfsnames = []
2146 for f in allfiles:
2147 try:
2148 open(f, "r").close()
2149 lfsnames.append(os.path.basename(f)[:-4])
2150 except IOError:
2151 pass
2152 lfsnames.sort()
2153 if len(lfsnames):
2154 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname, lfsnames))
2155 else:
2156 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname)
2157 self.execute(lfsdef)
2158 self.execute("\\normalsize%\n")
2159 self.execute("\\newdimen\\linewidth%\n")
2160 elif self.mode == "latex":
2161 if self.pyxgraphics:
2162 for pyxdef in ["pyx.def",
2163 os.path.join(sys.prefix, "share", "pyx", "pyx.def"),
2164 os.path.join(os.path.dirname(__file__), "..", "contrib", "pyx.def")]:
2165 try:
2166 open(pyxdef, "r").close()
2167 break
2168 except IOError:
2169 pass
2170 else:
2171 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
2172 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
2173 self.execute("\\makeatletter%\n"
2174 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
2175 "\\def\\ProcessOptions{%\n"
2176 "\\def\\Gin@driver{" + pyxdef + "}%\n"
2177 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
2178 "\\saveProcessOptions}%\n"
2179 "\\makeatother")
2180 if self.docopt is not None:
2181 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass), *self.texmessagedocclass)
2182 else:
2183 self.execute("\\documentclass{%s}" % self.docclass, *self.texmessagedocclass)
2184 self.preamblemode = oldpreamblemode
2185 self.executeid += 1
2186 if expr is not None: # TeX/LaTeX should process expr
2187 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
2188 if self.preamblemode:
2189 self.expr = ("%s%%\n" % expr +
2190 "\\PyXInput{%i}%%\n" % self.executeid)
2191 else:
2192 self.page += 1
2193 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
2194 "\\PyXInput{%i}%%\n" % self.executeid)
2195 else: # TeX/LaTeX should be finished
2196 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
2197 if self.mode == "latex":
2198 self.expr = "\\end{document}%\n"
2199 else:
2200 self.expr = "\\end%\n"
2201 if self.texdebug is not None:
2202 self.texdebug.write(self.expr)
2203 self.texinput.write(self.expr)
2204 gotevent = self.waitforevent(self.gotevent)
2205 self.gotevent.clear()
2206 if expr is None and gotevent: # TeX/LaTeX should have finished
2207 self.texruns = 0
2208 self.texdone = 1
2209 self.texinput.close() # close the input queue and
2210 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
2211 try:
2212 self.texmessage = ""
2213 while 1:
2214 self.texmessage += self.gotqueue.get_nowait()
2215 except Queue.Empty:
2216 pass
2217 self.texmessageparsed = self.texmessage
2218 if gotevent:
2219 if expr is not None:
2220 texmessage.inputmarker.check(self)
2221 if not self.preamblemode:
2222 texmessage.pyxbox.check(self)
2223 texmessage.pyxpageout.check(self)
2224 for check in checks:
2225 try:
2226 check.check(self)
2227 except TexResultWarning:
2228 traceback.print_exc()
2229 print "expr>>>%s<<<" % self.expr
2230 print "aaa>>>%s<<<" % self.texmessageparsed
2231 texmessage.emptylines.check(self)
2232 print "bbb>>>%s<<<" % self.texmessageparsed
2233 if len(self.texmessageparsed):
2234 raise TexResultError("unhandled TeX response (might be an error)", self)
2235 else:
2236 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
2238 def finishdvi(self):
2239 "finish TeX/LaTeX and read the dvifile"
2240 self.execute(None, *self.texmessageend)
2241 if self.dvicopy:
2242 os.system("dvicopy %(t)s.dvi %(t)s.dvicopy > %(t)s.dvicopyout 2> %(t)s.dvicopyerr" % {"t": self.texfilename})
2243 dvifilename = "%s.dvicopy" % self.texfilename
2244 else:
2245 dvifilename = "%s.dvi" % self.texfilename
2246 if not self.texipc:
2247 advifile = dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
2248 self.dvifiles.append(advifile)
2249 self.dvifiles[-1].readfile()
2250 self.dvinumber += 1
2252 def marker(self, dvinumber, page, marker):
2253 "return the marker position"
2254 if not self.texipc and not self.texdone:
2255 self.finishdvi()
2256 return self.dvifiles[dvinumber].marker(page, marker)
2258 def prolog(self, dvinumber, page):
2259 "return the dvifile prolog"
2260 if not self.texipc and not self.texdone:
2261 self.finishdvi()
2262 return self.dvifiles[dvinumber].prolog(page)
2264 def write(self, file, dvinumber, page):
2265 "write a page from the dvifile"
2266 if not self.texipc and not self.texdone:
2267 self.finishdvi()
2268 return self.dvifiles[dvinumber].write(file, page)
2270 def reset(self, reinit=0):
2271 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
2272 if self.texruns:
2273 self.finishdvi()
2274 if self.texdebug is not None:
2275 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
2276 self.executeid = 0
2277 self.page = 0
2278 self.texdone = 0
2279 if reinit:
2280 self.preamblemode = 1
2281 for expr, args in self.preambles:
2282 self.execute(expr, *args)
2283 if self.mode == "latex":
2284 self.execute("\\begin{document}", *self.texmessagebegindoc)
2285 self.preamblemode = 0
2286 else:
2287 self.preambles = []
2288 self.preamblemode = 1
2290 def set(self, mode=None,
2291 lfs=None,
2292 docclass=None,
2293 docopt=None,
2294 usefiles=None,
2295 fontmaps=None,
2296 waitfortex=None,
2297 showwaitfortex=None,
2298 texipc=None,
2299 texdebug=None,
2300 dvidebug=None,
2301 errordebug=None,
2302 dvicopy=None,
2303 pyxgraphics=None,
2304 texmessagestart=None,
2305 texmessagedocclass=None,
2306 texmessagebegindoc=None,
2307 texmessageend=None,
2308 texmessagedefaultpreamble=None,
2309 texmessagedefaultrun=None):
2310 """provide a set command for TeX/LaTeX settings
2311 - TeX/LaTeX must not yet been started
2312 - especially needed for the defaultrunner, where no access to
2313 the constructor is available"""
2314 if self.texruns:
2315 raise TexRunsError
2316 if mode is not None:
2317 mode = mode.lower()
2318 if mode != "tex" and mode != "latex":
2319 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
2320 self.mode = mode
2321 if lfs is not None:
2322 self.lfs = lfs
2323 if docclass is not None:
2324 self.docclass = docclass
2325 if docopt is not None:
2326 self.docopt = docopt
2327 if usefiles is not None:
2328 self.usefiles = helper.ensurelist(usefiles)
2329 if fontmaps is not None:
2330 self.fontmap = readfontmap(fontmaps.split())
2331 if waitfortex is not None:
2332 self.waitfortex = waitfortex
2333 if showwaitfortex is not None:
2334 self.showwaitfortex = showwaitfortex
2335 if texipc is not None:
2336 self.texipc = texipc
2337 if texdebug is not None:
2338 if self.texdebug is not None:
2339 self.texdebug.close()
2340 if texdebug[-4:] == ".tex":
2341 self.texdebug = open(texdebug, "w")
2342 else:
2343 self.texdebug = open("%s.tex" % texdebug, "w")
2344 if dvidebug is not None:
2345 self.dvidebug = dvidebug
2346 if errordebug is not None:
2347 self.errordebug = errordebug
2348 if dvicopy is not None:
2349 self.dvicopy = dvicopy
2350 if pyxgraphics is not None:
2351 self.pyxgraphics = pyxgraphics
2352 if errordebug is not None:
2353 self.errordebug = errordebug
2354 if texmessagestart is not None:
2355 texmessagestart = helper.ensuresequence(texmessagestart)
2356 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
2357 self.texmessagestart = texmessagestart
2358 if texmessagedocclass is not None:
2359 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
2360 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
2361 self.texmessagedocclass = texmessagedocclass
2362 if texmessagebegindoc is not None:
2363 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
2364 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
2365 self.texmessagebegindoc = texmessagebegindoc
2366 if texmessageend is not None:
2367 texmessageend = helper.ensuresequence(texmessageend)
2368 helper.checkattr(texmessageend, allowmulti=(texmessage,))
2369 self.texmessageend = texmessageend
2370 if texmessagedefaultpreamble is not None:
2371 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
2372 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
2373 self.texmessagedefaultpreamble = texmessagedefaultpreamble
2374 if texmessagedefaultrun is not None:
2375 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
2376 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
2377 self.texmessagedefaultrun = texmessagedefaultrun
2379 def preamble(self, expr, *args):
2380 r"""put something into the TeX/LaTeX preamble
2381 - in LaTeX, this is done before the \begin{document}
2382 (you might use \AtBeginDocument, when you're in need for)
2383 - it is not allowed to call preamble after calling the
2384 text method for the first time (for LaTeX this is needed
2385 due to \begin{document}; in TeX it is forced for compatibility
2386 (you should be able to switch from TeX to LaTeX, if you want,
2387 without breaking something
2388 - preamble expressions must not create any dvi output
2389 - args might contain texmessage instances"""
2390 if self.texdone or not self.preamblemode:
2391 raise TexNotInPreambleModeError
2392 helper.checkattr(args, allowmulti=(texmessage,))
2393 args = helper.getattrs(args, texmessage, default=self.texmessagedefaultpreamble)
2394 self.execute(expr, *args)
2395 self.preambles.append((expr, args))
2397 PyXBoxPattern = re.compile(r"PyXBox:page=(?P<page>\d+),lt=(?P<lt>-?\d*((\d\.?)|(\.?\d))\d*)pt,rt=(?P<rt>-?\d*((\d\.?)|(\.?\d))\d*)pt,ht=(?P<ht>-?\d*((\d\.?)|(\.?\d))\d*)pt,dp=(?P<dp>-?\d*((\d\.?)|(\.?\d))\d*)pt:")
2399 def _text(self, x, y, expr, *args):
2400 """create text by passing expr to TeX/LaTeX
2401 - returns a textbox containing the result from running expr thru TeX/LaTeX
2402 - the box center is set to x, y
2403 - *args may contain style parameters, namely:
2404 - an halign instance
2405 - _texsetting instances
2406 - texmessage instances
2407 - trafo._trafo instances
2408 - base.PathStyle instances"""
2409 if expr is None:
2410 raise ValueError("None expression is invalid")
2411 if self.texdone:
2412 self.reset(reinit=1)
2413 if self.preamblemode:
2414 if self.mode == "latex":
2415 self.execute("\\begin{document}", *self.texmessagebegindoc)
2416 self.preamblemode = 0
2417 if self.texipc:
2418 if self.dvicopy:
2419 raise RuntimeError("texipc and dvicopy can't be mixed up")
2420 self.dvifiles.append(dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug, ipcmode=1))
2421 helper.checkattr(args, allowmulti=(_texsetting, texmessage, trafo._trafo, base.PathStyle))
2422 #XXX: should we distiguish between StrokeStyle and FillStyle?
2423 texsettings = helper.getattrs(args, _texsetting, default=[])
2424 exclusive = []
2425 for texsetting in texsettings:
2426 if texsetting.exclusive:
2427 if texsetting.id not in exclusive:
2428 exclusive.append(texsetting.id)
2429 else:
2430 raise RuntimeError("multiple occurance of exclusive texsetting with id=%i" % texsetting.id)
2431 texsettings.sort()
2432 for texsetting in texsettings:
2433 expr = texsetting.modifyexpr(expr, texsettings, self)
2434 self.execute(expr, *helper.getattrs(args, texmessage, default=self.texmessagedefaultrun))
2435 if self.texipc:
2436 self.dvifiles[-1].readfile()
2437 match = self.PyXBoxPattern.search(self.texmessage)
2438 if not match or int(match.group("page")) != self.page:
2439 raise TexResultError("box extents not found", self)
2440 left, right, height, depth = map(lambda x: float(x) * 72.0 / 72.27, match.group("lt", "rt", "ht", "dp"))
2441 box = _textbox(x, y, left, right, height, depth, self, self.dvinumber, self.page,
2442 *helper.getattrs(args, base.PathStyle, default=[]))
2443 for t in helper.getattrs(args, trafo._trafo, default=()):
2444 box.reltransform(t)
2445 return box
2447 def text(self, x, y, expr, *args):
2448 return self._text(unit.topt(x), unit.topt(y), expr, *args)
2451 # the module provides an default texrunner and methods for direct access
2452 defaulttexrunner = texrunner()
2453 reset = defaulttexrunner.reset
2454 set = defaulttexrunner.set
2455 preamble = defaulttexrunner.preamble
2456 text = defaulttexrunner.text
2457 _text = defaulttexrunner._text