remove dvicopy
[PyX/mjg.git] / pyx / text.py
blob10e32dd6c64a463a8fb177ae78189f98f4252561
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 import glob, os, threading, Queue, traceback, re, tempfile, atexit, time, warnings
26 import config, siteconfig, unit, box, canvas, trafo, version, attr, style, dvifile
28 ###############################################################################
29 # texmessages
30 # - please don't get confused:
31 # - there is a texmessage (and a texmessageparsed) attribute within the
32 # texrunner; it contains TeX/LaTeX response from the last command execution
33 # - instances of classes derived from the class texmessage are used to
34 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
35 # attribute of a texrunner instance
36 # - the multiple usage of the name texmessage might be removed in the future
37 # - texmessage instances should implement _Itexmessage
38 ###############################################################################
40 class TexResultError(RuntimeError):
41 """specialized texrunner exception class
42 - it is raised by texmessage instances, when a texmessage indicates an error
43 - it is raised by the texrunner itself, whenever there is a texmessage left
44 after all parsing of this message (by texmessage instances)"""
46 def __init__(self, description, texrunner):
47 self.description = description
48 self.texrunner = texrunner
50 def __str__(self):
51 """prints a detailed report about the problem
52 - the verbose level is controlled by texrunner.errordebug"""
53 if self.texrunner.errordebug >= 2:
54 return ("%s\n" % self.description +
55 "The expression passed to TeX was:\n"
56 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
57 "The return message from TeX was:\n"
58 " %s\n" % self.texrunner.texmessage.replace("\n", "\n ").rstrip() +
59 "After parsing this message, the following was left:\n"
60 " %s" % self.texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
61 elif self.texrunner.errordebug == 1:
62 firstlines = self.texrunner.texmessageparsed.split("\n")
63 if len(firstlines) > 5:
64 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
65 return ("%s\n" % self.description +
66 "The expression passed to TeX was:\n"
67 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
68 "After parsing the return message from TeX, the following was left:\n" +
69 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
70 else:
71 return self.description
74 class _Itexmessage:
75 """validates/invalidates TeX/LaTeX response"""
77 def check(self, texrunner):
78 """check a Tex/LaTeX response and respond appropriate
79 - read the texrunners texmessageparsed attribute
80 - if there is an problem found, raise TexResultError
81 - remove any valid and identified TeX/LaTeX response
82 from the texrunners texmessageparsed attribute
83 -> finally, there should be nothing left in there,
84 otherwise it is interpreted as an error"""
87 class texmessage(attr.attr): pass
90 class _texmessagestart(texmessage):
91 """validates TeX/LaTeX startup"""
93 __implements__ = _Itexmessage
95 startpattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
97 def check(self, texrunner):
98 # check for "This is e-TeX"
99 m = self.startpattern.search(texrunner.texmessageparsed)
100 if not m:
101 raise TexResultError("TeX startup failed", texrunner)
102 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
104 # check for filename to be processed
105 try:
106 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
107 except (IndexError, ValueError):
108 raise TexResultError("TeX running startup file failed", texrunner)
110 # check for \raiseerror -- just to be sure that communication works
111 try:
112 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
113 except (IndexError, ValueError):
114 raise TexResultError("TeX scrollmode check failed", texrunner)
117 class _texmessagenoaux(texmessage):
118 """allows for LaTeXs no-aux-file warning"""
120 __implements__ = _Itexmessage
122 def check(self, texrunner):
123 try:
124 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
125 texrunner.texmessageparsed = s1 + s2
126 except (IndexError, ValueError):
127 try:
128 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
129 os.sep,
130 texrunner.texfilename), 1)
131 texrunner.texmessageparsed = s1 + s2
132 except (IndexError, ValueError):
133 pass
136 class _texmessageinputmarker(texmessage):
137 """validates the PyXInputMarker"""
139 __implements__ = _Itexmessage
141 def check(self, texrunner):
142 try:
143 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
144 texrunner.texmessageparsed = s1 + s2
145 except (IndexError, ValueError):
146 raise TexResultError("PyXInputMarker expected", texrunner)
149 class _texmessagepyxbox(texmessage):
150 """validates the PyXBox output"""
152 __implements__ = _Itexmessage
154 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:")
156 def check(self, texrunner):
157 m = self.pattern.search(texrunner.texmessageparsed)
158 if m and m.group("page") == str(texrunner.page):
159 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
160 else:
161 raise TexResultError("PyXBox expected", texrunner)
164 class _texmessagepyxpageout(texmessage):
165 """validates the dvi shipout message (writing a page to the dvi file)"""
167 __implements__ = _Itexmessage
169 def check(self, texrunner):
170 try:
171 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
172 texrunner.texmessageparsed = s1 + s2
173 except (IndexError, ValueError):
174 raise TexResultError("PyXPageOutMarker expected", texrunner)
177 class _texmessagetexend(texmessage):
178 """validates TeX/LaTeX finish"""
180 __implements__ = _Itexmessage
182 def check(self, texrunner):
183 try:
184 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
185 texrunner.texmessageparsed = s1 + s2
186 except (IndexError, ValueError):
187 try:
188 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
189 os.sep,
190 texrunner.texfilename), 1)
191 texrunner.texmessageparsed = s1 + s2
192 except (IndexError, ValueError):
193 pass
195 # pass font size summary over to PyX user
196 fontpattern = re.compile(r"LaTeX Font Warning: Size substitutions with differences\s*\(Font\).* have occurred.\s*")
197 m = fontpattern.search(texrunner.texmessageparsed)
198 if m:
199 warnings.warn("LaTeX has detected Font Size substituion differences.")
200 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
202 # check for "(see the transcript file for additional information)"
203 try:
204 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
205 texrunner.texmessageparsed = s1 + s2
206 except (IndexError, ValueError):
207 pass
209 # check for "Output written on ...dvi (1 page, 220 bytes)."
210 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
211 m = dvipattern.search(texrunner.texmessageparsed)
212 if texrunner.page:
213 if not m:
214 raise TexResultError("TeX dvifile messages expected", texrunner)
215 if m.group("page") != str(texrunner.page):
216 raise TexResultError("wrong number of pages reported", texrunner)
217 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
218 else:
219 try:
220 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
221 texrunner.texmessageparsed = s1 + s2
222 except (IndexError, ValueError):
223 raise TexResultError("no dvifile expected", texrunner)
225 # check for "Transcript written on ...log."
226 try:
227 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
228 texrunner.texmessageparsed = s1 + s2
229 except (IndexError, ValueError):
230 raise TexResultError("TeX logfile message expected", texrunner)
233 class _texmessageemptylines(texmessage):
234 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines
235 also clear TeX interactive mode warning (Please type a command or say `\\end')
238 __implements__ = _Itexmessage
240 def check(self, texrunner):
241 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(r"(Please type a command or say `\end')", "")
242 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
243 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
246 class _texmessageload(texmessage):
247 """validates inclusion of arbitrary files
248 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
249 <fielname> is a readable file and other stuff can be anything
250 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
251 - this is not always wanted, but we just assume that file inclusion is fine"""
253 __implements__ = _Itexmessage
255 pattern = re.compile(r" *\((?P<filename>[^()\s\n]+)[^()]*\) *")
257 def baselevels(self, s, maxlevel=1, brackets="()"):
258 """strip parts of a string above a given bracket level
259 - return a modified (some parts might be removed) version of the string s
260 where all parts inside brackets with level higher than maxlevel are
261 removed
262 - if brackets do not match (number of left and right brackets is wrong
263 or at some points there were more right brackets than left brackets)
264 just return the unmodified string"""
265 level = 0
266 highestlevel = 0
267 res = ""
268 for c in s:
269 if c == brackets[0]:
270 level += 1
271 if level > highestlevel:
272 highestlevel = level
273 if level <= maxlevel:
274 res += c
275 if c == brackets[1]:
276 level -= 1
277 if level == 0 and highestlevel > 0:
278 return res
280 def check(self, texrunner):
281 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
282 if lowestbracketlevel is not None:
283 m = self.pattern.search(lowestbracketlevel)
284 while m:
285 if os.access(m.group("filename"), os.R_OK):
286 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
287 else:
288 break
289 m = self.pattern.search(lowestbracketlevel)
290 else:
291 texrunner.texmessageparsed = lowestbracketlevel
294 class _texmessageloadfd(_texmessageload):
295 """validates the inclusion of font description files (fd-files)
296 - works like _texmessageload
297 - filename must end with .fd and no further text is allowed"""
299 pattern = re.compile(r" *\((?P<filename>[^)]+.fd)\) *")
302 class _texmessagegraphicsload(_texmessageload):
303 """validates the inclusion of files as the graphics packages writes it
304 - works like _texmessageload, but using "<" and ">" as delimiters
305 - filename must end with .eps and no further text is allowed"""
307 pattern = re.compile(r" *<(?P<filename>[^>]+.eps)> *")
309 def baselevels(self, s, brackets="<>", **args):
310 return _texmessageload.baselevels(self, s, brackets=brackets, **args)
313 class _texmessageignore(_texmessageload):
314 """validates any TeX/LaTeX response
315 - this might be used, when the expression is ok, but no suitable texmessage
316 parser is available
317 - PLEASE: - consider writing suitable tex message parsers
318 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
320 __implements__ = _Itexmessage
322 def check(self, texrunner):
323 texrunner.texmessageparsed = ""
326 texmessage.start = _texmessagestart()
327 texmessage.noaux = _texmessagenoaux()
328 texmessage.inputmarker = _texmessageinputmarker()
329 texmessage.pyxbox = _texmessagepyxbox()
330 texmessage.pyxpageout = _texmessagepyxpageout()
331 texmessage.texend = _texmessagetexend()
332 texmessage.emptylines = _texmessageemptylines()
333 texmessage.load = _texmessageload()
334 texmessage.loadfd = _texmessageloadfd()
335 texmessage.graphicsload = _texmessagegraphicsload()
336 texmessage.ignore = _texmessageignore()
339 class _texmessageallwarning(texmessage):
340 """validates a given pattern 'pattern' as a warning 'warning'"""
342 def check(self, texrunner):
343 warnings.warn("ignoring all warnings:\n%s" % texrunner.texmessageparsed)
344 texrunner.texmessageparsed = ""
346 texmessage.allwarning = _texmessageallwarning()
349 class _texmessagepatternwarning(texmessage):
350 """validates a given pattern 'pattern' as a warning 'warning'"""
352 def __init__(self, warning, pattern):
353 self.warning = warning
354 self.pattern = pattern
356 def check(self, texrunner):
357 m = self.pattern.search(texrunner.texmessageparsed)
358 while m:
359 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
360 warnings.warn("%s:\n%s" % (self.warning, m.string[m.start(): m.end()].rstrip()))
361 m = self.pattern.search(texrunner.texmessageparsed)
363 texmessage.fontwarning = _texmessagepatternwarning("ignoring font warning", re.compile(r"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re.MULTILINE))
364 texmessage.boxwarning = _texmessagepatternwarning("ignoring overfull/underfull box warning", re.compile(r"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re.MULTILINE))
368 ###############################################################################
369 # textattrs
370 ###############################################################################
372 _textattrspreamble = ""
374 class textattr:
375 "a textattr defines a apply method, which modifies a (La)TeX expression"
377 class _localattr: pass
379 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
380 \newdimen\PyXraggedskipplus%
381 \def\PyXragged{\PyXraggedskipplus=4em%
382 \leftskip=0pt plus \PyXFlushHAlign\PyXraggedskipplus%
383 \rightskip=0pt plus \PyXraggedskipplus%
384 \advance\rightskip0pt plus -\PyXFlushHAlign\PyXraggedskipplus%
385 \parfillskip=0pt%
386 \spaceskip=0.333em%
387 \xspaceskip=0.5em%
388 \pretolerance=9999%
389 \tolerance=9999%
390 \parindent=0pt%
391 \hyphenpenalty=9999%
392 \exhyphenpenalty=9999}%
395 class boxhalign(attr.exclusiveattr, textattr, _localattr):
397 def __init__(self, aboxhalign):
398 self.boxhalign = aboxhalign
399 attr.exclusiveattr.__init__(self, boxhalign)
401 def apply(self, expr):
402 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
404 boxhalign.left = boxhalign(0)
405 boxhalign.center = boxhalign(0.5)
406 boxhalign.right = boxhalign(1)
407 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
410 class flushhalign(attr.exclusiveattr, textattr, _localattr):
412 def __init__(self, aflushhalign):
413 self.flushhalign = aflushhalign
414 attr.exclusiveattr.__init__(self, flushhalign)
416 def apply(self, expr):
417 return r"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.flushhalign, expr)
419 flushhalign.left = flushhalign(0)
420 flushhalign.center = flushhalign(0.5)
421 flushhalign.right = flushhalign(1)
422 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
425 class halign(attr.exclusiveattr, textattr, boxhalign, flushhalign, _localattr):
427 def __init__(self, aboxhalign, aflushhalign):
428 self.boxhalign = aboxhalign
429 self.flushhalign = aflushhalign
430 attr.exclusiveattr.__init__(self, halign)
432 def apply(self, expr):
433 return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr)
435 halign.left = halign(0, 0)
436 halign.center = halign(0.5, 0.5)
437 halign.right = halign(1, 1)
438 halign.clear = attr.clearclass(halign)
439 halign.boxleft = boxhalign.left
440 halign.boxcenter = boxhalign.center
441 halign.boxright = boxhalign.right
442 halign.flushleft = halign.raggedright = flushhalign.left
443 halign.flushcenter = halign.raggedcenter = flushhalign.center
444 halign.flushright = halign.raggedleft = flushhalign.right
447 class _mathmode(attr.attr, textattr, _localattr):
448 "math mode"
450 def apply(self, expr):
451 return r"$\displaystyle{%s}$" % expr
453 mathmode = _mathmode()
454 nomathmode = attr.clearclass(_mathmode)
457 class _phantom(attr.attr, textattr, _localattr):
458 "phantom text"
460 def apply(self, expr):
461 return r"\phantom{%s}" % expr
463 phantom = _phantom()
464 nophantom = attr.clearclass(_phantom)
467 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
469 class size(attr.sortbeforeattr, textattr, _localattr):
470 "font size"
472 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
473 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
474 raise RuntimeError("either specify sizeindex or sizename")
475 attr.sortbeforeattr.__init__(self, [_mathmode])
476 if sizeindex is not None:
477 if sizeindex >= 0 and sizeindex < sizelist.index(None):
478 self.size = sizelist[sizeindex]
479 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
480 self.size = sizelist[sizeindex]
481 else:
482 raise IndexError("index out of sizelist range")
483 else:
484 self.size = sizename
486 def apply(self, expr):
487 return r"\%s{%s}" % (self.size, expr)
489 size.tiny = size(-4)
490 size.scriptsize = size.script = size(-3)
491 size.footnotesize = size.footnote = size(-2)
492 size.small = size(-1)
493 size.normalsize = size.normal = size(0)
494 size.large = size(1)
495 size.Large = size(2)
496 size.LARGE = size(3)
497 size.huge = size(4)
498 size.Huge = size(5)
499 size.clear = attr.clearclass(size)
502 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
504 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
506 top = 1
507 middle = 2
508 bottom = 3
510 def __init__(self, width, baseline=top):
511 self.width = width * 72.27 / (unit.scale["x"] * 72)
512 self.baseline = baseline
513 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
515 def apply(self, expr):
516 if self.baseline == self.top:
517 return r"\linewidth%.5ftruept\vtop{\hsize\linewidth{}%s}" % (self.width, expr)
518 elif self.baseline == self.middle:
519 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)
520 elif self.baseline == self.bottom:
521 return r"\linewidth%.5ftruept\vbox{\hsize\linewidth{}%s}" % (self.width, expr)
522 else:
523 RuntimeError("invalid baseline argument")
525 parbox_pt.clear = attr.clearclass(parbox_pt)
527 class parbox(parbox_pt):
529 def __init__(self, width, **kwargs):
530 parbox_pt.__init__(self, unit.topt(width), **kwargs)
532 parbox.clear = parbox_pt.clear
535 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
537 class valign(attr.sortbeforeexclusiveattr, textattr):
539 def __init__(self):
540 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
542 class _valigntop(valign):
544 def apply(self, expr):
545 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
547 class _valignmiddle(valign):
549 def apply(self, expr):
550 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
552 class _valignbottom(valign):
554 def apply(self, expr):
555 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
557 valign.top = _valigntop()
558 valign.middle = _valignmiddle()
559 valign.bottom = _valignbottom()
560 valign.clear = attr.clearclass(valign)
561 valign.baseline = valign.clear
564 class _vshift(attr.sortbeforeattr, textattr):
566 def __init__(self):
567 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
569 class vshift(_vshift):
570 "vertical down shift by a fraction of a character height"
572 def __init__(self, lowerratio, heightstr="0"):
573 _vshift.__init__(self)
574 self.lowerratio = lowerratio
575 self.heightstr = heightstr
577 def apply(self, expr):
578 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
580 class _vshiftmathaxis(_vshift):
581 "vertical down shift by the height of the math axis"
583 def apply(self, expr):
584 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
587 vshift.bottomzero = vshift(0)
588 vshift.middlezero = vshift(0.5)
589 vshift.topzero = vshift(1)
590 vshift.mathaxis = _vshiftmathaxis()
591 vshift.clear = attr.clearclass(_vshift)
594 ###############################################################################
595 # texrunner
596 ###############################################################################
599 class _readpipe(threading.Thread):
600 """threaded reader of TeX/LaTeX output
601 - sets an event, when a specific string in the programs output is found
602 - sets an event, when the terminal ends"""
604 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
605 """initialize the reader
606 - pipe: file to be read from
607 - expectqueue: keeps the next InputMarker to be wait for
608 - gotevent: the "got InputMarker" event
609 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
610 - quitevent: the "end of terminal" event"""
611 threading.Thread.__init__(self)
612 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
613 self.pipe = pipe
614 self.expectqueue = expectqueue
615 self.gotevent = gotevent
616 self.gotqueue = gotqueue
617 self.quitevent = quitevent
618 self.expect = None
619 self.start()
621 def run(self):
622 """thread routine"""
623 read = self.pipe.readline() # read, what comes in
624 try:
625 self.expect = self.expectqueue.get_nowait() # read, what should be expected
626 except Queue.Empty:
627 pass
628 while len(read):
629 # universal EOL handling (convert everything into unix like EOLs)
630 # XXX is this necessary on pipes?
631 read = read.replace("\r", "").replace("\n", "") + "\n"
632 self.gotqueue.put(read) # report, whats read
633 if self.expect is not None and read.find(self.expect) != -1:
634 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
635 read = self.pipe.readline() # read again
636 try:
637 self.expect = self.expectqueue.get_nowait()
638 except Queue.Empty:
639 pass
640 # EOF reached
641 self.pipe.close()
642 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
643 raise RuntimeError("TeX/LaTeX finished unexpectedly")
644 self.quitevent.set()
647 class textbox(box.rect, canvas._canvas):
648 """basically a box.rect, but it contains a text created by the texrunner
649 - texrunner._text and texrunner.text return such an object
650 - _textbox instances can be inserted into a canvas
651 - the output is contained in a page of the dvifile available thru the texrunner"""
652 # TODO: shouldn't all boxes become canvases? how about inserts then?
654 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
656 - finishdvi is a method to be called to get the dvicanvas
657 (e.g. the finishdvi calls the setdvicanvas method)
658 - attrs are fillstyles"""
659 self.left = left
660 self.right = right
661 self.width = left + right
662 self.height = height
663 self.depth = depth
664 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
665 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
666 canvas._canvas.__init__(self)
667 self.finishdvi = finishdvi
668 self.dvicanvas = None
669 self.set(attrs)
670 self.insertdvicanvas = 0
672 def transform(self, *trafos):
673 if self.insertdvicanvas:
674 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
675 box.rect.transform(self, *trafos)
676 for trafo in trafos:
677 self.texttrafo = trafo * self.texttrafo
679 def setdvicanvas(self, dvicanvas):
680 if self.dvicanvas is not None:
681 raise RuntimeError("multiple call to setdvicanvas")
682 self.dvicanvas = dvicanvas
684 def ensuredvicanvas(self):
685 if self.dvicanvas is None:
686 self.finishdvi()
687 assert self.dvicanvas is not None, "finishdvi is broken"
688 if not self.insertdvicanvas:
689 self.insert(self.dvicanvas, [self.texttrafo])
690 self.insertdvicanvas = 1
692 def marker(self, marker):
693 self.ensuredvicanvas()
694 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
696 def registerPS(self, registry):
697 self.ensuredvicanvas()
698 canvas._canvas.registerPS(self, registry)
700 def registerPDF(self, registry):
701 self.ensuredvicanvas()
702 canvas._canvas.registerPDF(self, registry)
704 def outputPS(self, file, writer, context):
705 self.ensuredvicanvas()
706 canvas._canvas.outputPS(self, file, writer, context)
708 def outputPDF(self, file, writer, context):
709 self.ensuredvicanvas()
710 canvas._canvas.outputPDF(self, file, writer, context)
713 def _cleantmp(texrunner):
714 """get rid of temporary files
715 - function to be registered by atexit
716 - files contained in usefiles are kept"""
717 if texrunner.texruns: # cleanup while TeX is still running?
718 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
719 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
720 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
721 else:
722 texrunner.texinput.write("\n\\end\n")
723 texrunner.texinput.close() # close the input queue and
724 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
725 return # didn't got a quit from TeX -> we can't do much more
726 texrunner.texruns = 0
727 texrunner.texdone = 1
728 for usefile in texrunner.usefiles:
729 extpos = usefile.rfind(".")
730 try:
731 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
732 except OSError:
733 pass
734 for file in glob.glob("%s.*" % texrunner.texfilename):
735 try:
736 os.unlink(file)
737 except OSError:
738 pass
739 if texrunner.texdebug is not None:
740 try:
741 texrunner.texdebug.close()
742 texrunner.texdebug = None
743 except IOError:
744 pass
747 class texrunner:
748 """TeX/LaTeX interface
749 - runs TeX/LaTeX expressions instantly
750 - checks TeX/LaTeX response
751 - the instance variable texmessage stores the last TeX
752 response as a string
753 - the instance variable texmessageparsed stores a parsed
754 version of texmessage; it should be empty after
755 texmessage.check was called, otherwise a TexResultError
756 is raised
757 - the instance variable errordebug controls the verbose
758 level of TexResultError"""
760 defaulttexmessagesstart = [texmessage.start]
761 defaulttexmessagesdocclass = [texmessage.load]
762 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
763 defaulttexmessagesend = [texmessage.texend]
764 defaulttexmessagesdefaultpreamble = [texmessage.load]
765 defaulttexmessagesdefaultrun = [texmessage.loadfd, texmessage.graphicsload,
766 texmessage.fontwarning, texmessage.boxwarning]
768 def __init__(self, mode="tex",
769 lfs="10pt",
770 docclass="article",
771 docopt=None,
772 usefiles=[],
773 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
774 waitfortex=config.getint("text", "waitfortex", 60),
775 showwaitfortex=config.getint("text", "showwaitfortex", 5),
776 texipc=config.getboolean("text", "texipc", 0),
777 texdebug=None,
778 dvidebug=0,
779 errordebug=1,
780 pyxgraphics=1,
781 texmessagesstart=[],
782 texmessagesdocclass=[],
783 texmessagesbegindoc=[],
784 texmessagesend=[],
785 texmessagesdefaultpreamble=[],
786 texmessagesdefaultrun=[]):
787 mode = mode.lower()
788 if mode != "tex" and mode != "latex":
789 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
790 self.mode = mode
791 self.lfs = lfs
792 self.docclass = docclass
793 self.docopt = docopt
794 self.usefiles = usefiles
795 self.fontmaps = fontmaps
796 self.waitfortex = waitfortex
797 self.showwaitfortex = showwaitfortex
798 self.texipc = texipc
799 if texdebug is not None:
800 if texdebug[-4:] == ".tex":
801 self.texdebug = open(texdebug, "w")
802 else:
803 self.texdebug = open("%s.tex" % texdebug, "w")
804 else:
805 self.texdebug = None
806 self.dvidebug = dvidebug
807 self.errordebug = errordebug
808 self.pyxgraphics = pyxgraphics
809 self.texmessagesstart = texmessagesstart
810 self.texmessagesdocclass = texmessagesdocclass
811 self.texmessagesbegindoc = texmessagesbegindoc
812 self.texmessagesend = texmessagesend
813 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
814 self.texmessagesdefaultrun = texmessagesdefaultrun
816 self.texruns = 0
817 self.texdone = 0
818 self.preamblemode = 1
819 self.executeid = 0
820 self.page = 0
821 self.preambles = []
822 self.needdvitextboxes = [] # when texipc-mode off
823 self.dvifile = None
824 self.textboxesincluded = 0
825 savetempdir = tempfile.tempdir
826 tempfile.tempdir = os.curdir
827 self.texfilename = os.path.basename(tempfile.mktemp())
828 tempfile.tempdir = savetempdir
830 def waitforevent(self, event):
831 """waits verbosely with an timeout for an event
832 - observes an event while periodly while printing messages
833 - returns the status of the event (isSet)
834 - does not clear the event"""
835 if self.showwaitfortex:
836 waited = 0
837 hasevent = 0
838 while waited < self.waitfortex and not hasevent:
839 if self.waitfortex - waited > self.showwaitfortex:
840 event.wait(self.showwaitfortex)
841 waited += self.showwaitfortex
842 else:
843 event.wait(self.waitfortex - waited)
844 waited += self.waitfortex - waited
845 hasevent = event.isSet()
846 if not hasevent:
847 if waited < self.waitfortex:
848 warnings.warn("still waiting for %s after %i (of %i) seconds..." % (self.mode, waited, self.waitfortex))
849 else:
850 warnings.warn("the timeout of %i seconds expired and %s did not respond." % (waited, self.mode))
851 return hasevent
852 else:
853 event.wait(self.waitfortex)
854 return event.isSet()
856 def execute(self, expr, texmessages):
857 """executes expr within TeX/LaTeX
858 - if self.texruns is not yet set, TeX/LaTeX is initialized,
859 self.texruns is set and self.preamblemode is set
860 - the method must not be called, when self.texdone is already set
861 - expr should be a string or None
862 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
863 self.texdone becomes set
864 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
865 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
866 - texmessages is a list of texmessage instances"""
867 if not self.texruns:
868 if self.texdebug is not None:
869 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
870 self.texdebug.write("%% mode: %s\n" % self.mode)
871 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
872 for usefile in self.usefiles:
873 extpos = usefile.rfind(".")
874 try:
875 os.rename(usefile, self.texfilename + usefile[extpos:])
876 except OSError:
877 pass
878 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
879 texfile.write("\\relax%\n")
880 texfile.close()
881 if self.texipc:
882 ipcflag = " --ipc"
883 else:
884 ipcflag = ""
885 try:
886 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
887 except ValueError:
888 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
889 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
890 atexit.register(_cleantmp, self)
891 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
892 self.gotevent = threading.Event() # keeps the got inputmarker event
893 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
894 self.quitevent = threading.Event() # keeps for end of terminal event
895 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
896 self.texruns = 1
897 self.fontmap = dvifile.readfontmap(self.fontmaps.split())
898 oldpreamblemode = self.preamblemode
899 self.preamblemode = 1
900 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
901 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
902 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
903 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
904 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
905 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
906 "\\newdimen\\PyXDimenHAlignRT%\n" +
907 _textattrspreamble + # insert preambles for textattrs macros
908 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
909 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
910 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
911 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
912 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
913 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
914 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
915 "lt=\\the\\PyXDimenHAlignLT,"
916 "rt=\\the\\PyXDimenHAlignRT,"
917 "ht=\\the\\ht\\PyXBox,"
918 "dp=\\the\\dp\\PyXBox:}%\n"
919 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
920 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
921 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
922 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
923 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
924 self.defaulttexmessagesstart + self.texmessagesstart)
925 os.remove("%s.tex" % self.texfilename)
926 if self.mode == "tex":
927 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
928 lfsname = self.lfs
929 else:
930 lfsname = "%s.lfs" % self.lfs
931 for fulllfsname in [lfsname,
932 os.path.join(siteconfig.lfsdir, lfsname)]:
933 try:
934 lfsfile = open(fulllfsname, "r")
935 lfsdef = lfsfile.read()
936 lfsfile.close()
937 break
938 except IOError:
939 pass
940 else:
941 allfiles = (glob.glob("*.lfs") +
942 glob.glob(os.path.join(siteconfig.lfsdir, "*.lfs")))
943 lfsnames = []
944 for f in allfiles:
945 try:
946 open(f, "r").close()
947 lfsnames.append(os.path.basename(f)[:-4])
948 except IOError:
949 pass
950 lfsnames.sort()
951 if len(lfsnames):
952 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname, lfsnames))
953 else:
954 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname)
955 self.execute(lfsdef, [])
956 self.execute("\\normalsize%\n", [])
957 self.execute("\\newdimen\\linewidth%\n", [])
958 elif self.mode == "latex":
959 if self.pyxgraphics:
960 pyxdef = os.path.join(siteconfig.sharedir, "pyx.def")
961 try:
962 open(pyxdef, "r").close()
963 except IOError:
964 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
965 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
966 self.execute("\\makeatletter%\n"
967 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
968 "\\def\\ProcessOptions{%\n"
969 "\\def\\Gin@driver{" + pyxdef + "}%\n"
970 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
971 "\\saveProcessOptions}%\n"
972 "\\makeatother",
974 if self.docopt is not None:
975 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
976 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
977 else:
978 self.execute("\\documentclass{%s}" % self.docclass,
979 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
980 self.preamblemode = oldpreamblemode
981 self.executeid += 1
982 if expr is not None: # TeX/LaTeX should process expr
983 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
984 if self.preamblemode:
985 self.expr = ("%s%%\n" % expr +
986 "\\PyXInput{%i}%%\n" % self.executeid)
987 else:
988 self.page += 1
989 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
990 "\\PyXInput{%i}%%\n" % self.executeid)
991 else: # TeX/LaTeX should be finished
992 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
993 if self.mode == "latex":
994 self.expr = "\\end{document}%\n"
995 else:
996 self.expr = "\\end%\n"
997 if self.texdebug is not None:
998 self.texdebug.write(self.expr)
999 self.texinput.write(self.expr)
1000 gotevent = self.waitforevent(self.gotevent)
1001 self.gotevent.clear()
1002 if expr is None and gotevent: # TeX/LaTeX should have finished
1003 self.texruns = 0
1004 self.texdone = 1
1005 self.texinput.close() # close the input queue and
1006 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
1007 try:
1008 self.texmessage = ""
1009 while 1:
1010 self.texmessage += self.gotqueue.get_nowait()
1011 except Queue.Empty:
1012 pass
1013 self.texmessageparsed = self.texmessage
1014 if gotevent:
1015 if expr is not None:
1016 texmessage.inputmarker.check(self)
1017 if not self.preamblemode:
1018 texmessage.pyxbox.check(self)
1019 texmessage.pyxpageout.check(self)
1020 texmessages = attr.mergeattrs(texmessages)
1021 for t in texmessages:
1022 t.check(self)
1023 texmessage.emptylines.check(self)
1024 if len(self.texmessageparsed):
1025 raise TexResultError("unhandled TeX response (might be an error)", self)
1026 else:
1027 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
1029 def finishdvi(self):
1030 """finish TeX/LaTeX and read the dvifile
1031 - this method ensures that all textboxes can access their
1032 dvicanvas"""
1033 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
1034 dvifilename = "%s.dvi" % self.texfilename
1035 if not self.texipc:
1036 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
1037 page = 1
1038 for box in self.needdvitextboxes:
1039 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0]))
1040 page += 1
1041 if self.dvifile.readpage(None) is not None:
1042 raise RuntimeError("end of dvifile expected")
1043 self.dvifile = None
1044 self.needdvitextboxes = []
1046 def reset(self, reinit=0):
1047 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1048 if self.texruns:
1049 self.finishdvi()
1050 if self.texdebug is not None:
1051 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
1052 self.executeid = 0
1053 self.page = 0
1054 self.texdone = 0
1055 if reinit:
1056 self.preamblemode = 1
1057 for expr, texmessages in self.preambles:
1058 self.execute(expr, texmessages)
1059 if self.mode == "latex":
1060 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1061 self.preamblemode = 0
1062 else:
1063 self.preambles = []
1064 self.preamblemode = 1
1066 def set(self, mode=None,
1067 lfs=None,
1068 docclass=None,
1069 docopt=None,
1070 usefiles=None,
1071 fontmaps=None,
1072 waitfortex=None,
1073 showwaitfortex=None,
1074 texipc=None,
1075 texdebug=None,
1076 dvidebug=None,
1077 errordebug=None,
1078 pyxgraphics=None,
1079 texmessagesstart=None,
1080 texmessagesdocclass=None,
1081 texmessagesbegindoc=None,
1082 texmessagesend=None,
1083 texmessagesdefaultpreamble=None,
1084 texmessagesdefaultrun=None):
1085 """provide a set command for TeX/LaTeX settings
1086 - TeX/LaTeX must not yet been started
1087 - especially needed for the defaultrunner, where no access to
1088 the constructor is available"""
1089 if self.texruns:
1090 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1091 if mode is not None:
1092 mode = mode.lower()
1093 if mode != "tex" and mode != "latex":
1094 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1095 self.mode = mode
1096 if lfs is not None:
1097 self.lfs = lfs
1098 if docclass is not None:
1099 self.docclass = docclass
1100 if docopt is not None:
1101 self.docopt = docopt
1102 if usefiles is not None:
1103 self.usefiles = usefiles
1104 if fontmaps is not None:
1105 self.fontmaps = fontmaps
1106 if waitfortex is not None:
1107 self.waitfortex = waitfortex
1108 if showwaitfortex is not None:
1109 self.showwaitfortex = showwaitfortex
1110 if texipc is not None:
1111 self.texipc = texipc
1112 if texdebug is not None:
1113 if self.texdebug is not None:
1114 self.texdebug.close()
1115 if texdebug[-4:] == ".tex":
1116 self.texdebug = open(texdebug, "w")
1117 else:
1118 self.texdebug = open("%s.tex" % texdebug, "w")
1119 if dvidebug is not None:
1120 self.dvidebug = dvidebug
1121 if errordebug is not None:
1122 self.errordebug = errordebug
1123 if pyxgraphics is not None:
1124 self.pyxgraphics = pyxgraphics
1125 if errordebug is not None:
1126 self.errordebug = errordebug
1127 if texmessagesstart is not None:
1128 self.texmessagesstart = texmessagesstart
1129 if texmessagesdocclass is not None:
1130 self.texmessagesdocclass = texmessagesdocclass
1131 if texmessagesbegindoc is not None:
1132 self.texmessagesbegindoc = texmessagesbegindoc
1133 if texmessagesend is not None:
1134 self.texmessagesend = texmessagesend
1135 if texmessagesdefaultpreamble is not None:
1136 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1137 if texmessagesdefaultrun is not None:
1138 self.texmessagesdefaultrun = texmessagesdefaultrun
1140 def preamble(self, expr, texmessages=[]):
1141 r"""put something into the TeX/LaTeX preamble
1142 - in LaTeX, this is done before the \begin{document}
1143 (you might use \AtBeginDocument, when you're in need for)
1144 - it is not allowed to call preamble after calling the
1145 text method for the first time (for LaTeX this is needed
1146 due to \begin{document}; in TeX it is forced for compatibility
1147 (you should be able to switch from TeX to LaTeX, if you want,
1148 without breaking something)
1149 - preamble expressions must not create any dvi output
1150 - args might contain texmessage instances"""
1151 if self.texdone or not self.preamblemode:
1152 raise RuntimeError("preamble calls disabled due to previous text calls")
1153 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1154 self.execute(expr, texmessages)
1155 self.preambles.append((expr, texmessages))
1157 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:")
1159 def text(self, x, y, expr, textattrs=[], texmessages=[]):
1160 """create text by passing expr to TeX/LaTeX
1161 - returns a textbox containing the result from running expr thru TeX/LaTeX
1162 - the box center is set to x, y
1163 - *args may contain attr parameters, namely:
1164 - textattr instances
1165 - texmessage instances
1166 - trafo._trafo instances
1167 - style.fillstyle instances"""
1168 if expr is None:
1169 raise ValueError("None expression is invalid")
1170 if self.texdone:
1171 self.reset(reinit=1)
1172 first = 0
1173 if self.preamblemode:
1174 if self.mode == "latex":
1175 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1176 self.preamblemode = 0
1177 first = 1
1178 textattrs = attr.mergeattrs(textattrs) # perform cleans
1179 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1180 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1181 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1182 textattrs = attr.getattrs(textattrs, [textattr])
1183 # reverse loop over the merged textattrs (last is applied first)
1184 lentextattrs = len(textattrs)
1185 for i in range(lentextattrs):
1186 expr = textattrs[lentextattrs-1-i].apply(expr)
1187 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1188 if self.texipc:
1189 if first:
1190 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1191 match = self.PyXBoxPattern.search(self.texmessage)
1192 if not match or int(match.group("page")) != self.page:
1193 raise TexResultError("box extents not found", self)
1194 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt", "rt", "ht", "dp")]
1195 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1196 for t in trafos:
1197 box.reltransform(t)
1198 if self.texipc:
1199 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0]))
1200 else:
1201 self.needdvitextboxes.append(box)
1202 return box
1204 def text_pt(self, x, y, expr, *args, **kwargs):
1205 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1207 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1209 def textboxes(self, text, pageshapes):
1210 # this is some experimental code to put text into several boxes
1211 # while the bounding shape changes from box to box (rectangles only)
1212 # first we load sev.tex
1213 if not self.textboxesincluded:
1214 self.execute(r"\input textboxes.tex", [texmessage.load])
1215 self.textboxesincluded = 1
1216 # define page shapes
1217 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1218 pageshapes_str += "\\lohsizes={%\n"
1219 for hsize, vsize in pageshapes[1:]:
1220 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1221 pageshapes_str += "{\\relax}%\n}%\n"
1222 pageshapes_str += "\\lovsizes={%\n"
1223 for hsize, vsize in pageshapes[1:]:
1224 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1225 pageshapes_str += "{\\relax}%\n}%\n"
1226 page = 0
1227 parnos = []
1228 parshapes = []
1229 loop = 0
1230 while 1:
1231 self.execute(pageshapes_str, [])
1232 parnos_str = "}{".join(parnos)
1233 if parnos_str:
1234 parnos_str = "{%s}" % parnos_str
1235 parnos_str = "\\parnos={%s{\\relax}}%%\n" % parnos_str
1236 self.execute(parnos_str, [])
1237 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1238 self.execute(parshapes_str, [])
1239 self.execute("\\global\\count0=1%%\n"
1240 "\\global\\parno=0%%\n"
1241 "\\global\\myprevgraf=0%%\n"
1242 "\\global\\showprevgraf=0%%\n"
1243 "\\global\\outputtype=0%%\n"
1244 "\\global\\leastcost=10000000%%\n"
1245 "%s%%\n"
1246 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1247 if self.texipc:
1248 if self.dvifile is None:
1249 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1250 else:
1251 raise RuntimeError("textboxes currently needs texipc")
1252 lastparnos = parnos
1253 parnos = []
1254 lastparshapes = parshapes
1255 parshapes = []
1256 pages = 0
1257 lastpar = prevgraf = -1
1258 m = self.PyXVariableBoxPattern.search(self.texmessage)
1259 while m:
1260 pages += 1
1261 page = int(m.group("page"))
1262 assert page == pages
1263 par = int(m.group("par"))
1264 prevgraf = int(m.group("prevgraf"))
1265 if page <= len(pageshapes):
1266 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1267 else:
1268 width = 72.27/72*unit.topt(pageshapes[-1][0])
1269 if page < len(pageshapes):
1270 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1271 else:
1272 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1274 if par != lastpar:
1275 # a new paragraph is to be broken
1276 parnos.append(str(par))
1277 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf)])
1278 if len(parshape):
1279 parshape = " 0pt " + parshape
1280 parshapes.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf + 1, parshape, nextwidth))
1281 elif prevgraf == lastprevgraf:
1282 pass
1283 else:
1284 # we have to append the breaking of the previous paragraph
1285 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1286 oldparshape = oldparshape.split('}')[0]
1287 if len(parshape):
1288 oldparshape = " " + oldparshape
1289 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - lastprevgraf)])
1290 if len(parshape):
1291 parshape = " 0pt " + parshape
1292 else:
1293 parshape = " "
1294 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1295 lastpar = par
1296 lastprevgraf = prevgraf
1297 nextpos = m.end()
1298 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1299 result = []
1300 for i in range(pages):
1301 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1302 if parnos == lastparnos and parshapes == lastparshapes:
1303 return result
1304 loop += 1
1305 if loop > 100:
1306 raise TexResultError("Too many loops in textboxes ", texrunner)
1309 # the module provides an default texrunner and methods for direct access
1310 defaulttexrunner = texrunner()
1311 reset = defaulttexrunner.reset
1312 set = defaulttexrunner.set
1313 preamble = defaulttexrunner.preamble
1314 text = defaulttexrunner.text
1315 text_pt = defaulttexrunner.text_pt
1317 def escapestring(s):
1318 """escape special TeX/LaTeX characters
1320 Returns a string, where some special characters of standard
1321 TeX/LaTeX are replaced by appropriate escaped versions. Note
1322 that we cannot handle the three ASCII characters '{', '}',
1323 and '\' that way, since they do not occure in the TeX default
1324 encoding and thus are more likely to need some special handling.
1325 All other ASCII characters should usually (but not always)
1326 work."""
1328 # ASCII strings only
1329 s = str(s)
1330 i = 0
1331 while i < len(s):
1332 if s[i] in "$&#_%":
1333 s = s[:i] + "\\" + s[i:]
1334 i += 1
1335 elif s[i] in "^~":
1336 s = s[:i] + r"\string" + s[i:]
1337 i += 7
1338 i += 1
1339 return s