- adjustments to the new graph data+style handling
[PyX/mjg.git] / pyx / text.py
blob2c83eeb2d5e205636ac97e8b5cbf30fa46e3e3a3
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 import glob, os, threading, Queue, traceback, re, tempfile, sys, atexit, time
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 TexResultWarning(TexResultError):
75 """as above, but with different handling of the exception
76 - when this exception is raised by a texmessage instance,
77 the information just get reported and the execution continues"""
78 pass
81 class _Itexmessage:
82 """validates/invalidates TeX/LaTeX response"""
84 def check(self, texrunner):
85 """check a Tex/LaTeX response and respond appropriate
86 - read the texrunners texmessageparsed attribute
87 - if there is an problem found, raise an appropriate
88 exception (TexResultError or TexResultWarning)
89 - remove any valid and identified TeX/LaTeX response
90 from the texrunners texmessageparsed attribute
91 -> finally, there should be nothing left in there,
92 otherwise it is interpreted as an error"""
95 class texmessage(attr.attr): pass
98 class _texmessagestart(texmessage):
99 """validates TeX/LaTeX startup"""
101 __implements__ = _Itexmessage
103 startpattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
105 def check(self, texrunner):
106 # check for "This is e-TeX"
107 m = self.startpattern.search(texrunner.texmessageparsed)
108 if not m:
109 raise TexResultError("TeX startup failed", texrunner)
110 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
112 # check for filename to be processed
113 try:
114 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
115 except (IndexError, ValueError):
116 raise TexResultError("TeX running startup file failed", texrunner)
118 # check for \raiseerror -- just to be sure that communication works
119 try:
120 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
121 except (IndexError, ValueError):
122 raise TexResultError("TeX scrollmode check failed", texrunner)
125 class _texmessagenoaux(texmessage):
126 """allows for LaTeXs no-aux-file warning"""
128 __implements__ = _Itexmessage
130 def check(self, texrunner):
131 try:
132 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
133 texrunner.texmessageparsed = s1 + s2
134 except (IndexError, ValueError):
135 try:
136 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
137 os.sep,
138 texrunner.texfilename), 1)
139 texrunner.texmessageparsed = s1 + s2
140 except (IndexError, ValueError):
141 pass
144 class _texmessageinputmarker(texmessage):
145 """validates the PyXInputMarker"""
147 __implements__ = _Itexmessage
149 def check(self, texrunner):
150 try:
151 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
152 texrunner.texmessageparsed = s1 + s2
153 except (IndexError, ValueError):
154 raise TexResultError("PyXInputMarker expected", texrunner)
157 class _texmessagepyxbox(texmessage):
158 """validates the PyXBox output"""
160 __implements__ = _Itexmessage
162 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:")
164 def check(self, texrunner):
165 m = self.pattern.search(texrunner.texmessageparsed)
166 if m and m.group("page") == str(texrunner.page):
167 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
168 else:
169 raise TexResultError("PyXBox expected", texrunner)
172 class _texmessagepyxpageout(texmessage):
173 """validates the dvi shipout message (writing a page to the dvi file)"""
175 __implements__ = _Itexmessage
177 def check(self, texrunner):
178 try:
179 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
180 texrunner.texmessageparsed = s1 + s2
181 except (IndexError, ValueError):
182 raise TexResultError("PyXPageOutMarker expected", texrunner)
185 class _texmessagefontsubstitution(texmessage):
186 """validates the font substituion Warning"""
188 __implements__ = _Itexmessage
190 pattern = re.compile("LaTeX Font Warning: Font shape (?P<font>.*) in size <(?P<orig>.*)> not available\s*\(Font\)(.*) size <(?P<subst>.*)> substituted on input line (?P<line>.*)\.")
192 def check(self, texrunner):
193 m = self.pattern.search(texrunner.texmessageparsed)
194 if m:
195 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
196 raise TexResultWarning("LaTeX Font Warning on input line %s" % (m.group('line')), texrunner)
199 class _texmessagetexend(texmessage):
200 """validates TeX/LaTeX finish"""
202 __implements__ = _Itexmessage
204 def check(self, texrunner):
205 try:
206 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
207 texrunner.texmessageparsed = s1 + s2
208 except (IndexError, ValueError):
209 try:
210 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
211 os.sep,
212 texrunner.texfilename), 1)
213 texrunner.texmessageparsed = s1 + s2
214 except (IndexError, ValueError):
215 pass
217 # pass font size summary over to PyX user
218 fontpattern = re.compile(r"LaTeX Font Warning: Size substitutions with differences\s*\(Font\).* have occurred.\s*")
219 m = fontpattern.search(texrunner.texmessageparsed)
220 if m:
221 sys.stderr.write("LaTeX has detected Font Size substituion differences.\n")
222 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
224 # check for "(see the transcript file for additional information)"
225 try:
226 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
227 texrunner.texmessageparsed = s1 + s2
228 except (IndexError, ValueError):
229 pass
231 # check for "Output written on ...dvi (1 page, 220 bytes)."
232 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
233 m = dvipattern.search(texrunner.texmessageparsed)
234 if texrunner.page:
235 if not m:
236 raise TexResultError("TeX dvifile messages expected", texrunner)
237 if m.group("page") != str(texrunner.page):
238 raise TexResultError("wrong number of pages reported", texrunner)
239 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
240 else:
241 try:
242 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
243 texrunner.texmessageparsed = s1 + s2
244 except (IndexError, ValueError):
245 raise TexResultError("no dvifile expected", texrunner)
247 # check for "Transcript written on ...log."
248 try:
249 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
250 texrunner.texmessageparsed = s1 + s2
251 except (IndexError, ValueError):
252 raise TexResultError("TeX logfile message expected", texrunner)
255 class _texmessageemptylines(texmessage):
256 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines
257 also clear TeX interactive mode warning (Please type a command or say `\\end')
260 __implements__ = _Itexmessage
262 def check(self, texrunner):
263 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(r"(Please type a command or say `\end')", "")
264 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
265 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
268 class _texmessageload(texmessage):
269 """validates inclusion of arbitrary files
270 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
271 <fielname> is a readable file and other stuff can be anything
272 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
273 - this is not always wanted, but we just assume that file inclusion is fine"""
275 __implements__ = _Itexmessage
277 pattern = re.compile(r" *\((?P<filename>[^()\s\n]+)[^()]*\) *")
279 def baselevels(self, s, maxlevel=1, brackets="()"):
280 """strip parts of a string above a given bracket level
281 - return a modified (some parts might be removed) version of the string s
282 where all parts inside brackets with level higher than maxlevel are
283 removed
284 - if brackets do not match (number of left and right brackets is wrong
285 or at some points there were more right brackets than left brackets)
286 just return the unmodified string"""
287 level = 0
288 highestlevel = 0
289 res = ""
290 for c in s:
291 if c == brackets[0]:
292 level += 1
293 if level > highestlevel:
294 highestlevel = level
295 if level <= maxlevel:
296 res += c
297 if c == brackets[1]:
298 level -= 1
299 if level == 0 and highestlevel > 0:
300 return res
302 def check(self, texrunner):
303 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
304 if lowestbracketlevel is not None:
305 m = self.pattern.search(lowestbracketlevel)
306 while m:
307 if os.access(m.group("filename"), os.R_OK):
308 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
309 else:
310 break
311 m = self.pattern.search(lowestbracketlevel)
312 else:
313 texrunner.texmessageparsed = lowestbracketlevel
316 class _texmessageloadfd(_texmessageload):
317 """validates the inclusion of font description files (fd-files)
318 - works like _texmessageload
319 - filename must end with .fd and no further text is allowed"""
321 pattern = re.compile(r" *\((?P<filename>[^)]+.fd)\) *")
324 class _texmessagegraphicsload(_texmessageload):
325 """validates the inclusion of files as the graphics packages writes it
326 - works like _texmessageload, but using "<" and ">" as delimiters
327 - filename must end with .eps and no further text is allowed"""
329 pattern = re.compile(r" *<(?P<filename>[^>]+.eps)> *")
331 def baselevels(self, s, brackets="<>", **args):
332 return _texmessageload.baselevels(self, s, brackets=brackets, **args)
335 class _texmessageignore(_texmessageload):
336 """validates any TeX/LaTeX response
337 - this might be used, when the expression is ok, but no suitable texmessage
338 parser is available
339 - PLEASE: - consider writing suitable tex message parsers
340 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
342 __implements__ = _Itexmessage
344 def check(self, texrunner):
345 texrunner.texmessageparsed = ""
348 class _texmessagewarning(_texmessageload):
349 """validates any TeX/LaTeX response
350 - this might be used, when the expression is ok, but no suitable texmessage
351 parser is available
352 - PLEASE: - consider writing suitable tex message parsers
353 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
355 __implements__ = _Itexmessage
357 def check(self, texrunner):
358 if len(texrunner.texmessageparsed):
359 texrunner.texmessageparsed = ""
360 raise TexResultWarning("TeX result is ignored", texrunner)
363 texmessage.start = _texmessagestart()
364 texmessage.noaux = _texmessagenoaux()
365 texmessage.inputmarker = _texmessageinputmarker()
366 texmessage.pyxbox = _texmessagepyxbox()
367 texmessage.pyxpageout = _texmessagepyxpageout()
368 texmessage.texend = _texmessagetexend()
369 texmessage.emptylines = _texmessageemptylines()
370 texmessage.load = _texmessageload()
371 texmessage.loadfd = _texmessageloadfd()
372 texmessage.graphicsload = _texmessagegraphicsload()
373 texmessage.ignore = _texmessageignore()
374 texmessage.warning = _texmessagewarning()
375 texmessage.fontsubstitution = _texmessagefontsubstitution()
378 ###############################################################################
379 # textattrs
380 ###############################################################################
382 _textattrspreamble = ""
384 class textattr:
385 "a textattr defines a apply method, which modifies a (La)TeX expression"
387 class halign(attr.exclusiveattr, textattr):
389 def __init__(self, hratio):
390 self.hratio = hratio
391 attr.exclusiveattr.__init__(self, halign)
393 def apply(self, expr):
394 return r"\gdef\PyXHAlign{%.5f}%s" % (self.hratio, expr)
396 halign.center = halign(0.5)
397 halign.right = halign(1)
398 halign.clear = attr.clearclass(halign)
399 halign.left = halign.clear
402 class _localattr: pass
404 class _mathmode(attr.attr, textattr, _localattr):
405 "math mode"
407 def apply(self, expr):
408 return r"$\displaystyle{%s}$" % expr
410 mathmode = _mathmode()
411 nomathmode = attr.clearclass(_mathmode)
414 class _phantom(attr.attr, textattr, _localattr):
415 "phatom text"
417 def apply(self, expr):
418 return r"\phantom{%s}" % expr
420 phantom = _phantom()
421 nophantom = attr.clearclass(_phantom)
424 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
426 class size(attr.sortbeforeattr, textattr, _localattr):
427 "font size"
429 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
430 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
431 raise RuntimeError("either specify sizeindex or sizename")
432 attr.sortbeforeattr.__init__(self, [_mathmode])
433 if sizeindex is not None:
434 if sizeindex >= 0 and sizeindex < sizelist.index(None):
435 self.size = sizelist[sizeindex]
436 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
437 self.size = sizelist[sizeindex]
438 else:
439 raise IndexError("index out of sizelist range")
440 else:
441 self.size = sizename
443 def apply(self, expr):
444 return r"\%s{%s}" % (self.size, expr)
446 size.tiny = size(-4)
447 size.scriptsize = size.script = size(-3)
448 size.footnotesize = size.footnote = size(-2)
449 size.small = size(-1)
450 size.normalsize = size.normal = size(0)
451 size.large = size(1)
452 size.Large = size(2)
453 size.LARGE = size(3)
454 size.huge = size(4)
455 size.Huge = size(5)
456 size.clear = attr.clearclass(size)
459 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
461 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
463 top = 1
464 middle = 2
465 bottom = 3
467 def __init__(self, width, baseline=top):
468 self.width = width
469 self.baseline = baseline
470 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
472 def apply(self, expr):
473 if self.baseline == self.top:
474 return r"\linewidth%.5ftruept\vtop{\hsize\linewidth{}%s}" % (self.width * 72.27 / 72, expr)
475 elif self.baseline == self.middle:
476 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)
477 elif self.baseline == self.bottom:
478 return r"\linewidth%.5ftruept\vbox{\hsize\linewidth{}%s}" % (self.width * 72.27 / 72, expr)
479 else:
480 RuntimeError("invalid baseline argument")
482 parbox_pt.clear = attr.clearclass(parbox_pt)
484 class parbox(parbox_pt):
486 def __init__(self, width, **kwargs):
487 parbox_pt.__init__(self, unit.topt(width), **kwargs)
489 parbox.clear = parbox_pt.clear
492 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
494 class valign(attr.sortbeforeexclusiveattr, textattr):
496 def __init__(self):
497 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
499 class _valigntop(valign):
501 def apply(self, expr):
502 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
504 class _valignmiddle(valign):
506 def apply(self, expr):
507 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
509 class _valignbottom(valign):
511 def apply(self, expr):
512 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
514 valign.top = _valigntop()
515 valign.middle = _valignmiddle()
516 valign.bottom = _valignbottom()
517 valign.clear = attr.clearclass(valign)
518 valign.baseline = valign.clear
521 class _vshift(attr.sortbeforeattr, textattr):
523 def __init__(self):
524 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
526 class vshift(_vshift):
527 "vertical down shift by a fraction of a character height"
529 def __init__(self, lowerratio, heightstr="0"):
530 _vshift.__init__(self)
531 self.lowerratio = lowerratio
532 self.heightstr = heightstr
534 def apply(self, expr):
535 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
537 class _vshiftmathaxis(_vshift):
538 "vertical down shift by the height of the math axis"
540 def apply(self, expr):
541 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
544 vshift.bottomzero = vshift(0)
545 vshift.middlezero = vshift(0.5)
546 vshift.topzero = vshift(1)
547 vshift.mathaxis = _vshiftmathaxis()
548 vshift.clear = attr.clearclass(_vshift)
551 ###############################################################################
552 # texrunner
553 ###############################################################################
556 class _readpipe(threading.Thread):
557 """threaded reader of TeX/LaTeX output
558 - sets an event, when a specific string in the programs output is found
559 - sets an event, when the terminal ends"""
561 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
562 """initialize the reader
563 - pipe: file to be read from
564 - expectqueue: keeps the next InputMarker to be wait for
565 - gotevent: the "got InputMarker" event
566 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
567 - quitevent: the "end of terminal" event"""
568 threading.Thread.__init__(self)
569 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
570 self.pipe = pipe
571 self.expectqueue = expectqueue
572 self.gotevent = gotevent
573 self.gotqueue = gotqueue
574 self.quitevent = quitevent
575 self.expect = None
576 self.start()
578 def run(self):
579 """thread routine"""
580 read = self.pipe.readline() # read, what comes in
581 try:
582 self.expect = self.expectqueue.get_nowait() # read, what should be expected
583 except Queue.Empty:
584 pass
585 while len(read):
586 # universal EOL handling (convert everything into unix like EOLs)
587 # XXX is this necessary on pipes?
588 read = read.replace("\r", "").replace("\n", "") + "\n"
589 self.gotqueue.put(read) # report, whats read
590 if self.expect is not None and read.find(self.expect) != -1:
591 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
592 read = self.pipe.readline() # read again
593 try:
594 self.expect = self.expectqueue.get_nowait()
595 except Queue.Empty:
596 pass
597 # EOF reached
598 self.pipe.close()
599 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
600 raise RuntimeError("TeX/LaTeX finished unexpectedly")
601 self.quitevent.set()
604 class textbox(box.rect, canvas._canvas):
605 """basically a box.rect, but it contains a text created by the texrunner
606 - texrunner._text and texrunner.text return such an object
607 - _textbox instances can be inserted into a canvas
608 - the output is contained in a page of the dvifile available thru the texrunner"""
609 # TODO: shouldn't all boxes become canvases? how about inserts then?
611 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
613 - finishdvi is a method to be called to get the dvicanvas
614 (e.g. the finishdvi calls the setdvicanvas method)
615 - attrs are fillstyles"""
616 self.left = left
617 self.right = right
618 self.width = left + right
619 self.height = height
620 self.depth = depth
621 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
622 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
623 canvas._canvas.__init__(self)
624 self.finishdvi = finishdvi
625 self.dvicanvas = None
626 self.set(attrs)
627 self.insertdvicanvas = 0
629 def transform(self, *trafos):
630 if self.insertdvicanvas:
631 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
632 box.rect.transform(self, *trafos)
633 for trafo in trafos:
634 self.texttrafo = trafo * self.texttrafo
636 def setdvicanvas(self, dvicanvas):
637 if self.dvicanvas is not None:
638 raise RuntimeError("multiple call to setdvicanvas")
639 self.dvicanvas = dvicanvas
641 def ensuredvicanvas(self):
642 if self.dvicanvas is None:
643 self.finishdvi()
644 assert self.dvicanvas is not None, "finishdvi is broken"
645 if not self.insertdvicanvas:
646 self.insert(self.dvicanvas, [self.texttrafo])
647 self.insertdvicanvas = 1
649 def marker(self, marker):
650 self.ensuredvicanvas()
651 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
653 def prolog(self):
654 self.ensuredvicanvas()
655 return canvas._canvas.prolog(self)
657 def outputPS(self, file):
658 self.ensuredvicanvas()
659 canvas._canvas.outputPS(self, file)
661 def outputPDF(self, file):
662 self.ensuredvicanvas()
663 canvas._canvas.outputPDF(self, file)
666 def _cleantmp(texrunner):
667 """get rid of temporary files
668 - function to be registered by atexit
669 - files contained in usefiles are kept"""
670 if texrunner.texruns: # cleanup while TeX is still running?
671 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
672 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
673 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
674 else:
675 texrunner.texinput.write("\n\\end\n")
676 texrunner.texinput.close() # close the input queue and
677 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
678 return # didn't got a quit from TeX -> we can't do much more
679 texrunner.texruns = 0
680 texrunner.texdone = 1
681 for usefile in texrunner.usefiles:
682 extpos = usefile.rfind(".")
683 try:
684 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
685 except OSError:
686 pass
687 for file in glob.glob("%s.*" % texrunner.texfilename):
688 try:
689 os.unlink(file)
690 except OSError:
691 pass
692 if texrunner.texdebug is not None:
693 try:
694 texrunner.texdebug.close()
695 texrunner.texdebug = None
696 except IOError:
697 pass
700 class texrunner:
701 """TeX/LaTeX interface
702 - runs TeX/LaTeX expressions instantly
703 - checks TeX/LaTeX response
704 - the instance variable texmessage stores the last TeX
705 response as a string
706 - the instance variable texmessageparsed stores a parsed
707 version of texmessage; it should be empty after
708 texmessage.check was called, otherwise a TexResultError
709 is raised
710 - the instance variable errordebug controls the verbose
711 level of TexResultError"""
713 defaulttexmessagesstart = [texmessage.start]
714 defaulttexmessagesdocclass = [texmessage.load]
715 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
716 defaulttexmessagesend = [texmessage.texend]
717 defaulttexmessagesdefaultpreamble = [texmessage.load]
718 defaulttexmessagesdefaultrun = [texmessage.loadfd, texmessage.graphicsload]
720 def __init__(self, mode="tex",
721 lfs="10pt",
722 docclass="article",
723 docopt=None,
724 usefiles=[],
725 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
726 waitfortex=config.getint("text", "waitfortex", 60),
727 showwaitfortex=config.getint("text", "showwaitfortex", 5),
728 texipc=config.getboolean("text", "texipc", 0),
729 texdebug=None,
730 dvidebug=0,
731 errordebug=1,
732 dvicopy=0,
733 pyxgraphics=1,
734 texmessagesstart=[],
735 texmessagesdocclass=[],
736 texmessagesbegindoc=[],
737 texmessagesend=[],
738 texmessagesdefaultpreamble=[],
739 texmessagesdefaultrun=[]):
740 mode = mode.lower()
741 if mode != "tex" and mode != "latex":
742 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
743 self.mode = mode
744 self.lfs = lfs
745 self.docclass = docclass
746 self.docopt = docopt
747 self.usefiles = usefiles
748 self.fontmaps = fontmaps
749 self.waitfortex = waitfortex
750 self.showwaitfortex = showwaitfortex
751 self.texipc = texipc
752 if texdebug is not None:
753 if texdebug[-4:] == ".tex":
754 self.texdebug = open(texdebug, "w")
755 else:
756 self.texdebug = open("%s.tex" % texdebug, "w")
757 else:
758 self.texdebug = None
759 self.dvidebug = dvidebug
760 self.errordebug = errordebug
761 self.dvicopy = dvicopy
762 self.pyxgraphics = pyxgraphics
763 self.texmessagesstart = texmessagesstart
764 self.texmessagesdocclass = texmessagesdocclass
765 self.texmessagesbegindoc = texmessagesbegindoc
766 self.texmessagesend = texmessagesend
767 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
768 self.texmessagesdefaultrun = texmessagesdefaultrun
770 self.texruns = 0
771 self.texdone = 0
772 self.preamblemode = 1
773 self.executeid = 0
774 self.page = 0
775 self.preambles = []
776 self.needdvitextboxes = [] # when texipc-mode off
777 self.dvifile = None
778 self.textboxesincluded = 0
779 savetempdir = tempfile.tempdir
780 tempfile.tempdir = os.curdir
781 self.texfilename = os.path.basename(tempfile.mktemp())
782 tempfile.tempdir = savetempdir
784 def waitforevent(self, event):
785 """waits verbosely with an timeout for an event
786 - observes an event while periodly while printing messages
787 - returns the status of the event (isSet)
788 - does not clear the event"""
789 if self.showwaitfortex:
790 waited = 0
791 hasevent = 0
792 while waited < self.waitfortex and not hasevent:
793 if self.waitfortex - waited > self.showwaitfortex:
794 event.wait(self.showwaitfortex)
795 waited += self.showwaitfortex
796 else:
797 event.wait(self.waitfortex - waited)
798 waited += self.waitfortex - waited
799 hasevent = event.isSet()
800 if not hasevent:
801 if waited < self.waitfortex:
802 sys.stderr.write("*** PyX Info: still waiting for %s after %i (of %i) seconds...\n" % (self.mode, waited, self.waitfortex))
803 else:
804 sys.stderr.write("*** PyX Error: the timeout of %i seconds expired and %s did not respond.\n" % (waited, self.mode))
805 return hasevent
806 else:
807 event.wait(self.waitfortex)
808 return event.isSet()
810 def execute(self, expr, texmessages):
811 """executes expr within TeX/LaTeX
812 - if self.texruns is not yet set, TeX/LaTeX is initialized,
813 self.texruns is set and self.preamblemode is set
814 - the method must not be called, when self.texdone is already set
815 - expr should be a string or None
816 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
817 self.texdone becomes set
818 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
819 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
820 - texmessages is a list of texmessage instances"""
821 if not self.texruns:
822 if self.texdebug is not None:
823 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
824 self.texdebug.write("%% mode: %s\n" % self.mode)
825 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
826 for usefile in self.usefiles:
827 extpos = usefile.rfind(".")
828 try:
829 os.rename(usefile, self.texfilename + usefile[extpos:])
830 except OSError:
831 pass
832 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
833 texfile.write("\\relax%\n")
834 texfile.close()
835 if self.texipc:
836 ipcflag = " --ipc"
837 else:
838 ipcflag = ""
839 try:
840 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
841 except ValueError:
842 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
843 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
844 atexit.register(_cleantmp, self)
845 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
846 self.gotevent = threading.Event() # keeps the got inputmarker event
847 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
848 self.quitevent = threading.Event() # keeps for end of terminal event
849 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
850 self.texruns = 1
851 self.fontmap = dvifile.readfontmap(self.fontmaps.split())
852 oldpreamblemode = self.preamblemode
853 self.preamblemode = 1
854 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
855 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
856 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
857 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
858 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
859 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
860 "\\newdimen\\PyXDimenHAlignRT%\n" +
861 _textattrspreamble + # insert preambles for textattrs macros
862 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
863 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
864 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
865 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
866 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
867 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
868 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
869 "lt=\\the\\PyXDimenHAlignLT,"
870 "rt=\\the\\PyXDimenHAlignRT,"
871 "ht=\\the\\ht\\PyXBox,"
872 "dp=\\the\\dp\\PyXBox:}%\n"
873 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
874 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
875 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
876 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
877 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
878 self.defaulttexmessagesstart + self.texmessagesstart)
879 os.remove("%s.tex" % self.texfilename)
880 if self.mode == "tex":
881 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
882 lfsname = self.lfs
883 else:
884 lfsname = "%s.lfs" % self.lfs
885 for fulllfsname in [lfsname,
886 os.path.join(siteconfig.lfsdir, lfsname)]:
887 try:
888 lfsfile = open(fulllfsname, "r")
889 lfsdef = lfsfile.read()
890 lfsfile.close()
891 break
892 except IOError:
893 pass
894 else:
895 allfiles = (glob.glob("*.lfs") +
896 glob.glob(os.path.join(siteconfig.lfsdir, "*.lfs")))
897 lfsnames = []
898 for f in allfiles:
899 try:
900 open(f, "r").close()
901 lfsnames.append(os.path.basename(f)[:-4])
902 except IOError:
903 pass
904 lfsnames.sort()
905 if len(lfsnames):
906 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname, lfsnames))
907 else:
908 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname)
909 self.execute(lfsdef, [])
910 self.execute("\\normalsize%\n", [])
911 self.execute("\\newdimen\\linewidth%\n", [])
912 elif self.mode == "latex":
913 if self.pyxgraphics:
914 pyxdef = os.path.join(siteconfig.sharedir, "pyx.def")
915 try:
916 open(pyxdef, "r").close()
917 except IOError:
918 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
919 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
920 self.execute("\\makeatletter%\n"
921 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
922 "\\def\\ProcessOptions{%\n"
923 "\\def\\Gin@driver{" + pyxdef + "}%\n"
924 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
925 "\\saveProcessOptions}%\n"
926 "\\makeatother",
928 if self.docopt is not None:
929 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
930 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
931 else:
932 self.execute("\\documentclass{%s}" % self.docclass,
933 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
934 self.preamblemode = oldpreamblemode
935 self.executeid += 1
936 if expr is not None: # TeX/LaTeX should process expr
937 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
938 if self.preamblemode:
939 self.expr = ("%s%%\n" % expr +
940 "\\PyXInput{%i}%%\n" % self.executeid)
941 else:
942 self.page += 1
943 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
944 "\\PyXInput{%i}%%\n" % self.executeid)
945 else: # TeX/LaTeX should be finished
946 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
947 if self.mode == "latex":
948 self.expr = "\\end{document}%\n"
949 else:
950 self.expr = "\\end%\n"
951 if self.texdebug is not None:
952 self.texdebug.write(self.expr)
953 self.texinput.write(self.expr)
954 gotevent = self.waitforevent(self.gotevent)
955 self.gotevent.clear()
956 if expr is None and gotevent: # TeX/LaTeX should have finished
957 self.texruns = 0
958 self.texdone = 1
959 self.texinput.close() # close the input queue and
960 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
961 try:
962 self.texmessage = ""
963 while 1:
964 self.texmessage += self.gotqueue.get_nowait()
965 except Queue.Empty:
966 pass
967 self.texmessageparsed = self.texmessage
968 if gotevent:
969 if expr is not None:
970 texmessage.inputmarker.check(self)
971 if not self.preamblemode:
972 texmessage.pyxbox.check(self)
973 texmessage.pyxpageout.check(self)
974 texmessages = attr.mergeattrs(texmessages)
975 # reverse loop over the merged texmessages (last is applied first)
976 lentexmessages = len(texmessages)
977 for i in range(lentexmessages):
978 try:
979 texmessages[lentexmessages-1-i].check(self)
980 except TexResultWarning:
981 traceback.print_exc()
982 texmessage.emptylines.check(self)
983 if len(self.texmessageparsed):
984 raise TexResultError("unhandled TeX response (might be an error)", self)
985 else:
986 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
988 def finishdvi(self):
989 """finish TeX/LaTeX and read the dvifile
990 - this method ensures that all textboxes can access their
991 dvicanvas"""
992 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
993 if self.dvicopy:
994 os.system("dvicopy %(t)s.dvi %(t)s.dvicopy > %(t)s.dvicopyout 2> %(t)s.dvicopyerr" % {"t": self.texfilename})
995 dvifilename = "%s.dvicopy" % self.texfilename
996 else:
997 dvifilename = "%s.dvi" % self.texfilename
998 if not self.texipc:
999 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
1000 page = 1
1001 for box in self.needdvitextboxes:
1002 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0]))
1003 page += 1
1004 if self.dvifile.readpage(None) is not None:
1005 raise RuntimeError("end of dvifile expected")
1006 self.dvifile = None
1007 self.needdvitextboxes = []
1009 def reset(self, reinit=0):
1010 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1011 if self.texruns:
1012 self.finishdvi()
1013 if self.texdebug is not None:
1014 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
1015 self.executeid = 0
1016 self.page = 0
1017 self.texdone = 0
1018 if reinit:
1019 self.preamblemode = 1
1020 for expr, texmessages in self.preambles:
1021 self.execute(expr, texmessages)
1022 if self.mode == "latex":
1023 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1024 self.preamblemode = 0
1025 else:
1026 self.preambles = []
1027 self.preamblemode = 1
1029 def set(self, mode=None,
1030 lfs=None,
1031 docclass=None,
1032 docopt=None,
1033 usefiles=None,
1034 fontmaps=None,
1035 waitfortex=None,
1036 showwaitfortex=None,
1037 texipc=None,
1038 texdebug=None,
1039 dvidebug=None,
1040 errordebug=None,
1041 dvicopy=None,
1042 pyxgraphics=None,
1043 texmessagesstart=None,
1044 texmessagesdocclass=None,
1045 texmessagesbegindoc=None,
1046 texmessagesend=None,
1047 texmessagesdefaultpreamble=None,
1048 texmessagesdefaultrun=None):
1049 """provide a set command for TeX/LaTeX settings
1050 - TeX/LaTeX must not yet been started
1051 - especially needed for the defaultrunner, where no access to
1052 the constructor is available"""
1053 if self.texruns:
1054 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1055 if mode is not None:
1056 mode = mode.lower()
1057 if mode != "tex" and mode != "latex":
1058 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1059 self.mode = mode
1060 if lfs is not None:
1061 self.lfs = lfs
1062 if docclass is not None:
1063 self.docclass = docclass
1064 if docopt is not None:
1065 self.docopt = docopt
1066 if usefiles is not None:
1067 self.usefiles = usefiles
1068 if fontmaps is not None:
1069 self.fontmaps = fontmaps
1070 if waitfortex is not None:
1071 self.waitfortex = waitfortex
1072 if showwaitfortex is not None:
1073 self.showwaitfortex = showwaitfortex
1074 if texipc is not None:
1075 self.texipc = texipc
1076 if texdebug is not None:
1077 if self.texdebug is not None:
1078 self.texdebug.close()
1079 if texdebug[-4:] == ".tex":
1080 self.texdebug = open(texdebug, "w")
1081 else:
1082 self.texdebug = open("%s.tex" % texdebug, "w")
1083 if dvidebug is not None:
1084 self.dvidebug = dvidebug
1085 if errordebug is not None:
1086 self.errordebug = errordebug
1087 if dvicopy is not None:
1088 self.dvicopy = dvicopy
1089 if pyxgraphics is not None:
1090 self.pyxgraphics = pyxgraphics
1091 if errordebug is not None:
1092 self.errordebug = errordebug
1093 if texmessagesstart is not None:
1094 self.texmessagesstart = texmessagesstart
1095 if texmessagesdocclass is not None:
1096 self.texmessagesdocclass = texmessagesdocclass
1097 if texmessagesbegindoc is not None:
1098 self.texmessagesbegindoc = texmessagesbegindoc
1099 if texmessagesend is not None:
1100 self.texmessagesend = texmessagesend
1101 if texmessagesdefaultpreamble is not None:
1102 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1103 if texmessagesdefaultrun is not None:
1104 self.texmessagesdefaultrun = texmessagesdefaultrun
1106 def preamble(self, expr, texmessages=[]):
1107 r"""put something into the TeX/LaTeX preamble
1108 - in LaTeX, this is done before the \begin{document}
1109 (you might use \AtBeginDocument, when you're in need for)
1110 - it is not allowed to call preamble after calling the
1111 text method for the first time (for LaTeX this is needed
1112 due to \begin{document}; in TeX it is forced for compatibility
1113 (you should be able to switch from TeX to LaTeX, if you want,
1114 without breaking something)
1115 - preamble expressions must not create any dvi output
1116 - args might contain texmessage instances"""
1117 if self.texdone or not self.preamblemode:
1118 raise RuntimeError("preamble calls disabled due to previous text calls")
1119 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1120 self.execute(expr, texmessages)
1121 self.preambles.append((expr, texmessages))
1123 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:")
1125 def text(self, x, y, expr, textattrs=[], texmessages=[]):
1126 """create text by passing expr to TeX/LaTeX
1127 - returns a textbox containing the result from running expr thru TeX/LaTeX
1128 - the box center is set to x, y
1129 - *args may contain attr parameters, namely:
1130 - textattr instances
1131 - texmessage instances
1132 - trafo._trafo instances
1133 - style.fillstyle instances"""
1134 if expr is None:
1135 raise ValueError("None expression is invalid")
1136 if self.texdone:
1137 self.reset(reinit=1)
1138 first = 0
1139 if self.preamblemode:
1140 if self.mode == "latex":
1141 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1142 self.preamblemode = 0
1143 first = 1
1144 if self.texipc and self.dvicopy:
1145 raise RuntimeError("texipc and dvicopy can't be mixed up")
1146 textattrs = attr.mergeattrs(textattrs) # perform cleans
1147 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1148 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1149 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1150 textattrs = attr.getattrs(textattrs, [textattr])
1151 # reverse loop over the merged textattrs (last is applied first)
1152 lentextattrs = len(textattrs)
1153 for i in range(lentextattrs):
1154 expr = textattrs[lentextattrs-1-i].apply(expr)
1155 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1156 if self.texipc:
1157 if first:
1158 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1159 match = self.PyXBoxPattern.search(self.texmessage)
1160 if not match or int(match.group("page")) != self.page:
1161 raise TexResultError("box extents not found", self)
1162 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt", "rt", "ht", "dp")]
1163 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1164 for t in trafos:
1165 box.reltransform(t)
1166 if self.texipc:
1167 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0]))
1168 else:
1169 self.needdvitextboxes.append(box)
1170 return box
1172 def text_pt(self, x, y, expr, *args, **kwargs):
1173 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1175 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1177 def textboxes(self, text, pageshapes):
1178 # this is some experimental code to put text into several boxes
1179 # while the bounding shape changes from box to box (rectangles only)
1180 # first we load sev.tex
1181 if not self.textboxesincluded:
1182 self.execute(r"\input textboxes.tex", [texmessage.load])
1183 self.textboxesincluded = 1
1184 # define page shapes
1185 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1186 pageshapes_str += "\\lohsizes={%\n"
1187 for hsize, vsize in pageshapes[1:]:
1188 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1189 pageshapes_str += "{\\relax}%\n}%\n"
1190 pageshapes_str += "\\lovsizes={%\n"
1191 for hsize, vsize in pageshapes[1:]:
1192 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1193 pageshapes_str += "{\\relax}%\n}%\n"
1194 page = 0
1195 parnos = []
1196 parshapes = []
1197 loop = 0
1198 while 1:
1199 self.execute(pageshapes_str, [])
1200 parnos_str = "}{".join(parnos)
1201 if parnos_str:
1202 parnos_str = "{%s}" % parnos_str
1203 parnos_str = "\\parnos={%s{\\relax}}%%\n" % parnos_str
1204 self.execute(parnos_str, [])
1205 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1206 self.execute(parshapes_str, [])
1207 self.execute("\\global\\count0=1%%\n"
1208 "\\global\\parno=0%%\n"
1209 "\\global\\myprevgraf=0%%\n"
1210 "\\global\\showprevgraf=0%%\n"
1211 "\\global\\outputtype=0%%\n"
1212 "\\global\\leastcost=10000000%%\n"
1213 "%s%%\n"
1214 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1215 if self.texipc:
1216 if self.dvifile is None:
1217 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1218 else:
1219 raise RuntimeError("textboxes currently needs texipc")
1220 lastparnos = parnos
1221 parnos = []
1222 lastparshapes = parshapes
1223 parshapes = []
1224 pages = 0
1225 lastpar = prevgraf = -1
1226 m = self.PyXVariableBoxPattern.search(self.texmessage)
1227 while m:
1228 pages += 1
1229 page = int(m.group("page"))
1230 assert page == pages
1231 par = int(m.group("par"))
1232 prevgraf = int(m.group("prevgraf"))
1233 if page <= len(pageshapes):
1234 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1235 else:
1236 width = 72.27/72*unit.topt(pageshapes[-1][0])
1237 if page < len(pageshapes):
1238 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1239 else:
1240 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1242 if par != lastpar:
1243 # a new paragraph is to be broken
1244 parnos.append(str(par))
1245 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf)])
1246 if len(parshape):
1247 parshape = " 0pt " + parshape
1248 parshapes.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf + 1, parshape, nextwidth))
1249 elif prevgraf == lastprevgraf:
1250 pass
1251 else:
1252 # we have to append the breaking of the previous paragraph
1253 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1254 oldparshape = oldparshape.split('}')[0]
1255 if len(parshape):
1256 oldparshape = " " + oldparshape
1257 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - lastprevgraf)])
1258 if len(parshape):
1259 parshape = " 0pt " + parshape
1260 else:
1261 parshape = " "
1262 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1263 lastpar = par
1264 lastprevgraf = prevgraf
1265 nextpos = m.end()
1266 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1267 result = []
1268 for i in range(pages):
1269 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1270 if parnos == lastparnos and parshapes == lastparshapes:
1271 return result
1272 loop += 1
1273 if loop > 100:
1274 raise TexResultError("Too many loops in textboxes ", texrunner)
1277 # the module provides an default texrunner and methods for direct access
1278 defaulttexrunner = texrunner()
1279 reset = defaulttexrunner.reset
1280 set = defaulttexrunner.set
1281 preamble = defaulttexrunner.preamble
1282 text = defaulttexrunner.text
1283 text_pt = defaulttexrunner.text_pt
1285 def escapestring(s):
1286 """escape special TeX/LaTeX characters
1288 Returns a string, where some special characters of standard
1289 TeX/LaTeX are replaced by appropriate escaped versions. Note
1290 that we cannot handle the three ASCII characters '{', '}',
1291 and '\' that way, since they do not occure in the TeX default
1292 encoding and thus are more likely to need some special handling.
1293 All other ASCII characters should usually (but not always)
1294 work."""
1296 # ASCII strings only
1297 s = str(s)
1298 i = 0
1299 while i < len(s):
1300 if s[i] in "$&#_%":
1301 s = s[:i] + "\\" + s[i:]
1302 i += 1
1303 elif s[i] in "^~":
1304 s = s[:i] + r"\string" + s[i:]
1305 i += 7
1306 i += 1
1307 return s