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