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