removed some old code
[PyX/mjg.git] / pyx / text.py
blob89f529f0d6889d871f7f939fa00e731b0a3eeeb9
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 copy, cStringIO, exceptions, glob, os, threading, Queue, traceback, re, struct, string, tempfile, sys, atexit, time
26 import config, helper, unit, bbox, box, base, canvas, color, trafo, path, prolog, pykpathsea, version, attr, style
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 close(self):
58 self.file.close()
60 def tell(self):
61 return self.file.tell()
63 def eof(self):
64 return self.file.eof()
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]
112 class stringbinfile(binfile):
114 def __init__(self, s):
115 self.file = cStringIO.StringIO(s)
118 # class tokenfile:
119 # """ ascii file containing tokens separated by spaces.
121 # Comments beginning with % are ignored. Strings containing spaces
122 # are not handled correctly
123 # """
125 # def __init__(self, filename):
126 # self.file = open(filename, "r")
127 # self.line = None
129 # def gettoken(self):
130 # """ return next token or None if EOF """
131 # while not self.line:
132 # line = self.file.readline()
133 # if line == "":
134 # return None
135 # self.line = line.split("%")[0].split()
136 # token = self.line[0]
137 # self.line = self.line[1:]
138 # return token
140 # def close(self):
141 # self.file.close()
144 ##############################################################################
145 # TFM file handling
146 ##############################################################################
148 class TFMError(exceptions.Exception): pass
151 class char_info_word:
152 def __init__(self, word):
153 self.width_index = int((word & 0xFF000000L) >> 24) #make sign-safe
154 self.height_index = (word & 0x00F00000) >> 20
155 self.depth_index = (word & 0x000F0000) >> 16
156 self.italic_index = (word & 0x0000FC00) >> 10
157 self.tag = (word & 0x00000300) >> 8
158 self.remainder = (word & 0x000000FF)
161 class tfmfile:
162 def __init__(self, name, debug=0):
163 self.file = binfile(name, "rb")
164 self.debug = debug
167 # read pre header
170 self.lf = self.file.readint16()
171 self.lh = self.file.readint16()
172 self.bc = self.file.readint16()
173 self.ec = self.file.readint16()
174 self.nw = self.file.readint16()
175 self.nh = self.file.readint16()
176 self.nd = self.file.readint16()
177 self.ni = self.file.readint16()
178 self.nl = self.file.readint16()
179 self.nk = self.file.readint16()
180 self.ne = self.file.readint16()
181 self.np = self.file.readint16()
183 if not (self.bc-1 <= self.ec <= 255 and
184 self.ne <= 256 and
185 self.lf == 6+self.lh+(self.ec-self.bc+1)+self.nw+self.nh+self.nd
186 +self.ni+self.nl+self.nk+self.ne+self.np):
187 raise TFMError, "error in TFM pre-header"
189 if debug:
190 print "lh=%d" % self.lh
193 # read header
196 self.checksum = self.file.readint32()
197 self.designsizeraw = self.file.readint32()
198 assert self.designsizeraw > 0, "invald design size"
199 self.designsize = fix_word(self.designsizeraw)
200 if self.lh > 2:
201 assert self.lh > 11, "inconsistency in TFM file: incomplete field"
202 self.charcoding = self.file.readstring(40)
203 else:
204 self.charcoding = None
206 if self.lh > 12:
207 assert self.lh > 16, "inconsistency in TFM file: incomplete field"
208 self.fontfamily = self.file.readstring(20)
209 else:
210 self.fontfamily = None
212 if self.debug:
213 print "(FAMILY %s)" % self.fontfamily
214 print "(CODINGSCHEME %s)" % self.charcoding
215 print "(DESINGSIZE R %f)" % self.designsize
217 if self.lh > 17:
218 self.sevenbitsave = self.file.readuchar()
219 # ignore the following two bytes
220 self.file.readint16()
221 facechar = self.file.readuchar()
222 # decode ugly face specification into the Knuth suggested string
223 if facechar < 18:
224 if facechar >= 12:
225 self.face = "E"
226 facechar -= 12
227 elif facechar >= 6:
228 self.face = "C"
229 facechar -= 6
230 else:
231 self.face = "R"
233 if facechar >= 4:
234 self.face = "L" + self.face
235 facechar -= 4
236 elif facechar >= 2:
237 self.face = "B" + self.face
238 facechar -= 2
239 else:
240 self.face = "M" + self.face
242 if facechar == 1:
243 self.face = self.face[0] + "I" + self.face[1]
244 else:
245 self.face = self.face[0] + "R" + self.face[1]
247 else:
248 self.face = None
249 else:
250 self.sevenbitsave = self.face = None
252 if self.lh > 18:
253 # just ignore the rest
254 print self.file.read((self.lh-18)*4)
257 # read char_info
260 self.char_info = [None]*(self.ec+1)
262 for charcode in range(self.bc, self.ec+1):
263 self.char_info[charcode] = char_info_word(self.file.readint32())
264 if self.char_info[charcode].width_index == 0:
265 # disable character if width_index is zero
266 self.char_info[charcode] = None
269 # read widths
272 self.width = [None for width_index in range(self.nw)]
273 for width_index in range(self.nw):
274 # self.width[width_index] = fix_word(self.file.readint32())
275 self.width[width_index] = self.file.readint32()
278 # read heights
281 self.height = [None for height_index in range(self.nh)]
282 for height_index in range(self.nh):
283 # self.height[height_index] = fix_word(self.file.readint32())
284 self.height[height_index] = self.file.readint32()
287 # read depths
290 self.depth = [None for depth_index in range(self.nd)]
291 for depth_index in range(self.nd):
292 # self.depth[depth_index] = fix_word(self.file.readint32())
293 self.depth[depth_index] = self.file.readint32()
296 # read italic
299 self.italic = [None for italic_index in range(self.ni)]
300 for italic_index in range(self.ni):
301 # self.italic[italic_index] = fix_word(self.file.readint32())
302 self.italic[italic_index] = self.file.readint32()
305 # read lig_kern
308 # XXX decode to lig_kern_command
310 self.lig_kern = [None for lig_kern_index in range(self.nl)]
311 for lig_kern_index in range(self.nl):
312 self.lig_kern[lig_kern_index] = self.file.readint32()
315 # read kern
318 self.kern = [None for kern_index in range(self.nk)]
319 for kern_index in range(self.nk):
320 # self.kern[kern_index] = fix_word(self.file.readint32())
321 self.kern[kern_index] = self.file.readint32()
324 # read exten
327 # XXX decode to extensible_recipe
329 self.exten = [None for exten_index in range(self.ne)]
330 for exten_index in range(self.ne):
331 self.exten[exten_index] = self.file.readint32()
334 # read param
337 # XXX decode
339 self.param = [None for param_index in range(self.np)]
340 for param_index in range(self.np):
341 self.param[param_index] = self.file.readint32()
343 self.file.close()
346 # class FontEncoding:
348 # def __init__(self, filename):
349 # """ font encoding contained in filename """
350 # encpath = pykpathsea.find_file(filename, pykpathsea.kpse_tex_ps_header_format)
351 # encfile = tokenfile(encpath)
353 # # name of encoding
354 # self.encname = encfile.gettoken()
355 # token = encfile.gettoken()
356 # if token != "[":
357 # raise RuntimeError("cannot parse encoding file '%s', expecting '[' got '%s'" % (filename, token))
358 # self.encvector = []
359 # for i in range(256):
360 # token = encfile.gettoken()
361 # if token is None or token=="]":
362 # raise RuntimeError("not enough charcodes in encoding file '%s'" % filename)
363 # self.encvector.append(token)
364 # if encfile.gettoken() != "]":
365 # raise RuntimeError("too many charcodes in encoding file '%s'" % filename)
366 # token = encfile.gettoken()
367 # if token != "def":
368 # raise RuntimeError("cannot parse encoding file '%s', expecting 'def' got '%s'" % (filename, token))
369 # token = encfile.gettoken()
370 # if token != None:
371 # raise RuntimeError("encoding file '%s' too long" % filename)
372 # encfile.close()
374 # def encode(self, charcode):
375 # return self.encvector[charcode]
377 ##############################################################################
378 # Font handling
379 ##############################################################################
381 _ReEncodeFont = prolog.definition("ReEncodeFont", """{
382 5 dict
383 begin
384 /newencoding exch def
385 /newfontname exch def
386 /basefontname exch def
387 /basefontdict basefontname findfont def
388 /newfontdict basefontdict maxlength dict def
389 basefontdict {
390 exch dup dup /FID ne exch /Encoding ne and
391 { exch newfontdict 3 1 roll put }
392 { pop pop }
393 ifelse
394 } forall
395 newfontdict /FontName newfontname put
396 newfontdict /Encoding newencoding put
397 newfontname newfontdict definefont pop
399 }""")
402 # PostScript font selection and output primitives
405 class _selectfont(base.PSOp):
406 def __init__(self, name, size):
407 self.name = name
408 self.size = size
410 def write(self, file):
411 file.write("/%s %f selectfont\n" % (self.name, self.size))
414 class selectfont(_selectfont):
415 def __init__(self, font):
416 _selectfont.__init__(self, font.getpsname(), font.getsize())
417 self.font = font
419 def prolog(self):
420 result = [prolog.fontdefinition(self.font.getbasepsname(),
421 self.font.getfontfile(),
422 self.font.getencodingfile(),
423 self.font.usedchars)]
424 if self.font.getencoding():
425 result.append(_ReEncodeFont)
426 result.append(prolog.fontencoding(self.font.getencoding(), self.font.getencodingfile()))
427 result.append(prolog.fontreencoding(self.font.getpsname(), self.font.getbasepsname(), self.font.getencoding()))
428 return result
431 class _show(base.PSOp):
432 def __init__(self, x, y, s):
433 self.x = x
434 self.y = y
435 self.s = s
437 def write(self, file):
438 file.write("%f %f moveto (%s) show\n" % (self.x, self.y, self.s))
441 class fontmapping:
443 tokenpattern = re.compile(r'"(.*?)("\s+|"$|$)|(.*?)(\s+|$)')
445 def __init__(self, s):
446 """ construct font mapping from line s of font mapping file """
447 self.texname = self.basepsname = self.fontfile = None
449 # standard encoding
450 self.encodingfile = None
452 # supported postscript fragments occuring in psfonts.map
453 self.reencodefont = self.extendfont = self.slantfont = None
455 tokens = []
456 while len(s):
457 match = self.tokenpattern.match(s)
458 if match:
459 if match.groups()[0]:
460 tokens.append('"%s"' % match.groups()[0])
461 else:
462 tokens.append(match.groups()[2])
463 s = s[match.end():]
464 else:
465 raise RuntimeError("wrong syntax")
467 for token in tokens:
468 if token.startswith("<"):
469 if token.startswith("<<"):
470 # XXX: support non-partial download here
471 self.fontfile = token[2:]
472 elif token.startswith("<["):
473 self.encodingfile = token[2:]
474 elif token.endswith(".pfa") or token.endswith(".pfb"):
475 self.fontfile = token[1:]
476 elif token.endswith(".enc"):
477 self.encodingfile = token[1:]
478 else:
479 raise RuntimeError("wrong syntax")
480 elif token.startswith('"'):
481 pscode = token[1:-1].split()
482 # parse standard postscript code fragments
483 while pscode:
484 try:
485 arg, cmd = pscode[:2]
486 except:
487 raise RuntimeError("Unsupported Postscript fragment '%s'" % pscode)
488 pscode = pscode[2:]
489 if cmd == "ReEncodeFont":
490 self.reencodefont = arg
491 elif cmd == "ExtendFont":
492 self.extendfont = arg
493 elif cmd == "SlantFont":
494 self.slantfont = arg
495 else:
496 raise RuntimeError("Unsupported Postscript fragment '%s %s'" % (arg, cmd))
497 else:
498 if self.texname is None:
499 self.texname = token
500 else:
501 self.basepsname = token
502 if self.basepsname is None:
503 self.basepsname = self.texname
505 def __str__(self):
506 return ("'%s' is '%s' read from '%s' encoded as '%s'" %
507 (self.texname, self.basepsname, self.fontfile, repr(self.encodingfile)))
509 # generate fontmap
511 def readfontmap(filenames):
512 """ read font map from filename (without path) """
513 fontmap = {}
514 for filename in filenames:
515 mappath = pykpathsea.find_file(filename, pykpathsea.kpse_dvips_config_format)
516 if not mappath:
517 raise RuntimeError("cannot find font mapping file '%s'" % filename)
518 mapfile = open(mappath, "r")
519 lineno = 0
520 for line in mapfile.readlines():
521 lineno += 1
522 line = line.rstrip()
523 if not (line=="" or line[0] in (" ", "%", "*", ";" , "#")):
524 try:
525 fm = fontmapping(line)
526 except RuntimeError, e:
527 sys.stderr.write("*** PyX Warning: Ignoring line %i in mapping file '%s': %s\n" % (lineno, filename, e))
528 else:
529 fontmap[fm.texname] = fm
530 mapfile.close()
531 return fontmap
534 class font:
535 def __init__(self, name, c, q, d, debug=0):
536 self.name = name
537 self.q = q # desired size of font (fix_word) in tex points
538 self.d = d # design size of font (fix_word) in tex points
539 tfmpath = pykpathsea.find_file("%s.tfm" % self.name, pykpathsea.kpse_tfm_format)
540 if not tfmpath:
541 raise TFMError("cannot find %s.tfm" % self.name)
542 self.tfmfile = tfmfile(tfmpath, debug)
544 # We only check for equality of font checksums if none of them
545 # is zero. The case c == 0 happend in some VF files and
546 # according to the VFtoVP documentation, paragraph 40, a check
547 # is only performed if tfmfile.checksum > 0. Anyhow, begin
548 # more generous here seems to be reasonable
549 if self.tfmfile.checksum != c and self.tfmfile.checksum != 0 and c !=0:
550 raise DVIError("check sums do not agree: %d vs. %d" %
551 (self.tfmfile.checksum, c))
553 # tfmfile.designsizeraw is the design size of the font as a fix_word
554 if abs(self.tfmfile.designsizeraw - d) > 2:
555 raise DVIError("design sizes do not agree: %d vs. %d" % (tfmdesignsize, d))
556 if q < 0 or q > 134217728:
557 raise DVIError("font '%s' not loaded: bad scale" % self.name)
558 if d < 0 or d > 134217728:
559 raise DVIError("font '%s' not loaded: bad design size" % self.name)
561 self.scale = 1.0*q/d
563 # for bookkeeping of used characters
564 self.usedchars = [0] * 256
566 def __str__(self):
567 return "font %s designed at %g tex pts used at %g tex pts" % (self.name,
568 16.0*self.d/16777216L,
569 16.0*self.q/16777216L)
572 __repr__ = __str__
574 def getsize(self):
575 """ return size of font in (PS) points """
576 # The factor 16L/16777216L=2**(-20) converts a fix_word (here self.q)
577 # to the corresponding float. Furthermore, we have to convert from TeX
578 # points to points, hence the factor 72/72.27.
579 return 16L*self.q/16777216L*72/72.27
581 def _convert(self, width):
582 return 16L*width*self.q/16777216L
584 def getwidth(self, charcode):
585 return self._convert(self.tfmfile.width[self.tfmfile.char_info[charcode].width_index])
587 def getheight(self, charcode):
588 return self._convert(self.tfmfile.height[self.tfmfile.char_info[charcode].height_index])
590 def getdepth(self, charcode):
591 return self._convert(self.tfmfile.depth[self.tfmfile.char_info[charcode].depth_index])
593 def getitalic(self, charcode):
594 return self._convert(self.tfmfile.italic[self.tfmfile.char_info[charcode].italic_index])
596 def markcharused(self, charcode):
597 self.usedchars[charcode] = 1
599 def mergeusedchars(self, otherfont):
600 for i in range(len(self.usedchars)):
601 self.usedchars[i] = self.usedchars[i] or otherfont.usedchars[i]
603 def clearusedchars(self):
604 self.usedchars = [0] * 256
607 class type1font(font):
608 def __init__(self, name, c, q, d, fontmap, debug=0):
609 font.__init__(self, name, c, q, d, debug)
610 self.fontmapping = fontmap.get(name)
611 if self.fontmapping is None:
612 raise RuntimeError("no information for font '%s' found in font mapping file, aborting" % name)
614 def getbasepsname(self):
615 return self.fontmapping.basepsname
617 def getpsname(self):
618 if self.fontmapping.reencodefont:
619 return "%s-%s" % (self.fontmapping.basepsname, self.fontmapping.reencodefont)
620 else:
621 return self.fontmapping.basepsname
623 def getfontfile(self):
624 return self.fontmapping.fontfile
626 def getencoding(self):
627 return self.fontmapping.reencodefont
629 def getencodingfile(self):
630 return self.fontmapping.encodingfile
633 class virtualfont(font):
634 def __init__(self, name, c, q, d, fontmap, debug=0):
635 font.__init__(self, name, c, q, d, debug)
636 fontpath = pykpathsea.find_file(name, pykpathsea.kpse_vf_format)
637 if fontpath is None or not len(fontpath):
638 raise RuntimeError
639 self.vffile = vffile(fontpath, self.scale, fontmap, debug > 1)
641 def getfonts(self):
642 """ return fonts used in virtual font itself """
643 return self.vffile.getfonts()
645 def getchar(self, cc):
646 """ return dvi chunk corresponding to char code cc """
647 return self.vffile.getchar(cc)
650 ##############################################################################
651 # DVI file handling
652 ##############################################################################
654 _DVI_CHARMIN = 0 # typeset a character and move right (range min)
655 _DVI_CHARMAX = 127 # typeset a character and move right (range max)
656 _DVI_SET1234 = 128 # typeset a character and move right
657 _DVI_SETRULE = 132 # typeset a rule and move right
658 _DVI_PUT1234 = 133 # typeset a character
659 _DVI_PUTRULE = 137 # typeset a rule
660 _DVI_NOP = 138 # no operation
661 _DVI_BOP = 139 # beginning of page
662 _DVI_EOP = 140 # ending of page
663 _DVI_PUSH = 141 # save the current positions (h, v, w, x, y, z)
664 _DVI_POP = 142 # restore positions (h, v, w, x, y, z)
665 _DVI_RIGHT1234 = 143 # move right
666 _DVI_W0 = 147 # move right by w
667 _DVI_W1234 = 148 # move right and set w
668 _DVI_X0 = 152 # move right by x
669 _DVI_X1234 = 153 # move right and set x
670 _DVI_DOWN1234 = 157 # move down
671 _DVI_Y0 = 161 # move down by y
672 _DVI_Y1234 = 162 # move down and set y
673 _DVI_Z0 = 166 # move down by z
674 _DVI_Z1234 = 167 # move down and set z
675 _DVI_FNTNUMMIN = 171 # set current font (range min)
676 _DVI_FNTNUMMAX = 234 # set current font (range max)
677 _DVI_FNT1234 = 235 # set current font
678 _DVI_SPECIAL1234 = 239 # special (dvi extention)
679 _DVI_FNTDEF1234 = 243 # define the meaning of a font number
680 _DVI_PRE = 247 # preamble
681 _DVI_POST = 248 # postamble beginning
682 _DVI_POSTPOST = 249 # postamble ending
684 _DVI_VERSION = 2 # dvi version
686 # position variable indices
687 _POS_H = 0
688 _POS_V = 1
689 _POS_W = 2
690 _POS_X = 3
691 _POS_Y = 4
692 _POS_Z = 5
694 # reader states
695 _READ_PRE = 1
696 _READ_NOPAGE = 2
697 _READ_PAGE = 3
698 _READ_POST = 4 # XXX not used
699 _READ_POSTPOST = 5 # XXX not used
700 _READ_DONE = 6
703 class DVIError(exceptions.Exception): pass
705 # save and restore colors
707 class _savecolor(base.PSOp):
708 def write(self, file):
709 file.write("currentcolor currentcolorspace\n")
712 class _restorecolor(base.PSOp):
713 def write(self, file):
714 file.write("setcolorspace setcolor\n")
716 class _savetrafo(base.PSOp):
717 def write(self, file):
718 file.write("matrix currentmatrix\n")
721 class _restoretrafo(base.PSOp):
722 def write(self, file):
723 file.write("setmatrix\n")
726 class dvifile:
728 def __init__(self, filename, fontmap, debug=0):
729 """ opens the dvi file and reads the preamble """
730 self.filename = filename
731 self.fontmap = fontmap
732 self.debug = debug
734 self.fonts = {}
735 self.activefont = None
737 # stack of fonts and fontscale currently used (used for VFs)
738 self.fontstack = []
739 self.stack = []
741 # pointer to currently active page
742 self.actpage = None
744 # currently active output: position, content and type 1 font
745 self.actoutstart = None
746 self.actoutstring = ""
747 self.actoutfont = None
749 # stack for self.file, self.fonts and self.stack, needed for VF inclusion
750 self.statestack = []
752 self.file = binfile(self.filename, "rb")
754 # currently read byte in file (for debugging output)
755 self.filepos = None
757 self._read_pre()
759 # helper routines
761 def flushout(self):
762 """ flush currently active string """
763 if self.actoutstart:
764 x = self.actoutstart[0] * self.conv
765 y = -self.actoutstart[1] * self.conv
766 if self.debug:
767 print "[%s]" % self.actoutstring
768 self.actpage.insert(_show(x, y, self.actoutstring))
769 self.actoutstart = None
771 def putrule(self, height, width, inch=1):
772 self.flushout()
773 x1 = self.pos[_POS_H] * self.conv
774 y1 = -self.pos[_POS_V] * self.conv
775 w = width * self.conv
776 h = height * self.conv
778 if height > 0 and width > 0:
779 if self.debug:
780 pixelw = int(width*self.trueconv*self.mag/1000.0)
781 if pixelw < width*self.conv: pixelw += 1
782 pixelh = int(height*self.trueconv*self.mag/1000.0)
783 if pixelh < height*self.conv: pixelh += 1
785 print ("%d: %srule height %d, width %d (%dx%d pixels)" %
786 (self.filepos, inch and "set" or "put", height, width, pixelh, pixelw))
787 self.actpage.fill(path._rect(x1, y1, w, h))
788 else:
789 if self.debug:
790 print ("%d: %srule height %d, width %d (invisible)" %
791 (self.filepos, inch and "set" or "put", height, width))
793 if inch:
794 if self.debug:
795 print (" h:=%d+%d=%d, hh:=%d" %
796 (self.pos[_POS_H], width, self.pos[_POS_H]+width, 0))
797 self.pos[_POS_H] += width
799 def putchar(self, char, inch=1):
800 dx = inch and int(round(self.activefont.getwidth(char)*self.tfmconv)) or 0
802 if self.debug:
803 print ("%d: %schar%d h:=%d+%d=%d, hh:=%d" %
804 (self.filepos,
805 inch and "set" or "put",
806 char,
807 self.pos[_POS_H], dx, self.pos[_POS_H]+dx,
810 if isinstance(self.activefont, type1font):
811 if self.actoutstart is None:
812 self.actoutstart = self.pos[_POS_H], self.pos[_POS_V]
813 self.actoutstring = ""
814 if char > 32 and char < 127 and chr(char) not in "()[]<>":
815 ascii = "%s" % chr(char)
816 else:
817 ascii = "\\%03o" % char
818 self.actoutstring = self.actoutstring + ascii
820 self.activefont.markcharused(char)
821 self.pos[_POS_H] += dx
822 else:
823 # virtual font handling
824 afterpos = list(self.pos)
825 afterpos[_POS_H] += dx
826 self._push_dvistring(self.activefont.getchar(char), self.activefont.getfonts(), afterpos,
827 self.activefont.getsize())
830 if not inch:
831 # XXX: correct !?
832 self.flushout()
834 def usefont(self, fontnum):
835 if self.debug:
836 print ("%d: fntnum%i current font is %s" %
837 (self.filepos,
838 fontnum, self.fonts[fontnum].name))
840 self.activefont = self.fonts[fontnum]
842 # if the new font is a type 1 font and if it is not already the
843 # font being currently used for the output, we have to flush the
844 # output and insert a selectfont statement in the PS code
845 if isinstance(self.activefont, type1font) and self.actoutfont!=self.activefont:
846 self.flushout()
847 self.actpage.insert(selectfont(self.activefont))
848 self.actoutfont = self.activefont
850 def definefont(self, cmdnr, num, c, q, d, fontname):
851 # cmdnr: type of fontdef command (only used for debugging output)
852 # c: checksum
853 # q: scaling factor (fix_word)
854 # Note that q is actually s in large parts of the documentation.
855 # d: design size (fix_word)
857 try:
858 font = virtualfont(fontname, c, q/self.tfmconv, d/self.tfmconv, self.fontmap, self.debug > 1)
859 except (TypeError, RuntimeError):
860 font = type1font(fontname, c, q/self.tfmconv, d/self.tfmconv, self.fontmap, self.debug > 1)
862 self.fonts[num] = font
864 if self.debug:
865 print "%d: fntdef%d %i: %s" % (self.filepos, cmdnr, num, fontname)
867 # scale = round((1000.0*self.conv*q)/(self.trueconv*d))
868 # m = 1.0*q/d
869 # scalestring = scale!=1000 and " scaled %d" % scale or ""
870 # print ("Font %i: %s%s---loaded at size %d DVI units" %
871 # (num, fontname, scalestring, q))
872 # if scale!=1000:
873 # print " (this font is magnified %d%%)" % round(scale/10)
875 def special(self, s):
876 self.flushout()
878 # it is in general not safe to continue using the currently active font because
879 # the specials may involve some gsave/grestore operations
880 # XXX: reset actoutfont only where strictly needed
881 self.actoutfont = None
883 x = self.pos[_POS_H] * self.conv
884 y = -self.pos[_POS_V] * self.conv
885 if self.debug:
886 print "%d: xxx '%s'" % (self.filepos, s)
887 if not s.startswith("PyX:"):
888 if s.startswith("Warning:"):
889 sys.stderr.write("*** PyX Warning: ignoring special '%s'\n" % s)
890 return
891 else:
892 raise RuntimeError("the special '%s' cannot be handled by PyX, aborting" % s)
893 command, args = s[4:].split()[0], s[4:].split()[1:]
894 if command=="color_begin":
895 if args[0]=="cmyk":
896 c = color.cmyk(float(args[1]), float(args[2]), float(args[3]), float(args[4]))
897 elif args[0]=="gray":
898 c = color.gray(float(args[1]))
899 elif args[0]=="hsb":
900 c = color.hsb(float(args[1]), float(args[2]), float(args[3]))
901 elif args[0]=="rgb":
902 c = color.rgb(float(args[1]), float(args[2]), float(args[3]))
903 elif args[0]=="RGB":
904 c = color.rgb(int(args[1])/255.0, int(args[2])/255.0, int(args[3])/255.0)
905 elif args[0]=="texnamed":
906 try:
907 c = getattr(color.cmyk, args[1])
908 except AttributeError:
909 raise RuntimeError("unknown TeX color '%s', aborting" % args[1])
910 else:
911 raise RuntimeError("color model '%s' cannot be handled by PyX, aborting" % args[0])
912 self.actpage.insert(_savecolor())
913 self.actpage.insert(c)
914 elif command=="color_end":
915 self.actpage.insert(_restorecolor())
916 elif command=="rotate_begin":
917 self.actpage.insert(_savetrafo())
918 self.actpage.insert(trafo._rotate(float(args[0]), x, y))
919 elif command=="rotate_end":
920 self.actpage.insert(_restoretrafo())
921 elif command=="scale_begin":
922 self.actpage.insert(_savetrafo())
923 self.actpage.insert(trafo._scale(float(args[0]), float(args[1]), x, y))
924 elif command=="scale_end":
925 self.actpage.insert(_restoretrafo())
926 elif command=="epsinclude":
927 # XXX: we cannot include epsfile in the header because this would
928 # generate a cyclic import with the canvas and text modules
929 import epsfile
931 # parse arguments
932 argdict = {}
933 for arg in args:
934 name, value = arg.split("=")
935 argdict[name] = value
937 # construct kwargs for epsfile constructor
938 epskwargs = {}
939 epskwargs["filename"] = argdict["file"]
940 epskwargs["bbox"] = bbox._bbox(float(argdict["llx"]), float(argdict["lly"]),
941 float(argdict["urx"]), float(argdict["ury"]))
942 if argdict.has_key("width"):
943 epskwargs["width"] = unit.t_pt(float(argdict["width"]))
944 if argdict.has_key("height"):
945 epskwargs["height"] = unit.t_pt(float(argdict["height"]))
946 if argdict.has_key("clip"):
947 epskwargs["clip"] = int(argdict["clip"])
948 self.actpage.insert(epsfile.epsfile(unit.t_pt(x), unit.t_pt(y), **epskwargs))
949 elif command=="marker":
950 if len(args) != 1:
951 raise RuntimeError("marker contains spaces")
952 for c in args[0]:
953 if c not in string.digits + string.letters + "@":
954 raise RuntimeError("marker contains invalid characters")
955 if self.actpage.markers.has_key(args[0]):
956 raise RuntimeError("marker name occurred several times")
957 self.actpage.markers[args[0]] = unit.t_pt(x), unit.t_pt(y)
958 else:
959 raise RuntimeError("unknown PyX special '%s', aborting" % command)
961 # routines for pushing and popping different dvi chunks on the reader
963 def _push_dvistring(self, dvi, fonts, afterpos, fontsize):
964 """ push dvi string with defined fonts on top of reader
965 stack. Every positions gets scaled relatively by the factor
966 scale. When finished with interpreting the dvi chunk, we
967 continue with self.pos=afterpos. The designsize of the virtual
968 font is passed as a fix_word
972 if self.debug:
973 print "executing new dvi chunk"
974 self.statestack.append((self.file, self.fonts, self.activefont, afterpos, self.stack, self.conv, self.tfmconv))
976 # units in vf files are relative to the size of the font and given as fix_words
977 # which can be converted to floats by diving by 2**20
978 oldconv = self.conv
979 self.conv = fontsize/2**20
980 rescale = self.conv/oldconv
982 self.file = stringbinfile(dvi)
983 self.fonts = fonts
984 self.stack = []
985 self.filepos = 0
987 # we have to rescale self.pos in order to be consistent with the new scaling
988 self.pos = map(lambda x, rescale=rescale:1.0*x/rescale, self.pos)
990 # since tfmconv converts from tfm units to dvi units, we have to rescale it as well
991 self.tfmconv /= rescale
993 self.usefont(0)
995 def _pop_dvistring(self):
996 self.flushout()
997 if self.debug:
998 print "finished executing dvi chunk"
999 self.file.close()
1000 self.file, self.fonts, self.activefont, self.pos, self.stack, self.conv, self.tfmconv = self.statestack.pop()
1002 # routines corresponding to the different reader states of the dvi maschine
1004 def _read_pre(self):
1005 file = self.file
1006 while 1:
1007 self.filepos = file.tell()
1008 cmd = file.readuchar()
1009 if cmd == _DVI_NOP:
1010 pass
1011 elif cmd == _DVI_PRE:
1012 if self.file.readuchar() != _DVI_VERSION: raise DVIError
1013 num = file.readuint32()
1014 den = file.readuint32()
1015 self.mag = file.readuint32()
1017 # for the interpretation of all quantities, two conversion factors
1018 # are relevant:
1019 # - self.tfmconv (tfm units->dvi units)
1020 # - self.conv (dvi units-> (PostScript) points)
1022 self.tfmconv = (25400000.0/num)*(den/473628672.0)/16.0
1024 # calculate self.conv as described in the DVIType docu
1026 # resolution in dpi
1027 self.resolution = 300.0
1028 # self.trueconv = conv in DVIType docu
1029 self.trueconv = (num/254000.0)*(self.resolution/den)
1031 # self.conv is the conversion factor from the dvi units
1032 # to (PostScript) points. It consists of
1033 # - self.mag/1000.0: magstep scaling
1034 # - self.trueconv: conversion from dvi units to pixels
1035 # - 1/self.resolution: conversion from pixels to inch
1036 # - 72 : conversion from inch to points
1037 self.conv = self.mag/1000.0*self.trueconv/self.resolution*72
1039 comment = file.read(file.readuchar())
1040 return
1041 else:
1042 raise DVIError
1044 def readpage(self):
1045 """ reads a page from the dvi file
1047 This routine reads a page from the dvi file which is
1048 returned as a canvas. When there is no page left in the
1049 dvifile, None is returned and the file is closed properly."""
1052 while 1:
1053 self.filepos = self.file.tell()
1054 cmd = self.file.readuchar()
1055 if cmd == _DVI_NOP:
1056 pass
1057 elif cmd == _DVI_BOP:
1058 self.flushout()
1059 pagenos = [self.file.readuint32() for i in range(10)]
1060 if pagenos[:3] != [ord("P"), ord("y"), ord("X")] or pagenos[4:] != [0, 0, 0, 0, 0, 0]:
1061 raise DVIError("Page in dvi file is not a PyX page.")
1062 if self.debug:
1063 print "%d: beginning of page %i" % (self.filepos, pagenos[0])
1064 self.file.readuint32()
1065 break
1066 elif cmd == _DVI_POST:
1067 self.file.close()
1068 return None # nothing left
1069 else:
1070 raise DVIError
1072 actpage = canvas.canvas()
1073 self.actpage = actpage # XXX should be removed ...
1074 self.actpage.markers = {}
1075 self.pos = [0, 0, 0, 0, 0, 0]
1076 self.actoutfont = None
1078 # Since we do not know, which dvi pages the actual PS file contains later on,
1079 # we have to keep track of used char informations separately for each dvi page.
1080 # In order to do so, the already defined fonts have to be copied and their
1081 # used char informations have to be reset
1082 for nr in self.fonts.keys():
1083 self.fonts[nr] = copy.copy(self.fonts[nr])
1084 self.fonts[nr].clearusedchars()
1086 while 1:
1087 file = self.file
1088 self.filepos = file.tell()
1089 try:
1090 cmd = file.readuchar()
1091 except struct.error:
1092 # we most probably (if the dvi file is not corrupt) hit the end of a dvi chunk,
1093 # so we have to continue with the rest of the dvi file
1094 self._pop_dvistring()
1095 continue
1096 if cmd == _DVI_NOP:
1097 pass
1098 if cmd >= _DVI_CHARMIN and cmd <= _DVI_CHARMAX:
1099 self.putchar(cmd)
1100 elif cmd >= _DVI_SET1234 and cmd < _DVI_SET1234 + 4:
1101 self.putchar(file.readint(cmd - _DVI_SET1234 + 1))
1102 elif cmd == _DVI_SETRULE:
1103 self.putrule(file.readint32(), file.readint32())
1104 elif cmd >= _DVI_PUT1234 and cmd < _DVI_PUT1234 + 4:
1105 self.putchar(file.readint(cmd - _DVI_PUT1234 + 1), inch=0)
1106 elif cmd == _DVI_PUTRULE:
1107 self.putrule(file.readint32(), file.readint32(), 0)
1108 elif cmd == _DVI_EOP:
1109 self.flushout()
1110 if self.debug:
1111 print "%d: eop" % self.filepos
1112 print
1113 return actpage
1114 elif cmd == _DVI_PUSH:
1115 self.stack.append(list(self.pos))
1116 if self.debug:
1117 print "%d: push" % self.filepos
1118 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
1119 (( len(self.stack)-1,)+tuple(self.pos)))
1120 elif cmd == _DVI_POP:
1121 self.flushout()
1122 self.pos = self.stack.pop()
1123 if self.debug:
1124 print "%d: pop" % self.filepos
1125 print ("level %d:(h=%d,v=%d,w=%d,x=%d,y=%d,z=%d,hh=,vv=)" %
1126 (( len(self.stack),)+tuple(self.pos)))
1127 elif cmd >= _DVI_RIGHT1234 and cmd < _DVI_RIGHT1234 + 4:
1128 self.flushout()
1129 dh = file.readint(cmd - _DVI_RIGHT1234 + 1, 1)
1130 if self.debug:
1131 print ("%d: right%d %d h:=%d%+d=%d, hh:=" %
1132 (self.filepos,
1133 cmd - _DVI_RIGHT1234 + 1,
1135 self.pos[_POS_H],
1137 self.pos[_POS_H]+dh))
1138 self.pos[_POS_H] += dh
1139 elif cmd == _DVI_W0:
1140 self.flushout()
1141 if self.debug:
1142 print ("%d: w0 %d h:=%d%+d=%d, hh:=" %
1143 (self.filepos,
1144 self.pos[_POS_W],
1145 self.pos[_POS_H],
1146 self.pos[_POS_W],
1147 self.pos[_POS_H]+self.pos[_POS_W]))
1148 self.pos[_POS_H] += self.pos[_POS_W]
1149 elif cmd >= _DVI_W1234 and cmd < _DVI_W1234 + 4:
1150 self.flushout()
1151 self.pos[_POS_W] = file.readint(cmd - _DVI_W1234 + 1, 1)
1152 if self.debug:
1153 print ("%d: w%d %d h:=%d%+d=%d, hh:=" %
1154 (self.filepos,
1155 cmd - _DVI_W1234 + 1,
1156 self.pos[_POS_W],
1157 self.pos[_POS_H],
1158 self.pos[_POS_W],
1159 self.pos[_POS_H]+self.pos[_POS_W]))
1160 self.pos[_POS_H] += self.pos[_POS_W]
1161 elif cmd == _DVI_X0:
1162 self.flushout()
1163 self.pos[_POS_H] += self.pos[_POS_X]
1164 elif cmd >= _DVI_X1234 and cmd < _DVI_X1234 + 4:
1165 self.flushout()
1166 self.pos[_POS_X] = file.readint(cmd - _DVI_X1234 + 1, 1)
1167 self.pos[_POS_H] += self.pos[_POS_X]
1168 elif cmd >= _DVI_DOWN1234 and cmd < _DVI_DOWN1234 + 4:
1169 self.flushout()
1170 dv = file.readint(cmd - _DVI_DOWN1234 + 1, 1)
1171 if self.debug:
1172 print ("%d: down%d %d v:=%d%+d=%d, vv:=" %
1173 (self.filepos,
1174 cmd - _DVI_DOWN1234 + 1,
1176 self.pos[_POS_V],
1178 self.pos[_POS_V]+dv))
1179 self.pos[_POS_V] += dv
1180 elif cmd == _DVI_Y0:
1181 self.flushout()
1182 if self.debug:
1183 print ("%d: y0 %d v:=%d%+d=%d, vv:=" %
1184 (self.filepos,
1185 self.pos[_POS_Y],
1186 self.pos[_POS_V],
1187 self.pos[_POS_Y],
1188 self.pos[_POS_V]+self.pos[_POS_Y]))
1189 self.pos[_POS_V] += self.pos[_POS_Y]
1190 elif cmd >= _DVI_Y1234 and cmd < _DVI_Y1234 + 4:
1191 self.flushout()
1192 self.pos[_POS_Y] = file.readint(cmd - _DVI_Y1234 + 1, 1)
1193 if self.debug:
1194 print ("%d: y%d %d v:=%d%+d=%d, vv:=" %
1195 (self.filepos,
1196 cmd - _DVI_Y1234 + 1,
1197 self.pos[_POS_Y],
1198 self.pos[_POS_V],
1199 self.pos[_POS_Y],
1200 self.pos[_POS_V]+self.pos[_POS_Y]))
1201 self.pos[_POS_V] += self.pos[_POS_Y]
1202 elif cmd == _DVI_Z0:
1203 self.flushout()
1204 self.pos[_POS_V] += self.pos[_POS_Z]
1205 elif cmd >= _DVI_Z1234 and cmd < _DVI_Z1234 + 4:
1206 self.flushout()
1207 self.pos[_POS_Z] = file.readint(cmd - _DVI_Z1234 + 1, 1)
1208 self.pos[_POS_V] += self.pos[_POS_Z]
1209 elif cmd >= _DVI_FNTNUMMIN and cmd <= _DVI_FNTNUMMAX:
1210 self.usefont(cmd - _DVI_FNTNUMMIN)
1211 elif cmd >= _DVI_FNT1234 and cmd < _DVI_FNT1234 + 4:
1212 self.usefont(file.readint(cmd - _DVI_FNT1234 + 1, 1))
1213 elif cmd >= _DVI_SPECIAL1234 and cmd < _DVI_SPECIAL1234 + 4:
1214 self.special(file.read(file.readint(cmd - _DVI_SPECIAL1234 + 1)))
1215 elif cmd >= _DVI_FNTDEF1234 and cmd < _DVI_FNTDEF1234 + 4:
1216 if cmd == _DVI_FNTDEF1234:
1217 num = file.readuchar()
1218 elif cmd == _DVI_FNTDEF1234+1:
1219 num = file.readuint16()
1220 elif cmd == _DVI_FNTDEF1234+2:
1221 num = file.readuint24()
1222 elif cmd == _DVI_FNTDEF1234+3:
1223 # Cool, here we have according to docu a signed int. Why?
1224 num = file.readint32()
1225 self.definefont(cmd-_DVI_FNTDEF1234+1,
1226 num,
1227 file.readint32(),
1228 file.readint32(),
1229 file.readint32(),
1230 file.read(file.readuchar()+file.readuchar()))
1231 else:
1232 raise DVIError
1235 ##############################################################################
1236 # VF file handling
1237 ##############################################################################
1239 _VF_LONG_CHAR = 242 # character packet (long version)
1240 _VF_FNTDEF1234 = _DVI_FNTDEF1234 # font definition
1241 _VF_PRE = _DVI_PRE # preamble
1242 _VF_POST = _DVI_POST # postamble
1244 _VF_ID = 202 # VF id byte
1246 class VFError(exceptions.Exception): pass
1248 class vffile:
1249 def __init__(self, filename, scale, fontmap, debug=0):
1250 self.filename = filename
1251 self.scale = scale
1252 self.fontmap = fontmap
1253 self.debug = debug
1254 self.fonts = {} # used fonts
1255 self.widths = {} # widths of defined chars
1256 self.chardefs = {} # dvi chunks for defined chars
1258 file = binfile(self.filename, "rb")
1260 cmd = file.readuchar()
1261 if cmd == _VF_PRE:
1262 if file.readuchar() != _VF_ID: raise VFError
1263 comment = file.read(file.readuchar())
1264 self.cs = file.readuint32()
1265 self.ds = file.readuint32()
1266 else:
1267 raise VFError
1269 while 1:
1270 cmd = file.readuchar()
1271 if cmd >= _VF_FNTDEF1234 and cmd < _VF_FNTDEF1234 + 4:
1272 # font definition
1273 if cmd == _VF_FNTDEF1234:
1274 num = file.readuchar()
1275 elif cmd == _VF_FNTDEF1234+1:
1276 num = file.readuint16()
1277 elif cmd == _VF_FNTDEF1234+2:
1278 num = file.readuint24()
1279 elif cmd == _VF_FNTDEF1234+3:
1280 num = file.readint32()
1281 c = file.readint32()
1282 s = file.readint32() # relative scaling used for font (fix_word)
1283 d = file.readint32() # design size of font
1284 fontname = file.read(file.readuchar()+file.readuchar())
1286 # rescaled size of font: s is relative to the scaling
1287 # of the virtual font itself. Note that realscale has
1288 # to be a fix_word (like s)
1289 # Furthermore we have to correct for self.tfmconv
1291 reals = int(self.scale * float(fix_word(self.ds))*s)
1292 # print ("defining font %s -- VF scale: %g, VF design size: %g, relative font size: %g => real size: %g" %
1293 # (fontname, self.scale, fix_word(self.ds), fix_word(s), fix_word(reals))
1295 # reald = int(d)
1297 # XXX allow for virtual fonts here too
1298 self.fonts[num] = type1font(fontname, c, reals, d, self.fontmap, self.debug > 1)
1299 elif cmd == _VF_LONG_CHAR:
1300 # character packet (long form)
1301 pl = file.readuint32() # packet length
1302 cc = file.readuint32() # char code (assumed unsigned, but anyhow only 0 <= cc < 255 is actually used)
1303 tfm = file.readuint24() # character width
1304 dvi = file.read(pl) # dvi code of character
1305 self.widths[cc] = tfm
1306 self.chardefs[cc] = dvi
1307 elif cmd < _VF_LONG_CHAR:
1308 # character packet (short form)
1309 cc = file.readuchar() # char code
1310 tfm = file.readuint24() # character width
1311 dvi = file.read(cmd)
1312 self.widths[cc] = tfm
1313 self.chardefs[cc] = dvi
1314 elif cmd == _VF_POST:
1315 break
1316 else:
1317 raise VFError
1319 file.close()
1321 def getfonts(self):
1322 return self.fonts
1324 def getchar(self, cc):
1325 return self.chardefs[cc]
1328 ###############################################################################
1329 # texmessages
1330 # - please don't get confused:
1331 # - there is a texmessage (and a texmessageparsed) attribute within the
1332 # texrunner; it contains TeX/LaTeX response from the last command execution
1333 # - instances of classes derived from the class texmessage are used to
1334 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
1335 # attribute of a texrunner instance
1336 # - the multiple usage of the name texmessage might be removed in the future
1337 # - texmessage instances should implement _Itexmessage
1338 ###############################################################################
1340 class TexResultError(Exception):
1341 """specialized texrunner exception class
1342 - it is raised by texmessage instances, when a texmessage indicates an error
1343 - it is raised by the texrunner itself, whenever there is a texmessage left
1344 after all parsing of this message (by texmessage instances)"""
1346 def __init__(self, description, texrunner):
1347 self.description = description
1348 self.texrunner = texrunner
1350 def __str__(self):
1351 """prints a detailed report about the problem
1352 - the verbose level is controlled by texrunner.errordebug"""
1353 if self.texrunner.errordebug >= 2:
1354 return ("%s\n" % self.description +
1355 "The expression passed to TeX was:\n"
1356 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1357 "The return message from TeX was:\n"
1358 " %s\n" % self.texrunner.texmessage.replace("\n", "\n ").rstrip() +
1359 "After parsing this message, the following was left:\n"
1360 " %s" % self.texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
1361 elif self.texrunner.errordebug == 1:
1362 firstlines = self.texrunner.texmessageparsed.split("\n")
1363 if len(firstlines) > 5:
1364 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
1365 return ("%s\n" % self.description +
1366 "The expression passed to TeX was:\n"
1367 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
1368 "After parsing the return message from TeX, the following was left:\n" +
1369 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
1370 else:
1371 return self.description
1374 class TexResultWarning(TexResultError):
1375 """as above, but with different handling of the exception
1376 - when this exception is raised by a texmessage instance,
1377 the information just get reported and the execution continues"""
1378 pass
1381 class _Itexmessage:
1382 """validates/invalidates TeX/LaTeX response"""
1384 def check(self, texrunner):
1385 """check a Tex/LaTeX response and respond appropriate
1386 - read the texrunners texmessageparsed attribute
1387 - if there is an problem found, raise an appropriate
1388 exception (TexResultError or TexResultWarning)
1389 - remove any valid and identified TeX/LaTeX response
1390 from the texrunners texmessageparsed attribute
1391 -> finally, there should be nothing left in there,
1392 otherwise it is interpreted as an error"""
1395 class texmessage: pass
1398 class _texmessagestart(texmessage):
1399 """validates TeX/LaTeX startup"""
1401 __implements__ = _Itexmessage
1403 startpattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
1405 def check(self, texrunner):
1406 m = self.startpattern.search(texrunner.texmessageparsed)
1407 if not m:
1408 raise TexResultError("TeX startup failed", texrunner)
1409 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
1410 try:
1411 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
1412 except (IndexError, ValueError):
1413 raise TexResultError("TeX running startup file failed", texrunner)
1414 try:
1415 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
1416 except (IndexError, ValueError):
1417 raise TexResultError("TeX scrollmode check failed", texrunner)
1420 class _texmessagenoaux(texmessage):
1421 """allows for LaTeXs no-aux-file warning"""
1423 __implements__ = _Itexmessage
1425 def check(self, texrunner):
1426 try:
1427 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
1428 texrunner.texmessageparsed = s1 + s2
1429 except (IndexError, ValueError):
1430 try:
1431 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
1432 os.sep,
1433 texrunner.texfilename), 1)
1434 texrunner.texmessageparsed = s1 + s2
1435 except (IndexError, ValueError):
1436 pass
1439 class _texmessageinputmarker(texmessage):
1440 """validates the PyXInputMarker"""
1442 __implements__ = _Itexmessage
1444 def check(self, texrunner):
1445 try:
1446 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
1447 texrunner.texmessageparsed = s1 + s2
1448 except (IndexError, ValueError):
1449 raise TexResultError("PyXInputMarker expected", texrunner)
1452 class _texmessagepyxbox(texmessage):
1453 """validates the PyXBox output"""
1455 __implements__ = _Itexmessage
1457 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:")
1459 def check(self, texrunner):
1460 m = self.pattern.search(texrunner.texmessageparsed)
1461 if m and m.group("page") == str(texrunner.page):
1462 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1463 else:
1464 raise TexResultError("PyXBox expected", texrunner)
1467 class _texmessagepyxpageout(texmessage):
1468 """validates the dvi shipout message (writing a page to the dvi file)"""
1470 __implements__ = _Itexmessage
1472 def check(self, texrunner):
1473 try:
1474 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
1475 texrunner.texmessageparsed = s1 + s2
1476 except (IndexError, ValueError):
1477 raise TexResultError("PyXPageOutMarker expected", texrunner)
1480 class _texmessagetexend(texmessage):
1481 """validates TeX/LaTeX finish"""
1483 __implements__ = _Itexmessage
1485 def check(self, texrunner):
1486 try:
1487 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
1488 texrunner.texmessageparsed = s1 + s2
1489 except (IndexError, ValueError):
1490 try:
1491 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
1492 os.sep,
1493 texrunner.texfilename), 1)
1494 texrunner.texmessageparsed = s1 + s2
1495 except (IndexError, ValueError):
1496 pass
1497 try:
1498 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
1499 texrunner.texmessageparsed = s1 + s2
1500 except (IndexError, ValueError):
1501 pass
1502 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
1503 m = dvipattern.search(texrunner.texmessageparsed)
1504 if texrunner.page:
1505 if not m:
1506 raise TexResultError("TeX dvifile messages expected", texrunner)
1507 if m.group("page") != str(texrunner.page):
1508 raise TexResultError("wrong number of pages reported", texrunner)
1509 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
1510 else:
1511 try:
1512 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
1513 texrunner.texmessageparsed = s1 + s2
1514 except (IndexError, ValueError):
1515 raise TexResultError("no dvifile expected")
1516 try:
1517 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
1518 texrunner.texmessageparsed = s1 + s2
1519 except (IndexError, ValueError):
1520 raise TexResultError("TeX logfile message expected")
1523 class _texmessageemptylines(texmessage):
1524 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines"""
1526 __implements__ = _Itexmessage
1528 def check(self, texrunner):
1529 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
1530 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
1533 class _texmessageload(texmessage):
1534 """validates inclusion of arbitrary files
1535 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
1536 <fielname> is a readable file and other stuff can be anything
1537 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
1538 - this is not always wanted, but we just assume that file inclusion is fine"""
1540 __implements__ = _Itexmessage
1542 pattern = re.compile(r" *\((?P<filename>[^()\s\n]+)[^()]*\) *")
1544 def baselevels(self, s, maxlevel=1, brackets="()"):
1545 """strip parts of a string above a given bracket level
1546 - return a modified (some parts might be removed) version of the string s
1547 where all parts inside brackets with level higher than maxlevel are
1548 removed
1549 - if brackets do not match (number of left and right brackets is wrong
1550 or at some points there were more right brackets than left brackets)
1551 just return the unmodified string"""
1552 level = 0
1553 highestlevel = 0
1554 res = ""
1555 for c in s:
1556 if c == brackets[0]:
1557 level += 1
1558 if level > highestlevel:
1559 highestlevel = level
1560 if level <= maxlevel:
1561 res += c
1562 if c == brackets[1]:
1563 level -= 1
1564 if level == 0 and highestlevel > 0:
1565 return res
1567 def check(self, texrunner):
1568 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
1569 if lowestbracketlevel is not None:
1570 m = self.pattern.search(lowestbracketlevel)
1571 while m:
1572 if os.access(m.group("filename"), os.R_OK):
1573 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
1574 else:
1575 break
1576 m = self.pattern.search(lowestbracketlevel)
1577 else:
1578 texrunner.texmessageparsed = lowestbracketlevel
1581 class _texmessageloadfd(_texmessageload):
1582 """validates the inclusion of font description files (fd-files)
1583 - works like _texmessageload
1584 - filename must end with .fd and no further text is allowed"""
1586 pattern = re.compile(r" *\((?P<filename>[^)]+.fd)\) *")
1589 class _texmessagegraphicsload(_texmessageload):
1590 """validates the inclusion of files as the graphics packages writes it
1591 - works like _texmessageload, but using "<" and ">" as delimiters
1592 - filename must end with .eps and no further text is allowed"""
1594 pattern = re.compile(r" *<(?P<filename>[^>]+.eps)> *")
1596 def baselevels(self, s, brackets="<>", **args):
1597 return _texmessageload.baselevels(self, s, brackets=brackets, **args)
1600 class _texmessageignore(_texmessageload):
1601 """validates any TeX/LaTeX response
1602 - this might be used, when the expression is ok, but no suitable texmessage
1603 parser is available
1604 - PLEASE: - consider writing suitable tex message parsers
1605 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
1607 __implements__ = _Itexmessage
1609 def check(self, texrunner):
1610 texrunner.texmessageparsed = ""
1613 texmessage.start = _texmessagestart()
1614 texmessage.noaux = _texmessagenoaux()
1615 texmessage.inputmarker = _texmessageinputmarker()
1616 texmessage.pyxbox = _texmessagepyxbox()
1617 texmessage.pyxpageout = _texmessagepyxpageout()
1618 texmessage.texend = _texmessagetexend()
1619 texmessage.emptylines = _texmessageemptylines()
1620 texmessage.load = _texmessageload()
1621 texmessage.loadfd = _texmessageloadfd()
1622 texmessage.graphicsload = _texmessagegraphicsload()
1623 texmessage.ignore = _texmessageignore()
1626 ###############################################################################
1627 # textattrs
1628 ###############################################################################
1630 _textattrspreamble = ""
1632 class textattr:
1633 "a textattr defines a apply method, which modifies a (La)TeX expression"
1635 class halign(attr.exclusiveattr, textattr):
1637 def __init__(self, hratio):
1638 self.hratio = hratio
1639 attr.exclusiveattr.__init__(self, halign)
1641 def apply(self, expr):
1642 return r"\gdef\PyXHAlign{%.5f}%s" % (self.hratio, expr)
1644 halign.center = halign(0.5)
1645 halign.right = halign(1)
1646 halign.clear = attr.clearclass(halign)
1647 halign.left = halign.clear
1650 class _localattr: pass
1652 class _mathmode(attr.attr, textattr, _localattr):
1653 "math mode"
1655 def apply(self, expr):
1656 return r"$\displaystyle{%s}$" % expr
1658 mathmode = _mathmode()
1659 nomathmode = attr.clearclass(_mathmode)
1662 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
1664 class size(attr.sortbeforeattr, textattr, _localattr):
1665 "font size"
1667 def __init__(self, expr, sizelist=defaultsizelist):
1668 attr.sortbeforeattr.__init__(self, [_mathmode])
1669 if helper.isinteger(expr):
1670 if expr >= 0 and expr < sizelist.index(None):
1671 self.size = sizelist[expr]
1672 elif expr < 0 and expr + len(sizelist) > sizelist.index(None):
1673 self.size = sizelist[expr]
1674 else:
1675 raise IndexError("index out of sizelist range")
1676 else:
1677 self.size = expr
1679 def apply(self, expr):
1680 return r"\%s{%s}" % (self.size, expr)
1682 for s in defaultsizelist:
1683 if s is not None:
1684 setattr(size, s, size(s))
1687 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
1689 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
1691 top = 1
1692 middle = 2
1693 bottom = 3
1695 def __init__(self, width, baseline=top):
1696 self.width = width
1697 self.baseline = baseline
1698 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
1700 def apply(self, expr):
1701 if self.baseline == self.top:
1702 return r"\linewidth%.5ftruept\vtop{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, expr)
1703 elif self.baseline == self.middle:
1704 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)
1705 elif self.baseline == self.bottom:
1706 return r"\linewidth%.5ftruept\vbox{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, expr)
1707 else:
1708 RuntimeError("invalid baseline argument")
1710 class parbox(parbox_pt):
1712 def __init__(self, width, **kwargs):
1713 parbox_pt.__init__(self, unit.topt(width), **kwargs)
1716 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
1718 class valign(attr.sortbeforeexclusiveattr, textattr):
1720 def __init__(self):
1721 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
1723 class _valigntop(valign):
1725 def apply(self, expr):
1726 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1728 class _valignmiddle(valign):
1730 def apply(self, expr):
1731 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
1733 class _valignbottom(valign):
1735 def apply(self, expr):
1736 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
1738 valign.top = _valigntop()
1739 valign.middle = _valignmiddle()
1740 valign.bottom = _valignbottom()
1741 valign.clear = attr.clearclass(valign)
1742 valign.baseline = valign.clear
1745 class _vshift(attr.sortbeforeattr, textattr):
1747 def __init__(self):
1748 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
1750 class vshift(_vshift):
1751 "vertical down shift by a fraction of a character height"
1753 def __init__(self, lowerratio, heightstr="0"):
1754 _vshift.__init__(self)
1755 self.lowerratio = lowerratio
1756 self.heightstr = heightstr
1758 def apply(self, expr):
1759 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
1761 class _vshiftmathaxis(_vshift):
1762 "vertical down shift by the height of the math axis"
1764 def apply(self, expr):
1765 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
1768 vshift.bottomzero = vshift(0)
1769 vshift.middlezero = vshift(0.5)
1770 vshift.topzero = vshift(1)
1771 vshift.mathaxis = _vshiftmathaxis()
1774 ###############################################################################
1775 # texrunner
1776 ###############################################################################
1779 class _readpipe(threading.Thread):
1780 """threaded reader of TeX/LaTeX output
1781 - sets an event, when a specific string in the programs output is found
1782 - sets an event, when the terminal ends"""
1784 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
1785 """initialize the reader
1786 - pipe: file to be read from
1787 - expectqueue: keeps the next InputMarker to be wait for
1788 - gotevent: the "got InputMarker" event
1789 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
1790 - quitevent: the "end of terminal" event"""
1791 threading.Thread.__init__(self)
1792 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
1793 self.pipe = pipe
1794 self.expectqueue = expectqueue
1795 self.gotevent = gotevent
1796 self.gotqueue = gotqueue
1797 self.quitevent = quitevent
1798 self.expect = None
1799 self.start()
1801 def run(self):
1802 """thread routine"""
1803 read = self.pipe.readline() # read, what comes in
1804 try:
1805 self.expect = self.expectqueue.get_nowait() # read, what should be expected
1806 except Queue.Empty:
1807 pass
1808 while len(read):
1809 # universal EOL handling (convert everything into unix like EOLs)
1810 read.replace("\r", "")
1811 if not len(read) or read[-1] != "\n":
1812 read += "\n"
1813 self.gotqueue.put(read) # report, whats readed
1814 if self.expect is not None and read.find(self.expect) != -1:
1815 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
1816 read = self.pipe.readline() # read again
1817 try:
1818 self.expect = self.expectqueue.get_nowait()
1819 except Queue.Empty:
1820 pass
1821 # EOF reached
1822 self.pipe.close()
1823 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
1824 raise RuntimeError("TeX/LaTeX finished unexpectedly")
1825 self.quitevent.set()
1829 class textbox_pt(box._rect, base.PSCmd):
1830 """basically a box.rect, but it contains a text created by the texrunner
1831 - texrunner._text and texrunner.text return such an object
1832 - _textbox instances can be inserted into a canvas
1833 - the output is contained in a page of the dvifile available thru the texrunner"""
1835 def __init__(self, x, y, left, right, height, depth, texrunner, *attrs):
1836 self.texttrafo = trafo._translate(x, y)
1837 box._rect.__init__(self, x - left, y - depth,
1838 left + right, depth + height,
1839 abscenter = (left, depth))
1840 self.texrunner = texrunner
1841 self.attrs = attrs
1842 self.dvicanvas = None
1844 def transform(self, *trafos):
1845 box._rect.transform(self, *trafos)
1846 for trafo in trafos:
1847 self.texttrafo = trafo * self.texttrafo
1849 def setdvicanvas(self, dvicanvas):
1850 self.dvicanvas = dvicanvas
1852 def ensuredvicanvas(self):
1853 if self.dvicanvas is None:
1854 self.texrunner.finishdvi()
1855 assert self.dvicanvas is not None, "finishdvi is broken"
1857 def marker(self, marker):
1858 self.ensuredvicanvas()
1859 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
1861 def prolog(self):
1862 self.ensuredvicanvas()
1863 result = []
1864 for cmd in self.attrs:
1865 result.extend(cmd.prolog())
1866 return result + self.dvicanvas.prolog()
1868 def write(self, file):
1869 self.ensuredvicanvas()
1870 canvas._gsave().write(file) # XXX: canvas?, constructor call needed?
1871 self.texttrafo.write(file)
1872 for attr in self.attrs:
1873 attr.write(file)
1874 self.dvicanvas.write(file)
1875 canvas._grestore().write(file)
1879 class textbox(textbox_pt):
1881 def __init__(self, x, y, left, right, height, depth, texrunner, *attrs):
1882 textbox_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(left), unit.topt(right),
1883 unit.topt(height), unit.topt(depth), texrunner, *attrs)
1886 def _cleantmp(texrunner):
1887 """get rid of temporary files
1888 - function to be registered by atexit
1889 - files contained in usefiles are kept"""
1890 if texrunner.texruns: # cleanup while TeX is still running?
1891 texrunner.texruns = 0
1892 texrunner.texdone = 1
1893 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
1894 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
1895 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
1896 else:
1897 texrunner.texinput.write("\n\\end\n")
1898 texrunner.texinput.close() # close the input queue and
1899 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
1900 return # didn't got a quit from TeX -> we can't do much more
1901 for usefile in texrunner.usefiles:
1902 extpos = usefile.rfind(".")
1903 try:
1904 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
1905 except OSError:
1906 pass
1907 for file in glob.glob("%s.*" % texrunner.texfilename):
1908 try:
1909 os.unlink(file)
1910 except OSError:
1911 pass
1912 if texrunner.texdebug is not None:
1913 try:
1914 texrunner.texdebug.close()
1915 texrunner.texdebug = None
1916 except IOError:
1917 pass
1920 # texrunner state exceptions
1921 class TexRunsError(Exception): pass
1922 class TexDoneError(Exception): pass
1923 class TexNotInPreambleModeError(Exception): pass
1926 class texrunner:
1927 """TeX/LaTeX interface
1928 - runs TeX/LaTeX expressions instantly
1929 - checks TeX/LaTeX response
1930 - the instance variable texmessage stores the last TeX
1931 response as a string
1932 - the instance variable texmessageparsed stores a parsed
1933 version of texmessage; it should be empty after
1934 texmessage.check was called, otherwise a TexResultError
1935 is raised
1936 - the instance variable errordebug controls the verbose
1937 level of TexResultError"""
1939 def __init__(self, mode="tex",
1940 lfs="10pt",
1941 docclass="article",
1942 docopt=None,
1943 usefiles=None,
1944 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
1945 waitfortex=config.getint("text", "waitfortex", 60),
1946 showwaitfortex=config.getint("text", "showwaitfortex", 5),
1947 texipc=config.getboolean("text", "texipc", 0),
1948 texdebug=None,
1949 dvidebug=0,
1950 errordebug=1,
1951 dvicopy=0,
1952 pyxgraphics=1,
1953 texmessagestart=texmessage.start,
1954 texmessagedocclass=texmessage.load,
1955 texmessagebegindoc=(texmessage.load, texmessage.noaux),
1956 texmessageend=texmessage.texend,
1957 texmessagedefaultpreamble=texmessage.load,
1958 texmessagedefaultrun=texmessage.loadfd):
1959 mode = mode.lower()
1960 if mode != "tex" and mode != "latex":
1961 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1962 self.mode = mode
1963 self.lfs = lfs
1964 self.docclass = docclass
1965 self.docopt = docopt
1966 self.usefiles = helper.ensurelist(usefiles)
1967 self.fontmap = readfontmap(fontmaps.split())
1968 self.waitfortex = waitfortex
1969 self.showwaitfortex = showwaitfortex
1970 self.texipc = texipc
1971 if texdebug is not None:
1972 if texdebug[-4:] == ".tex":
1973 self.texdebug = open(texdebug, "w")
1974 else:
1975 self.texdebug = open("%s.tex" % texdebug, "w")
1976 else:
1977 self.texdebug = None
1978 self.dvidebug = dvidebug
1979 self.errordebug = errordebug
1980 self.dvicopy = dvicopy
1981 self.pyxgraphics = pyxgraphics
1982 texmessagestart = helper.ensuresequence(texmessagestart)
1983 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
1984 self.texmessagestart = texmessagestart
1985 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
1986 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
1987 self.texmessagedocclass = texmessagedocclass
1988 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
1989 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
1990 self.texmessagebegindoc = texmessagebegindoc
1991 texmessageend = helper.ensuresequence(texmessageend)
1992 helper.checkattr(texmessageend, allowmulti=(texmessage,))
1993 self.texmessageend = texmessageend
1994 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
1995 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
1996 self.texmessagedefaultpreamble = texmessagedefaultpreamble
1997 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
1998 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
1999 self.texmessagedefaultrun = texmessagedefaultrun
2001 self.texruns = 0
2002 self.texdone = 0
2003 self.preamblemode = 1
2004 self.executeid = 0
2005 self.page = 0
2006 self.preambles = []
2007 self.acttextboxes = [] # when texipc-mode off
2008 self.actdvifile = None # when texipc-mode on
2009 savetempdir = tempfile.tempdir
2010 tempfile.tempdir = os.curdir
2011 self.texfilename = os.path.basename(tempfile.mktemp())
2012 tempfile.tempdir = savetempdir
2014 def waitforevent(self, event):
2015 """waits verbosely with an timeout for an event
2016 - observes an event while periodly while printing messages
2017 - returns the status of the event (isSet)
2018 - does not clear the event"""
2019 if self.showwaitfortex:
2020 waited = 0
2021 hasevent = 0
2022 while waited < self.waitfortex and not hasevent:
2023 if self.waitfortex - waited > self.showwaitfortex:
2024 event.wait(self.showwaitfortex)
2025 waited += self.showwaitfortex
2026 else:
2027 event.wait(self.waitfortex - waited)
2028 waited += self.waitfortex - waited
2029 hasevent = event.isSet()
2030 if not hasevent:
2031 if waited < self.waitfortex:
2032 sys.stderr.write("*** PyX INFO: still waiting for %s after %i seconds...\n" % (self.mode, waited))
2033 else:
2034 sys.stderr.write("*** PyX ERROR: the timeout of %i seconds expired and %s did not respond.\n" % (waited, self.mode))
2035 return hasevent
2036 else:
2037 event.wait(self.waitfortex)
2038 return event.isSet()
2040 def execute(self, expr, *checks):
2041 """executes expr within TeX/LaTeX
2042 - if self.texruns is not yet set, TeX/LaTeX is initialized,
2043 self.texruns is set and self.preamblemode is set
2044 - the method must not be called, when self.texdone is already set
2045 - expr should be a string or None
2046 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
2047 while self.texdone becomes set
2048 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
2049 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
2051 if not self.texruns:
2052 if self.texdebug is not None:
2053 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
2054 self.texdebug.write("%% mode: %s\n" % self.mode)
2055 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
2056 for usefile in self.usefiles:
2057 extpos = usefile.rfind(".")
2058 try:
2059 os.rename(usefile, self.texfilename + usefile[extpos:])
2060 except OSError:
2061 pass
2062 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
2063 texfile.write("\\relax%\n")
2064 texfile.close()
2065 if self.texipc:
2066 ipcflag = " --ipc"
2067 else:
2068 ipcflag = ""
2069 try:
2070 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
2071 except ValueError:
2072 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
2073 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
2074 atexit.register(_cleantmp, self)
2075 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
2076 self.gotevent = threading.Event() # keeps the got inputmarker event
2077 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
2078 self.quitevent = threading.Event() # keeps for end of terminal event
2079 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
2080 self.texruns = 1
2081 oldpreamblemode = self.preamblemode
2082 self.preamblemode = 1
2083 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
2084 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
2085 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
2086 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
2087 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
2088 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
2089 "\\newdimen\\PyXDimenHAlignRT%\n" +
2090 _textattrspreamble + # insert preambles for textattrs macros
2091 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
2092 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
2093 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
2094 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
2095 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
2096 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
2097 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
2098 "lt=\\the\\PyXDimenHAlignLT,"
2099 "rt=\\the\\PyXDimenHAlignRT,"
2100 "ht=\\the\\ht\\PyXBox,"
2101 "dp=\\the\\dp\\PyXBox:}%\n"
2102 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
2103 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
2104 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
2105 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
2106 "\\def\\PyXMarker#1{\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
2107 *self.texmessagestart)
2108 os.remove("%s.tex" % self.texfilename)
2109 if self.mode == "tex":
2110 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
2111 lfsname = self.lfs
2112 else:
2113 lfsname = "%s.lfs" % self.lfs
2114 for fulllfsname in [lfsname,
2115 os.path.join(sys.prefix, "share", "pyx", lfsname),
2116 os.path.join(os.path.dirname(__file__), "lfs", lfsname)]:
2117 try:
2118 lfsfile = open(fulllfsname, "r")
2119 lfsdef = lfsfile.read()
2120 lfsfile.close()
2121 break
2122 except IOError:
2123 pass
2124 else:
2125 allfiles = (glob.glob("*.lfs") +
2126 glob.glob(os.path.join(sys.prefix, "share", "pyx", "*.lfs")) +
2127 glob.glob(os.path.join(os.path.dirname(__file__), "lfs", "*.lfs")))
2128 lfsnames = []
2129 for f in allfiles:
2130 try:
2131 open(f, "r").close()
2132 lfsnames.append(os.path.basename(f)[:-4])
2133 except IOError:
2134 pass
2135 lfsnames.sort()
2136 if len(lfsnames):
2137 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname, lfsnames))
2138 else:
2139 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname)
2140 self.execute(lfsdef)
2141 self.execute("\\normalsize%\n")
2142 self.execute("\\newdimen\\linewidth%\n")
2143 elif self.mode == "latex":
2144 if self.pyxgraphics:
2145 for pyxdef in ["pyx.def",
2146 os.path.join(sys.prefix, "share", "pyx", "pyx.def"),
2147 os.path.join(os.path.dirname(__file__), "..", "contrib", "pyx.def")]:
2148 try:
2149 open(pyxdef, "r").close()
2150 break
2151 except IOError:
2152 pass
2153 else:
2154 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
2155 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
2156 self.execute("\\makeatletter%\n"
2157 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
2158 "\\def\\ProcessOptions{%\n"
2159 "\\def\\Gin@driver{" + pyxdef + "}%\n"
2160 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
2161 "\\saveProcessOptions}%\n"
2162 "\\makeatother")
2163 if self.docopt is not None:
2164 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass), *self.texmessagedocclass)
2165 else:
2166 self.execute("\\documentclass{%s}" % self.docclass, *self.texmessagedocclass)
2167 self.preamblemode = oldpreamblemode
2168 self.executeid += 1
2169 if expr is not None: # TeX/LaTeX should process expr
2170 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
2171 if self.preamblemode:
2172 self.expr = ("%s%%\n" % expr +
2173 "\\PyXInput{%i}%%\n" % self.executeid)
2174 else:
2175 self.page += 1
2176 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
2177 "\\PyXInput{%i}%%\n" % self.executeid)
2178 else: # TeX/LaTeX should be finished
2179 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
2180 if self.mode == "latex":
2181 self.expr = "\\end{document}%\n"
2182 else:
2183 self.expr = "\\end%\n"
2184 if self.texdebug is not None:
2185 self.texdebug.write(self.expr)
2186 self.texinput.write(self.expr)
2187 gotevent = self.waitforevent(self.gotevent)
2188 self.gotevent.clear()
2189 if expr is None and gotevent: # TeX/LaTeX should have finished
2190 self.texruns = 0
2191 self.texdone = 1
2192 self.texinput.close() # close the input queue and
2193 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
2194 try:
2195 self.texmessage = ""
2196 while 1:
2197 self.texmessage += self.gotqueue.get_nowait()
2198 except Queue.Empty:
2199 pass
2200 self.texmessageparsed = self.texmessage
2201 if gotevent:
2202 if expr is not None:
2203 texmessage.inputmarker.check(self)
2204 if not self.preamblemode:
2205 texmessage.pyxbox.check(self)
2206 texmessage.pyxpageout.check(self)
2207 for check in checks:
2208 try:
2209 check.check(self)
2210 except TexResultWarning:
2211 traceback.print_exc()
2212 texmessage.emptylines.check(self)
2213 if len(self.texmessageparsed):
2214 raise TexResultError("unhandled TeX response (might be an error)", self)
2215 else:
2216 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
2218 def finishdvi(self):
2219 """finish TeX/LaTeX and read the dvifile
2220 - this method ensures that all textboxes can access their
2221 dvicanvas"""
2222 self.execute(None, *self.texmessageend)
2223 if self.dvicopy:
2224 os.system("dvicopy %(t)s.dvi %(t)s.dvicopy > %(t)s.dvicopyout 2> %(t)s.dvicopyerr" % {"t": self.texfilename})
2225 dvifilename = "%s.dvicopy" % self.texfilename
2226 else:
2227 dvifilename = "%s.dvi" % self.texfilename
2228 if not self.texipc:
2229 self.dvifile = dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
2230 for box in self.acttextboxes:
2231 box.setdvicanvas(self.dvifile.readpage())
2232 if self.dvifile.readpage() is not None:
2233 raise RuntimeError("end of dvifile expected")
2234 self.dvifile = None
2235 self.acttextboxes = []
2237 def reset(self, reinit=0):
2238 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
2239 if self.texruns:
2240 self.finishdvi()
2241 if self.texdebug is not None:
2242 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
2243 self.executeid = 0
2244 self.page = 0
2245 self.texdone = 0
2246 if reinit:
2247 self.preamblemode = 1
2248 for expr, args in self.preambles:
2249 self.execute(expr, *args)
2250 if self.mode == "latex":
2251 self.execute("\\begin{document}", *self.texmessagebegindoc)
2252 self.preamblemode = 0
2253 else:
2254 self.preambles = []
2255 self.preamblemode = 1
2257 def set(self, mode=None,
2258 lfs=None,
2259 docclass=None,
2260 docopt=None,
2261 usefiles=None,
2262 fontmaps=None,
2263 waitfortex=None,
2264 showwaitfortex=None,
2265 texipc=None,
2266 texdebug=None,
2267 dvidebug=None,
2268 errordebug=None,
2269 dvicopy=None,
2270 pyxgraphics=None,
2271 texmessagestart=None,
2272 texmessagedocclass=None,
2273 texmessagebegindoc=None,
2274 texmessageend=None,
2275 texmessagedefaultpreamble=None,
2276 texmessagedefaultrun=None):
2277 """provide a set command for TeX/LaTeX settings
2278 - TeX/LaTeX must not yet been started
2279 - especially needed for the defaultrunner, where no access to
2280 the constructor is available"""
2281 if self.texruns:
2282 raise TexRunsError
2283 if mode is not None:
2284 mode = mode.lower()
2285 if mode != "tex" and mode != "latex":
2286 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
2287 self.mode = mode
2288 if lfs is not None:
2289 self.lfs = lfs
2290 if docclass is not None:
2291 self.docclass = docclass
2292 if docopt is not None:
2293 self.docopt = docopt
2294 if usefiles is not None:
2295 self.usefiles = helper.ensurelist(usefiles)
2296 if fontmaps is not None:
2297 self.fontmap = readfontmap(fontmaps.split())
2298 if waitfortex is not None:
2299 self.waitfortex = waitfortex
2300 if showwaitfortex is not None:
2301 self.showwaitfortex = showwaitfortex
2302 if texipc is not None:
2303 self.texipc = texipc
2304 if texdebug is not None:
2305 if self.texdebug is not None:
2306 self.texdebug.close()
2307 if texdebug[-4:] == ".tex":
2308 self.texdebug = open(texdebug, "w")
2309 else:
2310 self.texdebug = open("%s.tex" % texdebug, "w")
2311 if dvidebug is not None:
2312 self.dvidebug = dvidebug
2313 if errordebug is not None:
2314 self.errordebug = errordebug
2315 if dvicopy is not None:
2316 self.dvicopy = dvicopy
2317 if pyxgraphics is not None:
2318 self.pyxgraphics = pyxgraphics
2319 if errordebug is not None:
2320 self.errordebug = errordebug
2321 if texmessagestart is not None:
2322 texmessagestart = helper.ensuresequence(texmessagestart)
2323 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
2324 self.texmessagestart = texmessagestart
2325 if texmessagedocclass is not None:
2326 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
2327 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
2328 self.texmessagedocclass = texmessagedocclass
2329 if texmessagebegindoc is not None:
2330 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
2331 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
2332 self.texmessagebegindoc = texmessagebegindoc
2333 if texmessageend is not None:
2334 texmessageend = helper.ensuresequence(texmessageend)
2335 helper.checkattr(texmessageend, allowmulti=(texmessage,))
2336 self.texmessageend = texmessageend
2337 if texmessagedefaultpreamble is not None:
2338 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
2339 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
2340 self.texmessagedefaultpreamble = texmessagedefaultpreamble
2341 if texmessagedefaultrun is not None:
2342 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
2343 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
2344 self.texmessagedefaultrun = texmessagedefaultrun
2346 def preamble(self, expr, *args):
2347 r"""put something into the TeX/LaTeX preamble
2348 - in LaTeX, this is done before the \begin{document}
2349 (you might use \AtBeginDocument, when you're in need for)
2350 - it is not allowed to call preamble after calling the
2351 text method for the first time (for LaTeX this is needed
2352 due to \begin{document}; in TeX it is forced for compatibility
2353 (you should be able to switch from TeX to LaTeX, if you want,
2354 without breaking something
2355 - preamble expressions must not create any dvi output
2356 - args might contain texmessage instances"""
2357 if self.texdone or not self.preamblemode:
2358 raise TexNotInPreambleModeError
2359 helper.checkattr(args, allowmulti=(texmessage,))
2360 args = helper.getattrs(args, texmessage, default=self.texmessagedefaultpreamble)
2361 self.execute(expr, *args)
2362 self.preambles.append((expr, args))
2364 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:")
2366 def text_pt(self, x, y, expr, *args):
2367 """create text by passing expr to TeX/LaTeX
2368 - returns a textbox containing the result from running expr thru TeX/LaTeX
2369 - the box center is set to x, y
2370 - *args may contain attr parameters, namely:
2371 - textattr instances
2372 - texmessage instances
2373 - trafo._trafo instances
2374 - style.fillstyle instances"""
2375 if expr is None:
2376 raise ValueError("None expression is invalid")
2377 if self.texdone:
2378 self.reset(reinit=1)
2379 first = 0
2380 if self.preamblemode:
2381 if self.mode == "latex":
2382 self.execute("\\begin{document}", *self.texmessagebegindoc)
2383 self.preamblemode = 0
2384 first = 1
2385 if self.texipc and self.dvicopy:
2386 raise RuntimeError("texipc and dvicopy can't be mixed up")
2387 helper.checkattr(args, allowmulti=(textattr, texmessage, trafo._trafo, style.fillstyle))
2388 textattrs = attr.getattrs(args, [textattr])
2389 textattrs = attr.mergeattrs(textattrs)
2390 lentextattrs = len(textattrs)
2391 for i in range(lentextattrs):
2392 expr = textattrs[lentextattrs-1-i].apply(expr)
2393 self.execute(expr, *helper.getattrs(args, texmessage, default=self.texmessagedefaultrun))
2394 if self.texipc:
2395 if first:
2396 self.dvifile = dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
2397 match = self.PyXBoxPattern.search(self.texmessage)
2398 if not match or int(match.group("page")) != self.page:
2399 raise TexResultError("box extents not found", self)
2400 left, right, height, depth = map(lambda x: float(x) * 72.0 / 72.27, match.group("lt", "rt", "ht", "dp"))
2401 box = textbox_pt(x, y, left, right, height, depth, self,
2402 *helper.getattrs(args, style.fillstyle, default=[]))
2403 for t in helper.getattrs(args, trafo._trafo, default=()):
2404 box.reltransform(t)
2405 if self.texipc:
2406 box.setdvicanvas(self.dvifile.readpage())
2407 self.acttextboxes.append(box)
2408 return box
2410 def text(self, x, y, expr, *args):
2411 return self.text_pt(unit.topt(x), unit.topt(y), expr, *args)
2414 # the module provides an default texrunner and methods for direct access
2415 defaulttexrunner = texrunner()
2416 reset = defaulttexrunner.reset
2417 set = defaulttexrunner.set
2418 preamble = defaulttexrunner.preamble
2419 text = defaulttexrunner.text
2420 text_pt = defaulttexrunner.text_pt