changes in the error verbose level
[PyX/mjg.git] / pyx / text.py
blob7bd372a092aac9b5d011fe3c4c4d9df69f07f50b
1 #!/usr/bin/env python
4 # Copyright (C) 2002-2003 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2002-2003 André Wobst <wobsta@users.sourceforge.net>
6 # Copyright (C) 2003 Michael Schindler <m-schindler@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 import exceptions, glob, os, threading, Queue, traceback, re, struct, tempfile, sys, atexit
25 import helper, unit, bbox, box, base, canvas, color, trafo, path, pykpathsea
27 class fix_word:
28 def __init__(self, word):
29 if word >= 0:
30 self.sign = 1
31 else:
32 self.sign = -1
34 self.precomma = abs(word) >> 20
35 self.postcomma = abs(word) & 0xFFFFF
37 def __float__(self):
38 return self.sign * (self.precomma + 1.0*self.postcomma/0xFFFFF)
40 def __mul__(self, other):
41 # hey, it's Q&D
42 result = fix_word(0)
44 result.sign = self.sign*other.sign
45 c = self.postcomma*other.precomma + self.precomma*other.postcomma
46 result.precomma = self.precomma*other.precomma + (c >> 20)
47 result.postcomma = c & 0xFFFFF + ((self.postcomma*other.postcomma) >> 40)
48 return result
51 class binfile:
53 def __init__(self, filename, mode="r"):
54 self.file = open(filename, mode)
56 def tell(self):
57 return self.file.tell()
59 def read(self, bytes):
60 return self.file.read(bytes)
62 def readint(self, bytes=4, signed=0):
63 first = 1
64 result = 0
65 while bytes:
66 value = ord(self.file.read(1))
67 if first and signed and value > 127:
68 value -= 256
69 first = 0
70 result = 256 * result + value
71 bytes -= 1
72 return result
74 def readint32(self):
75 return struct.unpack(">l", self.file.read(4))[0]
77 def readuint32(self):
78 return struct.unpack(">L", self.file.read(4))[0]
80 def readint24(self):
81 # XXX: checkme
82 return struct.unpack(">l", "\0"+self.file.read(3))[0]
84 def readuint24(self):
85 # XXX: checkme
86 return struct.unpack(">L", "\0"+self.file.read(3))[0]
88 def readint16(self):
89 return struct.unpack(">h", self.file.read(2))[0]
91 def readuint16(self):
92 return struct.unpack(">H", self.file.read(2))[0]
94 def readchar(self):
95 return struct.unpack("b", self.file.read(1))[0]
97 def readuchar(self):
98 return struct.unpack("B", self.file.read(1))[0]
100 def readstring(self, bytes):
101 l = self.readuchar()
102 assert l <= bytes-1, "inconsistency in file: string too long"
103 return self.file.read(bytes-1)[:l]
105 class tokenfile:
106 """ ascii file containing tokens separated by spaces.
108 Comments beginning with % are ignored. Strings containing spaces
109 are not handled correctly
112 def __init__(self, filename):
113 self.file = open(filename, "r")
114 self.line = None
116 def gettoken(self):
117 """ return next token or None if EOF """
118 while not self.line:
119 line = self.file.readline()
120 if line == "":
121 return None
122 self.line = line.split("%")[0].split()
123 token = self.line[0]
124 self.line = self.line[1:]
125 return token
127 def close(self):
128 self.file.close()
131 class DVIError(exceptions.Exception): pass
134 class TFMError(exceptions.Exception): pass
137 class char_info_word:
138 def __init__(self, word):
139 self.width_index = int((word & 0xFF000000L) >> 24) #make sign-safe
140 self.height_index = (word & 0x00F00000) >> 20
141 self.depth_index = (word & 0x000F0000) >> 16
142 self.italic_index = (word & 0x0000FC00) >> 10
143 self.tag = (word & 0x00000300) >> 8
144 self.remainder = (word & 0x000000FF)
147 class TFMFile:
148 def __init__(self, name, debug=0):
149 self.file = binfile(name, "rb")
150 self.debug = debug
153 # read pre header
156 self.lf = self.file.readint16()
157 self.lh = self.file.readint16()
158 self.bc = self.file.readint16()
159 self.ec = self.file.readint16()
160 self.nw = self.file.readint16()
161 self.nh = self.file.readint16()
162 self.nd = self.file.readint16()
163 self.ni = self.file.readint16()
164 self.nl = self.file.readint16()
165 self.nk = self.file.readint16()
166 self.ne = self.file.readint16()
167 self.np = self.file.readint16()
169 if not (self.bc-1 <= self.ec <= 255 and
170 self.ne <= 256 and
171 self.lf == 6+self.lh+(self.ec-self.bc+1)+self.nw+self.nh+self.nd
172 +self.ni+self.nl+self.nk+self.ne+self.np):
173 raise TFMError, "error in TFM pre-header"
175 if debug:
176 print "lh=%d" % self.lh
179 # read header
182 self.checksum = self.file.readint32()
183 self.designsizeraw = self.file.readint32()
184 assert self.designsizeraw > 0, "invald design size"
185 self.designsize = fix_word(self.designsizeraw)
186 if self.lh > 2:
187 assert self.lh > 11, "inconsistency in TFM file: incomplete field"
188 self.charcoding = self.file.readstring(40)
189 else:
190 self.charcoding = None
192 if self.lh > 12:
193 assert self.lh > 16, "inconsistency in TFM file: incomplete field"
194 self.fontfamily = self.file.readstring(20)
195 else:
196 self.fontfamily = None
198 if self.debug:
199 print "(FAMILY %s)" % self.fontfamily
200 print "(CODINGSCHEME %s)" % self.charcoding
201 print "(DESINGSIZE R %f)" % self.designsize
203 if self.lh > 17:
204 self.sevenbitsave = self.file.readuchar()
205 # ignore the following two bytes
206 self.file.readint16()
207 facechar = self.file.readuchar()
208 # decode ugly face specification into the Knuth suggested string
209 if facechar < 18:
210 if facechar >= 12:
211 self.face = "E"
212 facechar -= 12
213 elif facechar >= 6:
214 self.face = "C"
215 facechar -= 6
216 else:
217 self.face = "R"
219 if facechar >= 4:
220 self.face = "L" + self.face
221 facechar -= 4
222 elif facechar >= 2:
223 self.face = "B" + self.face
224 facechar -= 2
225 else:
226 self.face = "M" + self.face
228 if facechar == 1:
229 self.face = self.face[0] + "I" + self.face[1]
230 else:
231 self.face = self.face[0] + "R" + self.face[1]
233 else:
234 self.face = None
235 else:
236 self.sevenbitsave = self.face = None
238 if self.lh > 18:
239 # just ignore the rest
240 print self.file.read((self.lh-18)*4)
243 # read char_info
246 self.char_info = [None]*(self.ec+1)
248 for charcode in range(self.bc, self.ec+1):
249 self.char_info[charcode] = char_info_word(self.file.readint32())
250 if self.char_info[charcode].width_index == 0:
251 # disable character if width_index is zero
252 self.char_info[charcode] = None
255 # read widths
258 self.width = [None for width_index in range(self.nw)]
259 for width_index in range(self.nw):
260 # self.width[width_index] = fix_word(self.file.readint32())
261 self.width[width_index] = self.file.readint32()
264 # read heights
267 self.height = [None for height_index in range(self.nh)]
268 for height_index in range(self.nh):
269 # self.height[height_index] = fix_word(self.file.readint32())
270 self.height[height_index] = self.file.readint32()
273 # read depths
276 self.depth = [None for depth_index in range(self.nd)]
277 for depth_index in range(self.nd):
278 # self.depth[depth_index] = fix_word(self.file.readint32())
279 self.depth[depth_index] = self.file.readint32()
282 # read italic
285 self.italic = [None for italic_index in range(self.ni)]
286 for italic_index in range(self.ni):
287 # self.italic[italic_index] = fix_word(self.file.readint32())
288 self.italic[italic_index] = self.file.readint32()
291 # read lig_kern
294 # XXX decode to lig_kern_command
296 self.lig_kern = [None for lig_kern_index in range(self.nl)]
297 for lig_kern_index in range(self.nl):
298 self.lig_kern[lig_kern_index] = self.file.readint32()
301 # read kern
304 self.kern = [None for kern_index in range(self.nk)]
305 for kern_index in range(self.nk):
306 # self.kern[kern_index] = fix_word(self.file.readint32())
307 self.kern[kern_index] = self.file.readint32()
310 # read exten
313 # XXX decode to extensible_recipe
315 self.exten = [None for exten_index in range(self.ne)]
316 for exten_index in range(self.ne):
317 self.exten[exten_index] = self.file.readint32()
320 # read param
323 # XXX decode
325 self.param = [None for param_index in range(self.np)]
326 for param_index in range(self.np):
327 self.param[param_index] = self.file.readint32()
329 self.file.file.close()
332 class FontEncoding:
334 def __init__(self, filename):
335 """ font encoding contained in filename """
336 encpath = pykpathsea.find_file(filename, pykpathsea.kpse_tex_ps_header_format)
337 encfile = tokenfile(encpath)
339 # name of encoding
340 self.encname = encfile.gettoken()
341 token = encfile.gettoken()
342 if token != "[":
343 raise RuntimeError("cannot parse encoding file '%s', expecting '[' got '%s'" % (filename, token))
344 self.encvector = []
345 for i in range(256):
346 token = encfile.gettoken()
347 if token is None or token=="]":
348 raise RuntimeError("not enough charcodes in encoding file '%s'" % filename)
349 self.encvector.append(token)
350 if encfile.gettoken() != "]":
351 raise RuntimeError("too many charcodes in encoding file '%s'" % filename)
352 token = encfile.gettoken()
353 if token != "def":
354 raise RuntimeError("cannot parse encoding file '%s', expecting 'def' got '%s'" % (filename, token))
355 token = encfile.gettoken()
356 if token != None:
357 raise RuntimeError("encoding file '%s' too long" % filename)
358 encfile.close()
360 def encode(self, charcode):
361 return self.encvector[charcode]
364 class FontMapping:
366 tokenpattern = re.compile(r'"(.*?)("\s+|"$|$)|(.*?)(\s+|$)')
368 def __init__(self, s):
369 """ construct font mapping from line s of dvips mapping file """
370 self.texname = self.psname = self.fontfile = self.encoding = None
372 # supported postscript fragments occuring in psfonts.map
373 self.reencodefont = self.extendfont = self.slantfont = None
375 tokens = []
376 while len(s):
377 match = self.tokenpattern.match(s)
378 if match:
379 if match.groups()[0]:
380 tokens.append('"%s"' % match.groups()[0])
381 else:
382 tokens.append(match.groups()[2])
383 s = s[match.end():]
384 else:
385 raise RuntimeError("wrong syntax in font catalog file 'psfonts.map'")
387 for token in tokens:
388 if token.startswith("<"):
389 if token.startswith("<<"):
390 # XXX: support non-partial download here
391 self.fontfile = token[2:]
392 elif token.startswith("<["):
393 self.encoding = token[2:]
394 elif token.endswith(".pfa") or token.endswith(".pfb"):
395 self.fontfile = token[1:]
396 elif token.endswith(".enc"):
397 self.encoding = token[1:]
398 else:
399 raise RuntimeError("wrong syntax in font catalog file 'psfonts.map'")
400 elif token.startswith('"'):
401 pscode = token[1:-1].split()
402 # parse standard postscript code fragments
403 while pscode:
404 try:
405 arg, cmd = pscode[:2]
406 except:
407 raise RuntimeError("Unsupported Postscript fragment '%s' in psfonts.map" % pscode)
408 pscode = pscode[2:]
409 if cmd == "ReEncodeFont":
410 self.reencodefont = arg
411 elif cmd == "ExtendFont":
412 self.extendfont = arg
413 elif cmd == "SlantFont":
414 self.slantfont = arg
415 else:
416 raise RuntimeError("Unsupported Postscript fragment '%s %s' in psfonts.map" % (arg, cmd))
417 else:
418 if self.texname is None:
419 self.texname = token
420 else:
421 self.psname = token
422 if self.psname is None:
423 self.psname = self.texname
425 def __str__(self):
426 return ("'%s' is '%s' read from '%s' encoded as '%s'" %
427 (self.texname, self.psname, self.fontfile, repr(self.encoding)))
430 # generate fontmap
432 fontmap = {}
433 mappath = pykpathsea.find_file("psfonts.map", pykpathsea.kpse_dvips_config_format)
434 if mappath is None:
435 raise RuntimeError("cannot find dvips font catalog 'psfonts.map'")
436 mapfile = open(mappath, "r")
438 for line in mapfile.readlines():
439 line = line.rstrip()
440 if not (line=="" or line[0] in (" ", "%", "*", ";" , "#")):
441 fontmapping = FontMapping(line)
442 fontmap[fontmapping.texname] = fontmapping
444 mapfile.close()
445 del mappath
446 del mapfile
448 class Font:
449 def __init__(self, name, c, q, d, tfmconv, debug=0):
450 self.name = name
451 self.tfmpath = pykpathsea.find_file("%s.tfm" % self.name, pykpathsea.kpse_tfm_format)
452 if self.tfmpath is None:
453 raise TFMError("cannot find %s.tfm" % self.name)
454 self.tfmfile = TFMFile(self.tfmpath, debug)
455 self.fontmapping = fontmap.get(name, None)
456 print "found mapping %s for font %s" % (self.fontmapping, self.name)
458 if self.tfmfile.checksum != c:
459 raise DVIError("check sums do not agree: %d vs. %d" %
460 (self.tfmfile.checksum, c))
462 self.tfmdesignsize = round(tfmconv*self.tfmfile.designsizeraw)
464 if abs(self.tfmdesignsize - d) > 2:
465 raise DVIError("design sizes do not agree: %d vs. %d" %
466 (self.tfmdesignsize, d))
467 if q < 0 or q > 134217728:
468 raise DVIError("font '%s' not loaded: bad scale" % self.name)
469 if d < 0 or d > 134217728:
470 raise DVIError("font '%s' not loaded: bad design size" % self.name)
472 self.scale = 1.0*q/d
473 self.alpha = 16;
474 self.q = self.qorig = q
475 while self.q >= 8388608:
476 self.q = self.q/2
477 self.alpha *= 2
479 self.beta = 256/self.alpha;
480 self.alpha = self.alpha*self.q;
482 # for bookkeeping of used characters
483 self.usedchars = [0] * 256
485 def __str__(self):
486 return "Font(%s, %d)" % (self.name, self.tfmdesignsize)
488 __repr__ = __str__
490 def convert(self, width):
491 # simplified version
492 return 16L*width*self.qorig/16777216L
494 # original algorithm of Knuth (at the moment not used)
495 b0 = width >> 24
496 b1 = (width >> 16) & 0xff
497 b2 = (width >> 8 ) & 0xff
498 b3 = (width ) & 0xff
500 if b0 == 0:
501 return (((((b3*self.q)/256)+(b2*self.q))/256)+(b1*self.q))/self.beta
502 elif b0 == 255:
503 return (((((b3*self.q)/256)+(b2*self.q))/256)+(b1*self.q))/self.beta-self.alpha
504 else:
505 raise TFMError("error in font size")
507 def getwidth(self, charcode):
508 return self.convert(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])
510 def getheight(self, charcode):
511 return self.convert(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])
513 def getdepth(self, charcode):
514 return self.convert(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])
516 def getitalic(self, charcode):
517 return self.convert(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])
519 def markcharused(self, charcode):
520 self.usedchars[charcode] = 1
522 def mergeusedchars(self, otherfont):
523 for i in range(len(self.usedchars)):
524 self.usedchars[i] = self.usedchars[i] or otherfont.usedchars[i]
526 def getpsname(self):
527 if self.fontmapping:
528 return self.fontmapping.psname
529 else:
530 return self.name.upper()
532 def getfontfile(self):
533 if self.fontmapping:
534 return self.fontmapping.fontfile
535 else:
536 return self.name+".pfb"
538 def getencoding(self):
539 if self.fontmapping:
540 return self.fontmapping.encoding
541 else:
542 return None
545 _DVI_CHARMIN = 0 # typeset a character and move right (range min)
546 _DVI_CHARMAX = 127 # typeset a character and move right (range max)
547 _DVI_SET1234 = 128 # typeset a character and move right
548 _DVI_SETRULE = 132 # typeset a rule and move right
549 _DVI_PUT1234 = 133 # typeset a character
550 _DVI_PUTRULE = 137 # typeset a rule
551 _DVI_NOP = 138 # no operation
552 _DVI_BOP = 139 # beginning of page
553 _DVI_EOP = 140 # ending of page
554 _DVI_PUSH = 141 # save the current positions (h, v, w, x, y, z)
555 _DVI_POP = 142 # restore positions (h, v, w, x, y, z)
556 _DVI_RIGHT1234 = 143 # move right
557 _DVI_W0 = 147 # move right by w
558 _DVI_W1234 = 148 # move right and set w
559 _DVI_X0 = 152 # move right by x
560 _DVI_X1234 = 153 # move right and set x
561 _DVI_DOWN1234 = 157 # move down
562 _DVI_Y0 = 161 # move down by y
563 _DVI_Y1234 = 162 # move down and set y
564 _DVI_Z0 = 166 # move down by z
565 _DVI_Z1234 = 167 # move down and set z
566 _DVI_FNTNUMMIN = 171 # set current font (range min)
567 _DVI_FNTNUMMAX = 234 # set current font (range max)
568 _DVI_FNT1234 = 235 # set current font
569 _DVI_SPECIAL1234 = 239 # special (dvi extention)
570 _DVI_FNTDEF1234 = 243 # define the meaning of a font number
571 _DVI_PRE = 247 # preamble
572 _DVI_POST = 248 # postamble beginning
573 _DVI_POSTPOST = 249 # postamble ending
575 _DVI_VERSION = 2 # dvi version
577 # position variable indices
578 _POS_H = 0
579 _POS_V = 1
580 _POS_W = 2
581 _POS_X = 3
582 _POS_Y = 4
583 _POS_Z = 5
585 # reader states
586 _READ_PRE = 1
587 _READ_NOPAGE = 2
588 _READ_PAGE = 3
589 _READ_POST = 4
590 _READ_POSTPOST = 5
591 _READ_DONE = 6
594 # PostScript font selection and output primitives
597 class _selectfont(base.PSOp):
598 def __init__(self, name, size):
599 self.name = name
600 self.size = size
601 def write(self, file):
602 file.write("/%s %f selectfont\n" % (self.name, self.size))
604 # XXX: should we provide a prolog method for the font inclusion
605 # instead of using the coarser logic in DVIFile.prolog
608 class _show(base.PSOp):
609 def __init__(self, x, y, s):
610 self.x = x
611 self.y = y
612 self.s = s
614 def write(self, file):
615 file.write("%f %f moveto (%s) show\n" % (self.x, self.y, self.s))
617 # save and restore colors
619 class _savecolor(base.PSOp):
620 def write(self, file):
621 file.write("currentcolor currentcolorspace\n")
624 class _restorecolor(base.PSOp):
625 def write(self, file):
626 file.write("setcolorspace setcolor\n")
628 class _savetrafo(base.PSOp):
629 def write(self, file):
630 file.write("matrix currentmatrix\n")
633 class _restoretrafo(base.PSOp):
634 def write(self, file):
635 file.write("setmatrix\n")
637 class DVIFile:
639 def __init__(self, filename, debug=0):
640 self.filename = filename
641 self.debug = debug
642 self.readfile()
644 # helper routines
646 def flushout(self):
647 """ flush currently active string """
648 if self.actoutstart:
649 x = unit.t_m(self.actoutstart[0] * self.conv * 0.0254 / self.resolution)
650 y = -unit.t_m(self.actoutstart[1] * self.conv * 0.0254 / self.resolution)
651 if self.debug:
652 print "[%s]" % self.actoutstring
653 self.actpage.insert(_show(unit.topt(x), unit.topt(y), self.actoutstring))
654 self.actoutstart = None
656 def putchar(self, char, inch=1):
657 if self.actoutstart is None:
658 self.actoutstart = self.pos[_POS_H], self.pos[_POS_V]
659 self.actoutstring = ""
660 if char > 32 and char < 128 and chr(char) not in "()[]<>":
661 ascii = "%s" % chr(char)
662 else:
663 ascii = "\\%03o" % char
664 self.actoutstring = self.actoutstring + ascii
665 dx = inch and self.fonts[self.activefont].getwidth(char) or 0
666 self.fonts[self.activefont].markcharused(char)
667 if self.debug:
668 print ("%d: %schar%d h:=%d+%d=%d, hh:=%d" %
669 (self.filepos,
670 inch and "set" or "put",
671 char,
672 self.pos[_POS_H], dx, self.pos[_POS_H]+dx,
674 self.pos[_POS_H] += dx
675 if not inch:
676 # XXX: correct !?
677 self.flushout()
679 def putrule(self, height, width, inch=1):
680 self.flushout()
681 x1 = unit.t_m(self.pos[_POS_H] * self.conv * 0.0254 / self.resolution)
682 y1 = -unit.t_m(self.pos[_POS_V] * self.conv * 0.0254 / self.resolution)
683 w = unit.t_m(width * self.conv * 0.0254 / self.resolution)
684 h = unit.t_m(height * self.conv * 0.0254 / self.resolution)
686 if height > 0 and width > 0:
687 if self.debug:
688 pixelw = int(width*self.conv)
689 if pixelw < width*self.conv: pixelw += 1
690 pixelh = int(height*self.conv)
691 if pixelh < height*self.conv: pixelh += 1
693 print ("%d: %srule height %d, width %d (%dx%d pixels)" %
694 (self.filepos, inch and "set" or "put", height, width, pixelh, pixelw))
695 self.actpage.fill(path.rect(x1, y1, w, h))
696 else:
697 if self.debug:
698 print ("%d: %srule height %d, width %d (invisible)" %
699 (self.filepos, inch and "set" or "put", height, width))
701 if inch:
702 if self.debug:
703 print (" h:=%d+%d=%d, hh:=%d" %
704 (self.pos[_POS_H], width, self.pos[_POS_H]+width, 0))
705 self.pos[_POS_H] += width
708 def usefont(self, fontnum):
709 self.flushout()
710 self.activefont = fontnum
712 fontpsname = self.fonts[self.activefont].getpsname()
713 fontscale = self.fonts[self.activefont].scale
714 fontdesignsize = float(self.fonts[self.activefont].tfmfile.designsize)
715 self.actpage.insert(_selectfont(fontpsname,
716 fontscale*fontdesignsize*72/72.27))
718 if self.debug:
719 print ("%d: fntnum%i current font is %s" %
720 (self.filepos,
721 self.activefont, self.fonts[fontnum].name))
723 def definefont(self, cmdnr, num, c, q, d, fontname):
724 # cmdnr: type of fontdef command (only used for debugging output)
725 # c: checksum
726 # q: scaling factor
727 # Note that q is actually s in large parts of the documentation.
728 # d: design size
730 self.fonts[num] = Font(fontname, c, q, d, self.tfmconv, self.debug > 1)
732 if self.debug:
733 print "%d: fntdef%d %i: %s" % (self.filepos, cmdnr, num, fontname)
735 # scale = round((1000.0*self.conv*q)/(self.trueconv*d))
736 # m = 1.0*q/d
737 # scalestring = scale!=1000 and " scaled %d" % scale or ""
738 # print ("Font %i: %s%s---loaded at size %d DVI units" %
739 # (num, fontname, scalestring, q))
740 # if scale!=1000:
741 # print " (this font is magnified %d%%)" % round(scale/10)
743 def special(self, s):
744 self.flushout()
745 x = unit.t_m(self.pos[_POS_H] * self.conv * 0.0254 / self.resolution)
746 y = -unit.t_m(self.pos[_POS_V] * self.conv * 0.0254 / self.resolution)
747 if self.debug:
748 print "%d: xxx '%s'" % (self.filepos, s)
749 if not s.startswith("PyX:"):
750 raise RuntimeError("the special '%s' cannot be handled by PyX, aborting" % s)
751 command, args = s[4:].split()[0], s[4:].split()[1:]
752 if command=="color_begin":
753 if args[0]=="cmyk":
754 c = color.cmyk(float(args[1]), float(args[2]), float(args[3]), float(args[4]))
755 elif args[0]=="gray":
756 c = color.gray(float(args[1]))
757 elif args[0]=="hsb":
758 c = color.hsb(float(args[1]), float(args[2]), float(args[3]))
759 elif args[0]=="rgb":
760 c = color.rgb(float(args[1]), float(args[2]), float(args[3]))
761 elif args[0]=="RGB":
762 c = color.rgb(int(args[1])/255.0, int(args[2])/255.0, int(args[3])/255.0)
763 elif args[0]=="texnamed":
764 try:
765 c = getattr(color.cmyk, args[1])
766 except AttributeError:
767 raise RuntimeError("unknown TeX color '%s', aborting" % args[1])
768 else:
769 raise RuntimeError("color model '%s' cannot be handled by PyX, aborting" % args[0])
770 self.actpage.insert(_savecolor())
771 self.actpage.insert(c)
772 elif command=="color_end":
773 self.actpage.insert(_restorecolor())
774 elif command=="rotate_begin":
775 self.actpage.insert(_savetrafo())
776 self.actpage.insert(trafo.rotate(float(args[0]), x, y))
777 elif command=="rotate_end":
778 self.actpage.insert(_restoretrafo())
779 elif command=="scale_begin":
780 self.actpage.insert(_savetrafo())
781 self.actpage.insert(trafo.scale(float(args[0]), float(args[1]), x, y))
782 elif command=="scale_end":
783 self.actpage.insert(_restoretrafo())
784 elif command=="epsinclude":
785 # XXX: we cannot include epsfile in the header because this would
786 # generate a cyclic import with the canvas and text modules
787 import epsfile
789 # parse arguments
790 argdict = {}
791 for arg in args:
792 name, value = arg.split("=")
793 argdict[name] = value
795 # construct kwargs for epsfile constructor
796 epskwargs = {}
797 epskwargs["filename"] = argdict["file"]
798 epskwargs["bbox"] = bbox._bbox(float(argdict["llx"]), float(argdict["lly"]),
799 float(argdict["urx"]), float(argdict["ury"]))
800 if argdict.has_key("width"):
801 epskwargs["width"] = unit.t_pt(float(argdict["width"]))
802 if argdict.has_key("height"):
803 epskwargs["height"] = unit.t_pt(float(argdict["height"]))
804 if argdict.has_key("clip"):
805 epskwargs["clip"] = int(argdict["clip"])
806 self.actpage.insert(epsfile.epsfile(x, y, **epskwargs))
807 else:
808 raise RuntimeError("unknown PyX special '%s', aborting" % command)
810 # routines corresponding to the different reader states of the dvi maschine
812 def _read_pre(self):
813 file = self.file
814 while 1:
815 self.filepos = file.tell()
816 cmd = file.readuchar()
817 if cmd == _DVI_NOP:
818 pass
819 elif cmd == _DVI_PRE:
820 if self.file.readuchar() != _DVI_VERSION: raise DVIError
821 num = file.readuint32()
822 den = file.readuint32()
823 mag = file.readuint32()
825 self.tfmconv = (25400000.0/num)*(den/473628672)/16.0;
826 # resolution in dpi
827 self.resolution = 300.0
828 # self.trueconv = conv in DVIType docu
829 self.trueconv = (num/254000.0)*(self.resolution/den)
830 self.conv = self.trueconv*(mag/1000.0)
832 comment = file.read(file.readuchar())
833 return _READ_NOPAGE
834 else:
835 raise DVIError
837 def _read_nopage(self):
838 file = self.file
839 while 1:
840 self.filepos = file.tell()
841 cmd = file.readuchar()
842 if cmd == _DVI_NOP:
843 pass
844 elif cmd == _DVI_BOP:
845 self.flushout()
846 if self.debug:
847 print "%d: beginning of page" % self.filepos,
848 print file.readuint32()
849 for i in range(9): file.readuint32()
850 else:
851 for i in range(10): file.readuint32()
852 file.readuint32()
853 return _READ_PAGE
854 elif cmd == _DVI_POST:
855 return _READ_DONE # we skip the rest
856 else:
857 raise DVIError
859 def _read_page(self):
860 self.pos = [0, 0, 0, 0, 0, 0]
861 self.pages.append(canvas.canvas())
862 self.actpage = self.pages[-1]
863 file = self.file
864 while 1:
865 self.filepos = file.tell()
866 cmd = file.readuchar()
867 if cmd == _DVI_NOP:
868 pass
869 if cmd >= _DVI_CHARMIN and cmd <= _DVI_CHARMAX:
870 self.putchar(cmd)
871 elif cmd >= _DVI_SET1234 and cmd < _DVI_SET1234 + 4:
872 self.putchar(file.readint(cmd - _DVI_SET1234 + 1))
873 elif cmd == _DVI_SETRULE:
874 self.putrule(file.readint32(), file.readint32())
875 elif cmd >= _DVI_PUT1234 and cmd < _DVI_PUT1234 + 4:
876 self.putchar(file.readint(cmd - _DVI_PUT1234 + 1), inch=0)
877 elif cmd == _DVI_PUTRULE:
878 self.putrule(file.readint32(), file.readint32(), 0)
879 elif cmd == _DVI_EOP:
880 self.flushout()
881 if self.debug:
882 print "%d: eop" % self.filepos
883 print
884 return _READ_NOPAGE
885 elif cmd == _DVI_PUSH:
886 self.stack.append(tuple(self.pos))
887 if self.debug:
888 print "%d: push" % self.filepos
889 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
890 (( len(self.stack)-1,)+tuple(self.pos)))
891 elif cmd == _DVI_POP:
892 self.flushout()
893 self.pos = list(self.stack[-1])
894 del self.stack[-1]
895 if self.debug:
896 print "%d: pop" % self.filepos
897 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
898 (( len(self.stack),)+tuple(self.pos)))
899 elif cmd >= _DVI_RIGHT1234 and cmd < _DVI_RIGHT1234 + 4:
900 self.flushout()
901 dh = file.readint(cmd - _DVI_RIGHT1234 + 1, 1)
902 if self.debug:
903 print ("%d: right%d %d h:=%d%+d=%d, hh:=" %
904 (self.filepos,
905 cmd - _DVI_RIGHT1234 + 1,
907 self.pos[_POS_H],
909 self.pos[_POS_H]+dh))
910 self.pos[_POS_H] += dh
911 elif cmd == _DVI_W0:
912 self.flushout()
913 if self.debug:
914 print ("%d: w0 %d h:=%d%+d=%d, hh:=" %
915 (self.filepos,
916 self.pos[_POS_W],
917 self.pos[_POS_H],
918 self.pos[_POS_W],
919 self.pos[_POS_H]+self.pos[_POS_W]))
920 self.pos[_POS_H] += self.pos[_POS_W]
921 elif cmd >= _DVI_W1234 and cmd < _DVI_W1234 + 4:
922 self.flushout()
923 self.pos[_POS_W] = file.readint(cmd - _DVI_W1234 + 1, 1)
924 if self.debug:
925 print ("%d: w%d %d h:=%d%+d=%d, hh:=" %
926 (self.filepos,
927 cmd - _DVI_W1234 + 1,
928 self.pos[_POS_W],
929 self.pos[_POS_H],
930 self.pos[_POS_W],
931 self.pos[_POS_H]+self.pos[_POS_W]))
932 self.pos[_POS_H] += self.pos[_POS_W]
933 elif cmd == _DVI_X0:
934 self.flushout()
935 self.pos[_POS_H] += self.pos[_POS_X]
936 elif cmd >= _DVI_X1234 and cmd < _DVI_X1234 + 4:
937 self.flushout()
938 self.pos[_POS_X] = file.readint(cmd - _DVI_X1234 + 1, 1)
939 self.pos[_POS_H] += self.pos[_POS_X]
940 elif cmd >= _DVI_DOWN1234 and cmd < _DVI_DOWN1234 + 4:
941 self.flushout()
942 dv = file.readint(cmd - _DVI_DOWN1234 + 1, 1)
943 if self.debug:
944 print ("%d: down%d %d v:=%d%+d=%d, vv:=" %
945 (self.filepos,
946 cmd - _DVI_DOWN1234 + 1,
948 self.pos[_POS_V],
950 self.pos[_POS_V]+dv))
951 self.pos[_POS_V] += dv
952 elif cmd == _DVI_Y0:
953 self.flushout()
954 if self.debug:
955 print ("%d: y0 %d v:=%d%+d=%d, vv:=" %
956 (self.filepos,
957 self.pos[_POS_Y],
958 self.pos[_POS_V],
959 self.pos[_POS_Y],
960 self.pos[_POS_V]+self.pos[_POS_Y]))
961 self.pos[_POS_V] += self.pos[_POS_Y]
962 elif cmd >= _DVI_Y1234 and cmd < _DVI_Y1234 + 4:
963 self.flushout()
964 self.pos[_POS_Y] = file.readint(cmd - _DVI_Y1234 + 1, 1)
965 if self.debug:
966 print ("%d: y%d %d v:=%d%+d=%d, vv:=" %
967 (self.filepos,
968 cmd - _DVI_Y1234 + 1,
969 self.pos[_POS_Y],
970 self.pos[_POS_V],
971 self.pos[_POS_Y],
972 self.pos[_POS_V]+self.pos[_POS_Y]))
973 self.pos[_POS_V] += self.pos[_POS_Y]
974 elif cmd == _DVI_Z0:
975 self.flushout()
976 self.pos[_POS_V] += self.pos[_POS_Z]
977 elif cmd >= _DVI_Z1234 and cmd < _DVI_Z1234 + 4:
978 self.flushout()
979 self.pos[_POS_Z] = file.readint(cmd - _DVI_Z1234 + 1, 1)
980 self.pos[_POS_V] += self.pos[_POS_Z]
981 elif cmd >= _DVI_FNTNUMMIN and cmd <= _DVI_FNTNUMMAX:
982 self.usefont(cmd - _DVI_FNTNUMMIN)
983 elif cmd >= _DVI_FNT1234 and cmd < _DVI_FNT1234 + 4:
984 self.usefont(file.readint(cmd - _DVI_FNT1234 + 1, 1))
985 elif cmd >= _DVI_SPECIAL1234 and cmd < _DVI_SPECIAL1234 + 4:
986 self.special(file.read(file.readint(cmd - _DVI_SPECIAL1234 + 1)))
987 elif cmd >= _DVI_FNTDEF1234 and cmd < _DVI_FNTDEF1234 + 4:
988 if cmd == _DVI_FNTDEF1234:
989 num=file.readuchar()
990 elif cmd == _DVI_FNTDEF1234+1:
991 num=file.readuint16()
992 elif cmd == _DVI_FNTDEF1234+2:
993 num=file.readuint24()
994 elif cmd == _DVI_FNTDEF1234+3:
995 # Cool, here we have according to docu a signed int. Why?
996 num = file.readint32()
997 self.definefont(cmd-_DVI_FNTDEF1234+1,
998 num,
999 file.readint32(),
1000 file.readint32(),
1001 file.readint32(),
1002 file.read(file.readuchar()+file.readuchar()))
1003 else: raise DVIError
1005 def readfile(self):
1006 """ reads and parses dvi file
1008 This routine reads the dvi file and generates a list
1009 of pages in self.pages. Each page consists itself of
1010 a list of PSCommands equivalent to the content of
1011 the dvi file. Furthermore, the list of used fonts
1012 can be extracted from the array self.fonts.
1015 # XXX max number of fonts
1016 self.fonts = [None for i in range(64)]
1017 self.activefont = None
1019 self.stack = []
1021 # here goes the result, for each page one list.
1022 self.pages = []
1024 # pointer to currently active page
1025 self.actpage = None
1027 # currently active output: position and content
1028 self.actoutstart = None
1029 self.actoutstring = ""
1031 self.file = binfile(self.filename, "rb")
1033 # currently read byte in file (for debugging output)
1034 self.filepos = None
1036 # start up reading process
1037 state = _READ_PRE
1038 while state!=_READ_DONE:
1039 if state == _READ_PRE:
1040 state = self._read_pre()
1041 elif state == _READ_NOPAGE:
1042 state = self._read_nopage()
1043 elif state == _READ_PAGE:
1044 state = self._read_page()
1045 else:
1046 raise DVIError # unexpected reader state, should not happen
1047 self.flushout()
1049 def prolog(self, page): # TODO: AW inserted this page argument -> should return the prolog needed for that page only!
1050 """ return prolog corresponding to contents of dvi file """
1051 # XXX replace this by prolog method in _selectfont
1052 result = []
1053 for font in self.fonts:
1054 if font: result.append(canvas.fontdefinition(font))
1055 result.extend(self.pages[page-1].prolog())
1056 return result
1058 def write(self, file, page):
1059 """write PostScript output for page into file"""
1060 # XXX: remove this method by return canvas to TexRunner
1061 if self.debug:
1062 print "dvifile(\"%s\").write() for page %s called" % (self.filename, page)
1063 self.pages[page-1].write(file)
1066 ###############################################################################
1067 # texmessages
1068 # - please don't get confused:
1069 # - there is a texmessage (and a texmessageparsed) attribute within the
1070 # texrunner; it contains TeX/LaTeX response from the last command execution
1071 # - instances of classes derived from the class texmessage are used to
1072 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
1073 # attribute of a texrunner instance
1074 # - the multiple usage of the name texmessage might be removed in the future
1075 # - texmessage instances should implement _Itexmessage
1076 ###############################################################################
1078 class TexResultError(Exception):
1079 """specialized texrunner exception class
1080 - it is raised by texmessage instances, when a texmessage indicates an error
1081 - it is raised by the texrunner itself, whenever there is a texmessage left
1082 after all parsing of this message (by texmessage instances)"""
1084 def __init__(self, description, texrunner):
1085 self.description = description
1086 self.texrunner = texrunner
1088 def __str__(self):
1089 """prints a detailed report about the problem
1090 - the verbose level is controlled by texrunner.errordebug"""
1091 if self.texrunner.errordebug >= 2:
1092 return ("%s\n" % self.description +
1093 "The expression passed to TeX was:\n"
1094 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1095 "The return message from TeX was:\n"
1096 " %s\n" % self.texrunner.texmessage.replace("\n", "\n ").rstrip() +
1097 "After parsing this message, the following was left:\n"
1098 " %s" % self.texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
1099 elif self.texrunner.errordebug == 1:
1100 firstlines = self.texrunner.texmessageparsed.split("\n")
1101 if len(firstlines) > 5:
1102 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
1103 return ("%s\n" % self.description +
1104 "The expression passed to TeX was:\n"
1105 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1106 "After parsing the return message from TeX, the following was left:\n" +
1107 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
1108 else:
1109 return self.description
1112 class TexResultWarning(TexResultError):
1113 """as above, but with different handling of the exception
1114 - when this exception is raised by a texmessage instance,
1115 the information just get reported and the execution continues"""
1116 pass
1119 class _Itexmessage:
1120 """validates/invalidates TeX/LaTeX response"""
1122 def check(self, texrunner):
1123 """check a Tex/LaTeX response and respond appropriate
1124 - read the texrunners texmessageparsed attribute
1125 - if there is an problem found, raise an appropriate
1126 exception (TexResultError or TexResultWarning)
1127 - remove any valid and identified TeX/LaTeX response
1128 from the texrunners texmessageparsed attribute
1129 -> finally, there should be nothing left in there,
1130 otherwise it is interpreted as an error"""
1133 class texmessage: pass
1136 class _texmessagestart(texmessage):
1137 """validates TeX/LaTeX startup"""
1139 __implements__ = _Itexmessage
1141 startpattern = re.compile(r"This is [0-9a-zA-Z\s_]*TeX")
1143 def check(self, texrunner):
1144 m = self.startpattern.search(texrunner.texmessageparsed)
1145 if not m:
1146 raise TexResultError("TeX startup failed", texrunner)
1147 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
1148 try:
1149 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
1150 except (IndexError, ValueError):
1151 raise TexResultError("TeX running startup file failed", texrunner)
1152 try:
1153 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
1154 except (IndexError, ValueError):
1155 raise TexResultError("TeX scrollmode check failed", texrunner)
1158 class _texmessagenoaux(texmessage):
1159 """allows for LaTeXs no-aux-file warning"""
1161 __implements__ = _Itexmessage
1163 def check(self, texrunner):
1164 try:
1165 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
1166 texrunner.texmessageparsed = s1 + s2
1167 except (IndexError, ValueError):
1168 try:
1169 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
1170 os.sep,
1171 texrunner.texfilename), 1)
1172 texrunner.texmessageparsed = s1 + s2
1173 except (IndexError, ValueError):
1174 pass
1177 class _texmessageinputmarker(texmessage):
1178 """validates the PyXInputMarker"""
1180 __implements__ = _Itexmessage
1182 def check(self, texrunner):
1183 try:
1184 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
1185 texrunner.texmessageparsed = s1 + s2
1186 except (IndexError, ValueError):
1187 raise TexResultError("PyXInputMarker expected", texrunner)
1190 class _texmessagepyxbox(texmessage):
1191 """validates the PyXBox output"""
1193 __implements__ = _Itexmessage
1195 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:")
1197 def check(self, texrunner):
1198 m = self.pattern.search(texrunner.texmessageparsed)
1199 if m and m.group("page") == str(texrunner.page):
1200 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1201 else:
1202 raise TexResultError("PyXBox expected", texrunner)
1205 class _texmessagepyxpageout(texmessage):
1206 """validates the dvi shipout message (writing a page to the dvi file)"""
1208 __implements__ = _Itexmessage
1210 def check(self, texrunner):
1211 try:
1212 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
1213 texrunner.texmessageparsed = s1 + s2
1214 except (IndexError, ValueError):
1215 raise TexResultError("PyXPageOutMarker expected", texrunner)
1218 class _texmessagetexend(texmessage):
1219 """validates TeX/LaTeX finish"""
1221 __implements__ = _Itexmessage
1223 def check(self, texrunner):
1224 try:
1225 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
1226 texrunner.texmessageparsed = s1 + s2
1227 except (IndexError, ValueError):
1228 try:
1229 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
1230 os.sep,
1231 texrunner.texfilename), 1)
1232 texrunner.texmessageparsed = s1 + s2
1233 except (IndexError, ValueError):
1234 pass
1235 try:
1236 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
1237 texrunner.texmessageparsed = s1 + s2
1238 except (IndexError, ValueError):
1239 pass
1240 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
1241 m = dvipattern.search(texrunner.texmessageparsed)
1242 if texrunner.page:
1243 if not m:
1244 raise TexResultError("TeX dvifile messages expected", texrunner)
1245 if m.group("page") != str(texrunner.page):
1246 raise TexResultError("wrong number of pages reported", texrunner)
1247 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1248 else:
1249 try:
1250 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
1251 texrunner.texmessageparsed = s1 + s2
1252 except (IndexError, ValueError):
1253 raise TexResultError("no dvifile expected")
1254 try:
1255 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
1256 texrunner.texmessageparsed = s1 + s2
1257 except (IndexError, ValueError):
1258 raise TexResultError("TeX logfile message expected")
1261 class _texmessageemptylines(texmessage):
1262 """validates empty and "*-only" (TeX/LaTeX input marker in interactive mode) lines"""
1264 __implements__ = _Itexmessage
1266 pattern = re.compile(r"^\*?\n", re.M)
1268 def check(self, texrunner):
1269 m = self.pattern.search(texrunner.texmessageparsed)
1270 while m:
1271 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1272 m = self.pattern.search(texrunner.texmessageparsed)
1275 class _texmessageload(texmessage):
1276 """validates inclusion of arbitrary files
1277 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
1278 <fielname> is a readable file and other stuff can be anything
1279 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
1280 - this is not always wanted, but we just assume that file inclusion is fine"""
1282 __implements__ = _Itexmessage
1284 pattern = re.compile(r"\((?P<filename>[^()\s\n]+)[^()]*\)")
1286 def baselevels(self, s, maxlevel=1, brackets="()"):
1287 """strip parts of a string above a given bracket level
1288 - return a modified (some parts might be removed) version of the string s
1289 where all parts inside brackets with level higher than maxlevel are
1290 removed
1291 - if brackets do not match (number of left and right brackets is wrong
1292 or at some points there were more right brackets than left brackets)
1293 just return the unmodified string"""
1294 level = 0
1295 highestlevel = 0
1296 res = ""
1297 for c in s:
1298 if c == brackets[0]:
1299 level += 1
1300 if level > highestlevel:
1301 highestlevel = level
1302 if level <= maxlevel:
1303 res += c
1304 if c == brackets[1]:
1305 level -= 1
1306 if level == 0 and highestlevel > 0:
1307 return res
1309 def check(self, texrunner):
1310 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
1311 if lowestbracketlevel is not None:
1312 m = self.pattern.search(lowestbracketlevel)
1313 while m:
1314 if os.access(m.group("filename"), os.R_OK):
1315 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
1316 else:
1317 break
1318 m = self.pattern.search(lowestbracketlevel)
1319 else:
1320 texrunner.texmessageparsed = lowestbracketlevel
1323 class _texmessageloadfd(_texmessageload):
1324 """validates the inclusion of font description files (fd-files)
1325 - works like _texmessageload
1326 - filename must end with .fd and no further text is allowed"""
1328 pattern = re.compile(r"\((?P<filename>[^)]+.fd)\)")
1331 class _texmessagegraphicsload(_texmessageload):
1332 """validates the inclusion of files as the graphics packages writes it
1333 - works like _texmessageload, but using "<" and ">" as delimiters
1334 - filename must end with .eps and no further text is allowed"""
1336 pattern = re.compile(r"<(?P<filename>[^>]+.eps)>")
1338 def baselevels(self, s, brackets="<>", **args):
1339 return _texmessageload.baselevels(self, s, brackets=brackets, **args)
1342 #class _texmessagepdfmapload(_texmessageload):
1343 # """validates the inclusion of files as the graphics packages writes it
1344 # - works like _texmessageload, but using "{" and "}" as delimiters
1345 # - filename must end with .map and no further text is allowed"""
1347 # pattern = re.compile(r"{(?P<filename>[^}]+.map)}")
1349 # def baselevels(self, s, brackets="{}", **args):
1350 # return _texmessageload.baselevels(self, s, brackets=brackets, **args)
1353 class _texmessageignore(_texmessageload):
1354 """validates any TeX/LaTeX response
1355 - this might be used, when the expression is ok, but no suitable texmessage
1356 parser is available
1357 - PLEASE: - consider writing suitable tex message parsers
1358 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
1360 __implements__ = _Itexmessage
1362 def check(self, texrunner):
1363 texrunner.texmessageparsed = ""
1366 texmessage.start = _texmessagestart()
1367 texmessage.noaux = _texmessagenoaux()
1368 texmessage.inputmarker = _texmessageinputmarker()
1369 texmessage.pyxbox = _texmessagepyxbox()
1370 texmessage.pyxpageout = _texmessagepyxpageout()
1371 texmessage.texend = _texmessagetexend()
1372 texmessage.emptylines = _texmessageemptylines()
1373 texmessage.load = _texmessageload()
1374 texmessage.loadfd = _texmessageloadfd()
1375 texmessage.graphicsload = _texmessagegraphicsload()
1376 texmessage.ignore = _texmessageignore()
1379 ###############################################################################
1380 # texsettings
1381 # - texsettings are used to modify a TeX/LaTeX expression
1382 # to fit the users need
1383 # - texsettings have an order attribute (id), because the order is usually
1384 # important (e.g. you can not change the fontsize in mathmode in LaTeX)
1385 # - lower id's get applied later (are more outside -> mathmode has a higher
1386 # id than fontsize)
1387 # - order attributes are used to exclude complementary settings (with the
1388 # same id)
1389 # - texsettings might (in rare cases) depend on each other (e.g. parbox and
1390 # valign)
1391 ###############################################################################
1393 class _Itexsetting:
1394 """tex setting
1395 - modifies a TeX/LaTeX expression"""
1397 id = 0
1398 """order attribute for TeX settings
1399 - higher id's will be applied first (most inside)"""
1401 exclusive = 0
1402 """marks exclusive effect of the setting
1403 - when set, settings with this id exclude each other
1404 - when unset, settings with this id do not exclude each other"""
1406 def modifyexpr(self, expr, texsettings, texrunner):
1407 """modifies the TeX/LaTeX expression
1408 - expr is the original expression
1409 - the return value is the modified expression
1410 - texsettings contains a list of all texsettings (in case a tex setting
1411 depends on another texsetting)
1412 - texrunner contains the texrunner in case the texsetting depends
1413 on it"""
1415 def __cmp__(self, other):
1416 """compare texsetting with other
1417 - other is a texsetting as well
1418 - performs an id comparison (NOTE: higher id's come first!!!)"""
1421 # preamble settings for texsetting macros
1422 _texsettingpreamble = ""
1424 class _texsetting:
1426 exclusive = 1
1428 def __cmp__(self, other):
1429 return -cmp(self.id, other.id) # note the sign!!!
1432 class halign(_texsetting):
1433 """horizontal alignment
1434 the left/right splitting is performed within the PyXBox routine"""
1436 __implements__ = _Itexsetting
1438 id = 1000
1440 def __init__(self, hratio):
1441 self.hratio = hratio
1443 def modifyexpr(self, expr, texsettings, texrunner):
1444 return r"\gdef\PyXHAlign{%.5f}%s" % (self.hratio, expr)
1446 halign.left = halign(0)
1447 halign.center = halign(0.5)
1448 halign.right = halign(1)
1451 _texsettingpreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
1453 class valign(_texsetting):
1454 "vertical alignment"
1456 id = 7000
1459 class _valigntop(valign):
1461 __implements__ = _Itexsetting
1463 def modifyexpr(self, expr, texsettings, texrunner):
1464 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1467 class _valignmiddle(valign):
1469 __implements__ = _Itexsetting
1471 def modifyexpr(self, expr, texsettings, texrunner):
1472 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
1475 class _valignbottom(valign):
1477 __implements__ = _Itexsetting
1479 def modifyexpr(self, expr, texsettings, texrunner):
1480 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1483 class _valignbaseline(valign):
1485 __implements__ = _Itexsetting
1487 def modifyexpr(self, expr, texsettings, texrunner):
1488 for texsetting in texsettings:
1489 if isinstance(texsetting, parbox):
1490 raise RuntimeError("valign.baseline: specify top/middle/bottom baseline for parbox")
1491 return expr
1494 class _valignxxxbaseline(valign):
1496 def modifyexpr(self, expr, texsettings, texrunner):
1497 for texsetting in texsettings:
1498 if isinstance(texsetting, parbox):
1499 break
1500 else:
1501 raise RuntimeError(self.noparboxmessage)
1502 return expr
1505 class _valigntopbaseline(_valignxxxbaseline):
1507 __implements__ = _Itexsetting
1509 noparboxmessage = "valign.topbaseline: no parbox defined"
1512 class _valignmiddlebaseline(_valignxxxbaseline):
1514 __implements__ = _Itexsetting
1516 noparboxmessage = "valign.middlebaseline: no parbox defined"
1519 class _valignbottombaseline(_valignxxxbaseline):
1521 __implements__ = _Itexsetting
1523 noparboxmessage = "valign.bottombaseline: no parbox defined"
1526 valign.top = _valigntop()
1527 valign.middle = _valignmiddle()
1528 valign.center = valign.middle
1529 valign.bottom = _valignbottom()
1530 valign.baseline = _valignbaseline()
1531 valign.topbaseline = _valigntopbaseline()
1532 valign.middlebaseline = _valignmiddlebaseline()
1533 valign.centerbaseline = valign.middlebaseline
1534 valign.bottombaseline = _valignbottombaseline()
1537 _texsettingpreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
1540 class _parbox(_texsetting):
1541 "goes into the vertical mode"
1543 __implements__ = _Itexsetting
1545 id = 7100
1547 def __init__(self, width):
1548 self.width = width
1550 def modifyexpr(self, expr, texsettings, texrunner):
1551 boxkind = "vtop"
1552 for texsetting in texsettings:
1553 if isinstance(texsetting, valign):
1554 if (not isinstance(texsetting, _valigntop) and
1555 not isinstance(texsetting, _valignmiddle) and
1556 not isinstance(texsetting, _valignbottom) and
1557 not isinstance(texsetting, _valigntopbaseline)):
1558 if isinstance(texsetting, _valignmiddlebaseline):
1559 boxkind = "vcenter"
1560 elif isinstance(texsetting, _valignbottombaseline):
1561 boxkind = "vbox"
1562 else:
1563 raise RuntimeError("parbox couldn'd identify the valign instance")
1564 if boxkind == "vcenter":
1565 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)
1566 else:
1567 return r"\linewidth%.5ftruept\%s{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, boxkind, expr)
1570 class parbox(_parbox):
1572 def __init__(self, width):
1573 _parbox.__init__(self, unit.topt(width))
1576 class vshift(_texsetting):
1578 exclusive = 0
1580 id = 5000
1583 class _vshiftchar(vshift):
1584 "vertical down shift by a fraction of a character height"
1586 def __init__(self, lowerratio, heightstr="0"):
1587 self.lowerratio = lowerratio
1588 self.heightstr = heightstr
1590 def modifyexpr(self, expr, texsettings, texrunner):
1591 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
1594 class _vshiftmathaxis(vshift):
1595 "vertical down shift by the height of the math axis"
1597 def modifyexpr(self, expr, texsettings, texrunner):
1598 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
1601 vshift.char = _vshiftchar
1602 vshift.bottomzero = vshift.char(0)
1603 vshift.middlezero = vshift.char(0.5)
1604 vshift.centerzero = vshift.middlezero
1605 vshift.topzero = vshift.char(1)
1606 vshift.mathaxis = _vshiftmathaxis()
1609 class _mathmode(_texsetting):
1610 "math mode"
1612 __implements__ = _Itexsetting
1614 id = 9000
1616 def modifyexpr(self, expr, texsettings, texrunner):
1617 return r"$\displaystyle{%s}$" % expr
1619 mathmode = _mathmode()
1622 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
1624 class size(_texsetting):
1625 "font size"
1627 __implements__ = _Itexsetting
1629 id = 3000
1631 def __init__(self, expr, sizelist=defaultsizelist):
1632 if helper.isinteger(expr):
1633 if expr >= 0 and expr < sizelist.index(None):
1634 self.size = sizelist[expr]
1635 elif expr < 0 and expr + len(sizelist) > sizelist.index(None):
1636 self.size = sizelist[expr]
1637 else:
1638 raise IndexError("index out of sizelist range")
1639 else:
1640 self.size = expr
1642 def modifyexpr(self, expr, texsettings, texrunner):
1643 return r"\%s{%s}" % (self.size, expr)
1645 for s in defaultsizelist:
1646 if s is not None:
1647 size.__dict__[s] = size(s)
1650 ###############################################################################
1651 # texrunner
1652 ###############################################################################
1655 class _readpipe(threading.Thread):
1656 """threaded reader of TeX/LaTeX output
1657 - sets an event, when a specific string in the programs output is found
1658 - sets an event, when the terminal ends"""
1660 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
1661 """initialize the reader
1662 - pipe: file to be read from
1663 - expectqueue: keeps the next InputMarker to be wait for
1664 - gotevent: the "got InputMarker" event
1665 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
1666 - quitevent: the "end of terminal" event"""
1667 threading.Thread.__init__(self)
1668 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
1669 self.pipe = pipe
1670 self.expectqueue = expectqueue
1671 self.gotevent = gotevent
1672 self.gotqueue = gotqueue
1673 self.quitevent = quitevent
1674 self.expect = None
1675 self.start()
1677 def run(self):
1678 """thread routine"""
1679 read = self.pipe.readline() # read, what comes in
1680 try:
1681 self.expect = self.expectqueue.get_nowait() # read, what should be expected
1682 except Queue.Empty:
1683 pass
1684 while len(read):
1685 # universal EOL handling (convert everything into unix like EOLs)
1686 read.replace("\r", "")
1687 if not len(read) or read[-1] != "\n":
1688 read += "\n"
1689 self.gotqueue.put(read) # report, whats readed
1690 if self.expect is not None and read.find(self.expect) != -1:
1691 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
1692 read = self.pipe.readline() # read again
1693 try:
1694 self.expect = self.expectqueue.get_nowait()
1695 except Queue.Empty:
1696 pass
1697 # EOF reached
1698 self.pipe.close()
1699 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
1700 raise RuntimeError("TeX/LaTeX finished unexpectedly")
1701 self.quitevent.set()
1705 class _textbox(box._rect, base.PSCmd):
1706 """basically a box.rect, but it contains a text created by the texrunner
1707 - texrunner._text and texrunner.text return such an object
1708 - _textbox instances can be inserted into a canvas
1709 - the output is contained in a page of the dvifile available thru the texrunner"""
1711 def __init__(self, x, y, left, right, height, depth, texrunner, dvinumber, page, *styles):
1712 self.texttrafo = trafo._translate(x, y)
1713 box._rect.__init__(self, x - left, y - depth,
1714 left + right, depth + height,
1715 abscenter = (left, depth))
1716 self.texrunner = texrunner
1717 self.dvinumber = dvinumber
1718 self.page = page
1719 self.styles = styles
1721 def transform(self, *trafos):
1722 box._rect.transform(self, *trafos)
1723 for trafo in trafos:
1724 self.texttrafo = trafo * self.texttrafo
1726 def prolog(self):
1727 result = []
1728 for cmd in self.styles:
1729 result.extend(cmd.prolog())
1730 return result + self.texrunner.prolog(self.dvinumber, self.page)
1732 def write(self, file):
1733 canvas._gsave().write(file) # XXX: canvas?, constructor call needed?
1734 self.texttrafo.write(file)
1735 for style in self.styles:
1736 style.write(file)
1737 self.texrunner.write(file, self.dvinumber, self.page)
1738 canvas._grestore().write(file)
1742 class textbox(_textbox):
1744 def __init__(self, x, y, left, right, height, depth, texrunner, page):
1745 _textbox.__init__(unit.topt(x), unit.topt(y), unit.topt(left), unit.topt(right),
1746 unit.topt(height), unit.topt(depth), texrunner, page)
1749 def _cleantmp(texrunner):
1750 """get rid of temporary files
1751 - function to be registered by atexit
1752 - files contained in usefiles are kept"""
1753 if texrunner.texruns: # cleanup while TeX is still running?
1754 texrunner.texruns = 0
1755 texrunner.texdone = 1
1756 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
1757 texrunner.texinput.close() # close the input queue and
1758 texrunner.quitevent.wait(texrunner.waitfortex) # wait for finish of the output
1759 if not texrunner.quitevent.isSet(): return # didn't got a quit from TeX -> we can't do much more
1760 for usefile in texrunner.usefiles:
1761 extpos = usefile.rfind(".")
1762 try:
1763 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
1764 except OSError:
1765 pass
1766 for file in glob.glob("%s.*" % texrunner.texfilename):
1767 try:
1768 os.unlink(file)
1769 except OSError:
1770 pass
1773 # texrunner state exceptions
1774 class TexRunsError(Exception): pass
1775 class TexDoneError(Exception): pass
1776 class TexNotInPreambleModeError(Exception): pass
1779 class texrunner:
1780 """TeX/LaTeX interface
1781 - runs TeX/LaTeX expressions instantly
1782 - checks TeX/LaTeX response
1783 - the instance variable texmessage stores the last TeX
1784 response as a string
1785 - the instance variable texmessageparsed stores a parsed
1786 version of texmessage; it should be empty after
1787 texmessage.check was called, otherwise a TexResultError
1788 is raised
1789 - the instance variable errordebug controls the verbose
1790 level of TexResultError"""
1792 def __init__(self, mode="tex",
1793 lfs="10pt",
1794 docclass="article",
1795 docopt=None,
1796 usefiles=None,
1797 waitfortex=5,
1798 texdebug=0,
1799 dvidebug=0,
1800 errordebug=1,
1801 dvicopy=0,
1802 pyxgraphics=1,
1803 texmessagestart=texmessage.start,
1804 texmessagedocclass=texmessage.load,
1805 texmessagebegindoc=(texmessage.load, texmessage.noaux),
1806 texmessageend=texmessage.texend,
1807 texmessagedefaultpreamble=texmessage.load,
1808 texmessagedefaultrun=texmessage.loadfd):
1809 mode = mode.lower()
1810 if mode != "tex" and mode != "latex":
1811 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1812 self.mode = mode
1813 self.lfs = lfs
1814 self.docclass = docclass
1815 self.docopt = docopt
1816 self.usefiles = helper.ensurelist(usefiles)
1817 self.waitfortex = waitfortex
1818 self.texdebug = texdebug
1819 self.dvidebug = dvidebug
1820 self.errordebug = errordebug
1821 self.dvicopy = dvicopy
1822 self.pyxgraphics = pyxgraphics
1823 texmessagestart = helper.ensuresequence(texmessagestart)
1824 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
1825 self.texmessagestart = texmessagestart
1826 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
1827 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
1828 self.texmessagedocclass = texmessagedocclass
1829 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
1830 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
1831 self.texmessagebegindoc = texmessagebegindoc
1832 texmessageend = helper.ensuresequence(texmessageend)
1833 helper.checkattr(texmessageend, allowmulti=(texmessage,))
1834 self.texmessageend = texmessageend
1835 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
1836 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
1837 self.texmessagedefaultpreamble = texmessagedefaultpreamble
1838 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
1839 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
1840 self.texmessagedefaultrun = texmessagedefaultrun
1842 self.texruns = 0
1843 self.texdone = 0
1844 self.preamblemode = 1
1845 self.executeid = 0
1846 self.page = 0
1847 self.dvinumber = 0
1848 self.dvifiles = []
1849 savetempdir = tempfile.tempdir
1850 tempfile.tempdir = os.curdir
1851 self.texfilename = os.path.basename(tempfile.mktemp())
1852 tempfile.tempdir = savetempdir
1854 def execute(self, expr, *checks):
1855 """executes expr within TeX/LaTeX
1856 - if self.texruns is not yet set, TeX/LaTeX is initialized,
1857 self.texruns is set and self.preamblemode is set
1858 - the method must not be called, when self.texdone is already set
1859 - expr should be a string or None
1860 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
1861 while self.texdone becomes set
1862 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
1863 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
1865 if not self.texruns:
1866 for usefile in self.usefiles:
1867 extpos = usefile.rfind(".")
1868 try:
1869 os.rename(usefile, self.texfilename + usefile[extpos:])
1870 except OSError:
1871 pass
1872 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
1873 texfile.write("\\relax\n")
1874 texfile.close()
1875 try:
1876 self.texinput, self.texoutput = os.popen4("%s %s" % (self.mode, self.texfilename), "t", 0)
1877 except ValueError:
1878 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
1879 self.texinput, self.texoutput = os.popen4("%s %s" % (self.mode, self.texfilename), "t")
1880 atexit.register(_cleantmp, self)
1881 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
1882 self.gotevent = threading.Event() # keeps the got inputmarker event
1883 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
1884 self.quitevent = threading.Event() # keeps for end of terminal event
1885 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
1886 self.texruns = 1
1887 oldpreamblemode = self.preamblemode
1888 self.preamblemode = 1
1889 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1890 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1891 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
1892 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1893 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1894 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1895 "\\newdimen\\PyXDimenHAlignRT%\n" +
1896 _texsettingpreamble + # insert preambles for texsetting macros
1897 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1898 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1899 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1900 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1901 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1902 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
1903 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1904 "lt=\\the\\PyXDimenHAlignLT,"
1905 "rt=\\the\\PyXDimenHAlignRT,"
1906 "ht=\\the\\ht\\PyXBox,"
1907 "dp=\\the\\dp\\PyXBox:}%\n"
1908 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1909 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1910 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1911 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}", # write PyXInputMarker to stdout
1912 *self.texmessagestart)
1913 os.remove("%s.tex" % self.texfilename)
1914 if self.mode == "tex":
1915 try:
1916 LocalLfsName = str(self.lfs) + ".lfs"
1917 lfsdef = open(LocalLfsName, "r").read()
1918 except IOError:
1919 try:
1920 try:
1921 SysLfsName = os.path.join(sys.prefix, "share", "pyx", str(self.lfs) + ".lfs")
1922 lfsdef = open(SysLfsName, "r").read()
1923 except IOError:
1924 SysLfsName = os.path.join(os.path.dirname(__file__), "lfs", str(self.lfs) + ".lfs")
1925 lfsdef = open(SysLfsName, "r").read()
1926 except IOError:
1927 allfiles = []
1928 try:
1929 allfiles += os.listdir(".")
1930 except OSError:
1931 pass
1932 try:
1933 allfiles += os.listdir(os.path.join(sys.prefix, "share", "pyx"))
1934 except OSError:
1935 pass
1936 try:
1937 allfiles += os.listdir(os.path.join(os.path.dirname(__file__), "lfs"))
1938 except OSError:
1939 pass
1940 files = map(lambda x: x[:-4], filter(lambda x: x[-4:] == ".lfs", allfiles))
1941 raise IOError("file '%s.lfs' not found. Available latex font sizes:\n%s" % (self.lfs, files))
1942 self.execute(lfsdef)
1943 self.execute("\\normalsize%\n")
1944 self.execute("\\newdimen\\linewidth%\n")
1945 elif self.mode == "latex":
1946 if self.pyxgraphics:
1947 self.execute("\\makeatletter%\n"
1948 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
1949 "\\def\\ProcessOptions{%\n"
1950 "\\saveProcessOptions%\n"
1951 "\\def\\Gin@driver{../../contrib/pyx.def}%\n"
1952 "\\def\\c@lor@namefile{dvipsnam.def}}%\n"
1953 "\\makeatother")
1954 if self.docopt is not None:
1955 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass), *self.texmessagedocclass)
1956 else:
1957 self.execute("\\documentclass{%s}" % self.docclass, *self.texmessagedocclass)
1958 self.preamblemode = oldpreamblemode
1959 self.executeid += 1
1960 if expr is not None: # TeX/LaTeX should process expr
1961 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
1962 if self.preamblemode:
1963 self.expr = ("%s%%\n" % expr +
1964 "\\PyXInput{%i}%%\n" % self.executeid)
1965 else:
1966 self.page += 1
1967 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
1968 "\\PyXInput{%i}%%\n" % self.executeid)
1969 else: # TeX/LaTeX should be finished
1970 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
1971 if self.mode == "latex":
1972 self.expr = "\\end{document}\n"
1973 else:
1974 self.expr = "\\end\n"
1975 if self.texdebug:
1976 print "pass the following expression to TeX/LaTeX:\n %s" % self.expr.replace("\n", "\n ").rstrip()
1977 self.texinput.write(self.expr)
1978 self.gotevent.wait(self.waitfortex) # wait for the expected output
1979 gotevent = self.gotevent.isSet()
1980 self.gotevent.clear()
1981 if expr is None and gotevent: # TeX/LaTeX should have finished
1982 self.texruns = 0
1983 self.texdone = 1
1984 self.texinput.close() # close the input queue and
1985 self.quitevent.wait(self.waitfortex) # wait for finish of the output
1986 gotevent = self.quitevent.isSet()
1987 try:
1988 self.texmessage = ""
1989 while 1:
1990 self.texmessage += self.gotqueue.get_nowait()
1991 except Queue.Empty:
1992 pass
1993 self.texmessageparsed = self.texmessage
1994 if gotevent:
1995 if expr is not None:
1996 texmessage.inputmarker.check(self)
1997 if not self.preamblemode:
1998 texmessage.pyxbox.check(self)
1999 texmessage.pyxpageout.check(self)
2000 for check in checks:
2001 try:
2002 check.check(self)
2003 except TexResultWarning:
2004 traceback.print_exc()
2005 texmessage.emptylines.check(self)
2006 if len(self.texmessageparsed):
2007 raise TexResultError("unhandled TeX response (might be an error)", self)
2008 else:
2009 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
2011 def getdvi(self):
2012 "finish TeX/LaTeX and read the dvifile"
2013 self.execute(None, *self.texmessageend)
2014 if self.dvicopy:
2015 os.system("dvicopy %s.dvi %s.dvicopy" % (self.texfilename, self.texfilename))
2016 dvifilename = "%s.dvicopy" % self.texfilename
2017 else:
2018 dvifilename = "%s.dvi" % self.texfilename
2019 self.dvifiles.append(DVIFile(dvifilename, debug=self.dvidebug))
2020 self.dvinumber += 1
2022 def prolog(self, dvinumber, page):
2023 "return the dvifile prolog"
2024 if not self.texdone:
2025 self.getdvi()
2026 return self.dvifiles[dvinumber].prolog(page)
2028 def write(self, file, dvinumber, page):
2029 "write a page from the dvifile"
2030 if not self.texdone:
2031 self.getdvi()
2032 return self.dvifiles[dvinumber].write(file, page)
2034 def reset(self):
2035 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
2036 if self.texruns:
2037 if not self.texdone:
2038 self.getdvi()
2039 self.preamblemode = 1
2040 self.executeid = 0
2041 self.page = 0
2042 self.texdone = 0
2044 def settex(self, mode=None,
2045 lfs=None,
2046 docclass=None,
2047 docopt=None,
2048 usefiles=None,
2049 waitfortex=None,
2050 dvicopy=None,
2051 pyxgraphics=None,
2052 texmessagestart=None,
2053 texmessagedocclass=None,
2054 texmessagebegindoc=None,
2055 texmessageend=None,
2056 texmessagedefaultpreamble=None,
2057 texmessagedefaultrun=None):
2058 """provide a set command for TeX/LaTeX settings
2059 - TeX/LaTeX must not yet been started
2060 - especially needed for the defaultrunner, where no access to
2061 the constructor is available
2062 - do not call this method directly; better use the set method below"""
2063 if self.texruns:
2064 raise TexRunsError
2065 if mode is not None:
2066 mode = mode.lower()
2067 if mode != "tex" and mode != "latex":
2068 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
2069 self.mode = mode
2070 if lfs is not None:
2071 self.lfs = lfs
2072 if docclass is not None:
2073 self.docclass = docclass
2074 if docopt is not None:
2075 self.docopt = docopt
2076 if self.usefiles is not None:
2077 self.usefiles = helper.ensurelist(usefiles)
2078 if waitfortex is not None:
2079 self.waitfortex = waitfortex
2080 if dvicopy is not None:
2081 self.dvicopy = dvicopy
2082 if dvicopy is not None:
2083 self.pyxgraphics = pyxgraphics
2084 if texmessagestart is not None:
2085 texmessagestart = helper.ensuresequence(texmessagestart)
2086 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
2087 self.texmessagestart = texmessagestart
2088 if texmessagedocclass is not None:
2089 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
2090 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
2091 self.texmessagedocclass = texmessagedocclass
2092 if texmessagebegindoc is not None:
2093 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
2094 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
2095 self.texmessagebegindoc = texmessagebegindoc
2096 if texmessageend is not None:
2097 texmessageend = helper.ensuresequence(texmessageend)
2098 helper.checkattr(texmessageend, allowmulti=(texmessage,))
2099 self.texmessageend = texmessageend
2100 if texmessagedefaultpreamble is not None:
2101 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
2102 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
2103 self.texmessagedefaultpreamble = texmessagedefaultpreamble
2104 if texmessagedefaultrun is not None:
2105 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
2106 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
2107 self.texmessagedefaultrun = texmessagedefaultrun
2109 def set(self, texdebug=None, dvidebug=None, errordebug=None, **args):
2110 """as above, but contains all settings
2111 - the debug level might be changed during TeX/LaTeX execution
2112 - dvidebug gets used only once, namely when TeX/LaTeX is being finished"""
2113 if self.texdone:
2114 raise TexDoneError
2115 if texdebug is not None:
2116 self.texdebug = texdebug
2117 if dvidebug is not None:
2118 self.dvidebug = dvidebug
2119 if errordebug is not None:
2120 self.errordebug = errordebug
2121 if len(args.keys()):
2122 self.settex(**args)
2124 def bracketcheck(self, expr):
2125 """a helper method for consistant usage of "{" and "}"
2126 - Michael Schindler claims that this is not necessary"""
2127 pass
2129 # def bracketcheck(self, expr):
2130 # """a helper method for consistant usage of "{" and "}"
2131 # - prevent to pass unbalanced expressions to TeX
2132 # - raises an appropriate ValueError"""
2133 # depth = 0
2134 # esc = 0
2135 # for c in expr:
2136 # if c == "{" and not esc:
2137 # depth = depth + 1
2138 # if c == "}" and not esc:
2139 # depth = depth - 1
2140 # if depth < 0:
2141 # raise ValueError("unmatched '}'")
2142 # if c == "\\":
2143 # esc = (esc + 1) % 2
2144 # else:
2145 # esc = 0
2146 # if depth > 0:
2147 # raise ValueError("unmatched '{'")
2149 def preamble(self, expr, *args):
2150 r"""put something into the TeX/LaTeX preamble
2151 - in LaTeX, this is done before the \begin{document}
2152 (you might use \AtBeginDocument, when you're in need for)
2153 - it is not allowed to call preamble after calling the
2154 text method for the first time (for LaTeX this is needed
2155 due to \begin{document}; in TeX it is forced for compatibility
2156 (you should be able to switch from TeX to LaTeX, if you want,
2157 without breaking something
2158 - preamble expressions must not create any dvi output
2159 - args might contain texmessage instances
2160 - a bracketcheck is performed on the expression"""
2161 if self.texdone:
2162 raise TexDoneError
2163 if not self.preamblemode:
2164 raise TexNotInPreambleModeError
2165 self.bracketcheck(expr)
2166 helper.checkattr(args, allowmulti=(texmessage,))
2167 self.execute(expr, *helper.getattrs(args, texmessage, default=self.texmessagedefaultpreamble))
2169 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:")
2171 def _text(self, x, y, expr, *args):
2172 """create text by passing expr to TeX/LaTeX
2173 - returns a textbox containing the result from running expr thru TeX/LaTeX
2174 - the box center is set to x, y
2175 - *args may contain style parameters, namely:
2176 - an halign instance
2177 - _texsetting instances
2178 - texmessage instances
2179 - trafo._trafo instances
2180 - base.PathStyle instances
2181 - a bracketcheck is performed on the expression"""
2182 if expr is None:
2183 raise ValueError("None expression is invalid")
2184 if self.texdone:
2185 raise TexDoneError
2186 if self.preamblemode:
2187 if self.mode == "latex":
2188 self.execute("\\begin{document}", *self.texmessagebegindoc)
2189 self.preamblemode = 0
2190 helper.checkattr(args, allowmulti=(_texsetting, texmessage, trafo._trafo, base.PathStyle))
2191 #XXX: should we distiguish between StrokeStyle and FillStyle?
2192 texsettings = helper.getattrs(args, _texsetting, default=[])
2193 exclusive = []
2194 for texsetting in texsettings:
2195 if texsetting.exclusive:
2196 if texsetting.id not in exclusive:
2197 exclusive.append(texsetting.id)
2198 else:
2199 raise RuntimeError("multiple occurance of exclusive texsetting with id=%i" % texsetting.id)
2200 texsettings.sort()
2201 for texsetting in texsettings:
2202 expr = texsetting.modifyexpr(expr, texsettings, self)
2203 self.bracketcheck(expr)
2204 self.execute(expr, *helper.getattrs(args, texmessage, default=self.texmessagedefaultrun))
2205 match = self.PyXBoxPattern.search(self.texmessage)
2206 if not match or int(match.group("page")) != self.page:
2207 raise TexResultError("box extents not found", self)
2208 left, right, height, depth = map(lambda x: float(x) * 72.0 / 72.27, match.group("lt", "rt", "ht", "dp"))
2209 box = _textbox(x, y, left, right, height, depth, self, self.dvinumber, self.page,
2210 *helper.getattrs(args, base.PathStyle, default=[]))
2211 for t in helper.getattrs(args, trafo._trafo, default=()):
2212 box.reltransform(t)
2213 return box
2215 def text(self, x, y, expr, *args):
2216 return self._text(unit.topt(x), unit.topt(y), expr, *args)
2219 # the module provides an default texrunner and methods for direct access
2220 defaulttexrunner = texrunner()
2221 reset = defaulttexrunner.reset
2222 set = defaulttexrunner.set
2223 preamble = defaulttexrunner.preamble
2224 text = defaulttexrunner.text
2225 _text = defaulttexrunner._text