plain -> regular
[PyX.git] / pyx / text.py
blob7a0ec354e9ec9e17a759932be0950d4d2f17eae3
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, 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 m = self.startpattern.search(texrunner.texmessageparsed)
107 if not m:
108 raise TexResultError("TeX startup failed", texrunner)
109 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
110 try:
111 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
112 except (IndexError, ValueError):
113 raise TexResultError("TeX running startup file failed", texrunner)
114 try:
115 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
116 except (IndexError, ValueError):
117 raise TexResultError("TeX scrollmode check failed", texrunner)
120 class _texmessagenoaux(texmessage):
121 """allows for LaTeXs no-aux-file warning"""
123 __implements__ = _Itexmessage
125 def check(self, texrunner):
126 try:
127 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
128 texrunner.texmessageparsed = s1 + s2
129 except (IndexError, ValueError):
130 try:
131 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
132 os.sep,
133 texrunner.texfilename), 1)
134 texrunner.texmessageparsed = s1 + s2
135 except (IndexError, ValueError):
136 pass
139 class _texmessageinputmarker(texmessage):
140 """validates the PyXInputMarker"""
142 __implements__ = _Itexmessage
144 def check(self, texrunner):
145 try:
146 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
147 texrunner.texmessageparsed = s1 + s2
148 except (IndexError, ValueError):
149 raise TexResultError("PyXInputMarker expected", texrunner)
152 class _texmessagepyxbox(texmessage):
153 """validates the PyXBox output"""
155 __implements__ = _Itexmessage
157 pattern = re.compile(r"PyXBox:page=(?P<page>\d+),lt=-?\d*((\d\.?)|(\.?\d))\d*pt,rt=-?\d*((\d\.?)|(\.?\d))\d*pt,ht=-?\d*((\d\.?)|(\.?\d))\d*pt,dp=-?\d*((\d\.?)|(\.?\d))\d*pt:")
159 def check(self, texrunner):
160 m = self.pattern.search(texrunner.texmessageparsed)
161 if m and m.group("page") == str(texrunner.page):
162 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
163 else:
164 raise TexResultError("PyXBox expected", texrunner)
167 class _texmessagepyxpageout(texmessage):
168 """validates the dvi shipout message (writing a page to the dvi file)"""
170 __implements__ = _Itexmessage
172 def check(self, texrunner):
173 try:
174 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
175 texrunner.texmessageparsed = s1 + s2
176 except (IndexError, ValueError):
177 raise TexResultError("PyXPageOutMarker expected", texrunner)
180 class _texmessagetexend(texmessage):
181 """validates TeX/LaTeX finish"""
183 __implements__ = _Itexmessage
185 def check(self, texrunner):
186 try:
187 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
188 texrunner.texmessageparsed = s1 + s2
189 except (IndexError, ValueError):
190 try:
191 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
192 os.sep,
193 texrunner.texfilename), 1)
194 texrunner.texmessageparsed = s1 + s2
195 except (IndexError, ValueError):
196 pass
197 try:
198 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
199 texrunner.texmessageparsed = s1 + s2
200 except (IndexError, ValueError):
201 pass
202 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
203 m = dvipattern.search(texrunner.texmessageparsed)
204 if texrunner.page:
205 if not m:
206 raise TexResultError("TeX dvifile messages expected", texrunner)
207 if m.group("page") != str(texrunner.page):
208 raise TexResultError("wrong number of pages reported", texrunner)
209 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
210 else:
211 try:
212 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
213 texrunner.texmessageparsed = s1 + s2
214 except (IndexError, ValueError):
215 raise TexResultError("no dvifile expected", texrunner)
216 try:
217 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
218 texrunner.texmessageparsed = s1 + s2
219 except (IndexError, ValueError):
220 raise TexResultError("TeX logfile message expected", texrunner)
223 class _texmessageemptylines(texmessage):
224 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines
225 also clear TeX interactive mode warning (Please type a command or say `\\end')
228 __implements__ = _Itexmessage
230 def check(self, texrunner):
231 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(r"(Please type a command or say `\end')", "")
232 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
233 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
236 class _texmessageload(texmessage):
237 """validates inclusion of arbitrary files
238 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
239 <fielname> is a readable file and other stuff can be anything
240 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
241 - this is not always wanted, but we just assume that file inclusion is fine"""
243 __implements__ = _Itexmessage
245 pattern = re.compile(r" *\((?P<filename>[^()\s\n]+)[^()]*\) *")
247 def baselevels(self, s, maxlevel=1, brackets="()"):
248 """strip parts of a string above a given bracket level
249 - return a modified (some parts might be removed) version of the string s
250 where all parts inside brackets with level higher than maxlevel are
251 removed
252 - if brackets do not match (number of left and right brackets is wrong
253 or at some points there were more right brackets than left brackets)
254 just return the unmodified string"""
255 level = 0
256 highestlevel = 0
257 res = ""
258 for c in s:
259 if c == brackets[0]:
260 level += 1
261 if level > highestlevel:
262 highestlevel = level
263 if level <= maxlevel:
264 res += c
265 if c == brackets[1]:
266 level -= 1
267 if level == 0 and highestlevel > 0:
268 return res
270 def check(self, texrunner):
271 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
272 if lowestbracketlevel is not None:
273 m = self.pattern.search(lowestbracketlevel)
274 while m:
275 if os.access(m.group("filename"), os.R_OK):
276 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
277 else:
278 break
279 m = self.pattern.search(lowestbracketlevel)
280 else:
281 texrunner.texmessageparsed = lowestbracketlevel
284 class _texmessageloadfd(_texmessageload):
285 """validates the inclusion of font description files (fd-files)
286 - works like _texmessageload
287 - filename must end with .fd and no further text is allowed"""
289 pattern = re.compile(r" *\((?P<filename>[^)]+.fd)\) *")
292 class _texmessagegraphicsload(_texmessageload):
293 """validates the inclusion of files as the graphics packages writes it
294 - works like _texmessageload, but using "<" and ">" as delimiters
295 - filename must end with .eps and no further text is allowed"""
297 pattern = re.compile(r" *<(?P<filename>[^>]+.eps)> *")
299 def baselevels(self, s, brackets="<>", **args):
300 return _texmessageload.baselevels(self, s, brackets=brackets, **args)
303 class _texmessageignore(_texmessageload):
304 """validates any TeX/LaTeX response
305 - this might be used, when the expression is ok, but no suitable texmessage
306 parser is available
307 - PLEASE: - consider writing suitable tex message parsers
308 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
310 __implements__ = _Itexmessage
312 def check(self, texrunner):
313 texrunner.texmessageparsed = ""
316 class _texmessagewarning(_texmessageload):
317 """validates any TeX/LaTeX response
318 - this might be used, when the expression is ok, but no suitable texmessage
319 parser is available
320 - PLEASE: - consider writing suitable tex message parsers
321 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
323 __implements__ = _Itexmessage
325 def check(self, texrunner):
326 if len(texrunner.texmessageparsed):
327 texrunner.texmessageparsed = ""
328 raise TexResultWarning("TeX result is ignored", texrunner)
331 texmessage.start = _texmessagestart()
332 texmessage.noaux = _texmessagenoaux()
333 texmessage.inputmarker = _texmessageinputmarker()
334 texmessage.pyxbox = _texmessagepyxbox()
335 texmessage.pyxpageout = _texmessagepyxpageout()
336 texmessage.texend = _texmessagetexend()
337 texmessage.emptylines = _texmessageemptylines()
338 texmessage.load = _texmessageload()
339 texmessage.loadfd = _texmessageloadfd()
340 texmessage.graphicsload = _texmessagegraphicsload()
341 texmessage.ignore = _texmessageignore()
342 texmessage.warning = _texmessagewarning()
345 ###############################################################################
346 # textattrs
347 ###############################################################################
349 _textattrspreamble = ""
351 class textattr:
352 "a textattr defines a apply method, which modifies a (La)TeX expression"
354 class halign(attr.exclusiveattr, textattr):
356 def __init__(self, hratio):
357 self.hratio = hratio
358 attr.exclusiveattr.__init__(self, halign)
360 def apply(self, expr):
361 return r"\gdef\PyXHAlign{%.5f}%s" % (self.hratio, expr)
363 halign.center = halign(0.5)
364 halign.right = halign(1)
365 halign.clear = attr.clearclass(halign)
366 halign.left = halign.clear
369 class _localattr: pass
371 class _mathmode(attr.attr, textattr, _localattr):
372 "math mode"
374 def apply(self, expr):
375 return r"$\displaystyle{%s}$" % expr
377 mathmode = _mathmode()
378 nomathmode = attr.clearclass(_mathmode)
381 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
383 class size(attr.sortbeforeattr, textattr, _localattr):
384 "font size"
386 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
387 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
388 raise RuntimeError("either specify sizeindex or sizename")
389 attr.sortbeforeattr.__init__(self, [_mathmode])
390 if sizeindex is not None:
391 if sizeindex >= 0 and sizeindex < sizelist.index(None):
392 self.size = sizelist[sizeindex]
393 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
394 self.size = sizelist[sizeindex]
395 else:
396 raise IndexError("index out of sizelist range")
397 else:
398 self.size = sizename
400 def apply(self, expr):
401 return r"\%s{%s}" % (self.size, expr)
403 size.tiny = size(-4)
404 size.scriptsize = size.script = size(-3)
405 size.footnotesize = size.footnote = size(-2)
406 size.small = size(-1)
407 size.normalsize = size.normal = size(0)
408 size.large = size(1)
409 size.Large = size(2)
410 size.LARGE = size(3)
411 size.huge = size(4)
412 size.Huge = size(5)
413 size.clear = attr.clearclass(size)
416 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
418 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
420 top = 1
421 middle = 2
422 bottom = 3
424 def __init__(self, width, baseline=top):
425 self.width = width
426 self.baseline = baseline
427 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
429 def apply(self, expr):
430 if self.baseline == self.top:
431 return r"\linewidth%.5ftruept\vtop{\hsize\linewidth{}%s}" % (self.width * 72.27 / 72, expr)
432 elif self.baseline == self.middle:
433 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)
434 elif self.baseline == self.bottom:
435 return r"\linewidth%.5ftruept\vbox{\hsize\linewidth{}%s}" % (self.width * 72.27 / 72, expr)
436 else:
437 RuntimeError("invalid baseline argument")
439 parbox_pt.clear = attr.clearclass(parbox_pt)
441 class parbox(parbox_pt):
443 def __init__(self, width, **kwargs):
444 parbox_pt.__init__(self, unit.topt(width), **kwargs)
446 parbox.clear = parbox_pt.clear
449 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
451 class valign(attr.sortbeforeexclusiveattr, textattr):
453 def __init__(self):
454 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
456 class _valigntop(valign):
458 def apply(self, expr):
459 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
461 class _valignmiddle(valign):
463 def apply(self, expr):
464 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
466 class _valignbottom(valign):
468 def apply(self, expr):
469 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
471 valign.top = _valigntop()
472 valign.middle = _valignmiddle()
473 valign.bottom = _valignbottom()
474 valign.clear = attr.clearclass(valign)
475 valign.baseline = valign.clear
478 class _vshift(attr.sortbeforeattr, textattr):
480 def __init__(self):
481 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
483 class vshift(_vshift):
484 "vertical down shift by a fraction of a character height"
486 def __init__(self, lowerratio, heightstr="0"):
487 _vshift.__init__(self)
488 self.lowerratio = lowerratio
489 self.heightstr = heightstr
491 def apply(self, expr):
492 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
494 class _vshiftmathaxis(_vshift):
495 "vertical down shift by the height of the math axis"
497 def apply(self, expr):
498 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
501 vshift.bottomzero = vshift(0)
502 vshift.middlezero = vshift(0.5)
503 vshift.topzero = vshift(1)
504 vshift.mathaxis = _vshiftmathaxis()
505 vshift.clear = attr.clearclass(_vshift)
508 ###############################################################################
509 # texrunner
510 ###############################################################################
513 class _readpipe(threading.Thread):
514 """threaded reader of TeX/LaTeX output
515 - sets an event, when a specific string in the programs output is found
516 - sets an event, when the terminal ends"""
518 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
519 """initialize the reader
520 - pipe: file to be read from
521 - expectqueue: keeps the next InputMarker to be wait for
522 - gotevent: the "got InputMarker" event
523 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
524 - quitevent: the "end of terminal" event"""
525 threading.Thread.__init__(self)
526 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
527 self.pipe = pipe
528 self.expectqueue = expectqueue
529 self.gotevent = gotevent
530 self.gotqueue = gotqueue
531 self.quitevent = quitevent
532 self.expect = None
533 self.start()
535 def run(self):
536 """thread routine"""
537 read = self.pipe.readline() # read, what comes in
538 try:
539 self.expect = self.expectqueue.get_nowait() # read, what should be expected
540 except Queue.Empty:
541 pass
542 while len(read):
543 # universal EOL handling (convert everything into unix like EOLs)
544 read.replace("\r", "")
545 if not len(read) or read[-1] != "\n":
546 read += "\n"
547 self.gotqueue.put(read) # report, whats readed
548 if self.expect is not None and read.find(self.expect) != -1:
549 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
550 read = self.pipe.readline() # read again
551 try:
552 self.expect = self.expectqueue.get_nowait()
553 except Queue.Empty:
554 pass
555 # EOF reached
556 self.pipe.close()
557 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
558 raise RuntimeError("TeX/LaTeX finished unexpectedly")
559 self.quitevent.set()
562 class textbox(box.rect, canvas._canvas):
563 """basically a box.rect, but it contains a text created by the texrunner
564 - texrunner._text and texrunner.text return such an object
565 - _textbox instances can be inserted into a canvas
566 - the output is contained in a page of the dvifile available thru the texrunner"""
567 # TODO: shouldn't all boxes become canvases? how about inserts then?
569 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
571 - finishdvi is a method to be called to get the dvicanvas
572 (e.g. the finishdvi calls the setdvicanvas method)
573 - attrs are fillstyles"""
574 self.left = left
575 self.right = right
576 self.width = left + right
577 self.height = height
578 self.depth = depth
579 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
580 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
581 canvas._canvas.__init__(self)
582 self.finishdvi = finishdvi
583 self.dvicanvas = None
584 self.set(attrs)
585 self.insertdvicanvas = 0
587 def transform(self, *trafos):
588 if self.insertdvicanvas:
589 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
590 box.rect.transform(self, *trafos)
591 for trafo in trafos:
592 self.texttrafo = trafo * self.texttrafo
594 def setdvicanvas(self, dvicanvas):
595 if self.dvicanvas is not None:
596 raise RuntimeError("multiple call to setdvicanvas")
597 self.dvicanvas = dvicanvas
599 def ensuredvicanvas(self):
600 if self.dvicanvas is None:
601 self.finishdvi()
602 assert self.dvicanvas is not None, "finishdvi is broken"
603 if not self.insertdvicanvas:
604 self.insert(self.dvicanvas, [self.texttrafo])
605 self.insertdvicanvas = 1
607 def marker(self, marker):
608 self.ensuredvicanvas()
609 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
611 def prolog(self):
612 self.ensuredvicanvas()
613 return canvas._canvas.prolog(self)
615 def outputPS(self, file):
616 self.ensuredvicanvas()
617 canvas._canvas.outputPS(self, file)
620 def _cleantmp(texrunner):
621 """get rid of temporary files
622 - function to be registered by atexit
623 - files contained in usefiles are kept"""
624 if texrunner.texruns: # cleanup while TeX is still running?
625 texrunner.texruns = 0
626 texrunner.texdone = 1
627 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
628 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
629 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
630 else:
631 texrunner.texinput.write("\n\\end\n")
632 texrunner.texinput.close() # close the input queue and
633 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
634 return # didn't got a quit from TeX -> we can't do much more
635 for usefile in texrunner.usefiles:
636 extpos = usefile.rfind(".")
637 try:
638 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
639 except OSError:
640 pass
641 for file in glob.glob("%s.*" % texrunner.texfilename):
642 try:
643 os.unlink(file)
644 except OSError:
645 pass
646 if texrunner.texdebug is not None:
647 try:
648 texrunner.texdebug.close()
649 texrunner.texdebug = None
650 except IOError:
651 pass
654 class texrunner:
655 """TeX/LaTeX interface
656 - runs TeX/LaTeX expressions instantly
657 - checks TeX/LaTeX response
658 - the instance variable texmessage stores the last TeX
659 response as a string
660 - the instance variable texmessageparsed stores a parsed
661 version of texmessage; it should be empty after
662 texmessage.check was called, otherwise a TexResultError
663 is raised
664 - the instance variable errordebug controls the verbose
665 level of TexResultError"""
667 defaulttexmessagesstart = [texmessage.start]
668 defaulttexmessagesdocclass = [texmessage.load]
669 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
670 defaulttexmessagesend = [texmessage.texend]
671 defaulttexmessagesdefaultpreamble = [texmessage.load]
672 defaulttexmessagesdefaultrun = [texmessage.loadfd, texmessage.graphicsload]
674 def __init__(self, mode="tex",
675 lfs="10pt",
676 docclass="article",
677 docopt=None,
678 usefiles=[],
679 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
680 waitfortex=config.getint("text", "waitfortex", 60),
681 showwaitfortex=config.getint("text", "showwaitfortex", 5),
682 texipc=config.getboolean("text", "texipc", 0),
683 texdebug=None,
684 dvidebug=0,
685 errordebug=1,
686 dvicopy=0,
687 pyxgraphics=1,
688 texmessagesstart=[],
689 texmessagesdocclass=[],
690 texmessagesbegindoc=[],
691 texmessagesend=[],
692 texmessagesdefaultpreamble=[],
693 texmessagesdefaultrun=[]):
694 mode = mode.lower()
695 if mode != "tex" and mode != "latex":
696 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
697 self.mode = mode
698 self.lfs = lfs
699 self.docclass = docclass
700 self.docopt = docopt
701 self.usefiles = usefiles
702 self.fontmap = dvifile.readfontmap(fontmaps.split())
703 self.waitfortex = waitfortex
704 self.showwaitfortex = showwaitfortex
705 self.texipc = texipc
706 if texdebug is not None:
707 if texdebug[-4:] == ".tex":
708 self.texdebug = open(texdebug, "w")
709 else:
710 self.texdebug = open("%s.tex" % texdebug, "w")
711 else:
712 self.texdebug = None
713 self.dvidebug = dvidebug
714 self.errordebug = errordebug
715 self.dvicopy = dvicopy
716 self.pyxgraphics = pyxgraphics
717 self.texmessagesstart = texmessagesstart
718 self.texmessagesdocclass = texmessagesdocclass
719 self.texmessagesbegindoc = texmessagesbegindoc
720 self.texmessagesend = texmessagesend
721 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
722 self.texmessagesdefaultrun = texmessagesdefaultrun
724 self.texruns = 0
725 self.texdone = 0
726 self.preamblemode = 1
727 self.executeid = 0
728 self.page = 0
729 self.preambles = []
730 self.needdvitextboxes = [] # when texipc-mode off
731 self.dvifile = None
732 self.textboxesincluded = 0
733 savetempdir = tempfile.tempdir
734 tempfile.tempdir = os.curdir
735 self.texfilename = os.path.basename(tempfile.mktemp())
736 tempfile.tempdir = savetempdir
738 def waitforevent(self, event):
739 """waits verbosely with an timeout for an event
740 - observes an event while periodly while printing messages
741 - returns the status of the event (isSet)
742 - does not clear the event"""
743 if self.showwaitfortex:
744 waited = 0
745 hasevent = 0
746 while waited < self.waitfortex and not hasevent:
747 if self.waitfortex - waited > self.showwaitfortex:
748 event.wait(self.showwaitfortex)
749 waited += self.showwaitfortex
750 else:
751 event.wait(self.waitfortex - waited)
752 waited += self.waitfortex - waited
753 hasevent = event.isSet()
754 if not hasevent:
755 if waited < self.waitfortex:
756 sys.stderr.write("*** PyX Info: still waiting for %s after %i (of %i) seconds...\n" % (self.mode, waited, self.waitfortex))
757 else:
758 sys.stderr.write("*** PyX Error: the timeout of %i seconds expired and %s did not respond.\n" % (waited, self.mode))
759 return hasevent
760 else:
761 event.wait(self.waitfortex)
762 return event.isSet()
764 def execute(self, expr, texmessages):
765 """executes expr within TeX/LaTeX
766 - if self.texruns is not yet set, TeX/LaTeX is initialized,
767 self.texruns is set and self.preamblemode is set
768 - the method must not be called, when self.texdone is already set
769 - expr should be a string or None
770 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
771 while self.texdone becomes set
772 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
773 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
774 - texmessages is a list of texmessage instances"""
775 if not self.texruns:
776 if self.texdebug is not None:
777 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
778 self.texdebug.write("%% mode: %s\n" % self.mode)
779 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
780 for usefile in self.usefiles:
781 extpos = usefile.rfind(".")
782 try:
783 os.rename(usefile, self.texfilename + usefile[extpos:])
784 except OSError:
785 pass
786 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
787 texfile.write("\\relax%\n")
788 texfile.close()
789 if self.texipc:
790 ipcflag = " --ipc"
791 else:
792 ipcflag = ""
793 try:
794 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
795 except ValueError:
796 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
797 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
798 atexit.register(_cleantmp, self)
799 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
800 self.gotevent = threading.Event() # keeps the got inputmarker event
801 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
802 self.quitevent = threading.Event() # keeps for end of terminal event
803 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
804 self.texruns = 1
805 oldpreamblemode = self.preamblemode
806 self.preamblemode = 1
807 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
808 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
809 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
810 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
811 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
812 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
813 "\\newdimen\\PyXDimenHAlignRT%\n" +
814 _textattrspreamble + # insert preambles for textattrs macros
815 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
816 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
817 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
818 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
819 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
820 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
821 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
822 "lt=\\the\\PyXDimenHAlignLT,"
823 "rt=\\the\\PyXDimenHAlignRT,"
824 "ht=\\the\\ht\\PyXBox,"
825 "dp=\\the\\dp\\PyXBox:}%\n"
826 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
827 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
828 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
829 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
830 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
831 self.defaulttexmessagesstart + self.texmessagesstart)
832 os.remove("%s.tex" % self.texfilename)
833 if self.mode == "tex":
834 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
835 lfsname = self.lfs
836 else:
837 lfsname = "%s.lfs" % self.lfs
838 for fulllfsname in [lfsname,
839 os.path.join(sys.prefix, "share", "pyx", lfsname),
840 os.path.join(os.path.dirname(__file__), "lfs", lfsname)]:
841 try:
842 lfsfile = open(fulllfsname, "r")
843 lfsdef = lfsfile.read()
844 lfsfile.close()
845 break
846 except IOError:
847 pass
848 else:
849 allfiles = (glob.glob("*.lfs") +
850 glob.glob(os.path.join(sys.prefix, "share", "pyx", "*.lfs")) +
851 glob.glob(os.path.join(os.path.dirname(__file__), "lfs", "*.lfs")))
852 lfsnames = []
853 for f in allfiles:
854 try:
855 open(f, "r").close()
856 lfsnames.append(os.path.basename(f)[:-4])
857 except IOError:
858 pass
859 lfsnames.sort()
860 if len(lfsnames):
861 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname, lfsnames))
862 else:
863 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname)
864 self.execute(lfsdef, [])
865 self.execute("\\normalsize%\n", [])
866 self.execute("\\newdimen\\linewidth%\n", [])
867 elif self.mode == "latex":
868 if self.pyxgraphics:
869 for pyxdef in ["pyx.def",
870 os.path.join(sys.prefix, "share", "pyx", "pyx.def"),
871 os.path.join(os.path.dirname(__file__), "..", "contrib", "pyx.def")]:
872 try:
873 open(pyxdef, "r").close()
874 break
875 except IOError:
876 pass
877 else:
878 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
879 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
880 self.execute("\\makeatletter%\n"
881 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
882 "\\def\\ProcessOptions{%\n"
883 "\\def\\Gin@driver{" + pyxdef + "}%\n"
884 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
885 "\\saveProcessOptions}%\n"
886 "\\makeatother",
888 if self.docopt is not None:
889 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
890 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
891 else:
892 self.execute("\\documentclass{%s}" % self.docclass,
893 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
894 self.preamblemode = oldpreamblemode
895 self.executeid += 1
896 if expr is not None: # TeX/LaTeX should process expr
897 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
898 if self.preamblemode:
899 self.expr = ("%s%%\n" % expr +
900 "\\PyXInput{%i}%%\n" % self.executeid)
901 else:
902 self.page += 1
903 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
904 "\\PyXInput{%i}%%\n" % self.executeid)
905 else: # TeX/LaTeX should be finished
906 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
907 if self.mode == "latex":
908 self.expr = "\\end{document}%\n"
909 else:
910 self.expr = "\\end%\n"
911 if self.texdebug is not None:
912 self.texdebug.write(self.expr)
913 self.texinput.write(self.expr)
914 gotevent = self.waitforevent(self.gotevent)
915 self.gotevent.clear()
916 if expr is None and gotevent: # TeX/LaTeX should have finished
917 self.texruns = 0
918 self.texdone = 1
919 self.texinput.close() # close the input queue and
920 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
921 try:
922 self.texmessage = ""
923 while 1:
924 self.texmessage += self.gotqueue.get_nowait()
925 except Queue.Empty:
926 pass
927 self.texmessageparsed = self.texmessage
928 if gotevent:
929 if expr is not None:
930 texmessage.inputmarker.check(self)
931 if not self.preamblemode:
932 texmessage.pyxbox.check(self)
933 texmessage.pyxpageout.check(self)
934 texmessages = attr.mergeattrs(texmessages)
935 # reverse loop over the merged texmessages (last is applied first)
936 lentexmessages = len(texmessages)
937 for i in range(lentexmessages):
938 try:
939 texmessages[lentexmessages-1-i].check(self)
940 except TexResultWarning:
941 traceback.print_exc()
942 texmessage.emptylines.check(self)
943 if len(self.texmessageparsed):
944 raise TexResultError("unhandled TeX response (might be an error)", self)
945 else:
946 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
948 def finishdvi(self):
949 """finish TeX/LaTeX and read the dvifile
950 - this method ensures that all textboxes can access their
951 dvicanvas"""
952 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
953 if self.dvicopy:
954 os.system("dvicopy %(t)s.dvi %(t)s.dvicopy > %(t)s.dvicopyout 2> %(t)s.dvicopyerr" % {"t": self.texfilename})
955 dvifilename = "%s.dvicopy" % self.texfilename
956 else:
957 dvifilename = "%s.dvi" % self.texfilename
958 if not self.texipc:
959 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
960 page = 1
961 for box in self.needdvitextboxes:
962 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0]))
963 page += 1
964 if self.dvifile.readpage(None) is not None:
965 raise RuntimeError("end of dvifile expected")
966 self.dvifile = None
967 self.needdvitextboxes = []
969 def reset(self, reinit=0):
970 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
971 if self.texruns:
972 self.finishdvi()
973 if self.texdebug is not None:
974 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
975 self.executeid = 0
976 self.page = 0
977 self.texdone = 0
978 if reinit:
979 self.preamblemode = 1
980 for expr, texmessages in self.preambles:
981 self.execute(expr, texmessages)
982 if self.mode == "latex":
983 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
984 self.preamblemode = 0
985 else:
986 self.preambles = []
987 self.preamblemode = 1
989 def set(self, mode=None,
990 lfs=None,
991 docclass=None,
992 docopt=None,
993 usefiles=None,
994 fontmaps=None,
995 waitfortex=None,
996 showwaitfortex=None,
997 texipc=None,
998 texdebug=None,
999 dvidebug=None,
1000 errordebug=None,
1001 dvicopy=None,
1002 pyxgraphics=None,
1003 texmessagesstart=None,
1004 texmessagesdocclass=None,
1005 texmessagesbegindoc=None,
1006 texmessagesend=None,
1007 texmessagesdefaultpreamble=None,
1008 texmessagesdefaultrun=None):
1009 """provide a set command for TeX/LaTeX settings
1010 - TeX/LaTeX must not yet been started
1011 - especially needed for the defaultrunner, where no access to
1012 the constructor is available"""
1013 if self.texruns:
1014 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1015 if mode is not None:
1016 mode = mode.lower()
1017 if mode != "tex" and mode != "latex":
1018 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1019 self.mode = mode
1020 if lfs is not None:
1021 self.lfs = lfs
1022 if docclass is not None:
1023 self.docclass = docclass
1024 if docopt is not None:
1025 self.docopt = docopt
1026 if usefiles is not None:
1027 self.usefiles = usefiles
1028 if fontmaps is not None:
1029 self.fontmap = dvifile.readfontmap(fontmaps.split())
1030 if waitfortex is not None:
1031 self.waitfortex = waitfortex
1032 if showwaitfortex is not None:
1033 self.showwaitfortex = showwaitfortex
1034 if texipc is not None:
1035 self.texipc = texipc
1036 if texdebug is not None:
1037 if self.texdebug is not None:
1038 self.texdebug.close()
1039 if texdebug[-4:] == ".tex":
1040 self.texdebug = open(texdebug, "w")
1041 else:
1042 self.texdebug = open("%s.tex" % texdebug, "w")
1043 if dvidebug is not None:
1044 self.dvidebug = dvidebug
1045 if errordebug is not None:
1046 self.errordebug = errordebug
1047 if dvicopy is not None:
1048 self.dvicopy = dvicopy
1049 if pyxgraphics is not None:
1050 self.pyxgraphics = pyxgraphics
1051 if errordebug is not None:
1052 self.errordebug = errordebug
1053 if texmessagesstart is not None:
1054 self.texmessagesstart = texmessagesstart
1055 if texmessagesdocclass is not None:
1056 self.texmessagesdocclass = texmessagesdocclass
1057 if texmessagesbegindoc is not None:
1058 self.texmessagesbegindoc = texmessagesbegindoc
1059 if texmessagesend is not None:
1060 self.texmessagesend = texmessagesend
1061 if texmessagesdefaultpreamble is not None:
1062 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1063 if texmessagesdefaultrun is not None:
1064 self.texmessagesdefaultrun = texmessagesdefaultrun
1066 def preamble(self, expr, texmessages=[]):
1067 r"""put something into the TeX/LaTeX preamble
1068 - in LaTeX, this is done before the \begin{document}
1069 (you might use \AtBeginDocument, when you're in need for)
1070 - it is not allowed to call preamble after calling the
1071 text method for the first time (for LaTeX this is needed
1072 due to \begin{document}; in TeX it is forced for compatibility
1073 (you should be able to switch from TeX to LaTeX, if you want,
1074 without breaking something)
1075 - preamble expressions must not create any dvi output
1076 - args might contain texmessage instances"""
1077 if self.texdone or not self.preamblemode:
1078 raise RuntimeError("preamble calls disabled due to previous text calls")
1079 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1080 self.execute(expr, texmessages)
1081 self.preambles.append((expr, texmessages))
1083 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:")
1085 def text(self, x, y, expr, textattrs=[], texmessages=[]):
1086 """create text by passing expr to TeX/LaTeX
1087 - returns a textbox containing the result from running expr thru TeX/LaTeX
1088 - the box center is set to x, y
1089 - *args may contain attr parameters, namely:
1090 - textattr instances
1091 - texmessage instances
1092 - trafo._trafo instances
1093 - style.fillstyle instances"""
1094 if expr is None:
1095 raise ValueError("None expression is invalid")
1096 if self.texdone:
1097 self.reset(reinit=1)
1098 first = 0
1099 if self.preamblemode:
1100 if self.mode == "latex":
1101 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1102 self.preamblemode = 0
1103 first = 1
1104 if self.texipc and self.dvicopy:
1105 raise RuntimeError("texipc and dvicopy can't be mixed up")
1106 textattrs = attr.mergeattrs(textattrs) # perform cleans
1107 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1108 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1109 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1110 textattrs = attr.getattrs(textattrs, [textattr])
1111 # reverse loop over the merged textattrs (last is applied first)
1112 lentextattrs = len(textattrs)
1113 for i in range(lentextattrs):
1114 expr = textattrs[lentextattrs-1-i].apply(expr)
1115 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1116 if self.texipc:
1117 if first:
1118 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1119 match = self.PyXBoxPattern.search(self.texmessage)
1120 if not match or int(match.group("page")) != self.page:
1121 raise TexResultError("box extents not found", self)
1122 left, right, height, depth = [unit.x_pt(float(xxx)*72/72.27) for xxx in match.group("lt", "rt", "ht", "dp")]
1123 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1124 for t in trafos:
1125 box.reltransform(t)
1126 if self.texipc:
1127 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0]))
1128 else:
1129 self.needdvitextboxes.append(box)
1130 return box
1132 def text_pt(self, x, y, expr, *args, **kwargs):
1133 return self.text(unit.t_pt(x), unit.t_pt(y), expr, *args, **kwargs)
1135 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1137 def textboxes(self, text, pageshapes):
1138 # this is some experimental code to put text into several boxes
1139 # while the bounding shape changes from box to box (rectangles only)
1140 # first we load sev.tex
1141 if not self.textboxesincluded:
1142 self.execute(r"\input textboxes.tex", [texmessage.load])
1143 self.textboxesincluded = 1
1144 # define page shapes
1145 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1146 pageshapes_str += "\\lohsizes={%\n"
1147 for hsize, vsize in pageshapes[1:]:
1148 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1149 pageshapes_str += "{\\relax}%\n}%\n"
1150 pageshapes_str += "\\lovsizes={%\n"
1151 for hsize, vsize in pageshapes[1:]:
1152 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1153 pageshapes_str += "{\\relax}%\n}%\n"
1154 page = 0
1155 parnos = []
1156 parshapes = []
1157 loop = 0
1158 while 1:
1159 self.execute(pageshapes_str, [])
1160 parnos_str = "}{".join(parnos)
1161 if parnos_str:
1162 parnos_str = "{%s}" % parnos_str
1163 parnos_str = "\\parnos={%s{\\relax}}%%\n" % parnos_str
1164 self.execute(parnos_str, [])
1165 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1166 self.execute(parshapes_str, [])
1167 self.execute("\\global\\count0=1%%\n"
1168 "\\global\\parno=0%%\n"
1169 "\\global\\myprevgraf=0%%\n"
1170 "\\global\\showprevgraf=0%%\n"
1171 "\\global\\outputtype=0%%\n"
1172 "\\global\\leastcost=10000000%%\n"
1173 "%s%%\n"
1174 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1175 if self.texipc:
1176 if self.dvifile is None:
1177 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1178 else:
1179 raise RuntimeError("textboxes currently needs texipc")
1180 lastparnos = parnos
1181 parnos = []
1182 lastparshapes = parshapes
1183 parshapes = []
1184 pages = 0
1185 lastpar = prevgraf = -1
1186 m = self.PyXVariableBoxPattern.search(self.texmessage)
1187 while m:
1188 pages += 1
1189 page = int(m.group("page"))
1190 assert page == pages
1191 par = int(m.group("par"))
1192 prevgraf = int(m.group("prevgraf"))
1193 if page <= len(pageshapes):
1194 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1195 else:
1196 width = 72.27/72*unit.topt(pageshapes[-1][0])
1197 if page < len(pageshapes):
1198 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1199 else:
1200 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1202 if par != lastpar:
1203 # a new paragraph is to be broken
1204 parnos.append(str(par))
1205 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf)])
1206 if len(parshape):
1207 parshape = " 0pt " + parshape
1208 parshapes.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf + 1, parshape, nextwidth))
1209 elif prevgraf == lastprevgraf:
1210 pass
1211 else:
1212 # we have to append the breaking of the previous paragraph
1213 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1214 oldparshape = oldparshape.split('}')[0]
1215 if len(parshape):
1216 oldparshape = " " + oldparshape
1217 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - lastprevgraf)])
1218 if len(parshape):
1219 parshape = " 0pt " + parshape
1220 else:
1221 parshape = " "
1222 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1223 lastpar = par
1224 lastprevgraf = prevgraf
1225 nextpos = m.end()
1226 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1227 result = []
1228 for i in range(pages):
1229 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1230 if parnos == lastparnos and parshapes == lastparshapes:
1231 return result
1232 loop += 1
1233 if loop > 100:
1234 raise TexResultError("Too many loops in textboxes ", texrunner)
1237 # the module provides an default texrunner and methods for direct access
1238 defaulttexrunner = texrunner()
1239 reset = defaulttexrunner.reset
1240 set = defaulttexrunner.set
1241 preamble = defaulttexrunner.preamble
1242 text = defaulttexrunner.text
1243 text_pt = defaulttexrunner.text_pt