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