textboxes (experimental)
[PyX/mjg.git] / pyx / text.py
blob75e7c139078e50bd24ff8a3a0d7dc96e2fbb0680
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_pt(box.rect_pt, 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.texttrafo = trafo.translate_pt(x, y)
575 box.rect_pt.__init__(self, x - left, y - depth,
576 left + right, depth + height,
577 abscenter = (left, depth))
578 canvas._canvas.__init__(self)
579 self.finishdvi = finishdvi
580 self.dvicanvas = None
581 self.set(attrs)
582 self.insertdvicanvas = 0
584 def transform(self, *trafos):
585 if self.insertdvicanvas:
586 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
587 box.rect_pt.transform(self, *trafos)
588 for trafo in trafos:
589 self.texttrafo = trafo * self.texttrafo
591 def setdvicanvas(self, dvicanvas):
592 if self.dvicanvas is not None:
593 raise RuntimeError("multiple call to setdvicanvas")
594 self.dvicanvas = dvicanvas
596 def ensuredvicanvas(self):
597 if self.dvicanvas is None:
598 self.finishdvi()
599 assert self.dvicanvas is not None, "finishdvi is broken"
600 if not self.insertdvicanvas:
601 self.insert(self.dvicanvas, [self.texttrafo])
602 self.insertdvicanvas = 1
604 def marker(self, marker):
605 self.ensuredvicanvas()
606 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
608 def prolog(self):
609 self.ensuredvicanvas()
610 return canvas._canvas.prolog(self)
612 def write(self, file):
613 self.ensuredvicanvas()
614 canvas._canvas.write(self, file)
617 class textbox(textbox_pt):
619 def __init__(self, x, y, left, right, height, depth, texrunner, attrs):
620 textbox_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(left), unit.topt(right),
621 unit.topt(height), unit.topt(depth), texrunner, attrs)
624 def _cleantmp(texrunner):
625 """get rid of temporary files
626 - function to be registered by atexit
627 - files contained in usefiles are kept"""
628 if texrunner.texruns: # cleanup while TeX is still running?
629 texrunner.texruns = 0
630 texrunner.texdone = 1
631 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
632 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
633 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
634 else:
635 texrunner.texinput.write("\n\\end\n")
636 texrunner.texinput.close() # close the input queue and
637 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
638 return # didn't got a quit from TeX -> we can't do much more
639 for usefile in texrunner.usefiles:
640 extpos = usefile.rfind(".")
641 try:
642 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
643 except OSError:
644 pass
645 for file in glob.glob("%s.*" % texrunner.texfilename):
646 try:
647 os.unlink(file)
648 except OSError:
649 pass
650 if texrunner.texdebug is not None:
651 try:
652 texrunner.texdebug.close()
653 texrunner.texdebug = None
654 except IOError:
655 pass
658 class texrunner:
659 """TeX/LaTeX interface
660 - runs TeX/LaTeX expressions instantly
661 - checks TeX/LaTeX response
662 - the instance variable texmessage stores the last TeX
663 response as a string
664 - the instance variable texmessageparsed stores a parsed
665 version of texmessage; it should be empty after
666 texmessage.check was called, otherwise a TexResultError
667 is raised
668 - the instance variable errordebug controls the verbose
669 level of TexResultError"""
671 defaulttexmessagesstart = [texmessage.start]
672 defaulttexmessagesdocclass = [texmessage.load]
673 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
674 defaulttexmessagesend = [texmessage.texend]
675 defaulttexmessagesdefaultpreamble = [texmessage.load]
676 defaulttexmessagesdefaultrun = [texmessage.loadfd, texmessage.graphicsload]
678 def __init__(self, mode="tex",
679 lfs="10pt",
680 docclass="article",
681 docopt=None,
682 usefiles=[],
683 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
684 waitfortex=config.getint("text", "waitfortex", 60),
685 showwaitfortex=config.getint("text", "showwaitfortex", 5),
686 texipc=config.getboolean("text", "texipc", 0),
687 texdebug=None,
688 dvidebug=0,
689 errordebug=1,
690 dvicopy=0,
691 pyxgraphics=1,
692 texmessagesstart=[],
693 texmessagesdocclass=[],
694 texmessagesbegindoc=[],
695 texmessagesend=[],
696 texmessagesdefaultpreamble=[],
697 texmessagesdefaultrun=[]):
698 mode = mode.lower()
699 if mode != "tex" and mode != "latex":
700 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
701 self.mode = mode
702 self.lfs = lfs
703 self.docclass = docclass
704 self.docopt = docopt
705 self.usefiles = usefiles
706 self.fontmap = dvifile.readfontmap(fontmaps.split())
707 self.waitfortex = waitfortex
708 self.showwaitfortex = showwaitfortex
709 self.texipc = texipc
710 if texdebug is not None:
711 if texdebug[-4:] == ".tex":
712 self.texdebug = open(texdebug, "w")
713 else:
714 self.texdebug = open("%s.tex" % texdebug, "w")
715 else:
716 self.texdebug = None
717 self.dvidebug = dvidebug
718 self.errordebug = errordebug
719 self.dvicopy = dvicopy
720 self.pyxgraphics = pyxgraphics
721 self.texmessagesstart = texmessagesstart
722 self.texmessagesdocclass = texmessagesdocclass
723 self.texmessagesbegindoc = texmessagesbegindoc
724 self.texmessagesend = texmessagesend
725 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
726 self.texmessagesdefaultrun = texmessagesdefaultrun
728 self.texruns = 0
729 self.texdone = 0
730 self.preamblemode = 1
731 self.executeid = 0
732 self.page = 0
733 self.preambles = []
734 self.needdvitextboxes = [] # when texipc-mode off
735 self.dvifile = None
736 savetempdir = tempfile.tempdir
737 tempfile.tempdir = os.curdir
738 self.texfilename = os.path.basename(tempfile.mktemp())
739 tempfile.tempdir = savetempdir
741 def waitforevent(self, event):
742 """waits verbosely with an timeout for an event
743 - observes an event while periodly while printing messages
744 - returns the status of the event (isSet)
745 - does not clear the event"""
746 if self.showwaitfortex:
747 waited = 0
748 hasevent = 0
749 while waited < self.waitfortex and not hasevent:
750 if self.waitfortex - waited > self.showwaitfortex:
751 event.wait(self.showwaitfortex)
752 waited += self.showwaitfortex
753 else:
754 event.wait(self.waitfortex - waited)
755 waited += self.waitfortex - waited
756 hasevent = event.isSet()
757 if not hasevent:
758 if waited < self.waitfortex:
759 sys.stderr.write("*** PyX INFO: still waiting for %s after %i seconds...\n" % (self.mode, waited))
760 else:
761 sys.stderr.write("*** PyX ERROR: the timeout of %i seconds expired and %s did not respond.\n" % (waited, self.mode))
762 return hasevent
763 else:
764 event.wait(self.waitfortex)
765 return event.isSet()
767 def execute(self, expr, texmessages):
768 """executes expr within TeX/LaTeX
769 - if self.texruns is not yet set, TeX/LaTeX is initialized,
770 self.texruns is set and self.preamblemode is set
771 - the method must not be called, when self.texdone is already set
772 - expr should be a string or None
773 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
774 while self.texdone becomes set
775 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
776 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
777 - texmessages is a list of texmessage instances"""
778 if not self.texruns:
779 if self.texdebug is not None:
780 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
781 self.texdebug.write("%% mode: %s\n" % self.mode)
782 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
783 for usefile in self.usefiles:
784 extpos = usefile.rfind(".")
785 try:
786 os.rename(usefile, self.texfilename + usefile[extpos:])
787 except OSError:
788 pass
789 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
790 texfile.write("\\relax%\n")
791 texfile.close()
792 if self.texipc:
793 ipcflag = " --ipc"
794 else:
795 ipcflag = ""
796 try:
797 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
798 except ValueError:
799 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
800 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
801 atexit.register(_cleantmp, self)
802 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
803 self.gotevent = threading.Event() # keeps the got inputmarker event
804 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
805 self.quitevent = threading.Event() # keeps for end of terminal event
806 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
807 self.texruns = 1
808 oldpreamblemode = self.preamblemode
809 self.preamblemode = 1
810 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
811 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
812 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
813 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
814 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
815 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
816 "\\newdimen\\PyXDimenHAlignRT%\n" +
817 _textattrspreamble + # insert preambles for textattrs macros
818 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
819 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
820 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
821 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
822 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
823 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
824 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
825 "lt=\\the\\PyXDimenHAlignLT,"
826 "rt=\\the\\PyXDimenHAlignRT,"
827 "ht=\\the\\ht\\PyXBox,"
828 "dp=\\the\\dp\\PyXBox:}%\n"
829 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
830 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
831 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
832 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
833 "\\def\\PyXMarker#1{\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
834 self.defaulttexmessagesstart + self.texmessagesstart)
835 os.remove("%s.tex" % self.texfilename)
836 if self.mode == "tex":
837 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
838 lfsname = self.lfs
839 else:
840 lfsname = "%s.lfs" % self.lfs
841 for fulllfsname in [lfsname,
842 os.path.join(sys.prefix, "share", "pyx", lfsname),
843 os.path.join(os.path.dirname(__file__), "lfs", lfsname)]:
844 try:
845 lfsfile = open(fulllfsname, "r")
846 lfsdef = lfsfile.read()
847 lfsfile.close()
848 break
849 except IOError:
850 pass
851 else:
852 allfiles = (glob.glob("*.lfs") +
853 glob.glob(os.path.join(sys.prefix, "share", "pyx", "*.lfs")) +
854 glob.glob(os.path.join(os.path.dirname(__file__), "lfs", "*.lfs")))
855 lfsnames = []
856 for f in allfiles:
857 try:
858 open(f, "r").close()
859 lfsnames.append(os.path.basename(f)[:-4])
860 except IOError:
861 pass
862 lfsnames.sort()
863 if len(lfsnames):
864 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname, lfsnames))
865 else:
866 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname)
867 self.execute(lfsdef, [])
868 self.execute("\\normalsize%\n", [])
869 self.execute("\\newdimen\\linewidth%\n", [])
870 elif self.mode == "latex":
871 if self.pyxgraphics:
872 for pyxdef in ["pyx.def",
873 os.path.join(sys.prefix, "share", "pyx", "pyx.def"),
874 os.path.join(os.path.dirname(__file__), "..", "contrib", "pyx.def")]:
875 try:
876 open(pyxdef, "r").close()
877 break
878 except IOError:
879 pass
880 else:
881 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
882 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
883 self.execute("\\makeatletter%\n"
884 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
885 "\\def\\ProcessOptions{%\n"
886 "\\def\\Gin@driver{" + pyxdef + "}%\n"
887 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
888 "\\saveProcessOptions}%\n"
889 "\\makeatother",
891 if self.docopt is not None:
892 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
893 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
894 else:
895 self.execute("\\documentclass{%s}" % self.docclass,
896 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
897 self.preamblemode = oldpreamblemode
898 self.executeid += 1
899 if expr is not None: # TeX/LaTeX should process expr
900 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
901 if self.preamblemode:
902 self.expr = ("%s%%\n" % expr +
903 "\\PyXInput{%i}%%\n" % self.executeid)
904 else:
905 self.page += 1
906 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
907 "\\PyXInput{%i}%%\n" % self.executeid)
908 else: # TeX/LaTeX should be finished
909 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
910 if self.mode == "latex":
911 self.expr = "\\end{document}%\n"
912 else:
913 self.expr = "\\end%\n"
914 if self.texdebug is not None:
915 self.texdebug.write(self.expr)
916 self.texinput.write(self.expr)
917 gotevent = self.waitforevent(self.gotevent)
918 self.gotevent.clear()
919 if expr is None and gotevent: # TeX/LaTeX should have finished
920 self.texruns = 0
921 self.texdone = 1
922 self.texinput.close() # close the input queue and
923 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
924 try:
925 self.texmessage = ""
926 while 1:
927 self.texmessage += self.gotqueue.get_nowait()
928 except Queue.Empty:
929 pass
930 self.texmessageparsed = self.texmessage
931 if gotevent:
932 if expr is not None:
933 texmessage.inputmarker.check(self)
934 if not self.preamblemode:
935 texmessage.pyxbox.check(self)
936 texmessage.pyxpageout.check(self)
937 texmessages = attr.mergeattrs(texmessages)
938 # reverse loop over the merged texmessages (last is applied first)
939 lentexmessages = len(texmessages)
940 for i in range(lentexmessages):
941 try:
942 texmessages[lentexmessages-1-i].check(self)
943 except TexResultWarning:
944 traceback.print_exc()
945 texmessage.emptylines.check(self)
946 if len(self.texmessageparsed):
947 raise TexResultError("unhandled TeX response (might be an error)", self)
948 else:
949 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
951 def finishdvi(self):
952 """finish TeX/LaTeX and read the dvifile
953 - this method ensures that all textboxes can access their
954 dvicanvas"""
955 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
956 if self.dvicopy:
957 os.system("dvicopy %(t)s.dvi %(t)s.dvicopy > %(t)s.dvicopyout 2> %(t)s.dvicopyerr" % {"t": self.texfilename})
958 dvifilename = "%s.dvicopy" % self.texfilename
959 else:
960 dvifilename = "%s.dvi" % self.texfilename
961 if not self.texipc:
962 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
963 page = 1
964 for box in self.needdvitextboxes:
965 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0]))
966 page += 1
967 if self.dvifile.readpage(None) is not None:
968 raise RuntimeError("end of dvifile expected")
969 self.dvifile = None
970 self.needdvitextboxes = []
972 def reset(self, reinit=0):
973 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
974 if self.texruns:
975 self.finishdvi()
976 if self.texdebug is not None:
977 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
978 self.executeid = 0
979 self.page = 0
980 self.texdone = 0
981 if reinit:
982 self.preamblemode = 1
983 for expr, texmessages in self.preambles:
984 self.execute(expr, texmessages)
985 if self.mode == "latex":
986 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
987 self.preamblemode = 0
988 else:
989 self.preambles = []
990 self.preamblemode = 1
992 def set(self, mode=None,
993 lfs=None,
994 docclass=None,
995 docopt=None,
996 usefiles=None,
997 fontmaps=None,
998 waitfortex=None,
999 showwaitfortex=None,
1000 texipc=None,
1001 texdebug=None,
1002 dvidebug=None,
1003 errordebug=None,
1004 dvicopy=None,
1005 pyxgraphics=None,
1006 texmessagesstart=None,
1007 texmessagesdocclass=None,
1008 texmessagesbegindoc=None,
1009 texmessagesend=None,
1010 texmessagesdefaultpreamble=None,
1011 texmessagesdefaultrun=None):
1012 """provide a set command for TeX/LaTeX settings
1013 - TeX/LaTeX must not yet been started
1014 - especially needed for the defaultrunner, where no access to
1015 the constructor is available"""
1016 if self.texruns:
1017 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1018 if mode is not None:
1019 mode = mode.lower()
1020 if mode != "tex" and mode != "latex":
1021 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1022 self.mode = mode
1023 if lfs is not None:
1024 self.lfs = lfs
1025 if docclass is not None:
1026 self.docclass = docclass
1027 if docopt is not None:
1028 self.docopt = docopt
1029 if usefiles is not None:
1030 self.usefiles = usefiles
1031 if fontmaps is not None:
1032 self.fontmap = dvifile.readfontmap(fontmaps.split())
1033 if waitfortex is not None:
1034 self.waitfortex = waitfortex
1035 if showwaitfortex is not None:
1036 self.showwaitfortex = showwaitfortex
1037 if texipc is not None:
1038 self.texipc = texipc
1039 if texdebug is not None:
1040 if self.texdebug is not None:
1041 self.texdebug.close()
1042 if texdebug[-4:] == ".tex":
1043 self.texdebug = open(texdebug, "w")
1044 else:
1045 self.texdebug = open("%s.tex" % texdebug, "w")
1046 if dvidebug is not None:
1047 self.dvidebug = dvidebug
1048 if errordebug is not None:
1049 self.errordebug = errordebug
1050 if dvicopy is not None:
1051 self.dvicopy = dvicopy
1052 if pyxgraphics is not None:
1053 self.pyxgraphics = pyxgraphics
1054 if errordebug is not None:
1055 self.errordebug = errordebug
1056 if texmessagesstart is not None:
1057 self.texmessagesstart = texmessagesstart
1058 if texmessagesdocclass is not None:
1059 self.texmessagesdocclass = texmessagesdocclass
1060 if texmessagesbegindoc is not None:
1061 self.texmessagesbegindoc = texmessagesbegindoc
1062 if texmessagesend is not None:
1063 self.texmessagesend = texmessagesend
1064 if texmessagesdefaultpreamble is not None:
1065 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1066 if texmessagesdefaultrun is not None:
1067 self.texmessagesdefaultrun = texmessagesdefaultrun
1069 def preamble(self, expr, texmessages=[]):
1070 r"""put something into the TeX/LaTeX preamble
1071 - in LaTeX, this is done before the \begin{document}
1072 (you might use \AtBeginDocument, when you're in need for)
1073 - it is not allowed to call preamble after calling the
1074 text method for the first time (for LaTeX this is needed
1075 due to \begin{document}; in TeX it is forced for compatibility
1076 (you should be able to switch from TeX to LaTeX, if you want,
1077 without breaking something)
1078 - preamble expressions must not create any dvi output
1079 - args might contain texmessage instances"""
1080 if self.texdone or not self.preamblemode:
1081 raise RuntimeError("preamble calls disabled due to previous text calls")
1082 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1083 self.execute(expr, texmessages)
1084 self.preambles.append((expr, texmessages))
1086 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:")
1088 def text_pt(self, x, y, expr, textattrs=[], texmessages=[]):
1089 """create text by passing expr to TeX/LaTeX
1090 - returns a textbox containing the result from running expr thru TeX/LaTeX
1091 - the box center is set to x, y
1092 - *args may contain attr parameters, namely:
1093 - textattr instances
1094 - texmessage instances
1095 - trafo._trafo instances
1096 - style.fillstyle instances"""
1097 if expr is None:
1098 raise ValueError("None expression is invalid")
1099 if self.texdone:
1100 self.reset(reinit=1)
1101 first = 0
1102 if self.preamblemode:
1103 if self.mode == "latex":
1104 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1105 self.preamblemode = 0
1106 first = 1
1107 if self.texipc and self.dvicopy:
1108 raise RuntimeError("texipc and dvicopy can't be mixed up")
1109 textattrs = attr.mergeattrs(textattrs) # perform cleans
1110 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1111 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1112 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1113 textattrs = attr.getattrs(textattrs, [textattr])
1114 # reverse loop over the merged textattrs (last is applied first)
1115 lentextattrs = len(textattrs)
1116 for i in range(lentextattrs):
1117 expr = textattrs[lentextattrs-1-i].apply(expr)
1118 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1119 if self.texipc:
1120 if first:
1121 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1122 match = self.PyXBoxPattern.search(self.texmessage)
1123 if not match or int(match.group("page")) != self.page:
1124 raise TexResultError("box extents not found", self)
1125 left, right, height, depth = map(lambda x: float(x) * 72.0 / 72.27, match.group("lt", "rt", "ht", "dp"))
1126 box = textbox_pt(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1127 for t in trafos:
1128 box.reltransform(t)
1129 if self.texipc:
1130 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0]))
1131 else:
1132 self.needdvitextboxes.append(box)
1133 return box
1135 def text(self, x, y, expr, *args, **kwargs):
1136 return self.text_pt(unit.topt(x), unit.topt(y), expr, *args, **kwargs)
1138 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1140 def textboxes(self, text, pageshapes):
1141 # this is some experimental code to put text into several boxes
1142 # while the bounding shape changes from box to box (rectangles only)
1143 # first we load sev.tex
1144 self.execute(r"\input textboxes.tex", [texmessage.load])
1145 # define page shapes
1146 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1147 pageshapes_str += "\\lohsizes={%\n"
1148 for hsize, vsize in pageshapes[1:]:
1149 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1150 pageshapes_str += "{\\relax}%\n}%\n"
1151 pageshapes_str += "\\lovsizes={%\n"
1152 for hsize, vsize in pageshapes[1:]:
1153 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1154 pageshapes_str += "{\\relax}%\n}%\n"
1155 page = 0
1156 parnos = []
1157 parshapes = []
1158 loop = 0
1159 while 1:
1160 self.execute(pageshapes_str, [])
1161 # print pageshapes_str
1162 parnos_str = "\\parnos={%s 0}%%\n" % " ".join(parnos)
1163 self.execute(parnos_str, [])
1164 print parnos_str
1165 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1166 self.execute(parshapes_str, [])
1167 print parshapes_str
1168 self.execute("\\global\\count0=1%%\n"
1169 "\\global\\parno=0%%\n"
1170 "\\global\\myprevgraf=0%%\n"
1171 "\\global\\showprevgraf=0%%\n"
1172 "\\global\\outputtype=0%%\n"
1173 "\\global\\leastcost=10000000%%\n"
1174 "%s%%\n"
1175 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1176 if self.texipc:
1177 if self.dvifile is None:
1178 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1179 else:
1180 RuntimeError("textboxes currently needs texipc")
1181 lastparnos = parnos
1182 parnos = []
1183 lastparshapes = parshapes
1184 parshapes = []
1185 pages = 0
1186 lastpar = prevgraf = -1
1187 m = self.PyXVariableBoxPattern.search(self.texmessage)
1188 while m:
1189 print m.string[m.start(): m.end()]
1190 pages += 1
1191 page = int(m.group("page"))
1192 assert page == pages
1193 par = int(m.group("par"))
1194 prevgraf = int(m.group("prevgraf"))
1195 if page <= len(pageshapes):
1196 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1197 else:
1198 width = 72.27/72*unit.topt(pageshapes[-1][0])
1199 if page < len(pageshapes):
1200 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1201 else:
1202 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1203 parnos.append(str(par))
1204 if par != lastpar:
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 else:
1210 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - 1)])
1211 if len(parshape):
1212 parshape = " 0pt " + parshape
1213 oldparshape = parshapes[-1].split(None, 2)[2][:-1]
1214 if len(parshape):
1215 oldparshape = " " + oldparshape
1216 prevgraf = prevgraf + lastprevgraf
1217 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1218 lastpar = par
1219 lastprevgraf = prevgraf
1220 nextpos = m.end()
1221 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1222 result = []
1223 for i in range(pages):
1224 result.append(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), i, 0, 0, 0, 0, 0, 0]))
1225 if parnos == lastparnos and parshapes == lastparshapes:
1226 return result
1227 loop += 1
1230 # the module provides an default texrunner and methods for direct access
1231 defaulttexrunner = texrunner()
1232 reset = defaulttexrunner.reset
1233 set = defaulttexrunner.set
1234 preamble = defaulttexrunner.preamble
1235 text = defaulttexrunner.text
1236 text_pt = defaulttexrunner.text_pt