1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2007 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2006 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 import glob
, os
, threading
, Queue
, re
, tempfile
, atexit
, time
, warnings
25 import config
, siteconfig
, unit
, box
, canvas
, trafo
, version
, attr
, style
26 from pyx
.dvi
import dvifile
27 import bbox
as bboxmodule
29 ###############################################################################
31 # - please don't get confused:
32 # - there is a texmessage (and a texmessageparsed) attribute within the
33 # texrunner; it contains TeX/LaTeX response from the last command execution
34 # - instances of classes derived from the class texmessage are used to
35 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
36 # attribute of a texrunner instance
37 # - the multiple usage of the name texmessage might be removed in the future
38 # - texmessage instances should implement _Itexmessage
39 ###############################################################################
41 class TexResultError(RuntimeError):
42 """specialized texrunner exception class
43 - it is raised by texmessage instances, when a texmessage indicates an error
44 - it is raised by the texrunner itself, whenever there is a texmessage left
45 after all parsing of this message (by texmessage instances)
46 prints a detailed report about the problem
47 - the verbose level is controlled by texrunner.errordebug"""
49 def __init__(self
, description
, texrunner
):
50 if texrunner
.errordebug
>= 2:
51 self
.description
= ("%s\n" % description
+
52 "The expression passed to TeX was:\n"
53 " %s\n" % texrunner
.expr
.replace("\n", "\n ").rstrip() +
54 "The return message from TeX was:\n"
55 " %s\n" % texrunner
.texmessage
.replace("\n", "\n ").rstrip() +
56 "After parsing this message, the following was left:\n"
57 " %s" % texrunner
.texmessageparsed
.replace("\n", "\n ").rstrip())
58 elif texrunner
.errordebug
== 1:
59 firstlines
= texrunner
.texmessageparsed
.split("\n")
60 if len(firstlines
) > 5:
61 firstlines
= firstlines
[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
62 self
.description
= ("%s\n" % description
+
63 "The expression passed to TeX was:\n"
64 " %s\n" % texrunner
.expr
.replace("\n", "\n ").rstrip() +
65 "After parsing the return message from TeX, the following was left:\n" +
66 reduce(lambda x
, y
: "%s %s\n" % (x
,y
), firstlines
, "").rstrip())
68 self
.description
= description
71 return self
.description
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
)
101 raise TexResultError("TeX startup failed", texrunner
)
102 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[m
.end():]
104 # check for filename to be processed
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
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 _texmessagenofile(texmessage
):
118 """allows for LaTeXs no-file warning"""
120 __implements__
= _Itexmessage
122 def __init__(self
, fileending
):
123 self
.fileending
= fileending
125 def check(self
, texrunner
):
127 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s.%s." % (texrunner
.texfilename
, self
.fileending
), 1)
128 texrunner
.texmessageparsed
= s1
+ s2
129 except (IndexError, ValueError):
131 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s%s%s.%s." % (os
.curdir
,
133 texrunner
.texfilename
,
135 texrunner
.texmessageparsed
= s1
+ s2
136 except (IndexError, ValueError):
140 class _texmessageinputmarker(texmessage
):
141 """validates the PyXInputMarker"""
143 __implements__
= _Itexmessage
145 def check(self
, texrunner
):
147 s1
, s2
= texrunner
.texmessageparsed
.split("PyXInputMarker:executeid=%s:" % texrunner
.executeid
, 1)
148 texrunner
.texmessageparsed
= s1
+ s2
149 except (IndexError, ValueError):
150 raise TexResultError("PyXInputMarker expected", texrunner
)
153 class _texmessagepyxbox(texmessage
):
154 """validates the PyXBox output"""
156 __implements__
= _Itexmessage
158 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:")
160 def check(self
, texrunner
):
161 m
= self
.pattern
.search(texrunner
.texmessageparsed
)
162 if m
and m
.group("page") == str(texrunner
.page
):
163 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[:m
.start()] + texrunner
.texmessageparsed
[m
.end():]
165 raise TexResultError("PyXBox expected", texrunner
)
168 class _texmessagepyxpageout(texmessage
):
169 """validates the dvi shipout message (writing a page to the dvi file)"""
171 __implements__
= _Itexmessage
173 def check(self
, texrunner
):
175 s1
, s2
= texrunner
.texmessageparsed
.split("[80.121.88.%s]" % texrunner
.page
, 1)
176 texrunner
.texmessageparsed
= s1
+ s2
177 except (IndexError, ValueError):
178 raise TexResultError("PyXPageOutMarker expected", texrunner
)
181 class _texmessageend(texmessage
):
182 """validates TeX/LaTeX finish"""
184 __implements__
= _Itexmessage
186 def check(self
, texrunner
):
188 s1
, s2
= texrunner
.texmessageparsed
.split("(%s.aux)" % texrunner
.texfilename
, 1)
189 texrunner
.texmessageparsed
= s1
+ s2
190 except (IndexError, ValueError):
192 s1
, s2
= texrunner
.texmessageparsed
.split("(%s%s%s.aux)" % (os
.curdir
,
194 texrunner
.texfilename
), 1)
195 texrunner
.texmessageparsed
= s1
+ s2
196 except (IndexError, ValueError):
199 # check for "(see the transcript file for additional information)"
201 s1
, s2
= texrunner
.texmessageparsed
.split("(see the transcript file for additional information)", 1)
202 texrunner
.texmessageparsed
= s1
+ s2
203 except (IndexError, ValueError):
206 # check for "Output written on ...dvi (1 page, 220 bytes)."
207 dvipattern
= re
.compile(r
"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner
.texfilename
)
208 m
= dvipattern
.search(texrunner
.texmessageparsed
)
211 raise TexResultError("TeX dvifile messages expected", texrunner
)
212 if m
.group("page") != str(texrunner
.page
):
213 raise TexResultError("wrong number of pages reported", texrunner
)
214 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[:m
.start()] + texrunner
.texmessageparsed
[m
.end():]
217 s1
, s2
= texrunner
.texmessageparsed
.split("No pages of output.", 1)
218 texrunner
.texmessageparsed
= s1
+ s2
219 except (IndexError, ValueError):
220 raise TexResultError("no dvifile expected", texrunner
)
222 # check for "Transcript written on ...log."
224 s1
, s2
= texrunner
.texmessageparsed
.split("Transcript written on %s.log." % texrunner
.texfilename
, 1)
225 texrunner
.texmessageparsed
= s1
+ s2
226 except (IndexError, ValueError):
227 raise TexResultError("TeX logfile message expected", texrunner
)
230 class _texmessageemptylines(texmessage
):
231 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines
232 also clear TeX interactive mode warning (Please type a command or say `\\end')
235 __implements__
= _Itexmessage
237 def check(self
, texrunner
):
238 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.replace(r
"(Please type a command or say `\end')", "")
239 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.replace(" ", "")
240 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.replace("*\n", "")
241 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.replace("\n", "")
244 class _texmessageload(texmessage
):
245 """validates inclusion of arbitrary files
246 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
247 <filename> is a readable file and other stuff can be anything
248 - If the filename is enclosed in double quotes, it may contain blank space.
249 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
250 - this is not always wanted, but we just assume that file inclusion is fine"""
252 __implements__
= _Itexmessage
254 pattern
= re
.compile(r
"\([\"]?
(?P
<filename
>(?
:(?
<!\")[^
()\s
\n]+(?
!\"))|
[^
()\"\n]+)[\"]?
(?P
<additional
>[^
()]*)\
)")
256 def baselevels(self, s, maxlevel=1, brackets="()"):
257 """strip parts of a string above a given bracket level
258 - return a modified (some parts might be removed) version of the string s
259 where all parts inside brackets with level higher than maxlevel are
261 - if brackets do not match (number of left and right brackets is wrong
262 or at some points there were more right brackets than left brackets)
263 just return the unmodified string"""
270 if level > highestlevel:
272 if level <= maxlevel:
276 if level == 0 and highestlevel > 0:
279 def check(self, texrunner):
280 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
281 if lowestbracketlevel is not None:
282 m = self.pattern.search(lowestbracketlevel)
284 filename = m.group("filename
").replace("\n", "")
286 additional = m.group("additional
")
289 if (os.access(filename, os.R_OK) or
290 len(additional) and additional[0] == "\n" and os.access(filename+additional.split()[0], os.R_OK)):
291 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
294 m = self.pattern.search(lowestbracketlevel)
296 texrunner.texmessageparsed = lowestbracketlevel
299 class _texmessageloaddef(_texmessageload):
300 """validates the inclusion of font description files (fd-files)
301 - works like _texmessageload
302 - filename must end with .def or .fd and no further text is allowed"""
304 pattern = re.compile(r"\
((?P
<filename
>[^
)]+(\
.fd|\
.def))\
)")
306 def baselevels(self, s, **kwargs):
310 class _texmessagegraphicsload(_texmessageload):
311 """validates the inclusion of files as the graphics packages writes it
312 - works like _texmessageload, but using "<" and ">" as delimiters
313 - filename must end with .eps and no further text is allowed"""
315 pattern = re.compile(r"<(?P
<filename
>[^
>]+.eps
)>")
317 def baselevels(self, s, **kwargs):
321 class _texmessageignore(_texmessageload):
322 """validates any TeX/LaTeX response
323 - this might be used, when the expression is ok, but no suitable texmessage
325 - PLEASE: - consider writing suitable tex message parsers
326 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
328 __implements__ = _Itexmessage
330 def check(self, texrunner):
331 texrunner.texmessageparsed = ""
334 texmessage.start = _texmessagestart()
335 texmessage.noaux = _texmessagenofile("aux
")
336 texmessage.nonav = _texmessagenofile("nav
")
337 texmessage.end = _texmessageend()
338 texmessage.load = _texmessageload()
339 texmessage.loaddef = _texmessageloaddef()
340 texmessage.graphicsload = _texmessagegraphicsload()
341 texmessage.ignore = _texmessageignore()
344 texmessage.inputmarker = _texmessageinputmarker()
345 texmessage.pyxbox = _texmessagepyxbox()
346 texmessage.pyxpageout = _texmessagepyxpageout()
347 texmessage.emptylines = _texmessageemptylines()
350 class _texmessageallwarning(texmessage):
351 """validates a given pattern 'pattern' as a warning 'warning'"""
353 def check(self, texrunner):
354 if texrunner.texmessageparsed:
355 warnings.warn("ignoring all warnings
:\n%s" % texrunner.texmessageparsed)
356 texrunner.texmessageparsed = ""
358 texmessage.allwarning = _texmessageallwarning()
361 class texmessagepattern(texmessage):
362 """validates a given pattern and issue a warning (when set)"""
364 def __init__(self, pattern, warning=None):
365 self.pattern = pattern
366 self.warning = warning
368 def check(self, texrunner):
369 m = self.pattern.search(texrunner.texmessageparsed)
371 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
373 warnings.warn("%s:\n%s" % (self.warning, m.string[m.start(): m.end()].rstrip()))
374 m = self.pattern.search(texrunner.texmessageparsed)
376 texmessage.fontwarning = texmessagepattern(re.compile(r"^LaTeX Font
Warning: .*$
(\n^\
(Font\
).*$
)*", re.MULTILINE), "ignoring font warning
")
377 texmessage.boxwarning = texmessagepattern(re.compile(r"^
(Overfull|Underfull
) \\[hv
]box
.*$
(\n^
..*$
)*\n^$
\n", re.MULTILINE), "ignoring overfull
/underfull box warning
")
378 texmessage.rerunwarning = texmessagepattern(re.compile(r"^
(LaTeX
Warning: Label\
(s\
) may have changed\
. Rerun to get cross
-references right\s
*\
.)$
", re.MULTILINE), "ignoring rerun warning
")
382 ###############################################################################
384 ###############################################################################
386 _textattrspreamble = ""
389 "a textattr defines a
apply method
, which modifies
a (La
)TeX expression
"
391 class _localattr: pass
393 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
395 \leftskip=0pt plus \PyXFlushHAlign fil%
396 \rightskip=0pt plus 1fil%
397 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
403 \exhyphenpenalty=9999}%
406 class boxhalign(attr.exclusiveattr, textattr, _localattr):
408 def __init__(self, aboxhalign):
409 self.boxhalign = aboxhalign
410 attr.exclusiveattr.__init__(self, boxhalign)
412 def apply(self, expr):
413 return r"\gdef\PyXBoxHAlign
{%.5f
}%s" % (self.boxhalign, expr)
415 boxhalign.left = boxhalign(0)
416 boxhalign.center = boxhalign(0.5)
417 boxhalign.right = boxhalign(1)
418 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
421 class flushhalign(attr.exclusiveattr, textattr, _localattr):
423 def __init__(self, aflushhalign):
424 self.flushhalign = aflushhalign
425 attr.exclusiveattr.__init__(self, flushhalign)
427 def apply(self, expr):
428 return r"\gdef\PyXFlushHAlign
{%.5f
}\PyXragged
{}%s" % (self.flushhalign, expr)
430 flushhalign.left = flushhalign(0)
431 flushhalign.center = flushhalign(0.5)
432 flushhalign.right = flushhalign(1)
433 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
436 class halign(attr.exclusiveattr, textattr, boxhalign, flushhalign, _localattr):
438 def __init__(self, aboxhalign, aflushhalign):
439 self.boxhalign = aboxhalign
440 self.flushhalign = aflushhalign
441 attr.exclusiveattr.__init__(self, halign)
443 def apply(self, expr):
444 return r"\gdef\PyXBoxHAlign
{%.5f
}\gdef\PyXFlushHAlign
{%.5f
}\PyXragged
{}%s" % (self.boxhalign, self.flushhalign, expr)
446 halign.left = halign(0, 0)
447 halign.center = halign(0.5, 0.5)
448 halign.right = halign(1, 1)
449 halign.clear = attr.clearclass(halign)
450 halign.boxleft = boxhalign.left
451 halign.boxcenter = boxhalign.center
452 halign.boxright = boxhalign.right
453 halign.flushleft = halign.raggedright = flushhalign.left
454 halign.flushcenter = halign.raggedcenter = flushhalign.center
455 halign.flushright = halign.raggedleft = flushhalign.right
458 class _mathmode(attr.attr, textattr, _localattr):
461 def apply(self, expr):
462 return r"$\displaystyle
{%s}$
" % expr
464 mathmode = _mathmode()
465 clearmathmode = attr.clearclass(_mathmode)
468 class _phantom(attr.attr, textattr, _localattr):
471 def apply(self, expr):
472 return r"\phantom
{%s}" % expr
475 clearphantom = attr.clearclass(_phantom)
478 _textattrspreamble += "\\newbox
\\PyXBoxVBox
%\n\\newdimen
\\PyXDimenVBox
%\n"
480 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
486 def __init__(self, width, baseline=top):
487 self.width = width * 72.27 / (unit.scale["x
"] * 72)
488 self.baseline = baseline
489 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
491 def apply(self, expr):
492 if self.baseline == self.top:
493 return r"\linewidth
=%.5ftruept
\vtop
{\hsize
=\linewidth
\textwidth
=\linewidth
{}%s}" % (self.width, expr)
494 elif self.baseline == self.middle:
495 return r"\linewidth
=%.5ftruept\setbox\PyXBoxVBox
=\hbox
{{\vtop
{\hsize
=\linewidth
\textwidth
=\linewidth
{}%s}}}\PyXDimenVBox
=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox
=\hbox
{{\vbox
{\hsize
=\linewidth
\textwidth
=\linewidth
{}%s}}}\advance\PyXDimenVBox by
-0.5\dp\PyXBoxVBox\lower\PyXDimenVBox
\box\PyXBoxVBox
" % (self.width, expr, expr)
496 elif self.baseline == self.bottom:
497 return r"\linewidth
=%.5ftruept
\vbox
{\hsize
=\linewidth
\textwidth
=\linewidth
{}%s}" % (self.width, expr)
499 RuntimeError("invalid baseline argument
")
501 parbox_pt.clear = attr.clearclass(parbox_pt)
503 class parbox(parbox_pt):
505 def __init__(self, width, **kwargs):
506 parbox_pt.__init__(self, unit.topt(width), **kwargs)
508 parbox.clear = parbox_pt.clear
511 _textattrspreamble += "\\newbox
\\PyXBoxVAlign
%\n\\newdimen
\\PyXDimenVAlign
%\n"
513 class valign(attr.sortbeforeexclusiveattr, textattr):
515 def __init__(self, avalign):
516 self.valign = avalign
517 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
519 def apply(self, expr):
520 return r"\setbox\PyXBoxVAlign
=\hbox
{{%s}}\PyXDimenVAlign
=%.5f\ht\PyXBoxVAlign
\advance\PyXDimenVAlign by
-%.5f\dp\PyXBoxVAlign\lower\PyXDimenVAlign
\box\PyXBoxVAlign
" % (expr, 1-self.valign, self.valign)
522 valign.top = valign(0)
523 valign.middle = valign(0.5)
524 valign.bottom = valign(1)
525 valign.clear = valign.baseline = attr.clearclass(valign)
528 _textattrspreamble += "\\newdimen
\\PyXDimenVShift
%\n"
530 class _vshift(attr.sortbeforeattr, textattr):
533 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
535 def apply(self, expr):
536 return r"%s\setbox0\hbox
{{%s}}\lower\PyXDimenVShift
\box
0" % (self.setheightexpr(), expr)
538 class vshift(_vshift):
539 "vertical down shift by a fraction of a character height
"
541 def __init__(self, lowerratio, heightstr="0"):
542 _vshift.__init__(self)
543 self.lowerratio = lowerratio
544 self.heightstr = heightstr
546 def setheightexpr(self):
547 return r"\setbox0\hbox
{{%s}}\PyXDimenVShift
=%.5f\ht0
" % (self.heightstr, self.lowerratio)
549 class _vshiftmathaxis(_vshift):
550 "vertical down shift by the height of the math axis
"
552 def setheightexpr(self):
553 return r"\setbox0\hbox
{$
\vcenter
{\vrule width0pt
}$
}\PyXDimenVShift
=\ht0
"
556 vshift.bottomzero = vshift(0)
557 vshift.middlezero = vshift(0.5)
558 vshift.topzero = vshift(1)
559 vshift.mathaxis = _vshiftmathaxis()
560 vshift.clear = attr.clearclass(_vshift)
563 defaultsizelist = ["normalsize
", "large
", "Large
", "LARGE
", "huge
", "Huge
",
564 None, "tiny
", "scriptsize
", "footnotesize
", "small
"]
566 class size(attr.sortbeforeattr, textattr):
569 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
570 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
571 raise RuntimeError("either specify sizeindex
or sizename
")
572 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
573 if sizeindex is not None:
574 if sizeindex >= 0 and sizeindex < sizelist.index(None):
575 self.size = sizelist[sizeindex]
576 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
577 self.size = sizelist[sizeindex]
579 raise IndexError("index out of sizelist
range")
583 def apply(self, expr):
584 return r"\
%s{}%s" % (self.size, expr)
587 size.scriptsize = size.script = size(-3)
588 size.footnotesize = size.footnote = size(-2)
589 size.small = size(-1)
590 size.normalsize = size.normal = size(0)
596 size.clear = attr.clearclass(size)
599 ###############################################################################
601 ###############################################################################
604 class _readpipe(threading.Thread):
605 """threaded reader of TeX/LaTeX output
606 - sets an event, when a specific string in the programs output is found
607 - sets an event, when the terminal ends"""
609 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
610 """initialize the reader
611 - pipe: file to be read from
612 - expectqueue: keeps the next InputMarker to be wait for
613 - gotevent: the "got InputMarker
" event
614 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
615 - quitevent: the "end of terminal
" event"""
616 threading.Thread.__init__(self)
617 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
619 self.expectqueue = expectqueue
620 self.gotevent = gotevent
621 self.gotqueue = gotqueue
622 self.quitevent = quitevent
628 read = self.pipe.readline() # read, what comes in
630 self.expect = self.expectqueue.get_nowait() # read, what should be expected
634 # universal EOL handling (convert everything into unix like EOLs)
635 # XXX is this necessary on pipes?
636 read = read.replace("\r", "").replace("\n", "") + "\n"
637 self.gotqueue.put(read) # report, whats read
638 if self.expect is not None and read.find(self.expect) != -1:
639 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
640 read = self.pipe.readline() # read again
642 self.expect = self.expectqueue.get_nowait()
647 if self.expect is not None and self.expect.find("PyXInputMarker
") != -1:
648 raise RuntimeError("TeX
/LaTeX finished unexpectedly
")
652 class textbox(box.rect, canvas._canvas):
653 """basically a box.rect, but it contains a text created by the texrunner
654 - texrunner._text and texrunner.text return such an object
655 - _textbox instances can be inserted into a canvas
656 - the output is contained in a page of the dvifile available thru the texrunner"""
657 # TODO: shouldn't all boxes become canvases? how about inserts then?
659 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
661 - finishdvi is a method to be called to get the dvicanvas
662 (e.g. the finishdvi calls the setdvicanvas method)
663 - attrs are fillstyles"""
666 self.width = left + right
669 self.texttrafo = trafo.scale(unit.scale["x
"]).translated(x, y)
670 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
671 canvas._canvas.__init__(self, attrs)
672 self.finishdvi = finishdvi
673 self.dvicanvas = None
674 self.insertdvicanvas = 0
676 def transform(self, *trafos):
677 if self.insertdvicanvas:
678 raise RuntimeError("can
't apply transformation after dvicanvas was inserted")
679 box.rect.transform(self, *trafos)
681 self.texttrafo = trafo * self.texttrafo
683 def setdvicanvas(self, dvicanvas):
684 if self.dvicanvas is not None:
685 raise RuntimeError("multiple call to setdvicanvas")
686 self.dvicanvas = dvicanvas
688 def ensuredvicanvas(self):
689 if self.dvicanvas is None:
691 assert self.dvicanvas is not None, "finishdvi is broken"
692 if not self.insertdvicanvas:
693 self.insert(self.dvicanvas, [self.texttrafo])
694 self.insertdvicanvas = 1
696 def marker(self, marker):
697 self.ensuredvicanvas()
698 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
700 def processPS(self, file, writer, context, registry, bbox):
701 self.ensuredvicanvas()
702 abbox = bboxmodule.empty()
703 canvas._canvas.processPS(self, file, writer, context, registry, abbox)
704 bbox += box.rect.bbox(self)
706 def processPDF(self, file, writer, context, registry, bbox):
707 self.ensuredvicanvas()
708 abbox = bboxmodule.empty()
709 canvas._canvas.processPDF(self, file, writer, context, registry, abbox)
710 bbox += box.rect.bbox(self)
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")
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(".")
731 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
734 for file in glob.glob("%s.*" % texrunner.texfilename):
739 if texrunner.texdebug is not None:
741 texrunner.texdebug.close()
742 texrunner.texdebug = None
751 """TeX/LaTeX interface
752 - runs TeX/LaTeX expressions instantly
753 - checks TeX/LaTeX response
754 - the instance variable texmessage stores the last TeX
756 - the instance variable texmessageparsed stores a parsed
757 version of texmessage; it should be empty after
758 texmessage.check was called, otherwise a TexResultError
760 - the instance variable errordebug controls the verbose
761 level of TexResultError"""
763 defaulttexmessagesstart = [texmessage.start]
764 defaulttexmessagesdocclass = [texmessage.load]
765 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
766 defaulttexmessagesend = [texmessage.end, texmessage.fontwarning, texmessage.rerunwarning]
767 defaulttexmessagesdefaultpreamble = [texmessage.load]
768 defaulttexmessagesdefaultrun = [texmessage.loaddef, texmessage.graphicsload,
769 texmessage.fontwarning, texmessage.boxwarning]
771 def __init__(self, mode="tex",
776 waitfortex=config.getint("text", "waitfortex", 60),
777 showwaitfortex=config.getint("text", "showwaitfortex", 5),
778 texipc=config.getboolean("text", "texipc", 0),
784 texmessagesdocclass=[],
785 texmessagesbegindoc=[],
787 texmessagesdefaultpreamble=[],
788 texmessagesdefaultrun=[]):
790 if mode != "tex" and mode != "latex":
791 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
794 self.docclass = docclass
796 self.usefiles = usefiles[:]
797 self.waitfortex = waitfortex
798 self.showwaitfortex = showwaitfortex
800 if texdebug is not None:
801 if texdebug[-4:] == ".tex":
802 self.texdebug = open(texdebug, "w")
804 self.texdebug = open("%s.tex" % texdebug, "w")
807 self.dvidebug = dvidebug
808 self.errordebug = errordebug
809 self.pyxgraphics = pyxgraphics
810 self.texmessagesstart = texmessagesstart[:]
811 self.texmessagesdocclass = texmessagesdocclass[:]
812 self.texmessagesbegindoc = texmessagesbegindoc[:]
813 self.texmessagesend = texmessagesend[:]
814 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble[:]
815 self.texmessagesdefaultrun = texmessagesdefaultrun[:]
819 self.preamblemode = 1
823 self.needdvitextboxes = [] # when texipc-mode off
825 self.textboxesincluded = 0
826 savetempdir = tempfile.tempdir
827 tempfile.tempdir = os.curdir
828 self.texfilename = os.path.basename(tempfile.mktemp())
829 tempfile.tempdir = savetempdir
831 def waitforevent(self, event):
832 """waits verbosely with an timeout for an event
833 - observes an event while periodly while printing messages
834 - returns the status of the event (isSet)
835 - does not clear the event"""
836 if self.showwaitfortex:
839 while waited < self.waitfortex and not hasevent:
840 if self.waitfortex - waited > self.showwaitfortex:
841 event.wait(self.showwaitfortex)
842 waited += self.showwaitfortex
844 event.wait(self.waitfortex - waited)
845 waited += self.waitfortex - waited
846 hasevent = event.isSet()
848 if waited < self.waitfortex:
849 warnings.warn("still waiting for %s after %i (of %i) seconds..." % (self.mode, waited, self.waitfortex))
851 warnings.warn("the timeout of %i seconds expired and %s did not respond." % (waited, self.mode))
854 event.wait(self.waitfortex)
857 def execute(self, expr, texmessages):
858 """executes expr within TeX/LaTeX
859 - if self.texruns is not yet set, TeX/LaTeX is initialized,
860 self.texruns is set and self.preamblemode is set
861 - the method must not be called, when self.texdone is already set
862 - expr should be a string or None
863 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
864 self.texdone becomes set
865 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
866 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
867 - texmessages is a list of texmessage instances"""
869 if self.texdebug is not None:
870 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
871 self.texdebug.write("%% mode: %s\n" % self.mode)
872 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
873 for usefile in self.usefiles:
874 extpos = usefile.rfind(".")
876 os.rename(usefile, self.texfilename + usefile[extpos:])
879 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
880 texfile.write("\\relax%\n")
887 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
889 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
890 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
891 atexit.register(_cleantmp, self)
892 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
893 self.gotevent = threading.Event() # keeps the got inputmarker event
894 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
895 self.quitevent = threading.Event() # keeps for end of terminal event
896 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
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}}%", # write PyXMarker special into the dvi-file
924 self.defaulttexmessagesstart + self.texmessagesstart)
925 os.remove("%s.tex" % self.texfilename)
926 if self.mode == "tex":
929 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
932 lfsname = "%s.lfs" % self.lfs
933 for fulllfsname in [lfsname,
934 os.path.join(siteconfig.lfsdir, lfsname)]:
936 lfsfile = open(fulllfsname, "r")
937 lfsdef = lfsfile.read()
943 lfserror = "File '%s' is not available or not readable. " % lfsname
946 if lfserror is not None:
947 allfiles = (glob.glob("*.lfs") +
948 glob.glob(os.path.join(siteconfig.lfsdir, "*.lfs")))
953 lfsnames.append(os.path.basename(f)[:-4])
958 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror, lfsnames))
960 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror)
961 self.execute(lfsdef, [])
962 self.execute("\\normalsize%\n", [])
963 self.execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [])
964 elif self.mode == "latex":
966 pyxdef = os.path.join(siteconfig.sharedir, "pyx.def")
968 open(pyxdef, "r").close()
970 IOError("file 'pyx
.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
971 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
972 self.execute("\\makeatletter%\n"
973 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
974 "\\def\\ProcessOptions{%\n"
975 "\\def\\Gin@driver{" + pyxdef + "}%\n"
976 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
977 "\\saveProcessOptions}%\n"
980 if self.docopt is not None:
981 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
982 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
984 self.execute("\\documentclass{%s}" % self.docclass,
985 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
986 self.preamblemode = oldpreamblemode
988 if expr is not None: # TeX/LaTeX should process expr
989 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
990 if self.preamblemode:
991 self.expr = ("%s%%\n" % expr +
992 "\\PyXInput{%i}%%\n" % self.executeid)
995 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
996 "\\PyXInput{%i}%%\n" % self.executeid)
997 else: # TeX/LaTeX should be finished
998 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
999 if self.mode == "latex":
1000 self.expr = "\\end{document}%\n"
1002 self.expr = "\\end%\n"
1003 if self.texdebug is not None:
1004 self.texdebug.write(self.expr)
1005 self.texinput.write(self.expr)
1006 gotevent = self.waitforevent(self.gotevent)
1007 self.gotevent.clear()
1008 if expr is None and gotevent: # TeX/LaTeX should have finished
1011 self.texinput.close() # close the input queue and
1012 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
1014 self.texmessage = ""
1016 self.texmessage += self.gotqueue.get_nowait()
1019 self.texmessage = self.texmessage.replace("\r\n", "\n").replace("\r", "\n")
1020 self.texmessageparsed = self.texmessage
1022 if expr is not None:
1023 texmessage.inputmarker.check(self)
1024 if not self.preamblemode:
1025 texmessage.pyxbox.check(self)
1026 texmessage.pyxpageout.check(self)
1027 texmessages = attr.mergeattrs(texmessages)
1028 for t in texmessages:
1030 keeptexmessageparsed = self.texmessageparsed
1031 texmessage.emptylines.check(self)
1032 if len(self.texmessageparsed):
1033 self.texmessageparsed = keeptexmessageparsed
1034 raise TexResultError("unhandled TeX response (might be an error)", self)
1036 raise TexResultError("TeX didn't respond
as expected within the timeout
period (%i seconds
)." % self.waitfortex, self)
1038 def finishdvi(self, ignoretail=0):
1039 """finish TeX/LaTeX and read the dvifile
1040 - this method ensures that all textboxes can access their
1042 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
1043 dvifilename = "%s.dvi
" % self.texfilename
1045 self.dvifile = dvifile.DVIfile(dvifilename, debug=self.dvidebug)
1047 for box in self.needdvitextboxes:
1048 box.setdvicanvas(self.dvifile.readpage([ord("P
"), ord("y
"), ord("X
"), page, 0, 0, 0, 0, 0, 0], fontmap=box.fontmap))
1050 if not ignoretail and self.dvifile.readpage(None) is not None:
1051 raise RuntimeError("end of dvifile expected
")
1053 self.needdvitextboxes = []
1055 def reset(self, reinit=0):
1056 "resets the tex runner to its initial
state (upto its record to old dvi
file(s
))"
1059 if self.texdebug is not None:
1060 self.texdebug.write("%s\n%% preparing restart of
%s\n" % ("%"*80, self.mode))
1065 self.preamblemode = 1
1066 for expr, texmessages in self.preambles:
1067 self.execute(expr, texmessages)
1068 if self.mode == "latex
":
1069 self.execute("\\begin{document}
", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1070 self.preamblemode = 0
1073 self.preamblemode = 1
1075 def set(self, mode=_unset,
1081 showwaitfortex=_unset,
1087 texmessagesstart=_unset,
1088 texmessagesdocclass=_unset,
1089 texmessagesbegindoc=_unset,
1090 texmessagesend=_unset,
1091 texmessagesdefaultpreamble=_unset,
1092 texmessagesdefaultrun=_unset):
1093 """provide a set command for TeX/LaTeX settings
1094 - TeX/LaTeX must not yet been started
1095 - especially needed for the defaultrunner, where no access to
1096 the constructor is available"""
1098 raise RuntimeError("set not allowed
-- TeX
/LaTeX already started
")
1099 if mode is not _unset:
1101 if mode != "tex
" and mode != "latex
":
1102 raise ValueError("mode
\"TeX
\" or \"LaTeX
\" expected
")
1104 if lfs is not _unset:
1106 if docclass is not _unset:
1107 self.docclass = docclass
1108 if docopt is not _unset:
1109 self.docopt = docopt
1110 if usefiles is not _unset:
1111 self.usefiles = usefiles
1112 if waitfortex is not _unset:
1113 self.waitfortex = waitfortex
1114 if showwaitfortex is not _unset:
1115 self.showwaitfortex = showwaitfortex
1116 if texipc is not _unset:
1117 self.texipc = texipc
1118 if texdebug is not _unset:
1119 if self.texdebug is not None:
1120 self.texdebug.close()
1121 if texdebug[-4:] == ".tex
":
1122 self.texdebug = open(texdebug, "w
")
1124 self.texdebug = open("%s.tex
" % texdebug, "w
")
1125 if dvidebug is not _unset:
1126 self.dvidebug = dvidebug
1127 if errordebug is not _unset:
1128 self.errordebug = errordebug
1129 if pyxgraphics is not _unset:
1130 self.pyxgraphics = pyxgraphics
1131 if errordebug is not _unset:
1132 self.errordebug = errordebug
1133 if texmessagesstart is not _unset:
1134 self.texmessagesstart = texmessagesstart
1135 if texmessagesdocclass is not _unset:
1136 self.texmessagesdocclass = texmessagesdocclass
1137 if texmessagesbegindoc is not _unset:
1138 self.texmessagesbegindoc = texmessagesbegindoc
1139 if texmessagesend is not _unset:
1140 self.texmessagesend = texmessagesend
1141 if texmessagesdefaultpreamble is not _unset:
1142 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1143 if texmessagesdefaultrun is not _unset:
1144 self.texmessagesdefaultrun = texmessagesdefaultrun
1146 def preamble(self, expr, texmessages=[]):
1147 r"""put something into the TeX/LaTeX preamble
1148 - in LaTeX, this is done before the \begin{document}
1149 (you might use \AtBeginDocument, when you're in need for)
1150 - it is not allowed to call preamble after calling the
1151 text method for the first time (for LaTeX this is needed
1152 due to \begin{document}; in TeX it is forced for compatibility
1153 (you should be able to switch from TeX to LaTeX, if you want,
1154 without breaking something)
1155 - preamble expressions must not create any dvi output
1156 - args might contain texmessage instances"""
1157 if self.texdone or not self.preamblemode:
1158 raise RuntimeError("preamble calls disabled due to previous text calls
")
1159 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1160 self.execute(expr, texmessages)
1161 self.preambles.append((expr, texmessages))
1163 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
:")
1165 def text(self, x, y, expr, textattrs=[], texmessages=[], fontmap=None):
1166 """create text by passing expr to TeX/LaTeX
1167 - returns a textbox containing the result from running expr thru TeX/LaTeX
1168 - the box center is set to x, y
1169 - *args may contain attr parameters, namely:
1170 - textattr instances
1171 - texmessage instances
1172 - trafo._trafo instances
1173 - style.fillstyle instances"""
1175 raise ValueError("None expression
is invalid
")
1177 self.reset(reinit=1)
1179 if self.preamblemode:
1180 if self.mode == "latex
":
1181 self.execute("\\begin{document}
", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1182 self.preamblemode = 0
1184 textattrs = attr.mergeattrs(textattrs) # perform cleans
1185 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1186 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1187 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1188 textattrs = attr.getattrs(textattrs, [textattr])
1189 # reverse loop over the merged textattrs (last is applied first)
1190 lentextattrs = len(textattrs)
1191 for i in range(lentextattrs):
1192 expr = textattrs[lentextattrs-1-i].apply(expr)
1194 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1195 except TexResultError:
1196 self.finishdvi(ignoretail=1)
1200 self.dvifile = dvifile.DVIfile("%s.dvi
" % self.texfilename, debug=self.dvidebug)
1201 match = self.PyXBoxPattern.search(self.texmessage)
1202 if not match or int(match.group("page
")) != self.page:
1203 raise TexResultError("box extents
not found
", self)
1204 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt
", "rt
", "ht
", "dp
")]
1205 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1207 box.reltransform(t) # TODO: should trafos really use reltransform???
1208 # this is quite different from what we do elsewhere!!!
1209 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1211 box.setdvicanvas(self.dvifile.readpage([ord("P
"), ord("y
"), ord("X
"), self.page, 0, 0, 0, 0, 0, 0], fontmap=fontmap))
1213 box.fontmap = fontmap
1214 self.needdvitextboxes.append(box)
1217 def text_pt(self, x, y, expr, *args, **kwargs):
1218 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1220 PyXVariableBoxPattern = re.compile(r"PyXVariableBox
:page
=(?P
<page
>\d
+),par
=(?P
<par
>\d
+),prevgraf
=(?P
<prevgraf
>\d
+):")
1222 def textboxes(self, text, pageshapes):
1223 # this is some experimental code to put text into several boxes
1224 # while the bounding shape changes from box to box (rectangles only)
1225 # first we load sev.tex
1226 if not self.textboxesincluded:
1227 self.execute(r"\
input textboxes
.tex
", [texmessage.load])
1228 self.textboxesincluded = 1
1229 # define page shapes
1230 pageshapes_str = "\\hsize
=%.5ftruept
%%\n\\vsize
=%.5ftruept
%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1231 pageshapes_str += "\\lohsizes
={%\n"
1232 for hsize, vsize in pageshapes[1:]:
1233 pageshapes_str += "{\\global\\hsize
=%.5ftruept
}%%\n" % (72.27/72*unit.topt(hsize))
1234 pageshapes_str += "{\\relax
}%\n}%\n"
1235 pageshapes_str += "\\lovsizes
={%\n"
1236 for hsize, vsize in pageshapes[1:]:
1237 pageshapes_str += "{\\global\\vsize
=%.5ftruept
}%%\n" % (72.27/72*unit.topt(vsize))
1238 pageshapes_str += "{\\relax
}%\n}%\n"
1244 self.execute(pageshapes_str, [])
1245 parnos_str = "}{".join(parnos)
1247 parnos_str = "{%s}" % parnos_str
1248 parnos_str = "\\parnos
={%s{\\relax
}}%%\n" % parnos_str
1249 self.execute(parnos_str, [])
1250 parshapes_str = "\\parshapes
={%%\n%s%%\n{\\relax
}%%\n}%%\n" % "%\n".join(parshapes)
1251 self.execute(parshapes_str, [])
1252 self.execute("\\global\\count0
=1%%\n"
1253 "\\global\\parno
=0%%\n"
1254 "\\global\\myprevgraf
=0%%\n"
1255 "\\global\\showprevgraf
=0%%\n"
1256 "\\global\\outputtype
=0%%\n"
1257 "\\global\\leastcost
=10000000%%\n"
1259 "\\vfill
\\supereject
%%\n" % text, [texmessage.ignore])
1261 if self.dvifile is None:
1262 self.dvifile = dvifile.DVIfile("%s.dvi
" % self.texfilename, debug=self.dvidebug)
1264 raise RuntimeError("textboxes currently needs texipc
")
1267 lastparshapes = parshapes
1270 lastpar = prevgraf = -1
1271 m = self.PyXVariableBoxPattern.search(self.texmessage)
1274 page = int(m.group("page
"))
1275 assert page == pages
1276 par = int(m.group("par
"))
1277 prevgraf = int(m.group("prevgraf
"))
1278 if page <= len(pageshapes):
1279 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1281 width = 72.27/72*unit.topt(pageshapes[-1][0])
1282 if page < len(pageshapes):
1283 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1285 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1288 # a new paragraph is to be broken
1289 parnos.append(str(par))
1290 parshape = " 0pt
".join(["%.5ftruept
" % width for i in range(prevgraf)])
1292 parshape = " 0pt
" + parshape
1293 parshapes.append("{\\parshape
%i%s 0pt
%.5ftruept
}" % (prevgraf + 1, parshape, nextwidth))
1294 elif prevgraf == lastprevgraf:
1297 # we have to append the breaking of the previous paragraph
1298 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1299 oldparshape = oldparshape.split('}')[0]
1301 oldparshape = " " + oldparshape
1302 parshape = " 0pt
".join(["%.5ftruept
" % width for i in range(prevgraf - lastprevgraf)])
1304 parshape = " 0pt
" + parshape
1307 parshapes[-1] = "{\\parshape
%i%s%s 0pt
%.5ftruept
}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1309 lastprevgraf = prevgraf
1311 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1313 for i in range(pages):
1314 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1315 if parnos == lastparnos and parshapes == lastparshapes:
1319 raise TexResultError("Too many loops
in textboxes
", texrunner)
1322 # the module provides an default texrunner and methods for direct access
1323 defaulttexrunner = texrunner()
1324 reset = defaulttexrunner.reset
1325 set = defaulttexrunner.set
1326 preamble = defaulttexrunner.preamble
1327 text = defaulttexrunner.text
1328 text_pt = defaulttexrunner.text_pt
1330 def escapestring(s, replace={" ": "~
",
1342 "\\": "{$\setminus$}",
1344 "escape all ascii characters such that they are printable by TeX/LaTeX"
1347 if not 32 <= ord(s
[i
]) < 127:
1348 raise ValueError("escapestring function handles ascii strings only")
1355 s
= s
[:i
] + r
+ s
[i
+1:]