text module: autoreset, texdebug, pyxgraphics, documentation
[PyX/mjg.git] / pyx / text.py
blob282e60308829392a15036cdd6aa6adcef5e8756f
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
26 import 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 tell(self):
58 return self.file.tell()
60 def read(self, bytes):
61 return self.file.read(bytes)
63 def readint(self, bytes=4, signed=0):
64 first = 1
65 result = 0
66 while bytes:
67 value = ord(self.file.read(1))
68 if first and signed and value > 127:
69 value -= 256
70 first = 0
71 result = 256 * result + value
72 bytes -= 1
73 return result
75 def readint32(self):
76 return struct.unpack(">l", self.file.read(4))[0]
78 def readuint32(self):
79 return struct.unpack(">L", self.file.read(4))[0]
81 def readint24(self):
82 # XXX: checkme
83 return struct.unpack(">l", "\0"+self.file.read(3))[0]
85 def readuint24(self):
86 # XXX: checkme
87 return struct.unpack(">L", "\0"+self.file.read(3))[0]
89 def readint16(self):
90 return struct.unpack(">h", self.file.read(2))[0]
92 def readuint16(self):
93 return struct.unpack(">H", self.file.read(2))[0]
95 def readchar(self):
96 return struct.unpack("b", self.file.read(1))[0]
98 def readuchar(self):
99 return struct.unpack("B", self.file.read(1))[0]
101 def readstring(self, bytes):
102 l = self.readuchar()
103 assert l <= bytes-1, "inconsistency in file: string too long"
104 return self.file.read(bytes-1)[:l]
107 # class tokenfile:
108 # """ ascii file containing tokens separated by spaces.
110 # Comments beginning with % are ignored. Strings containing spaces
111 # are not handled correctly
112 # """
114 # def __init__(self, filename):
115 # self.file = open(filename, "r")
116 # self.line = None
118 # def gettoken(self):
119 # """ return next token or None if EOF """
120 # while not self.line:
121 # line = self.file.readline()
122 # if line == "":
123 # return None
124 # self.line = line.split("%")[0].split()
125 # token = self.line[0]
126 # self.line = self.line[1:]
127 # return token
129 # def close(self):
130 # self.file.close()
133 ##############################################################################
134 # TFM file handling
135 ##############################################################################
137 class TFMError(exceptions.Exception): pass
140 class char_info_word:
141 def __init__(self, word):
142 self.width_index = int((word & 0xFF000000L) >> 24) #make sign-safe
143 self.height_index = (word & 0x00F00000) >> 20
144 self.depth_index = (word & 0x000F0000) >> 16
145 self.italic_index = (word & 0x0000FC00) >> 10
146 self.tag = (word & 0x00000300) >> 8
147 self.remainder = (word & 0x000000FF)
150 class TFMFile:
151 def __init__(self, name, debug=0):
152 self.file = binfile(name, "rb")
153 self.debug = debug
156 # read pre header
159 self.lf = self.file.readint16()
160 self.lh = self.file.readint16()
161 self.bc = self.file.readint16()
162 self.ec = self.file.readint16()
163 self.nw = self.file.readint16()
164 self.nh = self.file.readint16()
165 self.nd = self.file.readint16()
166 self.ni = self.file.readint16()
167 self.nl = self.file.readint16()
168 self.nk = self.file.readint16()
169 self.ne = self.file.readint16()
170 self.np = self.file.readint16()
172 if not (self.bc-1 <= self.ec <= 255 and
173 self.ne <= 256 and
174 self.lf == 6+self.lh+(self.ec-self.bc+1)+self.nw+self.nh+self.nd
175 +self.ni+self.nl+self.nk+self.ne+self.np):
176 raise TFMError, "error in TFM pre-header"
178 if debug:
179 print "lh=%d" % self.lh
182 # read header
185 self.checksum = self.file.readint32()
186 self.designsizeraw = self.file.readint32()
187 assert self.designsizeraw > 0, "invald design size"
188 self.designsize = fix_word(self.designsizeraw)
189 if self.lh > 2:
190 assert self.lh > 11, "inconsistency in TFM file: incomplete field"
191 self.charcoding = self.file.readstring(40)
192 else:
193 self.charcoding = None
195 if self.lh > 12:
196 assert self.lh > 16, "inconsistency in TFM file: incomplete field"
197 self.fontfamily = self.file.readstring(20)
198 else:
199 self.fontfamily = None
201 if self.debug:
202 print "(FAMILY %s)" % self.fontfamily
203 print "(CODINGSCHEME %s)" % self.charcoding
204 print "(DESINGSIZE R %f)" % self.designsize
206 if self.lh > 17:
207 self.sevenbitsave = self.file.readuchar()
208 # ignore the following two bytes
209 self.file.readint16()
210 facechar = self.file.readuchar()
211 # decode ugly face specification into the Knuth suggested string
212 if facechar < 18:
213 if facechar >= 12:
214 self.face = "E"
215 facechar -= 12
216 elif facechar >= 6:
217 self.face = "C"
218 facechar -= 6
219 else:
220 self.face = "R"
222 if facechar >= 4:
223 self.face = "L" + self.face
224 facechar -= 4
225 elif facechar >= 2:
226 self.face = "B" + self.face
227 facechar -= 2
228 else:
229 self.face = "M" + self.face
231 if facechar == 1:
232 self.face = self.face[0] + "I" + self.face[1]
233 else:
234 self.face = self.face[0] + "R" + self.face[1]
236 else:
237 self.face = None
238 else:
239 self.sevenbitsave = self.face = None
241 if self.lh > 18:
242 # just ignore the rest
243 print self.file.read((self.lh-18)*4)
246 # read char_info
249 self.char_info = [None]*(self.ec+1)
251 for charcode in range(self.bc, self.ec+1):
252 self.char_info[charcode] = char_info_word(self.file.readint32())
253 if self.char_info[charcode].width_index == 0:
254 # disable character if width_index is zero
255 self.char_info[charcode] = None
258 # read widths
261 self.width = [None for width_index in range(self.nw)]
262 for width_index in range(self.nw):
263 # self.width[width_index] = fix_word(self.file.readint32())
264 self.width[width_index] = self.file.readint32()
267 # read heights
270 self.height = [None for height_index in range(self.nh)]
271 for height_index in range(self.nh):
272 # self.height[height_index] = fix_word(self.file.readint32())
273 self.height[height_index] = self.file.readint32()
276 # read depths
279 self.depth = [None for depth_index in range(self.nd)]
280 for depth_index in range(self.nd):
281 # self.depth[depth_index] = fix_word(self.file.readint32())
282 self.depth[depth_index] = self.file.readint32()
285 # read italic
288 self.italic = [None for italic_index in range(self.ni)]
289 for italic_index in range(self.ni):
290 # self.italic[italic_index] = fix_word(self.file.readint32())
291 self.italic[italic_index] = self.file.readint32()
294 # read lig_kern
297 # XXX decode to lig_kern_command
299 self.lig_kern = [None for lig_kern_index in range(self.nl)]
300 for lig_kern_index in range(self.nl):
301 self.lig_kern[lig_kern_index] = self.file.readint32()
304 # read kern
307 self.kern = [None for kern_index in range(self.nk)]
308 for kern_index in range(self.nk):
309 # self.kern[kern_index] = fix_word(self.file.readint32())
310 self.kern[kern_index] = self.file.readint32()
313 # read exten
316 # XXX decode to extensible_recipe
318 self.exten = [None for exten_index in range(self.ne)]
319 for exten_index in range(self.ne):
320 self.exten[exten_index] = self.file.readint32()
323 # read param
326 # XXX decode
328 self.param = [None for param_index in range(self.np)]
329 for param_index in range(self.np):
330 self.param[param_index] = self.file.readint32()
332 self.file.file.close()
335 # class FontEncoding:
337 # def __init__(self, filename):
338 # """ font encoding contained in filename """
339 # encpath = pykpathsea.find_file(filename, pykpathsea.kpse_tex_ps_header_format)
340 # encfile = tokenfile(encpath)
342 # # name of encoding
343 # self.encname = encfile.gettoken()
344 # token = encfile.gettoken()
345 # if token != "[":
346 # raise RuntimeError("cannot parse encoding file '%s', expecting '[' got '%s'" % (filename, token))
347 # self.encvector = []
348 # for i in range(256):
349 # token = encfile.gettoken()
350 # if token is None or token=="]":
351 # raise RuntimeError("not enough charcodes in encoding file '%s'" % filename)
352 # self.encvector.append(token)
353 # if encfile.gettoken() != "]":
354 # raise RuntimeError("too many charcodes in encoding file '%s'" % filename)
355 # token = encfile.gettoken()
356 # if token != "def":
357 # raise RuntimeError("cannot parse encoding file '%s', expecting 'def' got '%s'" % (filename, token))
358 # token = encfile.gettoken()
359 # if token != None:
360 # raise RuntimeError("encoding file '%s' too long" % filename)
361 # encfile.close()
363 # def encode(self, charcode):
364 # return self.encvector[charcode]
366 ##############################################################################
367 # Font handling
368 ##############################################################################
370 _ReEncodeFont = prolog.definition("ReEncodeFont", """{
371 5 dict
372 begin
373 /newencoding exch def
374 /newfontname exch def
375 /basefontname exch def
376 /basefontdict basefontname findfont def
377 /newfontdict basefontdict maxlength dict def
378 basefontdict {
379 exch dup dup /FID ne exch /Encoding ne and
380 { exch newfontdict 3 1 roll put }
381 { pop pop }
382 ifelse
383 } forall
384 newfontdict /FontName newfontname put
385 newfontdict /Encoding newencoding put
386 newfontname newfontdict definefont pop
388 }""")
391 # PostScript font selection and output primitives
394 class _selectfont(base.PSOp):
395 def __init__(self, name, size):
396 self.name = name
397 self.size = size
399 def write(self, file):
400 file.write("/%s %f selectfont\n" % (self.name, self.size))
402 # XXX: should we provide a prolog method for the font inclusion
403 # instead of using the coarser logic in DVIFile.prolog
406 class _show(base.PSOp):
407 def __init__(self, x, y, s):
408 self.x = x
409 self.y = y
410 self.s = s
412 def write(self, file):
413 file.write("%f %f moveto (%s) show\n" % (self.x, self.y, self.s))
416 class FontMapping:
418 tokenpattern = re.compile(r'"(.*?)("\s+|"$|$)|(.*?)(\s+|$)')
420 def __init__(self, s):
421 """ construct font mapping from line s of dvips mapping file """
422 self.texname = self.basepsname = self.fontfile = None
424 # standard encoding
425 self.encodingfile = None
427 # supported postscript fragments occuring in psfonts.map
428 self.reencodefont = self.extendfont = self.slantfont = None
430 tokens = []
431 while len(s):
432 match = self.tokenpattern.match(s)
433 if match:
434 if match.groups()[0]:
435 tokens.append('"%s"' % match.groups()[0])
436 else:
437 tokens.append(match.groups()[2])
438 s = s[match.end():]
439 else:
440 raise RuntimeError("wrong syntax in font catalog file 'psfonts.map'")
442 for token in tokens:
443 if token.startswith("<"):
444 if token.startswith("<<"):
445 # XXX: support non-partial download here
446 self.fontfile = token[2:]
447 elif token.startswith("<["):
448 self.encodingfile = token[2:]
449 elif token.endswith(".pfa") or token.endswith(".pfb"):
450 self.fontfile = token[1:]
451 elif token.endswith(".enc"):
452 self.encodingfile = token[1:]
453 else:
454 raise RuntimeError("wrong syntax in font catalog file 'psfonts.map'")
455 elif token.startswith('"'):
456 pscode = token[1:-1].split()
457 # parse standard postscript code fragments
458 while pscode:
459 try:
460 arg, cmd = pscode[:2]
461 except:
462 raise RuntimeError("Unsupported Postscript fragment '%s' in psfonts.map" % pscode)
463 pscode = pscode[2:]
464 if cmd == "ReEncodeFont":
465 self.reencodefont = arg
466 elif cmd == "ExtendFont":
467 self.extendfont = arg
468 elif cmd == "SlantFont":
469 self.slantfont = arg
470 else:
471 raise RuntimeError("Unsupported Postscript fragment '%s %s' in psfonts.map" % (arg, cmd))
472 else:
473 if self.texname is None:
474 self.texname = token
475 else:
476 self.basepsname = token
477 if self.basepsname is None:
478 self.basepsname = self.texname
480 def __str__(self):
481 return ("'%s' is '%s' read from '%s' encoded as '%s'" %
482 (self.texname, self.basepsname, self.fontfile, repr(self.encodingfile)))
484 # generate fontmap
486 def readfontmap(filenames):
487 """ read font map from filename (without path) """
488 fontmap = {}
489 for filename in filenames:
490 mappath = pykpathsea.find_file(filename, pykpathsea.kpse_dvips_config_format)
491 if mappath is None:
492 raise RuntimeError("cannot find dvips font catalog '%s', aborting" % filename)
493 mapfile = open(mappath, "r")
494 for line in mapfile.readlines():
495 line = line.rstrip()
496 if not (line=="" or line[0] in (" ", "%", "*", ";" , "#")):
497 fontmapping = FontMapping(line)
498 fontmap[fontmapping.texname] = fontmapping
500 mapfile.close()
501 return fontmap
504 fontmap = readfontmap(["psfonts.map"])
507 class Font:
508 def __init__(self, name, c, q, d, tfmconv, debug=0):
509 self.name = name
510 self.tfmpath = pykpathsea.find_file("%s.tfm" % self.name, pykpathsea.kpse_tfm_format)
511 if self.tfmpath is None:
512 raise TFMError("cannot find %s.tfm" % self.name)
513 self.tfmfile = TFMFile(self.tfmpath, debug)
514 self.fontmapping = fontmap.get(name)
515 if self.fontmapping is None:
516 raise RuntimeError("no information for font '%s' found in font mapping file, aborting" % name)
517 # print "found mapping %s for font %s" % (self.fontmapping, self.name)
519 if self.tfmfile.checksum != c:
520 raise DVIError("check sums do not agree: %d vs. %d" %
521 (self.tfmfile.checksum, c))
523 self.tfmdesignsize = round(tfmconv*self.tfmfile.designsizeraw)
525 if abs(self.tfmdesignsize - d) > 2:
526 raise DVIError("design sizes do not agree: %d vs. %d" %
527 (self.tfmdesignsize, d))
528 if q < 0 or q > 134217728:
529 raise DVIError("font '%s' not loaded: bad scale" % self.name)
530 if d < 0 or d > 134217728:
531 raise DVIError("font '%s' not loaded: bad design size" % self.name)
533 self.scale = 1.0*q/d
534 self.alpha = 16;
535 self.q = self.qorig = q
536 while self.q >= 8388608:
537 self.q = self.q/2
538 self.alpha *= 2
540 self.beta = 256/self.alpha;
541 self.alpha = self.alpha*self.q;
543 # for bookkeeping of used characters
544 self.usedchars = [0] * 256
546 def __str__(self):
547 return "Font(%s, %d)" % (self.name, self.tfmdesignsize)
549 __repr__ = __str__
551 def convert(self, width):
552 # simplified version
553 return 16L*width*self.qorig/16777216L
555 # original algorithm of Knuth (at the moment not used)
556 b0 = width >> 24
557 b1 = (width >> 16) & 0xff
558 b2 = (width >> 8 ) & 0xff
559 b3 = (width ) & 0xff
561 if b0 == 0:
562 return (((((b3*self.q)/256)+(b2*self.q))/256)+(b1*self.q))/self.beta
563 elif b0 == 255:
564 return (((((b3*self.q)/256)+(b2*self.q))/256)+(b1*self.q))/self.beta-self.alpha
565 else:
566 raise TFMError("error in font size")
568 def getwidth(self, charcode):
569 return self.convert(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])
571 def getheight(self, charcode):
572 return self.convert(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])
574 def getdepth(self, charcode):
575 return self.convert(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])
577 def getitalic(self, charcode):
578 return self.convert(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])
580 def markcharused(self, charcode):
581 self.usedchars[charcode] = 1
583 def mergeusedchars(self, otherfont):
584 for i in range(len(self.usedchars)):
585 self.usedchars[i] = self.usedchars[i] or otherfont.usedchars[i]
587 def getbasepsname(self):
588 return self.fontmapping.basepsname
590 def getpsname(self):
591 if self.fontmapping.reencodefont:
592 return "%s-%s" % (self.fontmapping.basepsname, self.fontmapping.reencodefont)
593 else:
594 return self.fontmapping.basepsname
596 def getfontfile(self):
597 return self.fontmapping.fontfile
599 def getencoding(self):
600 return self.fontmapping.reencodefont
602 def getencodingfile(self):
603 return self.fontmapping.encodingfile
605 ##############################################################################
606 # DVI file handling
607 ##############################################################################
609 _DVI_CHARMIN = 0 # typeset a character and move right (range min)
610 _DVI_CHARMAX = 127 # typeset a character and move right (range max)
611 _DVI_SET1234 = 128 # typeset a character and move right
612 _DVI_SETRULE = 132 # typeset a rule and move right
613 _DVI_PUT1234 = 133 # typeset a character
614 _DVI_PUTRULE = 137 # typeset a rule
615 _DVI_NOP = 138 # no operation
616 _DVI_BOP = 139 # beginning of page
617 _DVI_EOP = 140 # ending of page
618 _DVI_PUSH = 141 # save the current positions (h, v, w, x, y, z)
619 _DVI_POP = 142 # restore positions (h, v, w, x, y, z)
620 _DVI_RIGHT1234 = 143 # move right
621 _DVI_W0 = 147 # move right by w
622 _DVI_W1234 = 148 # move right and set w
623 _DVI_X0 = 152 # move right by x
624 _DVI_X1234 = 153 # move right and set x
625 _DVI_DOWN1234 = 157 # move down
626 _DVI_Y0 = 161 # move down by y
627 _DVI_Y1234 = 162 # move down and set y
628 _DVI_Z0 = 166 # move down by z
629 _DVI_Z1234 = 167 # move down and set z
630 _DVI_FNTNUMMIN = 171 # set current font (range min)
631 _DVI_FNTNUMMAX = 234 # set current font (range max)
632 _DVI_FNT1234 = 235 # set current font
633 _DVI_SPECIAL1234 = 239 # special (dvi extention)
634 _DVI_FNTDEF1234 = 243 # define the meaning of a font number
635 _DVI_PRE = 247 # preamble
636 _DVI_POST = 248 # postamble beginning
637 _DVI_POSTPOST = 249 # postamble ending
639 _DVI_VERSION = 2 # dvi version
641 # position variable indices
642 _POS_H = 0
643 _POS_V = 1
644 _POS_W = 2
645 _POS_X = 3
646 _POS_Y = 4
647 _POS_Z = 5
649 # reader states
650 _READ_PRE = 1
651 _READ_NOPAGE = 2
652 _READ_PAGE = 3
653 _READ_POST = 4
654 _READ_POSTPOST = 5
655 _READ_DONE = 6
658 class DVIError(exceptions.Exception): pass
660 # save and restore colors
662 class _savecolor(base.PSOp):
663 def write(self, file):
664 file.write("currentcolor currentcolorspace\n")
667 class _restorecolor(base.PSOp):
668 def write(self, file):
669 file.write("setcolorspace setcolor\n")
671 class _savetrafo(base.PSOp):
672 def write(self, file):
673 file.write("matrix currentmatrix\n")
676 class _restoretrafo(base.PSOp):
677 def write(self, file):
678 file.write("setmatrix\n")
680 class DVIFile:
682 def __init__(self, filename, debug=0):
683 self.filename = filename
684 self.debug = debug
685 self.readfile()
687 # helper routines
689 def flushout(self):
690 """ flush currently active string """
691 if self.actoutstart:
692 x = unit.t_m(self.actoutstart[0] * self.conv * 0.0254 / self.resolution)
693 y = -unit.t_m(self.actoutstart[1] * self.conv * 0.0254 / self.resolution)
694 if self.debug:
695 print "[%s]" % self.actoutstring
696 self.actpage.insert(_show(unit.topt(x), unit.topt(y), self.actoutstring))
697 self.actoutstart = None
699 def putchar(self, char, inch=1):
700 if self.actoutstart is None:
701 self.actoutstart = self.pos[_POS_H], self.pos[_POS_V]
702 self.actoutstring = ""
703 if char > 32 and char < 127 and chr(char) not in "()[]<>":
704 ascii = "%s" % chr(char)
705 else:
706 ascii = "\\%03o" % char
707 self.actoutstring = self.actoutstring + ascii
708 dx = inch and self.fonts[self.activefont].getwidth(char) or 0
709 self.fonts[self.activefont].markcharused(char)
710 if self.debug:
711 print ("%d: %schar%d h:=%d+%d=%d, hh:=%d" %
712 (self.filepos,
713 inch and "set" or "put",
714 char,
715 self.pos[_POS_H], dx, self.pos[_POS_H]+dx,
717 self.pos[_POS_H] += dx
718 if not inch:
719 # XXX: correct !?
720 self.flushout()
722 def putrule(self, height, width, inch=1):
723 self.flushout()
724 x1 = unit.t_m(self.pos[_POS_H] * self.conv * 0.0254 / self.resolution)
725 y1 = -unit.t_m(self.pos[_POS_V] * self.conv * 0.0254 / self.resolution)
726 w = unit.t_m(width * self.conv * 0.0254 / self.resolution)
727 h = unit.t_m(height * self.conv * 0.0254 / self.resolution)
729 if height > 0 and width > 0:
730 if self.debug:
731 pixelw = int(width*self.conv)
732 if pixelw < width*self.conv: pixelw += 1
733 pixelh = int(height*self.conv)
734 if pixelh < height*self.conv: pixelh += 1
736 print ("%d: %srule height %d, width %d (%dx%d pixels)" %
737 (self.filepos, inch and "set" or "put", height, width, pixelh, pixelw))
738 self.actpage.fill(path.rect(x1, y1, w, h))
739 else:
740 if self.debug:
741 print ("%d: %srule height %d, width %d (invisible)" %
742 (self.filepos, inch and "set" or "put", height, width))
744 if inch:
745 if self.debug:
746 print (" h:=%d+%d=%d, hh:=%d" %
747 (self.pos[_POS_H], width, self.pos[_POS_H]+width, 0))
748 self.pos[_POS_H] += width
751 def usefont(self, fontnum):
752 self.flushout()
753 self.activefont = fontnum
755 fontpsname = self.fonts[self.activefont].getpsname()
756 fontscale = self.fonts[self.activefont].scale
757 fontdesignsize = float(self.fonts[self.activefont].tfmfile.designsize)
758 self.actpage.insert(_selectfont(fontpsname,
759 fontscale*fontdesignsize*72/72.27))
761 if self.debug:
762 print ("%d: fntnum%i current font is %s" %
763 (self.filepos,
764 self.activefont, self.fonts[fontnum].name))
766 def definefont(self, cmdnr, num, c, q, d, fontname):
767 # cmdnr: type of fontdef command (only used for debugging output)
768 # c: checksum
769 # q: scaling factor
770 # Note that q is actually s in large parts of the documentation.
771 # d: design size
773 self.fonts[num] = Font(fontname, c, q, d, self.tfmconv, self.debug > 1)
775 if self.debug:
776 print "%d: fntdef%d %i: %s" % (self.filepos, cmdnr, num, fontname)
778 # scale = round((1000.0*self.conv*q)/(self.trueconv*d))
779 # m = 1.0*q/d
780 # scalestring = scale!=1000 and " scaled %d" % scale or ""
781 # print ("Font %i: %s%s---loaded at size %d DVI units" %
782 # (num, fontname, scalestring, q))
783 # if scale!=1000:
784 # print " (this font is magnified %d%%)" % round(scale/10)
786 def special(self, s):
787 self.flushout()
788 x = unit.t_m(self.pos[_POS_H] * self.conv * 0.0254 / self.resolution)
789 y = -unit.t_m(self.pos[_POS_V] * self.conv * 0.0254 / self.resolution)
790 if self.debug:
791 print "%d: xxx '%s'" % (self.filepos, s)
792 if not s.startswith("PyX:"):
793 raise RuntimeError("the special '%s' cannot be handled by PyX, aborting" % s)
794 command, args = s[4:].split()[0], s[4:].split()[1:]
795 if command=="color_begin":
796 if args[0]=="cmyk":
797 c = color.cmyk(float(args[1]), float(args[2]), float(args[3]), float(args[4]))
798 elif args[0]=="gray":
799 c = color.gray(float(args[1]))
800 elif args[0]=="hsb":
801 c = color.hsb(float(args[1]), float(args[2]), float(args[3]))
802 elif args[0]=="rgb":
803 c = color.rgb(float(args[1]), float(args[2]), float(args[3]))
804 elif args[0]=="RGB":
805 c = color.rgb(int(args[1])/255.0, int(args[2])/255.0, int(args[3])/255.0)
806 elif args[0]=="texnamed":
807 try:
808 c = getattr(color.cmyk, args[1])
809 except AttributeError:
810 raise RuntimeError("unknown TeX color '%s', aborting" % args[1])
811 else:
812 raise RuntimeError("color model '%s' cannot be handled by PyX, aborting" % args[0])
813 self.actpage.insert(_savecolor())
814 self.actpage.insert(c)
815 elif command=="color_end":
816 self.actpage.insert(_restorecolor())
817 elif command=="rotate_begin":
818 self.actpage.insert(_savetrafo())
819 self.actpage.insert(trafo.rotate(float(args[0]), x, y))
820 elif command=="rotate_end":
821 self.actpage.insert(_restoretrafo())
822 elif command=="scale_begin":
823 self.actpage.insert(_savetrafo())
824 self.actpage.insert(trafo.scale(float(args[0]), float(args[1]), x, y))
825 elif command=="scale_end":
826 self.actpage.insert(_restoretrafo())
827 elif command=="epsinclude":
828 # XXX: we cannot include epsfile in the header because this would
829 # generate a cyclic import with the canvas and text modules
830 import epsfile
832 # parse arguments
833 argdict = {}
834 for arg in args:
835 name, value = arg.split("=")
836 argdict[name] = value
838 # construct kwargs for epsfile constructor
839 epskwargs = {}
840 epskwargs["filename"] = argdict["file"]
841 epskwargs["bbox"] = bbox._bbox(float(argdict["llx"]), float(argdict["lly"]),
842 float(argdict["urx"]), float(argdict["ury"]))
843 if argdict.has_key("width"):
844 epskwargs["width"] = unit.t_pt(float(argdict["width"]))
845 if argdict.has_key("height"):
846 epskwargs["height"] = unit.t_pt(float(argdict["height"]))
847 if argdict.has_key("clip"):
848 epskwargs["clip"] = int(argdict["clip"])
849 self.actpage.insert(epsfile.epsfile(x, y, **epskwargs))
850 else:
851 raise RuntimeError("unknown PyX special '%s', aborting" % command)
853 # routines corresponding to the different reader states of the dvi maschine
855 def _read_pre(self):
856 file = self.file
857 while 1:
858 self.filepos = file.tell()
859 cmd = file.readuchar()
860 if cmd == _DVI_NOP:
861 pass
862 elif cmd == _DVI_PRE:
863 if self.file.readuchar() != _DVI_VERSION: raise DVIError
864 num = file.readuint32()
865 den = file.readuint32()
866 mag = file.readuint32()
868 self.tfmconv = (25400000.0/num)*(den/473628672)/16.0;
869 # resolution in dpi
870 self.resolution = 300.0
871 # self.trueconv = conv in DVIType docu
872 self.trueconv = (num/254000.0)*(self.resolution/den)
873 self.conv = self.trueconv*(mag/1000.0)
875 comment = file.read(file.readuchar())
876 return _READ_NOPAGE
877 else:
878 raise DVIError
880 def _read_nopage(self):
881 file = self.file
882 while 1:
883 self.filepos = file.tell()
884 cmd = file.readuchar()
885 if cmd == _DVI_NOP:
886 pass
887 elif cmd == _DVI_BOP:
888 self.flushout()
889 if self.debug:
890 print "%d: beginning of page" % self.filepos,
891 print file.readuint32()
892 for i in range(9): file.readuint32()
893 else:
894 for i in range(10): file.readuint32()
895 file.readuint32()
896 return _READ_PAGE
897 elif cmd == _DVI_POST:
898 return _READ_DONE # we skip the rest
899 else:
900 raise DVIError
902 def _read_page(self):
903 self.pos = [0, 0, 0, 0, 0, 0]
904 self.pages.append(canvas.canvas())
905 self.actpage = self.pages[-1]
906 file = self.file
907 while 1:
908 self.filepos = file.tell()
909 cmd = file.readuchar()
910 if cmd == _DVI_NOP:
911 pass
912 if cmd >= _DVI_CHARMIN and cmd <= _DVI_CHARMAX:
913 self.putchar(cmd)
914 elif cmd >= _DVI_SET1234 and cmd < _DVI_SET1234 + 4:
915 self.putchar(file.readint(cmd - _DVI_SET1234 + 1))
916 elif cmd == _DVI_SETRULE:
917 self.putrule(file.readint32(), file.readint32())
918 elif cmd >= _DVI_PUT1234 and cmd < _DVI_PUT1234 + 4:
919 self.putchar(file.readint(cmd - _DVI_PUT1234 + 1), inch=0)
920 elif cmd == _DVI_PUTRULE:
921 self.putrule(file.readint32(), file.readint32(), 0)
922 elif cmd == _DVI_EOP:
923 self.flushout()
924 if self.debug:
925 print "%d: eop" % self.filepos
926 print
927 return _READ_NOPAGE
928 elif cmd == _DVI_PUSH:
929 self.stack.append(tuple(self.pos))
930 if self.debug:
931 print "%d: push" % self.filepos
932 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
933 (( len(self.stack)-1,)+tuple(self.pos)))
934 elif cmd == _DVI_POP:
935 self.flushout()
936 self.pos = list(self.stack[-1])
937 del self.stack[-1]
938 if self.debug:
939 print "%d: pop" % self.filepos
940 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
941 (( len(self.stack),)+tuple(self.pos)))
942 elif cmd >= _DVI_RIGHT1234 and cmd < _DVI_RIGHT1234 + 4:
943 self.flushout()
944 dh = file.readint(cmd - _DVI_RIGHT1234 + 1, 1)
945 if self.debug:
946 print ("%d: right%d %d h:=%d%+d=%d, hh:=" %
947 (self.filepos,
948 cmd - _DVI_RIGHT1234 + 1,
950 self.pos[_POS_H],
952 self.pos[_POS_H]+dh))
953 self.pos[_POS_H] += dh
954 elif cmd == _DVI_W0:
955 self.flushout()
956 if self.debug:
957 print ("%d: w0 %d h:=%d%+d=%d, hh:=" %
958 (self.filepos,
959 self.pos[_POS_W],
960 self.pos[_POS_H],
961 self.pos[_POS_W],
962 self.pos[_POS_H]+self.pos[_POS_W]))
963 self.pos[_POS_H] += self.pos[_POS_W]
964 elif cmd >= _DVI_W1234 and cmd < _DVI_W1234 + 4:
965 self.flushout()
966 self.pos[_POS_W] = file.readint(cmd - _DVI_W1234 + 1, 1)
967 if self.debug:
968 print ("%d: w%d %d h:=%d%+d=%d, hh:=" %
969 (self.filepos,
970 cmd - _DVI_W1234 + 1,
971 self.pos[_POS_W],
972 self.pos[_POS_H],
973 self.pos[_POS_W],
974 self.pos[_POS_H]+self.pos[_POS_W]))
975 self.pos[_POS_H] += self.pos[_POS_W]
976 elif cmd == _DVI_X0:
977 self.flushout()
978 self.pos[_POS_H] += self.pos[_POS_X]
979 elif cmd >= _DVI_X1234 and cmd < _DVI_X1234 + 4:
980 self.flushout()
981 self.pos[_POS_X] = file.readint(cmd - _DVI_X1234 + 1, 1)
982 self.pos[_POS_H] += self.pos[_POS_X]
983 elif cmd >= _DVI_DOWN1234 and cmd < _DVI_DOWN1234 + 4:
984 self.flushout()
985 dv = file.readint(cmd - _DVI_DOWN1234 + 1, 1)
986 if self.debug:
987 print ("%d: down%d %d v:=%d%+d=%d, vv:=" %
988 (self.filepos,
989 cmd - _DVI_DOWN1234 + 1,
991 self.pos[_POS_V],
993 self.pos[_POS_V]+dv))
994 self.pos[_POS_V] += dv
995 elif cmd == _DVI_Y0:
996 self.flushout()
997 if self.debug:
998 print ("%d: y0 %d v:=%d%+d=%d, vv:=" %
999 (self.filepos,
1000 self.pos[_POS_Y],
1001 self.pos[_POS_V],
1002 self.pos[_POS_Y],
1003 self.pos[_POS_V]+self.pos[_POS_Y]))
1004 self.pos[_POS_V] += self.pos[_POS_Y]
1005 elif cmd >= _DVI_Y1234 and cmd < _DVI_Y1234 + 4:
1006 self.flushout()
1007 self.pos[_POS_Y] = file.readint(cmd - _DVI_Y1234 + 1, 1)
1008 if self.debug:
1009 print ("%d: y%d %d v:=%d%+d=%d, vv:=" %
1010 (self.filepos,
1011 cmd - _DVI_Y1234 + 1,
1012 self.pos[_POS_Y],
1013 self.pos[_POS_V],
1014 self.pos[_POS_Y],
1015 self.pos[_POS_V]+self.pos[_POS_Y]))
1016 self.pos[_POS_V] += self.pos[_POS_Y]
1017 elif cmd == _DVI_Z0:
1018 self.flushout()
1019 self.pos[_POS_V] += self.pos[_POS_Z]
1020 elif cmd >= _DVI_Z1234 and cmd < _DVI_Z1234 + 4:
1021 self.flushout()
1022 self.pos[_POS_Z] = file.readint(cmd - _DVI_Z1234 + 1, 1)
1023 self.pos[_POS_V] += self.pos[_POS_Z]
1024 elif cmd >= _DVI_FNTNUMMIN and cmd <= _DVI_FNTNUMMAX:
1025 self.usefont(cmd - _DVI_FNTNUMMIN)
1026 elif cmd >= _DVI_FNT1234 and cmd < _DVI_FNT1234 + 4:
1027 self.usefont(file.readint(cmd - _DVI_FNT1234 + 1, 1))
1028 elif cmd >= _DVI_SPECIAL1234 and cmd < _DVI_SPECIAL1234 + 4:
1029 self.special(file.read(file.readint(cmd - _DVI_SPECIAL1234 + 1)))
1030 elif cmd >= _DVI_FNTDEF1234 and cmd < _DVI_FNTDEF1234 + 4:
1031 if cmd == _DVI_FNTDEF1234:
1032 num=file.readuchar()
1033 elif cmd == _DVI_FNTDEF1234+1:
1034 num=file.readuint16()
1035 elif cmd == _DVI_FNTDEF1234+2:
1036 num=file.readuint24()
1037 elif cmd == _DVI_FNTDEF1234+3:
1038 # Cool, here we have according to docu a signed int. Why?
1039 num = file.readint32()
1040 self.definefont(cmd-_DVI_FNTDEF1234+1,
1041 num,
1042 file.readint32(),
1043 file.readint32(),
1044 file.readint32(),
1045 file.read(file.readuchar()+file.readuchar()))
1046 else: raise DVIError
1048 def readfile(self):
1049 """ reads and parses dvi file
1051 This routine reads the dvi file and generates a list
1052 of pages in self.pages. Each page consists itself of
1053 a list of PSCommands equivalent to the content of
1054 the dvi file. Furthermore, the list of used fonts
1055 can be extracted from the array self.fonts.
1058 # XXX max number of fonts
1059 self.fonts = [None for i in range(64)]
1060 self.activefont = None
1062 self.stack = []
1064 # here goes the result, for each page one list.
1065 self.pages = []
1067 # pointer to currently active page
1068 self.actpage = None
1070 # currently active output: position and content
1071 self.actoutstart = None
1072 self.actoutstring = ""
1074 self.file = binfile(self.filename, "rb")
1076 # currently read byte in file (for debugging output)
1077 self.filepos = None
1079 # start up reading process
1080 state = _READ_PRE
1081 while state!=_READ_DONE:
1082 if state == _READ_PRE:
1083 state = self._read_pre()
1084 elif state == _READ_NOPAGE:
1085 state = self._read_nopage()
1086 elif state == _READ_PAGE:
1087 state = self._read_page()
1088 else:
1089 raise DVIError # unexpected reader state, should not happen
1090 self.flushout()
1092 def prolog(self, page): # TODO: AW inserted this page argument -> should return the prolog needed for that page only!
1093 """ return prolog corresponding to contents of dvi file """
1094 # XXX replace this by prolog method in _selectfont
1095 result = [_ReEncodeFont]
1096 for font in self.fonts:
1097 if font:
1098 result.append(prolog.fontdefinition(font.getbasepsname(),
1099 font.getfontfile(),
1100 font.getencodingfile(),
1101 font.usedchars))
1102 if font.getencoding():
1103 result.append(prolog.fontencoding(font.getencoding(), font.getencodingfile()))
1104 result.append(prolog.fontreencoding(font.getpsname(), font.getbasepsname(), font.getencoding()))
1105 result.extend(self.pages[page-1].prolog())
1106 return result
1108 def write(self, file, page):
1109 """write PostScript output for page into file"""
1110 # XXX: remove this method by return canvas to TexRunner
1111 if self.debug:
1112 print "dvifile(\"%s\").write() for page %s called" % (self.filename, page)
1113 self.pages[page-1].write(file)
1116 ###############################################################################
1117 # texmessages
1118 # - please don't get confused:
1119 # - there is a texmessage (and a texmessageparsed) attribute within the
1120 # texrunner; it contains TeX/LaTeX response from the last command execution
1121 # - instances of classes derived from the class texmessage are used to
1122 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
1123 # attribute of a texrunner instance
1124 # - the multiple usage of the name texmessage might be removed in the future
1125 # - texmessage instances should implement _Itexmessage
1126 ###############################################################################
1128 class TexResultError(Exception):
1129 """specialized texrunner exception class
1130 - it is raised by texmessage instances, when a texmessage indicates an error
1131 - it is raised by the texrunner itself, whenever there is a texmessage left
1132 after all parsing of this message (by texmessage instances)"""
1134 def __init__(self, description, texrunner):
1135 self.description = description
1136 self.texrunner = texrunner
1138 def __str__(self):
1139 """prints a detailed report about the problem
1140 - the verbose level is controlled by texrunner.errordebug"""
1141 if self.texrunner.errordebug >= 2:
1142 return ("%s\n" % self.description +
1143 "The expression passed to TeX was:\n"
1144 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1145 "The return message from TeX was:\n"
1146 " %s\n" % self.texrunner.texmessage.replace("\n", "\n ").rstrip() +
1147 "After parsing this message, the following was left:\n"
1148 " %s" % self.texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
1149 elif self.texrunner.errordebug == 1:
1150 firstlines = self.texrunner.texmessageparsed.split("\n")
1151 if len(firstlines) > 5:
1152 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
1153 return ("%s\n" % self.description +
1154 "The expression passed to TeX was:\n"
1155 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1156 "After parsing the return message from TeX, the following was left:\n" +
1157 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
1158 else:
1159 return self.description
1162 class TexResultWarning(TexResultError):
1163 """as above, but with different handling of the exception
1164 - when this exception is raised by a texmessage instance,
1165 the information just get reported and the execution continues"""
1166 pass
1169 class _Itexmessage:
1170 """validates/invalidates TeX/LaTeX response"""
1172 def check(self, texrunner):
1173 """check a Tex/LaTeX response and respond appropriate
1174 - read the texrunners texmessageparsed attribute
1175 - if there is an problem found, raise an appropriate
1176 exception (TexResultError or TexResultWarning)
1177 - remove any valid and identified TeX/LaTeX response
1178 from the texrunners texmessageparsed attribute
1179 -> finally, there should be nothing left in there,
1180 otherwise it is interpreted as an error"""
1183 class texmessage: pass
1186 class _texmessagestart(texmessage):
1187 """validates TeX/LaTeX startup"""
1189 __implements__ = _Itexmessage
1191 startpattern = re.compile(r"This is [0-9a-zA-Z\s_]*TeX")
1193 def check(self, texrunner):
1194 m = self.startpattern.search(texrunner.texmessageparsed)
1195 if not m:
1196 raise TexResultError("TeX startup failed", texrunner)
1197 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
1198 try:
1199 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
1200 except (IndexError, ValueError):
1201 raise TexResultError("TeX running startup file failed", texrunner)
1202 try:
1203 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
1204 except (IndexError, ValueError):
1205 raise TexResultError("TeX scrollmode check failed", texrunner)
1208 class _texmessagenoaux(texmessage):
1209 """allows for LaTeXs no-aux-file warning"""
1211 __implements__ = _Itexmessage
1213 def check(self, texrunner):
1214 try:
1215 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
1216 texrunner.texmessageparsed = s1 + s2
1217 except (IndexError, ValueError):
1218 try:
1219 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
1220 os.sep,
1221 texrunner.texfilename), 1)
1222 texrunner.texmessageparsed = s1 + s2
1223 except (IndexError, ValueError):
1224 pass
1227 class _texmessageinputmarker(texmessage):
1228 """validates the PyXInputMarker"""
1230 __implements__ = _Itexmessage
1232 def check(self, texrunner):
1233 try:
1234 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
1235 texrunner.texmessageparsed = s1 + s2
1236 except (IndexError, ValueError):
1237 raise TexResultError("PyXInputMarker expected", texrunner)
1240 class _texmessagepyxbox(texmessage):
1241 """validates the PyXBox output"""
1243 __implements__ = _Itexmessage
1245 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:")
1247 def check(self, texrunner):
1248 m = self.pattern.search(texrunner.texmessageparsed)
1249 if m and m.group("page") == str(texrunner.page):
1250 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1251 else:
1252 raise TexResultError("PyXBox expected", texrunner)
1255 class _texmessagepyxpageout(texmessage):
1256 """validates the dvi shipout message (writing a page to the dvi file)"""
1258 __implements__ = _Itexmessage
1260 def check(self, texrunner):
1261 try:
1262 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
1263 texrunner.texmessageparsed = s1 + s2
1264 except (IndexError, ValueError):
1265 raise TexResultError("PyXPageOutMarker expected", texrunner)
1268 class _texmessagetexend(texmessage):
1269 """validates TeX/LaTeX finish"""
1271 __implements__ = _Itexmessage
1273 def check(self, texrunner):
1274 try:
1275 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
1276 texrunner.texmessageparsed = s1 + s2
1277 except (IndexError, ValueError):
1278 try:
1279 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
1280 os.sep,
1281 texrunner.texfilename), 1)
1282 texrunner.texmessageparsed = s1 + s2
1283 except (IndexError, ValueError):
1284 pass
1285 try:
1286 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
1287 texrunner.texmessageparsed = s1 + s2
1288 except (IndexError, ValueError):
1289 pass
1290 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
1291 m = dvipattern.search(texrunner.texmessageparsed)
1292 if texrunner.page:
1293 if not m:
1294 raise TexResultError("TeX dvifile messages expected", texrunner)
1295 if m.group("page") != str(texrunner.page):
1296 raise TexResultError("wrong number of pages reported", texrunner)
1297 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1298 else:
1299 try:
1300 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
1301 texrunner.texmessageparsed = s1 + s2
1302 except (IndexError, ValueError):
1303 raise TexResultError("no dvifile expected")
1304 try:
1305 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
1306 texrunner.texmessageparsed = s1 + s2
1307 except (IndexError, ValueError):
1308 raise TexResultError("TeX logfile message expected")
1311 class _texmessageemptylines(texmessage):
1312 """validates empty and "*-only" (TeX/LaTeX input marker in interactive mode) lines"""
1314 __implements__ = _Itexmessage
1316 pattern = re.compile(r"^\*?\n", re.M)
1318 def check(self, texrunner):
1319 m = self.pattern.search(texrunner.texmessageparsed)
1320 while m:
1321 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1322 m = self.pattern.search(texrunner.texmessageparsed)
1325 class _texmessageload(texmessage):
1326 """validates inclusion of arbitrary files
1327 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
1328 <fielname> is a readable file and other stuff can be anything
1329 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
1330 - this is not always wanted, but we just assume that file inclusion is fine"""
1332 __implements__ = _Itexmessage
1334 pattern = re.compile(r"\((?P<filename>[^()\s\n]+)[^()]*\)")
1336 def baselevels(self, s, maxlevel=1, brackets="()"):
1337 """strip parts of a string above a given bracket level
1338 - return a modified (some parts might be removed) version of the string s
1339 where all parts inside brackets with level higher than maxlevel are
1340 removed
1341 - if brackets do not match (number of left and right brackets is wrong
1342 or at some points there were more right brackets than left brackets)
1343 just return the unmodified string"""
1344 level = 0
1345 highestlevel = 0
1346 res = ""
1347 for c in s:
1348 if c == brackets[0]:
1349 level += 1
1350 if level > highestlevel:
1351 highestlevel = level
1352 if level <= maxlevel:
1353 res += c
1354 if c == brackets[1]:
1355 level -= 1
1356 if level == 0 and highestlevel > 0:
1357 return res
1359 def check(self, texrunner):
1360 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
1361 if lowestbracketlevel is not None:
1362 m = self.pattern.search(lowestbracketlevel)
1363 while m:
1364 if os.access(m.group("filename"), os.R_OK):
1365 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
1366 else:
1367 break
1368 m = self.pattern.search(lowestbracketlevel)
1369 else:
1370 texrunner.texmessageparsed = lowestbracketlevel
1373 class _texmessageloadfd(_texmessageload):
1374 """validates the inclusion of font description files (fd-files)
1375 - works like _texmessageload
1376 - filename must end with .fd and no further text is allowed"""
1378 pattern = re.compile(r"\((?P<filename>[^)]+.fd)\)")
1381 class _texmessagegraphicsload(_texmessageload):
1382 """validates the inclusion of files as the graphics packages writes it
1383 - works like _texmessageload, but using "<" and ">" as delimiters
1384 - filename must end with .eps and no further text is allowed"""
1386 pattern = re.compile(r"<(?P<filename>[^>]+.eps)>")
1388 def baselevels(self, s, brackets="<>", **args):
1389 return _texmessageload.baselevels(self, s, brackets=brackets, **args)
1392 #class _texmessagepdfmapload(_texmessageload):
1393 # """validates the inclusion of files as the graphics packages writes it
1394 # - works like _texmessageload, but using "{" and "}" as delimiters
1395 # - filename must end with .map and no further text is allowed"""
1397 # pattern = re.compile(r"{(?P<filename>[^}]+.map)}")
1399 # def baselevels(self, s, brackets="{}", **args):
1400 # return _texmessageload.baselevels(self, s, brackets=brackets, **args)
1403 class _texmessageignore(_texmessageload):
1404 """validates any TeX/LaTeX response
1405 - this might be used, when the expression is ok, but no suitable texmessage
1406 parser is available
1407 - PLEASE: - consider writing suitable tex message parsers
1408 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
1410 __implements__ = _Itexmessage
1412 def check(self, texrunner):
1413 texrunner.texmessageparsed = ""
1416 texmessage.start = _texmessagestart()
1417 texmessage.noaux = _texmessagenoaux()
1418 texmessage.inputmarker = _texmessageinputmarker()
1419 texmessage.pyxbox = _texmessagepyxbox()
1420 texmessage.pyxpageout = _texmessagepyxpageout()
1421 texmessage.texend = _texmessagetexend()
1422 texmessage.emptylines = _texmessageemptylines()
1423 texmessage.load = _texmessageload()
1424 texmessage.loadfd = _texmessageloadfd()
1425 texmessage.graphicsload = _texmessagegraphicsload()
1426 texmessage.ignore = _texmessageignore()
1429 ###############################################################################
1430 # texsettings
1431 # - texsettings are used to modify a TeX/LaTeX expression
1432 # to fit the users need
1433 # - texsettings have an order attribute (id), because the order is usually
1434 # important (e.g. you can not change the fontsize in mathmode in LaTeX)
1435 # - lower id's get applied later (are more outside -> mathmode has a higher
1436 # id than fontsize)
1437 # - order attributes are used to exclude complementary settings (with the
1438 # same id)
1439 # - texsettings might (in rare cases) depend on each other (e.g. parbox and
1440 # valign)
1441 ###############################################################################
1443 class _Itexsetting:
1444 """tex setting
1445 - modifies a TeX/LaTeX expression"""
1447 id = 0
1448 """order attribute for TeX settings
1449 - higher id's will be applied first (most inside)"""
1451 exclusive = 0
1452 """marks exclusive effect of the setting
1453 - when set, settings with this id exclude each other
1454 - when unset, settings with this id do not exclude each other"""
1456 def modifyexpr(self, expr, texsettings, texrunner):
1457 """modifies the TeX/LaTeX expression
1458 - expr is the original expression
1459 - the return value is the modified expression
1460 - texsettings contains a list of all texsettings (in case a tex setting
1461 depends on another texsetting)
1462 - texrunner contains the texrunner in case the texsetting depends
1463 on it"""
1465 def __cmp__(self, other):
1466 """compare texsetting with other
1467 - other is a texsetting as well
1468 - performs an id comparison (NOTE: higher id's come first!!!)"""
1471 # preamble settings for texsetting macros
1472 _texsettingpreamble = ""
1474 class _texsetting:
1476 exclusive = 1
1478 def __cmp__(self, other):
1479 return -cmp(self.id, other.id) # note the sign!!!
1482 class halign(_texsetting):
1483 """horizontal alignment
1484 the left/right splitting is performed within the PyXBox routine"""
1486 __implements__ = _Itexsetting
1488 id = 1000
1490 def __init__(self, hratio):
1491 self.hratio = hratio
1493 def modifyexpr(self, expr, texsettings, texrunner):
1494 return r"\gdef\PyXHAlign{%.5f}%s" % (self.hratio, expr)
1496 halign.left = halign(0)
1497 halign.center = halign(0.5)
1498 halign.right = halign(1)
1501 _texsettingpreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
1503 class valign(_texsetting):
1504 "vertical alignment"
1506 id = 7000
1509 class _valigntop(valign):
1511 __implements__ = _Itexsetting
1513 def modifyexpr(self, expr, texsettings, texrunner):
1514 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1517 class _valignmiddle(valign):
1519 __implements__ = _Itexsetting
1521 def modifyexpr(self, expr, texsettings, texrunner):
1522 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
1525 class _valignbottom(valign):
1527 __implements__ = _Itexsetting
1529 def modifyexpr(self, expr, texsettings, texrunner):
1530 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1533 class _valignbaseline(valign):
1535 __implements__ = _Itexsetting
1537 def modifyexpr(self, expr, texsettings, texrunner):
1538 for texsetting in texsettings:
1539 if isinstance(texsetting, parbox):
1540 raise RuntimeError("valign.baseline: specify top/middle/bottom baseline for parbox")
1541 return expr
1544 class _valignxxxbaseline(valign):
1546 def modifyexpr(self, expr, texsettings, texrunner):
1547 for texsetting in texsettings:
1548 if isinstance(texsetting, parbox):
1549 break
1550 else:
1551 raise RuntimeError(self.noparboxmessage)
1552 return expr
1555 class _valigntopbaseline(_valignxxxbaseline):
1557 __implements__ = _Itexsetting
1559 noparboxmessage = "valign.topbaseline: no parbox defined"
1562 class _valignmiddlebaseline(_valignxxxbaseline):
1564 __implements__ = _Itexsetting
1566 noparboxmessage = "valign.middlebaseline: no parbox defined"
1569 class _valignbottombaseline(_valignxxxbaseline):
1571 __implements__ = _Itexsetting
1573 noparboxmessage = "valign.bottombaseline: no parbox defined"
1576 valign.top = _valigntop()
1577 valign.middle = _valignmiddle()
1578 valign.center = valign.middle
1579 valign.bottom = _valignbottom()
1580 valign.baseline = _valignbaseline()
1581 valign.topbaseline = _valigntopbaseline()
1582 valign.middlebaseline = _valignmiddlebaseline()
1583 valign.centerbaseline = valign.middlebaseline
1584 valign.bottombaseline = _valignbottombaseline()
1587 _texsettingpreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
1590 class _parbox(_texsetting):
1591 "goes into the vertical mode"
1593 __implements__ = _Itexsetting
1595 id = 7100
1597 def __init__(self, width):
1598 self.width = width
1600 def modifyexpr(self, expr, texsettings, texrunner):
1601 boxkind = "vtop"
1602 for texsetting in texsettings:
1603 if isinstance(texsetting, valign):
1604 if (not isinstance(texsetting, _valigntop) and
1605 not isinstance(texsetting, _valignmiddle) and
1606 not isinstance(texsetting, _valignbottom) and
1607 not isinstance(texsetting, _valigntopbaseline)):
1608 if isinstance(texsetting, _valignmiddlebaseline):
1609 boxkind = "vcenter"
1610 elif isinstance(texsetting, _valignbottombaseline):
1611 boxkind = "vbox"
1612 else:
1613 raise RuntimeError("parbox couldn'd identify the valign instance")
1614 if boxkind == "vcenter":
1615 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)
1616 else:
1617 return r"\linewidth%.5ftruept\%s{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, boxkind, expr)
1620 class parbox(_parbox):
1622 def __init__(self, width):
1623 _parbox.__init__(self, unit.topt(width))
1626 class vshift(_texsetting):
1628 exclusive = 0
1630 id = 5000
1633 class _vshiftchar(vshift):
1634 "vertical down shift by a fraction of a character height"
1636 def __init__(self, lowerratio, heightstr="0"):
1637 self.lowerratio = lowerratio
1638 self.heightstr = heightstr
1640 def modifyexpr(self, expr, texsettings, texrunner):
1641 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
1644 class _vshiftmathaxis(vshift):
1645 "vertical down shift by the height of the math axis"
1647 def modifyexpr(self, expr, texsettings, texrunner):
1648 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
1651 vshift.char = _vshiftchar
1652 vshift.bottomzero = vshift.char(0)
1653 vshift.middlezero = vshift.char(0.5)
1654 vshift.centerzero = vshift.middlezero
1655 vshift.topzero = vshift.char(1)
1656 vshift.mathaxis = _vshiftmathaxis()
1659 class _mathmode(_texsetting):
1660 "math mode"
1662 __implements__ = _Itexsetting
1664 id = 9000
1666 def modifyexpr(self, expr, texsettings, texrunner):
1667 return r"$\displaystyle{%s}$" % expr
1669 mathmode = _mathmode()
1672 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
1674 class size(_texsetting):
1675 "font size"
1677 __implements__ = _Itexsetting
1679 id = 3000
1681 def __init__(self, expr, sizelist=defaultsizelist):
1682 if helper.isinteger(expr):
1683 if expr >= 0 and expr < sizelist.index(None):
1684 self.size = sizelist[expr]
1685 elif expr < 0 and expr + len(sizelist) > sizelist.index(None):
1686 self.size = sizelist[expr]
1687 else:
1688 raise IndexError("index out of sizelist range")
1689 else:
1690 self.size = expr
1692 def modifyexpr(self, expr, texsettings, texrunner):
1693 return r"\%s{%s}" % (self.size, expr)
1695 for s in defaultsizelist:
1696 if s is not None:
1697 size.__dict__[s] = size(s)
1700 ###############################################################################
1701 # texrunner
1702 ###############################################################################
1705 class _readpipe(threading.Thread):
1706 """threaded reader of TeX/LaTeX output
1707 - sets an event, when a specific string in the programs output is found
1708 - sets an event, when the terminal ends"""
1710 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
1711 """initialize the reader
1712 - pipe: file to be read from
1713 - expectqueue: keeps the next InputMarker to be wait for
1714 - gotevent: the "got InputMarker" event
1715 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
1716 - quitevent: the "end of terminal" event"""
1717 threading.Thread.__init__(self)
1718 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
1719 self.pipe = pipe
1720 self.expectqueue = expectqueue
1721 self.gotevent = gotevent
1722 self.gotqueue = gotqueue
1723 self.quitevent = quitevent
1724 self.expect = None
1725 self.start()
1727 def run(self):
1728 """thread routine"""
1729 read = self.pipe.readline() # read, what comes in
1730 try:
1731 self.expect = self.expectqueue.get_nowait() # read, what should be expected
1732 except Queue.Empty:
1733 pass
1734 while len(read):
1735 # universal EOL handling (convert everything into unix like EOLs)
1736 read.replace("\r", "")
1737 if not len(read) or read[-1] != "\n":
1738 read += "\n"
1739 self.gotqueue.put(read) # report, whats readed
1740 if self.expect is not None and read.find(self.expect) != -1:
1741 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
1742 read = self.pipe.readline() # read again
1743 try:
1744 self.expect = self.expectqueue.get_nowait()
1745 except Queue.Empty:
1746 pass
1747 # EOF reached
1748 self.pipe.close()
1749 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
1750 raise RuntimeError("TeX/LaTeX finished unexpectedly")
1751 self.quitevent.set()
1755 class _textbox(box._rect, base.PSCmd):
1756 """basically a box.rect, but it contains a text created by the texrunner
1757 - texrunner._text and texrunner.text return such an object
1758 - _textbox instances can be inserted into a canvas
1759 - the output is contained in a page of the dvifile available thru the texrunner"""
1761 def __init__(self, x, y, left, right, height, depth, texrunner, dvinumber, page, *styles):
1762 self.texttrafo = trafo._translate(x, y)
1763 box._rect.__init__(self, x - left, y - depth,
1764 left + right, depth + height,
1765 abscenter = (left, depth))
1766 self.texrunner = texrunner
1767 self.dvinumber = dvinumber
1768 self.page = page
1769 self.styles = styles
1771 def transform(self, *trafos):
1772 box._rect.transform(self, *trafos)
1773 for trafo in trafos:
1774 self.texttrafo = trafo * self.texttrafo
1776 def prolog(self):
1777 result = []
1778 for cmd in self.styles:
1779 result.extend(cmd.prolog())
1780 return result + self.texrunner.prolog(self.dvinumber, self.page)
1782 def write(self, file):
1783 canvas._gsave().write(file) # XXX: canvas?, constructor call needed?
1784 self.texttrafo.write(file)
1785 for style in self.styles:
1786 style.write(file)
1787 self.texrunner.write(file, self.dvinumber, self.page)
1788 canvas._grestore().write(file)
1792 class textbox(_textbox):
1794 def __init__(self, x, y, left, right, height, depth, texrunner, dvinumber, page, *styles):
1795 _textbox.__init__(self, unit.topt(x), unit.topt(y), unit.topt(left), unit.topt(right),
1796 unit.topt(height), unit.topt(depth), texrunner, dvinumber, page, *styles)
1799 def _cleantmp(texrunner):
1800 """get rid of temporary files
1801 - function to be registered by atexit
1802 - files contained in usefiles are kept"""
1803 if texrunner.texruns: # cleanup while TeX is still running?
1804 texrunner.texruns = 0
1805 texrunner.texdone = 1
1806 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
1807 texrunner.texinput.close() # close the input queue and
1808 texrunner.quitevent.wait(texrunner.waitfortex) # wait for finish of the output
1809 if not texrunner.quitevent.isSet(): return # didn't got a quit from TeX -> we can't do much more
1810 for usefile in texrunner.usefiles:
1811 extpos = usefile.rfind(".")
1812 try:
1813 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
1814 except OSError:
1815 pass
1816 for file in glob.glob("%s.*" % texrunner.texfilename):
1817 try:
1818 os.unlink(file)
1819 except OSError:
1820 pass
1823 # texrunner state exceptions
1824 class TexRunsError(Exception): pass
1825 class TexDoneError(Exception): pass
1826 class TexNotInPreambleModeError(Exception): pass
1829 class texrunner:
1830 """TeX/LaTeX interface
1831 - runs TeX/LaTeX expressions instantly
1832 - checks TeX/LaTeX response
1833 - the instance variable texmessage stores the last TeX
1834 response as a string
1835 - the instance variable texmessageparsed stores a parsed
1836 version of texmessage; it should be empty after
1837 texmessage.check was called, otherwise a TexResultError
1838 is raised
1839 - the instance variable errordebug controls the verbose
1840 level of TexResultError"""
1842 def __init__(self, mode="tex",
1843 lfs="10pt",
1844 docclass="article",
1845 docopt=None,
1846 usefiles=None,
1847 waitfortex=5,
1848 texdebug=None,
1849 dvidebug=0,
1850 errordebug=1,
1851 dvicopy=0,
1852 pyxgraphics=1,
1853 texmessagestart=texmessage.start,
1854 texmessagedocclass=texmessage.load,
1855 texmessagebegindoc=(texmessage.load, texmessage.noaux),
1856 texmessageend=texmessage.texend,
1857 texmessagedefaultpreamble=texmessage.load,
1858 texmessagedefaultrun=texmessage.loadfd):
1859 mode = mode.lower()
1860 if mode != "tex" and mode != "latex":
1861 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1862 self.mode = mode
1863 self.lfs = lfs
1864 self.docclass = docclass
1865 self.docopt = docopt
1866 self.usefiles = helper.ensurelist(usefiles)
1867 self.waitfortex = waitfortex
1868 if texdebug is not None:
1869 if texdebug[-4:] == ".tex":
1870 self.texdebug = open(texdebug, "w")
1871 else:
1872 self.texdebug = open("%s.tex" % texdebug, "w")
1873 else:
1874 self.texdebug = None
1875 self.dvidebug = dvidebug
1876 self.errordebug = errordebug
1877 self.dvicopy = dvicopy
1878 self.pyxgraphics = pyxgraphics
1879 texmessagestart = helper.ensuresequence(texmessagestart)
1880 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
1881 self.texmessagestart = texmessagestart
1882 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
1883 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
1884 self.texmessagedocclass = texmessagedocclass
1885 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
1886 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
1887 self.texmessagebegindoc = texmessagebegindoc
1888 texmessageend = helper.ensuresequence(texmessageend)
1889 helper.checkattr(texmessageend, allowmulti=(texmessage,))
1890 self.texmessageend = texmessageend
1891 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
1892 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
1893 self.texmessagedefaultpreamble = texmessagedefaultpreamble
1894 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
1895 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
1896 self.texmessagedefaultrun = texmessagedefaultrun
1898 self.texruns = 0
1899 self.texdone = 0
1900 self.preamblemode = 1
1901 self.executeid = 0
1902 self.page = 0
1903 self.dvinumber = 0
1904 self.dvifiles = []
1905 self.preambles = []
1906 savetempdir = tempfile.tempdir
1907 tempfile.tempdir = os.curdir
1908 self.texfilename = os.path.basename(tempfile.mktemp())
1909 tempfile.tempdir = savetempdir
1911 def execute(self, expr, *checks):
1912 """executes expr within TeX/LaTeX
1913 - if self.texruns is not yet set, TeX/LaTeX is initialized,
1914 self.texruns is set and self.preamblemode is set
1915 - the method must not be called, when self.texdone is already set
1916 - expr should be a string or None
1917 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
1918 while self.texdone becomes set
1919 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
1920 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
1922 if not self.texruns:
1923 if self.texdebug is not None:
1924 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
1925 self.texdebug.write("%% mode: %s\n" % self.mode)
1926 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
1927 for usefile in self.usefiles:
1928 extpos = usefile.rfind(".")
1929 try:
1930 os.rename(usefile, self.texfilename + usefile[extpos:])
1931 except OSError:
1932 pass
1933 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
1934 texfile.write("\\relax\n")
1935 texfile.close()
1936 try:
1937 self.texinput, self.texoutput = os.popen4("%s %s" % (self.mode, self.texfilename), "t", 0)
1938 except ValueError:
1939 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
1940 self.texinput, self.texoutput = os.popen4("%s %s" % (self.mode, self.texfilename), "t")
1941 atexit.register(_cleantmp, self)
1942 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
1943 self.gotevent = threading.Event() # keeps the got inputmarker event
1944 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
1945 self.quitevent = threading.Event() # keeps for end of terminal event
1946 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
1947 self.texruns = 1
1948 oldpreamblemode = self.preamblemode
1949 self.preamblemode = 1
1950 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1951 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1952 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
1953 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1954 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1955 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1956 "\\newdimen\\PyXDimenHAlignRT%\n" +
1957 _texsettingpreamble + # insert preambles for texsetting macros
1958 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1959 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1960 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1961 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1962 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1963 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
1964 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1965 "lt=\\the\\PyXDimenHAlignLT,"
1966 "rt=\\the\\PyXDimenHAlignRT,"
1967 "ht=\\the\\ht\\PyXBox,"
1968 "dp=\\the\\dp\\PyXBox:}%\n"
1969 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1970 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1971 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1972 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}", # write PyXInputMarker to stdout
1973 *self.texmessagestart)
1974 os.remove("%s.tex" % self.texfilename)
1975 if self.mode == "tex":
1976 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
1977 lfsname = self.lfs
1978 else:
1979 lfsname = "%s.lfs" % self.lfs
1980 for fulllfsname in [lfsname,
1981 os.path.join(sys.prefix, "share", "pyx", lfsname),
1982 os.path.join(os.path.dirname(__file__), "lfs", lfsname)]:
1983 try:
1984 lfsdef = open(fulllfsname, "r").read()
1985 break
1986 except IOError:
1987 pass
1988 else:
1989 allfiles = (glob.glob("*.lfs") +
1990 glob.glob(os.path.join(sys.prefix, "share", "pyx", "*.lfs")) +
1991 glob.glob(os.path.join(os.path.dirname(__file__), "lfs", "*.lfs")))
1992 lfsnames = [os.path.basename(x)[:-4] for x in allfiles]
1993 lfsnames.sort()
1994 raise IOError("file '%s' not found. Available latex font sizes: %s" % (lfsname, lfsnames))
1995 self.execute(lfsdef)
1996 self.execute("\\normalsize%\n")
1997 self.execute("\\newdimen\\linewidth%\n")
1998 elif self.mode == "latex":
1999 if self.pyxgraphics:
2000 for pyxdef in ["pyx.def",
2001 os.path.join(sys.prefix, "share", "pyx", "pyx.def"),
2002 os.path.join(os.path.dirname(__file__), "..", "contrib", "pyx.def")]:
2003 if os.path.isfile(pyxdef):
2004 break
2005 else:
2006 IOError("could not find 'pyx.def'")
2007 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
2008 self.execute("\\makeatletter%\n"
2009 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
2010 "\\def\\ProcessOptions{%\n"
2011 "\\saveProcessOptions%\n"
2012 "\\def\\Gin@driver{" + pyxdef + "}%\n"
2013 "\\def\\c@lor@namefile{dvipsnam.def}}%\n"
2014 "\\makeatother")
2015 if self.docopt is not None:
2016 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass), *self.texmessagedocclass)
2017 else:
2018 self.execute("\\documentclass{%s}" % self.docclass, *self.texmessagedocclass)
2019 self.preamblemode = oldpreamblemode
2020 self.executeid += 1
2021 if expr is not None: # TeX/LaTeX should process expr
2022 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
2023 if self.preamblemode:
2024 self.expr = ("%s%%\n" % expr +
2025 "\\PyXInput{%i}%%\n" % self.executeid)
2026 else:
2027 self.page += 1
2028 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
2029 "\\PyXInput{%i}%%\n" % self.executeid)
2030 else: # TeX/LaTeX should be finished
2031 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
2032 if self.mode == "latex":
2033 self.expr = "\\end{document}\n"
2034 else:
2035 self.expr = "\\end\n"
2036 if self.texdebug is not None:
2037 self.texdebug.write(self.expr)
2038 self.texinput.write(self.expr)
2039 self.gotevent.wait(self.waitfortex) # wait for the expected output
2040 gotevent = self.gotevent.isSet()
2041 self.gotevent.clear()
2042 if expr is None and gotevent: # TeX/LaTeX should have finished
2043 self.texruns = 0
2044 self.texdone = 1
2045 self.texinput.close() # close the input queue and
2046 self.quitevent.wait(self.waitfortex) # wait for finish of the output
2047 gotevent = self.quitevent.isSet()
2048 try:
2049 self.texmessage = ""
2050 while 1:
2051 self.texmessage += self.gotqueue.get_nowait()
2052 except Queue.Empty:
2053 pass
2054 self.texmessageparsed = self.texmessage
2055 if gotevent:
2056 if expr is not None:
2057 texmessage.inputmarker.check(self)
2058 if not self.preamblemode:
2059 texmessage.pyxbox.check(self)
2060 texmessage.pyxpageout.check(self)
2061 for check in checks:
2062 try:
2063 check.check(self)
2064 except TexResultWarning:
2065 traceback.print_exc()
2066 texmessage.emptylines.check(self)
2067 if len(self.texmessageparsed):
2068 raise TexResultError("unhandled TeX response (might be an error)", self)
2069 else:
2070 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
2072 def getdvi(self):
2073 "finish TeX/LaTeX and read the dvifile"
2074 self.execute(None, *self.texmessageend)
2075 if self.dvicopy:
2076 os.system("dvicopy %s.dvi %s.dvicopy" % (self.texfilename, self.texfilename))
2077 dvifilename = "%s.dvicopy" % self.texfilename
2078 else:
2079 dvifilename = "%s.dvi" % self.texfilename
2080 self.dvifiles.append(DVIFile(dvifilename, debug=self.dvidebug))
2081 self.dvinumber += 1
2083 def prolog(self, dvinumber, page):
2084 "return the dvifile prolog"
2085 if not self.texdone:
2086 self.getdvi()
2087 return self.dvifiles[dvinumber].prolog(page)
2089 def write(self, file, dvinumber, page):
2090 "write a page from the dvifile"
2091 if not self.texdone:
2092 self.getdvi()
2093 return self.dvifiles[dvinumber].write(file, page)
2095 def reset(self, reinit=0):
2096 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
2097 if self.texruns:
2098 if not self.texdone:
2099 self.getdvi()
2100 self.preamblemode = 1
2101 self.executeid = 0
2102 self.page = 0
2103 self.texdone = 0
2104 if self.reinit:
2105 for expr, args in self.preambles:
2106 self.execute(expr, *args)
2107 else:
2108 self.preambles = []
2110 def set(self, mode=None,
2111 lfs=None,
2112 docclass=None,
2113 docopt=None,
2114 usefiles=None,
2115 waitfortex=None,
2116 texdebug=None,
2117 dvidebug=0,
2118 errordebug=None,
2119 dvicopy=None,
2120 pyxgraphics=None,
2121 texmessagestart=None,
2122 texmessagedocclass=None,
2123 texmessagebegindoc=None,
2124 texmessageend=None,
2125 texmessagedefaultpreamble=None,
2126 texmessagedefaultrun=None):
2127 """provide a set command for TeX/LaTeX settings
2128 - TeX/LaTeX must not yet been started
2129 - especially needed for the defaultrunner, where no access to
2130 the constructor is available"""
2131 if self.texruns:
2132 raise TexRunsError
2133 if mode is not None:
2134 mode = mode.lower()
2135 if mode != "tex" and mode != "latex":
2136 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
2137 self.mode = mode
2138 if lfs is not None:
2139 self.lfs = lfs
2140 if docclass is not None:
2141 self.docclass = docclass
2142 if docopt is not None:
2143 self.docopt = docopt
2144 if self.usefiles is not None:
2145 self.usefiles = helper.ensurelist(usefiles)
2146 if waitfortex is not None:
2147 self.waitfortex = waitfortex
2148 if texdebug is not None:
2149 if texdebug[-4:] == ".tex":
2150 self.texdebug = open(texdebug, "w")
2151 else:
2152 self.texdebug = open("%s.tex" % texdebug, "w")
2153 if dvidebug is not None:
2154 self.dvidebug = dvidebug
2155 if errordebug is not None:
2156 self.errordebug = errordebug
2157 if dvicopy is not None:
2158 self.dvicopy = dvicopy
2159 if pyxgraphics is not None:
2160 self.pyxgraphics = pyxgraphics
2161 if errordebug is not None:
2162 self.errordebug = errordebug
2163 if texmessagestart is not None:
2164 texmessagestart = helper.ensuresequence(texmessagestart)
2165 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
2166 self.texmessagestart = texmessagestart
2167 if texmessagedocclass is not None:
2168 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
2169 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
2170 self.texmessagedocclass = texmessagedocclass
2171 if texmessagebegindoc is not None:
2172 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
2173 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
2174 self.texmessagebegindoc = texmessagebegindoc
2175 if texmessageend is not None:
2176 texmessageend = helper.ensuresequence(texmessageend)
2177 helper.checkattr(texmessageend, allowmulti=(texmessage,))
2178 self.texmessageend = texmessageend
2179 if texmessagedefaultpreamble is not None:
2180 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
2181 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
2182 self.texmessagedefaultpreamble = texmessagedefaultpreamble
2183 if texmessagedefaultrun is not None:
2184 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
2185 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
2186 self.texmessagedefaultrun = texmessagedefaultrun
2188 def bracketcheck(self, expr):
2189 """a helper method to check the usage of "{" and "}"
2190 - Michael Schindler claims that this is not necessary"""
2191 pass
2193 # def bracketcheck(self, expr):
2194 # """a helper method for consistant usage of "{" and "}"
2195 # - prevent to pass unbalanced expressions to TeX
2196 # - raises an appropriate ValueError"""
2197 # depth = 0
2198 # esc = 0
2199 # for c in expr:
2200 # if c == "{" and not esc:
2201 # depth = depth + 1
2202 # if c == "}" and not esc:
2203 # depth = depth - 1
2204 # if depth < 0:
2205 # raise ValueError("unmatched '}'")
2206 # if c == "\\":
2207 # esc = (esc + 1) % 2
2208 # else:
2209 # esc = 0
2210 # if depth > 0:
2211 # raise ValueError("unmatched '{'")
2213 def preamble(self, expr, *args):
2214 r"""put something into the TeX/LaTeX preamble
2215 - in LaTeX, this is done before the \begin{document}
2216 (you might use \AtBeginDocument, when you're in need for)
2217 - it is not allowed to call preamble after calling the
2218 text method for the first time (for LaTeX this is needed
2219 due to \begin{document}; in TeX it is forced for compatibility
2220 (you should be able to switch from TeX to LaTeX, if you want,
2221 without breaking something
2222 - preamble expressions must not create any dvi output
2223 - args might contain texmessage instances
2224 - a bracketcheck is performed on the expression"""
2225 if self.texdone or not self.preamblemode:
2226 raise TexNotInPreambleModeError
2227 self.bracketcheck(expr)
2228 helper.checkattr(args, allowmulti=(texmessage,))
2229 args = helper.getattrs(args, texmessage, default=self.texmessagedefaultpreamble)
2230 self.execute(expr, *args)
2231 self.preambles.append((expr, args))
2233 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:")
2235 def _text(self, x, y, expr, *args):
2236 """create text by passing expr to TeX/LaTeX
2237 - returns a textbox containing the result from running expr thru TeX/LaTeX
2238 - the box center is set to x, y
2239 - *args may contain style parameters, namely:
2240 - an halign instance
2241 - _texsetting instances
2242 - texmessage instances
2243 - trafo._trafo instances
2244 - base.PathStyle instances
2245 - a bracketcheck is performed on the expression"""
2246 if expr is None:
2247 raise ValueError("None expression is invalid")
2248 if self.texdone:
2249 if self.texdebug is not None:
2250 self.texdebug.write("%s\n" % reduce(lambda x, y: "%" + x, range(80), ""))
2251 self.texdebug.write("%% a new instance of %s is started\n" % self.mode)
2252 self.reset(reinit=1)
2253 if self.preamblemode:
2254 if self.mode == "latex":
2255 self.execute("\\begin{document}", *self.texmessagebegindoc)
2256 self.preamblemode = 0
2257 helper.checkattr(args, allowmulti=(_texsetting, texmessage, trafo._trafo, base.PathStyle))
2258 #XXX: should we distiguish between StrokeStyle and FillStyle?
2259 texsettings = helper.getattrs(args, _texsetting, default=[])
2260 exclusive = []
2261 for texsetting in texsettings:
2262 if texsetting.exclusive:
2263 if texsetting.id not in exclusive:
2264 exclusive.append(texsetting.id)
2265 else:
2266 raise RuntimeError("multiple occurance of exclusive texsetting with id=%i" % texsetting.id)
2267 texsettings.sort()
2268 for texsetting in texsettings:
2269 expr = texsetting.modifyexpr(expr, texsettings, self)
2270 self.bracketcheck(expr)
2271 self.execute(expr, *helper.getattrs(args, texmessage, default=self.texmessagedefaultrun))
2272 match = self.PyXBoxPattern.search(self.texmessage)
2273 if not match or int(match.group("page")) != self.page:
2274 raise TexResultError("box extents not found", self)
2275 left, right, height, depth = map(lambda x: float(x) * 72.0 / 72.27, match.group("lt", "rt", "ht", "dp"))
2276 box = _textbox(x, y, left, right, height, depth, self, self.dvinumber, self.page,
2277 *helper.getattrs(args, base.PathStyle, default=[]))
2278 for t in helper.getattrs(args, trafo._trafo, default=()):
2279 box.reltransform(t)
2280 return box
2282 def text(self, x, y, expr, *args):
2283 return self._text(unit.topt(x), unit.topt(y), expr, *args)
2286 # the module provides an default texrunner and methods for direct access
2287 defaulttexrunner = texrunner()
2288 reset = defaulttexrunner.reset
2289 set = defaulttexrunner.set
2290 preamble = defaulttexrunner.preamble
2291 text = defaulttexrunner.text
2292 _text = defaulttexrunner._text