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 errno
, 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 \raiseerror -- just to be sure that communication works
106 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
107 except (IndexError, ValueError):
108 raise TexResultError("TeX scrollmode check failed", texrunner
)
111 class _texmessagenofile(texmessage
):
112 """allows for LaTeXs no-file warning"""
114 __implements__
= _Itexmessage
116 def __init__(self
, fileending
):
117 self
.fileending
= fileending
119 def check(self
, texrunner
):
121 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s.%s." % (texrunner
.texfilename
, self
.fileending
), 1)
122 texrunner
.texmessageparsed
= s1
+ s2
123 except (IndexError, ValueError):
125 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s%s%s.%s." % (os
.curdir
,
127 texrunner
.texfilename
,
129 texrunner
.texmessageparsed
= s1
+ s2
130 except (IndexError, ValueError):
134 class _texmessageinputmarker(texmessage
):
135 """validates the PyXInputMarker"""
137 __implements__
= _Itexmessage
139 def check(self
, texrunner
):
141 s1
, s2
= texrunner
.texmessageparsed
.split("PyXInputMarker:executeid=%s:" % texrunner
.executeid
, 1)
142 texrunner
.texmessageparsed
= s1
+ s2
143 except (IndexError, ValueError):
144 raise TexResultError("PyXInputMarker expected", texrunner
)
147 class _texmessagepyxbox(texmessage
):
148 """validates the PyXBox output"""
150 __implements__
= _Itexmessage
152 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:")
154 def check(self
, texrunner
):
155 m
= self
.pattern
.search(texrunner
.texmessageparsed
)
156 if m
and m
.group("page") == str(texrunner
.page
):
157 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[:m
.start()] + texrunner
.texmessageparsed
[m
.end():]
159 raise TexResultError("PyXBox expected", texrunner
)
162 class _texmessagepyxpageout(texmessage
):
163 """validates the dvi shipout message (writing a page to the dvi file)"""
165 __implements__
= _Itexmessage
167 def check(self
, texrunner
):
169 s1
, s2
= texrunner
.texmessageparsed
.split("[80.121.88.%s]" % texrunner
.page
, 1)
170 texrunner
.texmessageparsed
= s1
+ s2
171 except (IndexError, ValueError):
172 raise TexResultError("PyXPageOutMarker expected", texrunner
)
175 class _texmessageend(texmessage
):
176 """validates TeX/LaTeX finish"""
178 __implements__
= _Itexmessage
180 auxPattern
= re
.compile(r
"\(([^()]+\.aux|\"[^
\"]+\
.aux
\")\
)")
182 def check(self, texrunner):
183 m = self.auxPattern.search(texrunner.texmessageparsed)
185 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
187 # check for "(see the transcript
file for additional information
)"
189 s1, s2 = texrunner.texmessageparsed.split("(see the transcript
file for additional information
)", 1)
190 texrunner.texmessageparsed = s1 + s2
191 except (IndexError, ValueError):
194 # check for "Output written on
...dvi (1 page
, 220 bytes
)."
195 dvipattern = re.compile(r"Output written on
%s\
.dvi \
((?P
<page
>\d
+) pages?
, \d
+ bytes\
)\
." % texrunner.texfilename)
196 m = dvipattern.search(texrunner.texmessageparsed)
199 raise TexResultError("TeX dvifile messages expected
", texrunner)
200 if m.group("page
") != str(texrunner.page):
201 raise TexResultError("wrong number of pages reported
", texrunner)
202 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
205 s1, s2 = texrunner.texmessageparsed.split("No pages of output
.", 1)
206 texrunner.texmessageparsed = s1 + s2
207 except (IndexError, ValueError):
208 raise TexResultError("no dvifile expected
", texrunner)
210 # check for "Transcript written on
...log
."
212 s1, s2 = texrunner.texmessageparsed.split("Transcript written on
%s.log
." % texrunner.texfilename, 1)
213 texrunner.texmessageparsed = s1 + s2
214 except (IndexError, ValueError):
215 raise TexResultError("TeX logfile message expected
", texrunner)
218 class _texmessageemptylines(texmessage):
219 """validates "*-only
" (TeX/LaTeX input marker in interactive mode) and empty lines
220 also clear TeX interactive mode warning (Please type a command or say `\\end')
223 __implements__ = _Itexmessage
225 def check(self, texrunner):
226 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(r"(Please
type a command
or say `\end
')", "")
227 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(" ", "")
228 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
229 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
232 class _texmessageload(texmessage):
233 """validates inclusion of arbitrary files
234 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
235 <filename> is a readable file and other stuff can be anything
236 - If the filename is enclosed in double quotes, it may contain blank space.
237 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
238 - this is not always wanted, but we just assume that file inclusion is fine"""
240 __implements__ = _Itexmessage
242 pattern = re.compile(r"\([\"]?(?P<filename>(?:(?<!\")[^()\s\n]+(?!\"))|[^\"\n]+)[\"]?(?P<additional>[^()]*)\)")
244 def baselevels(self, s, maxlevel=1, brackets="()", quotes='""'):
245 """strip parts of a string above a given bracket level
246 - return a modified (some parts might be removed) version of the string s
247 where all parts inside brackets with level higher than maxlevel are
249 - if brackets do not match (number of left and right brackets is wrong
250 or at some points there were more right brackets than left brackets)
251 just return the unmodified string
252 - a quoted string immediately followed after a bracket is left untouched
253 even if it contains quotes itself"""
258 for i, c in enumerate(s):
259 if quotes and level <= maxlevel:
260 if not inquote and c == quotes[0] and i and s[i-1] == brackets[0]:
262 elif inquote and c == quotes[1]:
269 if level > highestlevel:
271 if level <= maxlevel:
275 if level == 0 and highestlevel > 0:
278 def check(self, texrunner):
279 search = self.baselevels(texrunner.texmessageparsed)
281 if search is not None:
282 m = self.pattern.search(search)
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 res.append(search[:m.start()])
293 res.append(search[:m.end()])
294 search = search[m.end():]
295 m = self.pattern.search(search)
298 texrunner.texmessageparsed = "".join(res)
301 class _texmessageloaddef(_texmessageload):
302 """validates the inclusion of font description files (fd-files)
303 - works like _texmessageload
304 - filename must end with .def or .fd and no further text is allowed"""
306 pattern = re.compile(r"\((?P<filename>[^)]+(\.fd|\.def))\)")
308 def baselevels(self, s, **kwargs):
312 class _texmessagegraphicsload(_texmessageload):
313 """validates the inclusion of files as the graphics packages writes it
314 - works like _texmessageload, but using "<" and ">" as delimiters
315 - filename must end with .eps and no further text is allowed"""
317 pattern = re.compile(r"<(?P<filename>[^>]+.eps)>")
319 def baselevels(self, s, **kwargs):
323 class _texmessageignore(_texmessageload):
324 """validates any TeX/LaTeX response
325 - this might be used, when the expression is ok, but no suitable texmessage
327 - PLEASE: - consider writing suitable tex message parsers
328 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
330 __implements__ = _Itexmessage
332 def check(self, texrunner):
333 texrunner.texmessageparsed = ""
336 texmessage.start = _texmessagestart()
337 texmessage.noaux = _texmessagenofile("aux")
338 texmessage.nonav = _texmessagenofile("nav")
339 texmessage.end = _texmessageend()
340 texmessage.load = _texmessageload()
341 texmessage.loaddef = _texmessageloaddef()
342 texmessage.graphicsload = _texmessagegraphicsload()
343 texmessage.ignore = _texmessageignore()
346 texmessage.inputmarker = _texmessageinputmarker()
347 texmessage.pyxbox = _texmessagepyxbox()
348 texmessage.pyxpageout = _texmessagepyxpageout()
349 texmessage.emptylines = _texmessageemptylines()
352 class _texmessageallwarning(texmessage):
353 """validates a given pattern 'pattern
' as a warning 'warning
'"""
355 def check(self, texrunner):
356 if texrunner.texmessageparsed:
357 warnings.warn("ignoring all warnings:\n%s" % texrunner.texmessageparsed)
358 texrunner.texmessageparsed = ""
360 texmessage.allwarning = _texmessageallwarning()
363 class texmessagepattern(texmessage):
364 """validates a given pattern and issue a warning (when set)"""
366 def __init__(self, pattern, warning=None):
367 self.pattern = pattern
368 self.warning = warning
370 def check(self, texrunner):
371 m = self.pattern.search(texrunner.texmessageparsed)
373 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
375 warnings.warn("%s:\n%s" % (self.warning, m.string[m.start(): m.end()].rstrip()))
376 m = self.pattern.search(texrunner.texmessageparsed)
378 texmessage.fontwarning = texmessagepattern(re.compile(r"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re.MULTILINE), "ignoring font warning")
379 texmessage.boxwarning = texmessagepattern(re.compile(r"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re.MULTILINE), "ignoring overfull/underfull box warning")
380 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")
381 texmessage.nobblwarning = texmessagepattern(re.compile(r"^[\s\*]*(No file .*\.bbl.)\s*", re.MULTILINE), "ignoring no-bbl warning")
385 ###############################################################################
387 ###############################################################################
389 _textattrspreamble = ""
392 "a textattr defines a apply method, which modifies a (La)TeX expression"
394 class _localattr: pass
396 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
398 \leftskip=0pt plus \PyXFlushHAlign fil%
399 \rightskip=0pt plus 1fil%
400 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
406 \exhyphenpenalty=9999}%
409 class boxhalign(attr.exclusiveattr, textattr, _localattr):
411 def __init__(self, aboxhalign):
412 self.boxhalign = aboxhalign
413 attr.exclusiveattr.__init__(self, boxhalign)
415 def apply(self, expr):
416 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
418 boxhalign.left = boxhalign(0)
419 boxhalign.center = boxhalign(0.5)
420 boxhalign.right = boxhalign(1)
421 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass
for boxhalign since it can
't clear a halign's boxhalign
424 class flushhalign(attr
.exclusiveattr
, textattr
, _localattr
):
426 def __init__(self
, aflushhalign
):
427 self
.flushhalign
= aflushhalign
428 attr
.exclusiveattr
.__init
__(self
, flushhalign
)
430 def apply(self
, expr
):
431 return r
"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.flushhalign
, expr
)
433 flushhalign
.left
= flushhalign(0)
434 flushhalign
.center
= flushhalign(0.5)
435 flushhalign
.right
= flushhalign(1)
436 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
439 class halign(attr
.exclusiveattr
, textattr
, boxhalign
, flushhalign
, _localattr
):
441 def __init__(self
, aboxhalign
, aflushhalign
):
442 self
.boxhalign
= aboxhalign
443 self
.flushhalign
= aflushhalign
444 attr
.exclusiveattr
.__init
__(self
, halign
)
446 def apply(self
, expr
):
447 return r
"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.boxhalign
, self
.flushhalign
, expr
)
449 halign
.left
= halign(0, 0)
450 halign
.center
= halign(0.5, 0.5)
451 halign
.right
= halign(1, 1)
452 halign
.clear
= attr
.clearclass(halign
)
453 halign
.boxleft
= boxhalign
.left
454 halign
.boxcenter
= boxhalign
.center
455 halign
.boxright
= boxhalign
.right
456 halign
.flushleft
= halign
.raggedright
= flushhalign
.left
457 halign
.flushcenter
= halign
.raggedcenter
= flushhalign
.center
458 halign
.flushright
= halign
.raggedleft
= flushhalign
.right
461 class _mathmode(attr
.attr
, textattr
, _localattr
):
464 def apply(self
, expr
):
465 return r
"$\displaystyle{%s}$" % expr
467 mathmode
= _mathmode()
468 clearmathmode
= attr
.clearclass(_mathmode
)
471 class _phantom(attr
.attr
, textattr
, _localattr
):
474 def apply(self
, expr
):
475 return r
"\phantom{%s}" % expr
478 clearphantom
= attr
.clearclass(_phantom
)
481 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
483 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
489 def __init__(self
, width
, baseline
=top
):
490 self
.width
= width
* 72.27 / (unit
.scale
["x"] * 72)
491 self
.baseline
= baseline
492 attr
.sortbeforeexclusiveattr
.__init
__(self
, parbox_pt
, [_localattr
])
494 def apply(self
, expr
):
495 if self
.baseline
== self
.top
:
496 return r
"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
497 elif self
.baseline
== self
.middle
:
498 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
)
499 elif self
.baseline
== self
.bottom
:
500 return r
"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
502 RuntimeError("invalid baseline argument")
504 parbox_pt
.clear
= attr
.clearclass(parbox_pt
)
506 class parbox(parbox_pt
):
508 def __init__(self
, width
, **kwargs
):
509 parbox_pt
.__init
__(self
, unit
.topt(width
), **kwargs
)
511 parbox
.clear
= parbox_pt
.clear
514 _textattrspreamble
+= "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
516 class valign(attr
.sortbeforeexclusiveattr
, textattr
):
518 def __init__(self
, avalign
):
519 self
.valign
= avalign
520 attr
.sortbeforeexclusiveattr
.__init
__(self
, valign
, [parbox_pt
, _localattr
])
522 def apply(self
, expr
):
523 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
)
525 valign
.top
= valign(0)
526 valign
.middle
= valign(0.5)
527 valign
.bottom
= valign(1)
528 valign
.clear
= valign
.baseline
= attr
.clearclass(valign
)
531 _textattrspreamble
+= "\\newdimen\\PyXDimenVShift%\n"
533 class _vshift(attr
.sortbeforeattr
, textattr
):
536 attr
.sortbeforeattr
.__init
__(self
, [valign
, parbox_pt
, _localattr
])
538 def apply(self
, expr
):
539 return r
"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self
.setheightexpr(), expr
)
541 class vshift(_vshift
):
542 "vertical down shift by a fraction of a character height"
544 def __init__(self
, lowerratio
, heightstr
="0"):
545 _vshift
.__init
__(self
)
546 self
.lowerratio
= lowerratio
547 self
.heightstr
= heightstr
549 def setheightexpr(self
):
550 return r
"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self
.heightstr
, self
.lowerratio
)
552 class _vshiftmathaxis(_vshift
):
553 "vertical down shift by the height of the math axis"
555 def setheightexpr(self
):
556 return r
"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
559 vshift
.bottomzero
= vshift(0)
560 vshift
.middlezero
= vshift(0.5)
561 vshift
.topzero
= vshift(1)
562 vshift
.mathaxis
= _vshiftmathaxis()
563 vshift
.clear
= attr
.clearclass(_vshift
)
566 defaultsizelist
= ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
567 None, "tiny", "scriptsize", "footnotesize", "small"]
569 class size(attr
.sortbeforeattr
, textattr
):
572 def __init__(self
, sizeindex
=None, sizename
=None, sizelist
=defaultsizelist
):
573 if (sizeindex
is None and sizename
is None) or (sizeindex
is not None and sizename
is not None):
574 raise RuntimeError("either specify sizeindex or sizename")
575 attr
.sortbeforeattr
.__init
__(self
, [_mathmode
, _vshift
])
576 if sizeindex
is not None:
577 if sizeindex
>= 0 and sizeindex
< sizelist
.index(None):
578 self
.size
= sizelist
[sizeindex
]
579 elif sizeindex
< 0 and sizeindex
+ len(sizelist
) > sizelist
.index(None):
580 self
.size
= sizelist
[sizeindex
]
582 raise IndexError("index out of sizelist range")
586 def apply(self
, expr
):
587 return r
"\%s{}%s" % (self
.size
, expr
)
590 size
.scriptsize
= size
.script
= size(-3)
591 size
.footnotesize
= size
.footnote
= size(-2)
592 size
.small
= size(-1)
593 size
.normalsize
= size
.normal
= size(0)
599 size
.clear
= attr
.clearclass(size
)
602 ###############################################################################
604 ###############################################################################
607 class _readpipe(threading
.Thread
):
608 """threaded reader of TeX/LaTeX output
609 - sets an event, when a specific string in the programs output is found
610 - sets an event, when the terminal ends"""
612 def __init__(self
, pipe
, expectqueue
, gotevent
, gotqueue
, quitevent
):
613 """initialize the reader
614 - pipe: file to be read from
615 - expectqueue: keeps the next InputMarker to be wait for
616 - gotevent: the "got InputMarker" event
617 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
618 - quitevent: the "end of terminal" event"""
619 threading
.Thread
.__init
__(self
)
620 self
.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
622 self
.expectqueue
= expectqueue
623 self
.gotevent
= gotevent
624 self
.gotqueue
= gotqueue
625 self
.quitevent
= quitevent
631 # catch interupted system call errors while reading
634 return self
.pipe
.readline()
636 if e
.errno
!= errno
.EINTR
:
638 read
= _read() # read, what comes in
640 self
.expect
= self
.expectqueue
.get_nowait() # read, what should be expected
644 # universal EOL handling (convert everything into unix like EOLs)
645 # XXX is this necessary on pipes?
646 read
= read
.replace("\r", "").replace("\n", "") + "\n"
647 self
.gotqueue
.put(read
) # report, whats read
648 if self
.expect
is not None and read
.find(self
.expect
) != -1:
649 self
.gotevent
.set() # raise the got event, when the output was expected (XXX: within a single line)
650 read
= _read() # read again
652 self
.expect
= self
.expectqueue
.get_nowait()
657 if self
.expect
is not None and self
.expect
.find("PyXInputMarker") != -1:
658 raise RuntimeError("TeX/LaTeX finished unexpectedly")
662 class textbox(box
.rect
, canvas
._canvas
):
663 """basically a box.rect, but it contains a text created by the texrunner
664 - texrunner._text and texrunner.text return such an object
665 - _textbox instances can be inserted into a canvas
666 - the output is contained in a page of the dvifile available thru the texrunner"""
667 # TODO: shouldn't all boxes become canvases? how about inserts then?
669 def __init__(self
, x
, y
, left
, right
, height
, depth
, finishdvi
, attrs
):
671 - finishdvi is a method to be called to get the dvicanvas
672 (e.g. the finishdvi calls the setdvicanvas method)
673 - attrs are fillstyles"""
676 self
.width
= left
+ right
679 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated(x
, y
)
680 box
.rect
.__init
__(self
, x
- left
, y
- depth
, left
+ right
, depth
+ height
, abscenter
= (left
, depth
))
681 canvas
._canvas
.__init
__(self
, attrs
)
682 self
.finishdvi
= finishdvi
683 self
.dvicanvas
= None
684 self
.insertdvicanvas
= 0
686 def transform(self
, *trafos
):
687 if self
.insertdvicanvas
:
688 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
689 box
.rect
.transform(self
, *trafos
)
691 self
.texttrafo
= trafo
* self
.texttrafo
693 def setdvicanvas(self
, dvicanvas
):
694 if self
.dvicanvas
is not None:
695 raise RuntimeError("multiple call to setdvicanvas")
696 self
.dvicanvas
= dvicanvas
698 def ensuredvicanvas(self
):
699 if self
.dvicanvas
is None:
701 assert self
.dvicanvas
is not None, "finishdvi is broken"
702 if not self
.insertdvicanvas
:
703 self
.insert(self
.dvicanvas
, [self
.texttrafo
])
704 self
.insertdvicanvas
= 1
706 def marker(self
, marker
):
707 self
.ensuredvicanvas()
708 return self
.texttrafo
.apply(*self
.dvicanvas
.markers
[marker
])
710 def processPS(self
, file, writer
, context
, registry
, bbox
):
711 self
.ensuredvicanvas()
712 abbox
= bboxmodule
.empty()
713 canvas
._canvas
.processPS(self
, file, writer
, context
, registry
, abbox
)
714 bbox
+= box
.rect
.bbox(self
)
716 def processPDF(self
, file, writer
, context
, registry
, bbox
):
717 self
.ensuredvicanvas()
718 abbox
= bboxmodule
.empty()
719 canvas
._canvas
.processPDF(self
, file, writer
, context
, registry
, abbox
)
720 bbox
+= box
.rect
.bbox(self
)
723 def _cleantmp(texrunner
):
724 """get rid of temporary files
725 - function to be registered by atexit
726 - files contained in usefiles are kept"""
727 if texrunner
.texruns
: # cleanup while TeX is still running?
728 texrunner
.expectqueue
.put_nowait(None) # do not expect any output anymore
729 if texrunner
.mode
== "latex": # try to immediately quit from TeX or LaTeX
730 texrunner
.texinput
.write("\n\\catcode`\\@11\\relax\\@@end\n")
732 texrunner
.texinput
.write("\n\\end\n")
733 texrunner
.texinput
.close() # close the input queue and
734 if not texrunner
.waitforevent(texrunner
.quitevent
): # wait for finish of the output
735 return # didn't got a quit from TeX -> we can't do much more
736 texrunner
.texruns
= 0
737 texrunner
.texdone
= 1
738 for usefile
in texrunner
.usefiles
:
739 extpos
= usefile
.rfind(".")
741 os
.rename(texrunner
.texfilename
+ usefile
[extpos
:], usefile
)
744 for file in glob
.glob("%s.*" % texrunner
.texfilename
) + ["%sNotes.bib" % texrunner
.texfilename
]:
749 if texrunner
.texdebug
is not None:
751 texrunner
.texdebug
.close()
752 texrunner
.texdebug
= None
761 """TeX/LaTeX interface
762 - runs TeX/LaTeX expressions instantly
763 - checks TeX/LaTeX response
764 - the instance variable texmessage stores the last TeX
766 - the instance variable texmessageparsed stores a parsed
767 version of texmessage; it should be empty after
768 texmessage.check was called, otherwise a TexResultError
770 - the instance variable errordebug controls the verbose
771 level of TexResultError"""
773 defaulttexmessagesstart
= [texmessage
.start
]
774 defaulttexmessagesdocclass
= [texmessage
.load
]
775 defaulttexmessagesbegindoc
= [texmessage
.load
, texmessage
.noaux
]
776 defaulttexmessagesend
= [texmessage
.end
, texmessage
.fontwarning
, texmessage
.rerunwarning
, texmessage
.nobblwarning
]
777 defaulttexmessagesdefaultpreamble
= [texmessage
.load
]
778 defaulttexmessagesdefaultrun
= [texmessage
.loaddef
, texmessage
.graphicsload
,
779 texmessage
.fontwarning
, texmessage
.boxwarning
]
781 def __init__(self
, mode
="tex",
786 waitfortex
=config
.getint("text", "waitfortex", 60),
787 showwaitfortex
=config
.getint("text", "showwaitfortex", 5),
788 texipc
=config
.getboolean("text", "texipc", 0),
794 texmessagesdocclass
=[],
795 texmessagesbegindoc
=[],
797 texmessagesdefaultpreamble
=[],
798 texmessagesdefaultrun
=[]):
800 if mode
!= "tex" and mode
!= "latex":
801 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
804 self
.docclass
= docclass
806 self
.usefiles
= usefiles
[:]
807 self
.waitfortex
= waitfortex
808 self
.showwaitfortex
= showwaitfortex
810 if texdebug
is not None:
811 if texdebug
[-4:] == ".tex":
812 self
.texdebug
= open(texdebug
, "w")
814 self
.texdebug
= open("%s.tex" % texdebug
, "w")
817 self
.dvidebug
= dvidebug
818 self
.errordebug
= errordebug
819 self
.pyxgraphics
= pyxgraphics
820 self
.texmessagesstart
= texmessagesstart
[:]
821 self
.texmessagesdocclass
= texmessagesdocclass
[:]
822 self
.texmessagesbegindoc
= texmessagesbegindoc
[:]
823 self
.texmessagesend
= texmessagesend
[:]
824 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
[:]
825 self
.texmessagesdefaultrun
= texmessagesdefaultrun
[:]
829 self
.preamblemode
= 1
833 self
.needdvitextboxes
= [] # when texipc-mode off
835 self
.textboxesincluded
= 0
836 savetempdir
= tempfile
.tempdir
837 tempfile
.tempdir
= os
.curdir
838 self
.texfilename
= os
.path
.basename(tempfile
.mktemp())
839 tempfile
.tempdir
= savetempdir
841 def waitforevent(self
, event
):
842 """waits verbosely with an timeout for an event
843 - observes an event while periodly while printing messages
844 - returns the status of the event (isSet)
845 - does not clear the event"""
846 if self
.showwaitfortex
:
849 while waited
< self
.waitfortex
and not hasevent
:
850 if self
.waitfortex
- waited
> self
.showwaitfortex
:
851 event
.wait(self
.showwaitfortex
)
852 waited
+= self
.showwaitfortex
854 event
.wait(self
.waitfortex
- waited
)
855 waited
+= self
.waitfortex
- waited
856 hasevent
= event
.isSet()
858 if waited
< self
.waitfortex
:
859 warnings
.warn("still waiting for %s after %i (of %i) seconds..." % (self
.mode
, waited
, self
.waitfortex
))
861 warnings
.warn("the timeout of %i seconds expired and %s did not respond." % (waited
, self
.mode
))
864 event
.wait(self
.waitfortex
)
867 def execute(self
, expr
, texmessages
):
868 """executes expr within TeX/LaTeX
869 - if self.texruns is not yet set, TeX/LaTeX is initialized,
870 self.texruns is set and self.preamblemode is set
871 - the method must not be called, when self.texdone is already set
872 - expr should be a string or None
873 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
874 self.texdone becomes set
875 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
876 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
877 - texmessages is a list of texmessage instances"""
879 if self
.texdebug
is not None:
880 self
.texdebug
.write("%% PyX %s texdebug file\n" % version
.version
)
881 self
.texdebug
.write("%% mode: %s\n" % self
.mode
)
882 self
.texdebug
.write("%% date: %s\n" % time
.asctime(time
.localtime(time
.time())))
883 for usefile
in self
.usefiles
:
884 extpos
= usefile
.rfind(".")
886 os
.rename(usefile
, self
.texfilename
+ usefile
[extpos
:])
889 texfile
= open("%s.tex" % self
.texfilename
, "w") # start with filename -> creates dvi file with that name
890 texfile
.write("\\relax%\n")
897 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t", 0)
899 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
900 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t")
901 atexit
.register(_cleantmp
, self
)
902 self
.expectqueue
= Queue
.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
903 self
.gotevent
= threading
.Event() # keeps the got inputmarker event
904 self
.gotqueue
= Queue
.Queue(0) # allow arbitrary number of entries
905 self
.quitevent
= threading
.Event() # keeps for end of terminal event
906 self
.readoutput
= _readpipe(self
.texoutput
, self
.expectqueue
, self
.gotevent
, self
.gotqueue
, self
.quitevent
)
908 oldpreamblemode
= self
.preamblemode
909 self
.preamblemode
= 1
910 self
.readoutput
.start()
911 self
.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
912 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
913 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
914 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
915 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
916 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
917 "\\newdimen\\PyXDimenHAlignRT%\n" +
918 _textattrspreamble
+ # insert preambles for textattrs macros
919 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
920 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
921 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
922 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
923 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
924 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
925 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
926 "lt=\\the\\PyXDimenHAlignLT,"
927 "rt=\\the\\PyXDimenHAlignRT,"
928 "ht=\\the\\ht\\PyXBox,"
929 "dp=\\the\\dp\\PyXBox:}%\n"
930 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
931 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
932 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
933 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
934 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
935 self
.defaulttexmessagesstart
+ self
.texmessagesstart
)
936 os
.remove("%s.tex" % self
.texfilename
)
937 if self
.mode
== "tex":
940 if len(self
.lfs
) > 4 and self
.lfs
[-4:] == ".lfs":
943 lfsname
= "%s.lfs" % self
.lfs
944 for fulllfsname
in [lfsname
,
945 os
.path
.join(siteconfig
.lfsdir
, lfsname
)]:
947 lfsfile
= open(fulllfsname
, "r")
948 lfsdef
= lfsfile
.read()
954 lfserror
= "File '%s' is not available or not readable. " % lfsname
957 if lfserror
is not None:
958 allfiles
= (glob
.glob("*.lfs") +
959 glob
.glob(os
.path
.join(siteconfig
.lfsdir
, "*.lfs")))
964 lfsnames
.append(os
.path
.basename(f
)[:-4])
969 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror
, lfsnames
))
971 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror
)
972 self
.execute(lfsdef
, [])
973 self
.execute("\\normalsize%\n", [])
974 self
.execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [])
975 elif self
.mode
== "latex":
977 pyxdef
= os
.path
.join(siteconfig
.sharedir
, "pyx.def")
979 open(pyxdef
, "r").close()
981 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
982 pyxdef
= os
.path
.abspath(pyxdef
).replace(os
.sep
, "/")
983 self
.execute("\\makeatletter%\n"
984 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
985 "\\def\\ProcessOptions{%\n"
986 "\\def\\Gin@driver{" + pyxdef
+ "}%\n"
987 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
988 "\\saveProcessOptions}%\n"
991 if self
.docopt
is not None:
992 self
.execute("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
993 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
995 self
.execute("\\documentclass{%s}" % self
.docclass
,
996 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
997 self
.preamblemode
= oldpreamblemode
999 if expr
is not None: # TeX/LaTeX should process expr
1000 self
.expectqueue
.put_nowait("PyXInputMarker:executeid=%i:" % self
.executeid
)
1001 if self
.preamblemode
:
1002 self
.expr
= ("%s%%\n" % expr
+
1003 "\\PyXInput{%i}%%\n" % self
.executeid
)
1006 self
.expr
= ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr
, self
.page
) +
1007 "\\PyXInput{%i}%%\n" % self
.executeid
)
1008 else: # TeX/LaTeX should be finished
1009 self
.expectqueue
.put_nowait("Transcript written on %s.log" % self
.texfilename
)
1010 if self
.mode
== "latex":
1011 self
.expr
= "\\end{document}%\n"
1013 self
.expr
= "\\end%\n"
1014 if self
.texdebug
is not None:
1015 self
.texdebug
.write(self
.expr
)
1016 self
.texinput
.write(self
.expr
)
1017 gotevent
= self
.waitforevent(self
.gotevent
)
1018 self
.gotevent
.clear()
1019 if expr
is None and gotevent
: # TeX/LaTeX should have finished
1022 self
.texinput
.close() # close the input queue and
1023 gotevent
= self
.waitforevent(self
.quitevent
) # wait for finish of the output
1025 self
.texmessage
= ""
1027 self
.texmessage
+= self
.gotqueue
.get_nowait()
1030 self
.texmessage
= self
.texmessage
.replace("\r\n", "\n").replace("\r", "\n")
1031 self
.texmessageparsed
= self
.texmessage
1033 if expr
is not None:
1034 texmessage
.inputmarker
.check(self
)
1035 if not self
.preamblemode
:
1036 texmessage
.pyxbox
.check(self
)
1037 texmessage
.pyxpageout
.check(self
)
1038 texmessages
= attr
.mergeattrs(texmessages
)
1039 for t
in texmessages
:
1041 keeptexmessageparsed
= self
.texmessageparsed
1042 texmessage
.emptylines
.check(self
)
1043 if len(self
.texmessageparsed
):
1044 self
.texmessageparsed
= keeptexmessageparsed
1045 raise TexResultError("unhandled TeX response (might be an error)", self
)
1047 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self
.waitfortex
, self
)
1049 def finishdvi(self
, ignoretail
=0):
1050 """finish TeX/LaTeX and read the dvifile
1051 - this method ensures that all textboxes can access their
1053 self
.execute(None, self
.defaulttexmessagesend
+ self
.texmessagesend
)
1054 dvifilename
= "%s.dvi" % self
.texfilename
1056 self
.dvifile
= dvifile
.DVIfile(dvifilename
, debug
=self
.dvidebug
)
1058 for box
in self
.needdvitextboxes
:
1059 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), page
, 0, 0, 0, 0, 0, 0], fontmap
=box
.fontmap
))
1061 if not ignoretail
and self
.dvifile
.readpage(None) is not None:
1062 raise RuntimeError("end of dvifile expected")
1064 self
.needdvitextboxes
= []
1066 def reset(self
, reinit
=0):
1067 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1070 if self
.texdebug
is not None:
1071 self
.texdebug
.write("%s\n%% preparing restart of %s\n" % ("%"*80, self
.mode
))
1076 self
.preamblemode
= 1
1077 for expr
, texmessages
in self
.preambles
:
1078 self
.execute(expr
, texmessages
)
1079 if self
.mode
== "latex":
1080 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1081 self
.preamblemode
= 0
1084 self
.preamblemode
= 1
1086 def set(self
, mode
=_unset
,
1092 showwaitfortex
=_unset
,
1098 texmessagesstart
=_unset
,
1099 texmessagesdocclass
=_unset
,
1100 texmessagesbegindoc
=_unset
,
1101 texmessagesend
=_unset
,
1102 texmessagesdefaultpreamble
=_unset
,
1103 texmessagesdefaultrun
=_unset
):
1104 """provide a set command for TeX/LaTeX settings
1105 - TeX/LaTeX must not yet been started
1106 - especially needed for the defaultrunner, where no access to
1107 the constructor is available"""
1109 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1110 if mode
is not _unset
:
1112 if mode
!= "tex" and mode
!= "latex":
1113 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1115 if lfs
is not _unset
:
1117 if docclass
is not _unset
:
1118 self
.docclass
= docclass
1119 if docopt
is not _unset
:
1120 self
.docopt
= docopt
1121 if usefiles
is not _unset
:
1122 self
.usefiles
= usefiles
1123 if waitfortex
is not _unset
:
1124 self
.waitfortex
= waitfortex
1125 if showwaitfortex
is not _unset
:
1126 self
.showwaitfortex
= showwaitfortex
1127 if texipc
is not _unset
:
1128 self
.texipc
= texipc
1129 if texdebug
is not _unset
:
1130 if self
.texdebug
is not None:
1131 self
.texdebug
.close()
1132 if texdebug
[-4:] == ".tex":
1133 self
.texdebug
= open(texdebug
, "w")
1135 self
.texdebug
= open("%s.tex" % texdebug
, "w")
1136 if dvidebug
is not _unset
:
1137 self
.dvidebug
= dvidebug
1138 if errordebug
is not _unset
:
1139 self
.errordebug
= errordebug
1140 if pyxgraphics
is not _unset
:
1141 self
.pyxgraphics
= pyxgraphics
1142 if errordebug
is not _unset
:
1143 self
.errordebug
= errordebug
1144 if texmessagesstart
is not _unset
:
1145 self
.texmessagesstart
= texmessagesstart
1146 if texmessagesdocclass
is not _unset
:
1147 self
.texmessagesdocclass
= texmessagesdocclass
1148 if texmessagesbegindoc
is not _unset
:
1149 self
.texmessagesbegindoc
= texmessagesbegindoc
1150 if texmessagesend
is not _unset
:
1151 self
.texmessagesend
= texmessagesend
1152 if texmessagesdefaultpreamble
is not _unset
:
1153 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
1154 if texmessagesdefaultrun
is not _unset
:
1155 self
.texmessagesdefaultrun
= texmessagesdefaultrun
1157 def preamble(self
, expr
, texmessages
=[]):
1158 r
"""put something into the TeX/LaTeX preamble
1159 - in LaTeX, this is done before the \begin{document}
1160 (you might use \AtBeginDocument, when you're in need for)
1161 - it is not allowed to call preamble after calling the
1162 text method for the first time (for LaTeX this is needed
1163 due to \begin{document}; in TeX it is forced for compatibility
1164 (you should be able to switch from TeX to LaTeX, if you want,
1165 without breaking something)
1166 - preamble expressions must not create any dvi output
1167 - args might contain texmessage instances"""
1168 if self
.texdone
or not self
.preamblemode
:
1169 raise RuntimeError("preamble calls disabled due to previous text calls")
1170 texmessages
= self
.defaulttexmessagesdefaultpreamble
+ self
.texmessagesdefaultpreamble
+ texmessages
1171 self
.execute(expr
, texmessages
)
1172 self
.preambles
.append((expr
, texmessages
))
1174 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:")
1176 def text(self
, x
, y
, expr
, textattrs
=[], texmessages
=[], fontmap
=None):
1177 """create text by passing expr to TeX/LaTeX
1178 - returns a textbox containing the result from running expr thru TeX/LaTeX
1179 - the box center is set to x, y
1180 - *args may contain attr parameters, namely:
1181 - textattr instances
1182 - texmessage instances
1183 - trafo._trafo instances
1184 - style.fillstyle instances"""
1186 raise ValueError("None expression is invalid")
1188 self
.reset(reinit
=1)
1190 if self
.preamblemode
:
1191 if self
.mode
== "latex":
1192 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1193 self
.preamblemode
= 0
1195 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1196 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1197 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1198 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1199 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1200 # reverse loop over the merged textattrs (last is applied first)
1201 lentextattrs
= len(textattrs
)
1202 for i
in range(lentextattrs
):
1203 expr
= textattrs
[lentextattrs
-1-i
].apply(expr
)
1205 self
.execute(expr
, self
.defaulttexmessagesdefaultrun
+ self
.texmessagesdefaultrun
+ texmessages
)
1206 except TexResultError
:
1207 self
.finishdvi(ignoretail
=1)
1211 self
.dvifile
= dvifile
.DVIfile("%s.dvi" % self
.texfilename
, debug
=self
.dvidebug
)
1212 match
= self
.PyXBoxPattern
.search(self
.texmessage
)
1213 if not match
or int(match
.group("page")) != self
.page
:
1214 raise TexResultError("box extents not found", self
)
1215 left
, right
, height
, depth
= [float(xxx
)*72/72.27*unit
.x_pt
for xxx
in match
.group("lt", "rt", "ht", "dp")]
1216 box
= textbox(x
, y
, left
, right
, height
, depth
, self
.finishdvi
, fillstyles
)
1218 box
.reltransform(t
) # TODO: should trafos really use reltransform???
1219 # this is quite different from what we do elsewhere!!!
1220 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1222 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), self
.page
, 0, 0, 0, 0, 0, 0], fontmap
=fontmap
))
1224 box
.fontmap
= fontmap
1225 self
.needdvitextboxes
.append(box
)
1228 def text_pt(self
, x
, y
, expr
, *args
, **kwargs
):
1229 return self
.text(x
* unit
.t_pt
, y
* unit
.t_pt
, expr
, *args
, **kwargs
)
1231 PyXVariableBoxPattern
= re
.compile(r
"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1233 def textboxes(self
, text
, pageshapes
):
1234 # this is some experimental code to put text into several boxes
1235 # while the bounding shape changes from box to box (rectangles only)
1236 # first we load sev.tex
1237 if not self
.textboxesincluded
:
1238 self
.execute(r
"\input textboxes.tex", [texmessage
.load
])
1239 self
.textboxesincluded
= 1
1240 # define page shapes
1241 pageshapes_str
= "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit
.topt(pageshapes
[0][0]), 72.27/72*unit
.topt(pageshapes
[0][1]))
1242 pageshapes_str
+= "\\lohsizes={%\n"
1243 for hsize
, vsize
in pageshapes
[1:]:
1244 pageshapes_str
+= "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(hsize
))
1245 pageshapes_str
+= "{\\relax}%\n}%\n"
1246 pageshapes_str
+= "\\lovsizes={%\n"
1247 for hsize
, vsize
in pageshapes
[1:]:
1248 pageshapes_str
+= "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(vsize
))
1249 pageshapes_str
+= "{\\relax}%\n}%\n"
1255 self
.execute(pageshapes_str
, [])
1256 parnos_str
= "}{".join(parnos
)
1258 parnos_str
= "{%s}" % parnos_str
1259 parnos_str
= "\\parnos={%s{\\relax}}%%\n" % parnos_str
1260 self
.execute(parnos_str
, [])
1261 parshapes_str
= "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes
)
1262 self
.execute(parshapes_str
, [])
1263 self
.execute("\\global\\count0=1%%\n"
1264 "\\global\\parno=0%%\n"
1265 "\\global\\myprevgraf=0%%\n"
1266 "\\global\\showprevgraf=0%%\n"
1267 "\\global\\outputtype=0%%\n"
1268 "\\global\\leastcost=10000000%%\n"
1270 "\\vfill\\supereject%%\n" % text
, [texmessage
.ignore
])
1272 if self
.dvifile
is None:
1273 self
.dvifile
= dvifile
.DVIfile("%s.dvi" % self
.texfilename
, debug
=self
.dvidebug
)
1275 raise RuntimeError("textboxes currently needs texipc")
1278 lastparshapes
= parshapes
1281 lastpar
= prevgraf
= -1
1282 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
)
1285 page
= int(m
.group("page"))
1286 assert page
== pages
1287 par
= int(m
.group("par"))
1288 prevgraf
= int(m
.group("prevgraf"))
1289 if page
<= len(pageshapes
):
1290 width
= 72.27/72*unit
.topt(pageshapes
[page
-1][0])
1292 width
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1293 if page
< len(pageshapes
):
1294 nextwidth
= 72.27/72*unit
.topt(pageshapes
[page
][0])
1296 nextwidth
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1299 # a new paragraph is to be broken
1300 parnos
.append(str(par
))
1301 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
)])
1303 parshape
= " 0pt " + parshape
1304 parshapes
.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf
+ 1, parshape
, nextwidth
))
1305 elif prevgraf
== lastprevgraf
:
1308 # we have to append the breaking of the previous paragraph
1309 oldparshape
= " ".join(parshapes
[-1].split(' ')[2:2+2*lastprevgraf
])
1310 oldparshape
= oldparshape
.split('}')[0]
1312 oldparshape
= " " + oldparshape
1313 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
- lastprevgraf
)])
1315 parshape
= " 0pt " + parshape
1318 parshapes
[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf
+ 1, oldparshape
, parshape
, nextwidth
)
1320 lastprevgraf
= prevgraf
1322 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
, nextpos
)
1324 for i
in range(pages
):
1325 result
.append(self
.dvifile
.readpage([i
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1326 if parnos
== lastparnos
and parshapes
== lastparshapes
:
1330 raise TexResultError("Too many loops in textboxes ", texrunner
)
1333 # the module provides an default texrunner and methods for direct access
1334 defaulttexrunner
= texrunner()
1335 reset
= defaulttexrunner
.reset
1336 set = defaulttexrunner
.set
1337 preamble
= defaulttexrunner
.preamble
1338 text
= defaulttexrunner
.text
1339 text_pt
= defaulttexrunner
.text_pt
1341 def escapestring(s
, replace
={" ": "~",
1353 "\\": "{$\setminus$}",
1355 "escape all ascii characters such that they are printable by TeX/LaTeX"
1358 if not 32 <= ord(s
[i
]) < 127:
1359 raise ValueError("escapestring function handles ascii strings only")
1366 s
= s
[:i
] + r
+ s
[i
+1:]