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