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