1 # Copyright (C) 2002-2013 Jörg Lehmann <joergl@users.sourceforge.net>
2 # Copyright (C) 2003-2011 Michael Schindler <m-schindler@users.sourceforge.net>
3 # Copyright (C) 2002-2013 André Wobst <wobsta@users.sourceforge.net>
5 # This file is part of PyX (http://pyx.sourceforge.net/).
7 # PyX is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # PyX is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with PyX; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21 import atexit
, errno
, functools
, glob
, inspect
, io
, itertools
, logging
, os
22 import queue
, re
, shutil
, sys
, tempfile
, textwrap
, threading
24 from pyx
import config
, unit
, box
, baseclasses
, trafo
, version
, attr
, style
, path
, canvas
25 from pyx
import bbox
as bboxmodule
26 from pyx
.dvi
import dvifile
28 logger
= logging
.getLogger("pyx")
31 def indent_text(text
):
32 "Prepends two spaces to each line in text."
33 return "".join(" " + line
for line
in text
.splitlines(True))
36 def remove_string(p
, s
):
37 """Removes a string from a string.
39 The function removes the first occurrence of a string in another string.
41 :param str p: string to be removed
42 :param str s: string to be searched
43 :returns: tuple of the result string and a success boolean (``True`` when
44 the string was removed)
45 :rtype: tuple of str and bool
48 >>> remove_string("XXX", "abcXXXdefXXXghi")
49 ('abcdefXXXghi', True)
52 r
= s
.replace(p
, '', 1)
56 def remove_pattern(p
, s
, ignore_nl
=True):
57 r
"""Removes a pattern from a string.
59 The function removes the first occurence of the pattern from a string. It
60 returns a tuple of the resulting string and the matching object (or
61 ``None``, if the pattern did not match).
63 :param re.regex p: pattern to be removed
64 :param str s: string to be searched
65 :param bool ignore_nl: When ``True``, newlines in the string are ignored
66 during the pattern search. The returned string will still contain all
67 newline characters outside of the matching part of the string, whereas
68 the returned matching object will not contain the newline characters
69 inside of the matching part of the string.
70 :returns: the result string and the match object or ``None`` if
72 :rtype: tuple of str and (re.match or None)
75 >>> r, m = remove_pattern(re.compile("XXX"), 'ab\ncXX\nXdefXX\nX')
78 >>> m.string[m.start():m.end()]
83 r
= s
.replace('\n', '')
90 s_start
= r_start
= m
.start()
91 s_end
= r_end
= m
.end()
102 return s
[:s_start
] + s
[s_end
:], m
107 """Return list of positions of a character in a string.
110 >>> index_all("X", "abXcdXef")
115 return [i
for i
, x
in enumerate(s
) if x
== c
]
119 """Returns iterator over pairs of data from an iterable.
122 >>> list(pairwise([1, 2, 3]))
126 a
, b
= itertools
.tee(i
)
131 def remove_nested_brackets(s
, openbracket
="(", closebracket
=")", quote
='"'):
132 """Remove nested brackets
134 Return a modified string with all nested brackets 1 removed, i.e. only
135 keep the first bracket nesting level. In case an opening bracket is
136 immediately followed by a quote, the quoted string is left untouched,
137 even if it contains brackets. The use-case for that are files in the
138 folder "Program Files (x86)".
140 If the bracket nesting level is broken (unbalanced), the unmodified
144 >>> remove_nested_brackets('aaa("bb()bb" cc(dd(ee))ff)ggg'*2)
145 'aaa("bb()bb" ccff)gggaaa("bb()bb" ccff)ggg'
148 openpos
= index_all(openbracket
, s
)
149 closepos
= index_all(closebracket
, s
)
150 if quote
is not None:
151 quotepos
= index_all(quote
, s
)
152 for openquote
, closequote
in pairwise(quotepos
):
153 if openquote
-1 in openpos
:
154 # ignore brackets in quoted string
155 openpos
= [pos
for pos
in openpos
156 if not (openquote
< pos
< closequote
)]
157 closepos
= [pos
for pos
in closepos
158 if not (openquote
< pos
< closequote
)]
159 if len(openpos
) != len(closepos
):
160 # unbalanced brackets
163 # keep the original string in case we need to return due to broken nesting levels
167 # Iterate over the bracket positions from the end.
168 # We go reversely to be able to immediately remove nested bracket levels
169 # without influencing bracket positions yet to come in the loop.
170 for pos
, leveldelta
in sorted(itertools
.chain(zip(openpos
, itertools
.repeat(-1)),
171 zip(closepos
, itertools
.repeat(1))),
173 # the current bracket nesting level
176 # unbalanced brackets
178 if leveldelta
== 1 and level
== 2:
179 # a closing bracket to cut after
181 if leveldelta
== -1 and level
== 1:
182 # an opening bracket to cut at -> remove
183 r
= r
[:pos
] + r
[endpos
:]
187 class TexResultError(ValueError):
188 "Error raised by :class:`texmessage` parsers."
193 """Collection of TeX output parsers.
195 This class is not meant to be instanciated. Instead, it serves as a
196 namespace for TeX output parsers, which are functions receiving a TeX
197 output and returning parsed output.
199 In addition, this class also contains some generator functions (namely
200 :attr:`texmessage.no_file` and :attr:`texmessage.pattern`), which return a
201 function according to the given parameters. They are used to generate some
202 of the parsers in this class and can be used to create others as well.
205 start_pattern
= re
.compile(r
"This is [-0-9a-zA-Z\s_]*TeX")
209 r
"""Validate TeX/LaTeX startup message including scrollmode test.
212 >>> texmessage.start(r'''
213 ... This is e-TeX (version)
214 ... *! Undefined control sequence.
221 # check for "This is e-TeX" etc.
222 if not texmessage
.start_pattern
.search(msg
):
223 raise TexResultError("TeX startup failed")
225 # check for \raiseerror -- just to be sure that communication works
226 new
= msg
.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[-1]
228 raise TexResultError("TeX scrollmode check failed")
232 def no_file(fileending
, qualname
=None):
233 "Generator function to ignore the missing file message for fileending."
235 "Ignore the missing {} file message."
236 return msg
.replace("No file texput.%s." % fileending
, "").replace("No file %s%stexput.%s." % (os
.curdir
, os
.sep
, fileending
), "")
237 check
.__doc
__ = check
.__doc
__.format(fileending
)
238 if qualname
is not None:
239 check
.__qualname
__ = qualname
242 no_aux
= staticmethod(no_file
.__func
__("aux", "texmessage.no_aux"))
243 no_nav
= staticmethod(no_file
.__func
__("nav", "texmessage.no_nav"))
245 aux_pattern
= re
.compile(r
'\(([^()]+\.aux|"[^"]+\.aux")\)')
246 log_pattern
= re
.compile(r
"Transcript written on .*texput\.log\.", re
.DOTALL
)
250 "Validate TeX shutdown message."
251 msg
= re
.sub(texmessage
.aux_pattern
, "", msg
).replace("(see the transcript file for additional information)", "")
253 # check for "Transcript written on ...log."
254 msg
, m
= remove_pattern(texmessage
.log_pattern
, msg
)
256 raise TexResultError("TeX logfile message expected")
259 quoted_file_pattern
= re
.compile(r
'\("(?P<filename>[^"]+)".*?\)')
260 file_pattern
= re
.compile(r
'\((?P<filename>[^"][^ )]*).*?\)', re
.DOTALL
)
264 """Ignore file loading messages.
266 Removes text starting with a round bracket followed by a filename
267 ignoring all further text until the corresponding closing bracket.
268 Quotes and/or line breaks in the filename are handled as needed for TeX
271 Without quoting the filename, the necessary removal of line breaks is
272 not well defined and the different possibilities are tested to check
273 whether one solution is ok. The last of the examples below checks this
277 >>> texmessage.load(r'''other (text.py) things''')
279 >>> texmessage.load(r'''other ("text.py") things''')
281 >>> texmessage.load(r'''other ("tex
282 ... t.py" further (ignored)
285 >>> texmessage.load(r'''other (t
289 ... ther (ignored) text) things''')
293 r
= remove_nested_brackets(msg
)
294 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
296 if not os
.path
.isfile(config
.get("text", "chroot", "") + m
.group("filename")):
298 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
299 r
, m
= remove_pattern(texmessage
.file_pattern
, r
, ignore_nl
=False)
301 for filename
in itertools
.accumulate(m
.group("filename").split("\n")):
302 if os
.path
.isfile(config
.get("text", "chroot", "") + filename
):
306 r
, m
= remove_pattern(texmessage
.file_pattern
, r
, ignore_nl
=False)
309 quoted_def_pattern
= re
.compile(r
'\("(?P<filename>[^"]+\.(fd|def))"\)')
310 def_pattern
= re
.compile(r
'\((?P<filename>[^"][^ )]*\.(fd|def))\)')
314 "Ignore font definition (``*.fd`` and ``*.def``) loading messages."
316 for p
in [texmessage
.quoted_def_pattern
, texmessage
.def_pattern
]:
317 r
, m
= remove_pattern(p
, r
)
319 if not os
.path
.isfile(config
.get("text", "chroot", "") + m
.group("filename")):
321 r
, m
= remove_pattern(p
, r
)
324 quoted_graphics_pattern
= re
.compile(r
'<"(?P<filename>[^"]+\.eps)">')
325 graphics_pattern
= re
.compile(r
'<(?P<filename>[^"][^>]*\.eps)>')
328 def load_graphics(msg
):
329 "Ignore graphics file (``*.eps``) loading messages."
331 for p
in [texmessage
.quoted_graphics_pattern
, texmessage
.graphics_pattern
]:
332 r
, m
= remove_pattern(p
, r
)
334 if not os
.path
.isfile(config
.get("text", "chroot", "") + m
.group("filename")):
336 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
341 """Ignore all messages.
343 Should be used as a last resort only. You should write a proper TeX
344 output parser function for the output you observe.
351 """Warn about all messages.
353 Similar to :attr:`ignore`, but writing a warning to the logger about
354 the TeX output. This is considered to be better when you need to get it
355 working quickly as you will still be prompted about the unresolved
356 output, while the processing continues.
360 logger
.warning("ignoring TeX warnings:\n%s" % indent_text(msg
.rstrip()))
364 def pattern(p
, warning
, qualname
=None):
365 "Warn by regular expression pattern matching."
368 msg
, m
= remove_pattern(p
, msg
, ignore_nl
=False)
370 logger
.warning("ignoring %s:\n%s" % (warning
, m
.string
[m
.start(): m
.end()].rstrip()))
371 msg
, m
= remove_pattern(p
, msg
, ignore_nl
=False)
373 check
.__doc
__ = check
.__doc
__.format(warning
)
374 if qualname
is not None:
375 check
.__qualname
__ = qualname
378 box_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re
.MULTILINE
),
379 "overfull/underfull box", qualname
="texmessage.box_warning"))
380 font_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re
.MULTILINE
),
381 "font substitutions of NFSS", qualname
="texmessage.font_warning"))
382 package_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^package\s+(?P<packagename>\S+)\s+warning\s*:[^\n]+(?:\n\(?(?P=packagename)\)?[^\n]*)*", re
.MULTILINE | re
.IGNORECASE
),
383 "generic package messages", qualname
="texmessage.package_warning"))
384 rerun_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^(LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\s*\.)$", re
.MULTILINE
),
385 "rerun required message", qualname
="texmessage.rerun_warning"))
386 nobbl_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^[\s\*]*(No file .*\.bbl.)\s*", re
.MULTILINE
),
387 "no-bbl message", qualname
="texmessage.nobbl_warning"))
390 ###############################################################################
392 ###############################################################################
394 _textattrspreamble
= ""
397 "a textattr defines a apply method, which modifies a (La)TeX expression"
399 class _localattr
: pass
401 _textattrspreamble
+= r
"""\gdef\PyXFlushHAlign{0}%
403 \leftskip=0pt plus \PyXFlushHAlign fil%
404 \rightskip=0pt plus 1fil%
405 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
411 \exhyphenpenalty=9999}%
414 class boxhalign(attr
.exclusiveattr
, textattr
, _localattr
):
416 def __init__(self
, aboxhalign
):
417 self
.boxhalign
= aboxhalign
418 attr
.exclusiveattr
.__init
__(self
, boxhalign
)
420 def apply(self
, expr
):
421 return r
"\gdef\PyXBoxHAlign{%.5f}%s" % (self
.boxhalign
, expr
)
423 boxhalign
.left
= boxhalign(0)
424 boxhalign
.center
= boxhalign(0.5)
425 boxhalign
.right
= boxhalign(1)
426 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
429 class flushhalign(attr
.exclusiveattr
, textattr
, _localattr
):
431 def __init__(self
, aflushhalign
):
432 self
.flushhalign
= aflushhalign
433 attr
.exclusiveattr
.__init
__(self
, flushhalign
)
435 def apply(self
, expr
):
436 return r
"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.flushhalign
, expr
)
438 flushhalign
.left
= flushhalign(0)
439 flushhalign
.center
= flushhalign(0.5)
440 flushhalign
.right
= flushhalign(1)
441 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
444 class halign(boxhalign
, flushhalign
, _localattr
):
446 def __init__(self
, aboxhalign
, aflushhalign
):
447 self
.boxhalign
= aboxhalign
448 self
.flushhalign
= aflushhalign
449 attr
.exclusiveattr
.__init
__(self
, halign
)
451 def apply(self
, expr
):
452 return r
"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.boxhalign
, self
.flushhalign
, expr
)
454 def apply_trafo(self
, textbox
):
455 return textbox
.transform(trafo
.translate_pt(-self
.boxhalign
*textbox
.bbox().width_pt(), 0), keep_anchor
=True)
457 halign
.left
= halign(0, 0)
458 halign
.center
= halign(0.5, 0.5)
459 halign
.right
= halign(1, 1)
460 halign
.clear
= attr
.clearclass(halign
)
461 halign
.boxleft
= boxhalign
.left
462 halign
.boxcenter
= boxhalign
.center
463 halign
.boxright
= boxhalign
.right
464 halign
.flushleft
= halign
.raggedright
= flushhalign
.left
465 halign
.flushcenter
= halign
.raggedcenter
= flushhalign
.center
466 halign
.flushright
= halign
.raggedleft
= flushhalign
.right
469 class _mathmode(attr
.exclusiveattr
, textattr
, _localattr
):
473 attr
.exclusiveattr
.__init
__(self
, _mathmode
)
475 def apply(self
, expr
):
476 return r
"$\displaystyle{%s}$" % expr
478 def apply_trafo(self
, textbox
):
481 mathmode
= _mathmode()
482 clearmathmode
= attr
.clearclass(_mathmode
)
485 class _phantom(attr
.attr
, textattr
, _localattr
):
488 def apply(self
, expr
):
489 return r
"\phantom{%s}" % expr
492 clearphantom
= attr
.clearclass(_phantom
)
495 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
497 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
503 def __init__(self
, width
, baseline
=top
):
504 self
.width
= width
* 72.27 / (unit
.scale
["x"] * 72)
505 self
.baseline
= baseline
506 attr
.sortbeforeexclusiveattr
.__init
__(self
, parbox_pt
, [_localattr
])
508 def apply(self
, expr
):
509 if self
.baseline
== self
.top
:
510 return r
"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
511 elif self
.baseline
== self
.middle
:
512 return r
"\linewidth=%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self
.width
, expr
, expr
)
513 elif self
.baseline
== self
.bottom
:
514 return r
"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
516 ValueError("invalid baseline argument")
518 parbox_pt
.clear
= attr
.clearclass(parbox_pt
)
520 class parbox(parbox_pt
):
522 def __init__(self
, width
, **kwargs
):
523 parbox_pt
.__init
__(self
, unit
.topt(width
), **kwargs
)
525 parbox
.clear
= parbox_pt
.clear
528 _textattrspreamble
+= "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
530 class valign(attr
.sortbeforeexclusiveattr
, textattr
):
532 def __init__(self
, avalign
):
533 self
.valign
= avalign
534 attr
.sortbeforeexclusiveattr
.__init
__(self
, valign
, [parbox_pt
, _localattr
])
536 def apply(self
, expr
):
537 return r
"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=%.5f\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -%.5f\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % (expr
, 1-self
.valign
, self
.valign
)
539 valign
.top
= valign(0)
540 valign
.middle
= valign(0.5)
541 valign
.bottom
= valign(1)
542 valign
.clear
= valign
.baseline
= attr
.clearclass(valign
)
545 _textattrspreamble
+= "\\newdimen\\PyXDimenVShift%\n"
547 class _vshift(attr
.sortbeforeattr
, textattr
):
550 attr
.sortbeforeattr
.__init
__(self
, [valign
, parbox_pt
, _localattr
])
552 def apply(self
, expr
):
553 return r
"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self
.setheightexpr(), expr
)
555 class vshift(_vshift
):
556 "vertical down shift by a fraction of a character height"
558 def __init__(self
, lowerratio
, heightstr
="0"):
559 _vshift
.__init
__(self
)
560 self
.lowerratio
= lowerratio
561 self
.heightstr
= heightstr
563 def setheightexpr(self
):
564 return r
"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self
.heightstr
, self
.lowerratio
)
566 class _vshiftmathaxis(_vshift
):
567 "vertical down shift by the height of the math axis"
569 def setheightexpr(self
):
570 return r
"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
572 def apply_trafo(self
, textbox
):
573 return textbox
.transform(trafo
.translate_pt(0, -textbox
.mathaxis_pt
*unit
.scale
["x"]), keep_anchor
=True)
576 vshift
.bottomzero
= vshift(0)
577 vshift
.middlezero
= vshift(0.5)
578 vshift
.topzero
= vshift(1)
579 vshift
.mathaxis
= _vshiftmathaxis()
580 vshift
.clear
= attr
.clearclass(_vshift
)
583 defaultsizelist
= ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
584 None, "tiny", "scriptsize", "footnotesize", "small"]
586 class size(attr
.sortbeforeattr
, textattr
):
589 def __init__(self
, sizeindex
=None, sizename
=None, sizelist
=defaultsizelist
):
590 if (sizeindex
is None and sizename
is None) or (sizeindex
is not None and sizename
is not None):
591 raise ValueError("either specify sizeindex or sizename")
592 attr
.sortbeforeattr
.__init
__(self
, [_mathmode
, _vshift
])
593 if sizeindex
is not None:
594 if sizeindex
>= 0 and sizeindex
< sizelist
.index(None):
595 self
.size
= sizelist
[sizeindex
]
596 elif sizeindex
< 0 and sizeindex
+ len(sizelist
) > sizelist
.index(None):
597 self
.size
= sizelist
[sizeindex
]
599 raise IndexError("index out of sizelist range")
603 def apply(self
, expr
):
604 return r
"\%s{}%s" % (self
.size
, expr
)
607 size
.scriptsize
= size
.script
= size(-3)
608 size
.footnotesize
= size
.footnote
= size(-2)
609 size
.small
= size(-1)
610 size
.normalsize
= size
.normal
= size(0)
616 size
.clear
= attr
.clearclass(size
)
619 ###############################################################################
621 ###############################################################################
624 class MonitorOutput(threading
.Thread
):
626 def __init__(self
, name
, output
):
627 """Deadlock-safe output stream reader and monitor.
629 An instance of this class creates a thread to continously read lines
630 from a stream. By that a deadlock due to a full pipe is prevented. In
631 addition, the stream content can be monitored for containing a certain
632 string (see :meth:`expect` and :meth:`wait`) and return all the
633 collected output (see :meth:`read`).
635 :param string name: name to be used while logging in :meth:`wait` and
637 :param file output: output stream
641 self
._expect
= queue
.Queue(1)
642 self
._received
= threading
.Event()
643 self
._output
= queue
.Queue()
644 threading
.Thread
.__init
__(self
, name
=name
)
649 """Expect a string on a **single** line in the output.
651 This method must be called **before** the output occurs, i.e. before
652 the input is written to the TeX/LaTeX process.
654 :param s: expected string or ``None`` if output is expected to become
659 self
._expect
.put_nowait(s
)
662 """Read all output collected since its previous call.
664 The output reading should be synchronized by the :meth:`expect`
665 and :meth:`wait` methods.
667 :returns: collected output from the stream
674 l
.append(self
._output
.get_nowait())
677 return "".join(l
).replace("\r\n", "\n").replace("\r", "\n")
679 def _wait(self
, waiter
, checker
):
680 """Helper method to implement :meth:`wait` and :meth:`done`.
682 Waits for an event using the *waiter* and *checker* functions while
683 providing user feedback to the ``pyx``-logger using the warning level
684 according to the ``wait`` and ``showwait`` from the ``text`` section of
685 the pyx :mod:`config`.
687 :param function waiter: callback to wait for (the function gets called
688 with a timeout parameter)
689 :param function checker: callback returing ``True`` if
690 waiting was successful
691 :returns: ``True`` when wait was successful
695 wait
= config
.getint("text", "wait", 60)
696 showwait
= config
.getint("text", "showwait", 5)
700 while waited
< wait
and not hasevent
:
701 if wait
- waited
> showwait
:
705 waiter(wait
- waited
)
706 waited
+= wait
- waited
710 logger
.warning("Still waiting for {} "
711 "after {} (of {}) seconds..."
712 .format(self
.name
, waited
, wait
))
714 logger
.warning("The timeout of {} seconds expired "
715 "and {} did not respond."
716 .format(waited
, self
.name
))
723 """Wait for the expected output to happen.
725 Waits either until a line containing the string set by the previous
726 :meth:`expect` call is found, or a timeout occurs.
728 :returns: ``True`` when the expected string was found
732 r
= self
._wait
(self
._received
.wait
, self
._received
.isSet
)
734 self
._received
.clear()
738 """Waits until the output becomes empty.
740 Waits either until the output becomes empty, or a timeout occurs.
741 The generated output can still be catched by :meth:`read` after
742 :meth:`done` was successful.
744 In the proper workflow :meth:`expect` should be called with ``None``
745 before the output completes, as otherwise a ``ValueError`` is raised
748 :returns: ``True`` when the output has become empty
752 return self
._wait
(self
.join
, lambda self
=self
: not self
.is_alive())
755 """Read a line from the output.
757 To be used **inside** the thread routine only.
759 :returns: one line of the output as a string
765 return self
.output
.readline()
767 if e
.errno
!= errno
.EINTR
:
773 **Not** to be called from outside.
775 :raises ValueError: output becomes empty while some string is expected
780 line
= self
._readline
()
783 expect
= self
._expect
.get_nowait()
788 self
._output
.put(line
)
789 if expect
is not None:
790 found
= line
.find(expect
)
795 if expect
is not None:
796 raise ValueError("{} finished unexpectedly".format(self
.name
))
799 class textbox_pt(box
.rect
, baseclasses
.canvasitem
): pass
802 class textextbox_pt(textbox_pt
):
804 def __init__(self
, x_pt
, y_pt
, left_pt
, right_pt
, height_pt
, depth_pt
, do_finish
, fontmap
, singlecharmode
, fillstyles
):
807 An instance of this class contains the text output generated by PyX. It
808 is a :class:`baseclasses.canvasitem` and thus can be inserted into a
811 .. A text has a center (x_pt, y_pt) as well as extents in x-direction
812 .. (left_pt and right_pt) and y-direction (hight_pt and depth_pt). The
813 .. textbox positions the output accordingly and scales it by the
814 .. x-scale from the :mod:`unit`.
816 .. :param float x_pt: x coordinate of the center in pts
817 .. :param float y_pt: y coordinate of the center in pts
818 .. :param float left_pt: unscaled left extent in pts
819 .. :param float right_pt: unscaled right extent in pts
820 .. :param float height_pt: unscaled height in pts
821 .. :param float depth_pt: unscaled depth in pts
822 .. :param function do_finish: callable to execute :meth:`readdvipage`
823 .. :param fontmap: force a fontmap to be used (instead of the default
824 .. depending on the output format)
825 .. :type fontmap: None or fontmap
826 .. :param bool singlecharmode: position each character separately
827 .. :param fillstyles: fill styles to be applied
828 .. :type fillstyles: list of fillstyles
831 self
.left
= left_pt
*unit
.x_pt
#: left extent of the text (PyX length)
832 self
.right
= right_pt
*unit
.x_pt
#: right extent of the text (PyX length)
833 self
.width
= self
.left
+ self
.right
#: width of the text (PyX length)
834 self
.height
= height_pt
*unit
.x_pt
#: height of the text (PyX length)
835 self
.depth
= depth_pt
*unit
.x_pt
#: height of the text (PyX length)
837 self
.do_finish
= do_finish
838 self
.fontmap
= fontmap
839 self
.singlecharmode
= singlecharmode
840 self
.fillstyles
= fillstyles
842 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated_pt(x_pt
, y_pt
)
843 box
.rect_pt
.__init
__(self
, x_pt
- left_pt
*unit
.scale
["x"], y_pt
- depth_pt
*unit
.scale
["x"],
844 (left_pt
+ right_pt
)*unit
.scale
["x"],
845 (depth_pt
+ height_pt
)*unit
.scale
["x"],
846 abscenter_pt
= (left_pt
*unit
.scale
["x"], depth_pt
*unit
.scale
["x"]))
848 self
._dvicanvas
= None
850 def transform(self
, *trafos
, keep_anchor
=False):
851 box
.rect
.transform(self
, *trafos
, keep_anchor
=keep_anchor
)
853 self
.texttrafo
= trafo
* self
.texttrafo
854 if self
._dvicanvas
is not None:
856 self
._dvicanvas
.trafo
= trafo
* self
._dvicanvas
.trafo
858 def readdvipage(self
, dvifile
, page
):
859 self
._dvicanvas
= dvifile
.readpage([ord("P"), ord("y"), ord("X"), page
, 0, 0, 0, 0, 0, 0],
860 fontmap
=self
.fontmap
, singlecharmode
=self
.singlecharmode
, attrs
=[self
.texttrafo
] + self
.fillstyles
)
864 if self
._dvicanvas
is None:
866 return self
._dvicanvas
868 def marker(self
, name
):
869 """Return the position of a marker.
871 :param str name: name of the marker
872 :returns: position of the marker
873 :rtype: tuple of two PyX lengths
875 This method returns the position of the marker of the given name
876 within, previously defined by the ``\\PyXMarker{name}`` macro in the
877 typeset text. Please do not use the ``@`` character within your name to
878 prevent name clashes with PyX internal uses (although we don’t the
879 marker feature internally right now).
881 Similar to generating actual output, the marker function accesses the
882 DVI output, stopping. The :ref:`texipc` feature will allow for this access
883 without stopping the TeX interpreter.
886 return self
.texttrafo
.apply(*self
.dvicanvas
.markers
[name
])
889 textpath
= path
.path()
890 for item
in self
.dvicanvas
.items
:
891 textpath
+= item
.textpath()
892 return textpath
.transformed(self
.texttrafo
)
894 def processPS(self
, file, writer
, context
, registry
, bbox
):
895 abbox
= bboxmodule
.empty()
896 self
.dvicanvas
.processPS(file, writer
, context
, registry
, abbox
)
897 bbox
+= box
.rect
.bbox(self
)
899 def processPDF(self
, file, writer
, context
, registry
, bbox
):
900 abbox
= bboxmodule
.empty()
901 self
.dvicanvas
.processPDF(file, writer
, context
, registry
, abbox
)
902 bbox
+= box
.rect
.bbox(self
)
904 def processSVG(self
, xml
, writer
, context
, registry
, bbox
):
905 abbox
= bboxmodule
.empty()
906 self
.dvicanvas
.processSVG(xml
, writer
, context
, registry
, abbox
)
907 bbox
+= box
.rect
.bbox(self
)
915 "Constants defining the verbosity of the :exc:`TexResultError`."
916 none
= 0 #: Without any input and output.
917 default
= 1 #: Input and parsed output shortend to 5 lines.
918 full
= 2 #: Full input and unparsed as well as parsed output.
923 def __init__(self
, *files
):
924 """Apply write, flush, and close to each of the given files."""
927 def write(self
, data
):
928 for file in self
.files
:
932 for file in self
.files
:
936 for file in self
.files
:
939 # The texrunner state represents the next (or current) execute state.
940 STATE_START
, STATE_PREAMBLE
, STATE_TYPESET
, STATE_DONE
= range(4)
941 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:")
942 dvi_pattern
= re
.compile(r
"Output written on .*texput\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\.", re
.DOTALL
)
944 class TexDoneError(Exception):
950 #: default :class:`texmessage` parsers at interpreter startup
951 texmessages_start_default
= [texmessage
.start
]
952 #: default :class:`texmessage` parsers at interpreter shutdown
953 texmessages_end_default
= [texmessage
.end
, texmessage
.font_warning
, texmessage
.rerun_warning
, texmessage
.nobbl_warning
]
954 #: default :class:`texmessage` parsers for preamble output
955 texmessages_preamble_default
= [texmessage
.load
]
956 #: default :class:`texmessage` parsers for typeset output
957 texmessages_run_default
= [texmessage
.font_warning
, texmessage
.box_warning
, texmessage
.package_warning
,
958 texmessage
.load_def
, texmessage
.load_graphics
]
960 def __init__(self
, cmd
,
963 texipc
=config
.getboolean("text", "texipc", 0),
966 errordetail
=errordetail
.default
,
967 texmessages_start
=[],
969 texmessages_preamble
=[],
971 """Base class for the TeX interface.
973 .. note:: This class cannot be used directly. It is the base class for
974 all texrunners and provides most of the implementation.
975 Still, to the end user the parameters except for *cmd*
976 are important, as they are preserved in derived classes
979 :param cmd: command and arguments to start the TeX interpreter
980 :type cmd: list of str
981 :param str texenc: encoding to use in the communication with the TeX
983 :param usefiles: list of supplementary files to be copied to and from
984 the temporary working directory (see :ref:`debug` for usage
986 :type usefiles: list of str
987 :param bool texipc: :ref:`texipc` flag.
988 :param copyinput: filename or file to be used to store a copy of all
989 the input passed to the TeX interpreter
990 :type copyinput: None or str or file
991 :param bool dvitype: flag to turn on dvitype-like output
992 :param errordetail: verbosity of the :exc:`TexResultError`
993 :type errordetail: :class:`errordetail`
994 :param texmessages_start: additional message parsers at interpreter
996 :type texmessages_start: list of :class:`texmessage` parsers
997 :param texmessages_end: additional message parsers at interpreter
999 :type texmessages_end: list of :class:`texmessage` parsers
1000 :param texmessages_preamble: additional message parsers for preamble
1002 :type texmessages_preamble: list of :class:`texmessage` parsers
1003 :param texmessages_run: additional message parsers for typset output
1004 :type texmessages_run: list of :class:`texmessage` parsers
1008 self
.texenc
= texenc
1009 self
.usefiles
= usefiles
1010 self
.texipc
= texipc
1011 self
.copyinput
= copyinput
1012 self
.dvitype
= dvitype
1013 self
.errordetail
= errordetail
1014 self
.texmessages_start
= texmessages_start
1015 self
.texmessages_end
= texmessages_end
1016 self
.texmessages_preamble
= texmessages_preamble
1017 self
.texmessages_run
= texmessages_run
1019 self
.state
= STATE_START
1023 self
.needdvitextboxes
= [] # when texipc-mode off
1027 """Clean-up TeX interpreter and tmp directory.
1029 This funtion is hooked up in atexit to quit the TeX interpreter, to
1030 save the contents of usefiles, and to remove the temporary directory.
1034 if self
.state
> STATE_START
:
1035 if self
.state
< STATE_DONE
:
1036 self
.do_finish(cleanup
=False)
1037 if self
.state
< STATE_DONE
: # cleanup while TeX is still running?
1038 self
.texoutput
.expect(None)
1040 for f
, msg
in [(self
.texinput
.close
, "We tried to communicate to %s to quit itself, but this seem to have failed. Trying by terminate signal now ...".format(self
.name
)),
1041 (self
.popen
.terminate
, "Failed, too. Trying by kill signal now ..."),
1042 (self
.popen
.kill
, "We tried very hard to get rid of the %s process, but we ultimately failed (as far as we can tell). Sorry.".format(self
.name
))]:
1044 if self
.texoutput
.done():
1047 for usefile
in self
.usefiles
:
1048 extpos
= usefile
.rfind(".")
1050 shutil
.move(os
.path
.join(self
.tmpdir
, "texput" + usefile
[extpos
:]), usefile
)
1051 except EnvironmentError:
1052 logger
.warning("Could not save '{}'.".format(usefile
))
1053 if os
.path
.isfile(usefile
):
1056 except EnvironmentError:
1057 logger
.warning("Failed to remove spurious file '{}'.".format(usefile
))
1059 shutil
.rmtree(self
.tmpdir
, ignore_errors
=True)
1061 def _execute(self
, expr
, texmessages
, oldstate
, newstate
):
1062 """Execute TeX expression.
1064 :param str expr: expression to be passed to TeX
1065 :param texmessages: message parsers to analyse the textual output of
1067 :type texmessages: list of :class:`texmessage` parsers
1068 :param int oldstate: state of the TeX interpreter prior to the
1069 expression execution
1070 :param int newstate: state of the TeX interpreter after to the
1071 expression execution
1074 assert STATE_PREAMBLE
<= oldstate
<= STATE_TYPESET
1075 assert oldstate
== self
.state
1076 assert newstate
>= oldstate
1077 if newstate
== STATE_DONE
:
1078 self
.texoutput
.expect(None)
1079 self
.texinput
.write(expr
)
1082 # test to encode expr early to not pile up expected results
1083 # if the expression won't make it to the texinput at all
1084 # (which would otherwise harm a proper cleanup)
1085 expr
.encode(self
.texenc
)
1087 if oldstate
== newstate
== STATE_TYPESET
:
1089 expr
= "\\ProcessPyXBox{%s%%\n}{%i}" % (expr
, self
.page
)
1091 self
.texoutput
.expect("PyXInputMarker:executeid=%i:" % self
.executeid
)
1092 expr
+= "%%\n\\PyXInput{%i}%%\n" % self
.executeid
1093 self
.texinput
.write(expr
)
1094 self
.texinput
.flush()
1095 self
.state
= newstate
1096 if newstate
== STATE_DONE
:
1097 wait_ok
= self
.texoutput
.done()
1099 wait_ok
= self
.texoutput
.wait()
1101 parsed
= unparsed
= self
.texoutput
.read()
1103 raise TexResultError("TeX didn't respond as expected within the timeout period.")
1104 if newstate
!= STATE_DONE
:
1105 parsed
, m
= remove_string("PyXInputMarker:executeid=%s:" % self
.executeid
, parsed
)
1107 raise TexResultError("PyXInputMarker expected")
1108 if oldstate
== newstate
== STATE_TYPESET
:
1109 parsed
, m
= remove_pattern(PyXBoxPattern
, parsed
, ignore_nl
=False)
1111 raise TexResultError("PyXBox expected")
1112 if m
.group("page") != str(self
.page
):
1113 raise TexResultError("Wrong page number in PyXBox")
1114 extent_pt
= [float(x
)*72/72.27 for x
in m
.group("lt", "rt", "ht", "dp")]
1115 parsed
, m
= remove_string("[80.121.88.%s]" % self
.page
, parsed
)
1117 raise TexResultError("PyXPageOutMarker expected")
1119 # check for "Output written on ...dvi (1 page, 220 bytes)."
1121 parsed
, m
= remove_pattern(dvi_pattern
, parsed
)
1123 raise TexResultError("TeX dvifile messages expected")
1124 if m
.group("page") != str(self
.page
):
1125 raise TexResultError("wrong number of pages reported")
1127 parsed
, m
= remove_string("No pages of output.", parsed
)
1129 raise TexResultError("no dvifile expected")
1131 for t
in texmessages
:
1133 if parsed
.replace(r
"(Please type a command or say `\end')", "").replace(" ", "").replace("*\n", "").replace("\n", ""):
1134 raise TexResultError("unhandled TeX response (might be an error)")
1135 except TexResultError
as e
:
1136 if self
.errordetail
> errordetail
.none
:
1137 def add(msg
): e
.args
= (e
.args
[0] + msg
,)
1138 add("\nThe expression passed to TeX was:\n{}".format(indent_text(expr
.rstrip())))
1139 if self
.errordetail
== errordetail
.full
:
1140 add("\nThe return message from TeX was:\n{}".format(indent_text(unparsed
.rstrip())))
1141 if self
.errordetail
== errordetail
.default
:
1142 if parsed
.count('\n') > 6:
1143 parsed
= "\n".join(parsed
.split("\n")[:5] + ["(cut after 5 lines; use errordetail.full for all output)"])
1144 add("\nAfter parsing the return message from TeX, the following was left:\n{}".format(indent_text(parsed
.rstrip())))
1146 if oldstate
== newstate
== STATE_TYPESET
:
1150 """Setup environment and start TeX interpreter."""
1151 assert self
.state
== STATE_START
1152 self
.state
= STATE_PREAMBLE
1154 chroot
= config
.get("text", "chroot", "")
1156 chroot_tmpdir
= config
.get("text", "tmpdir", "/tmp")
1157 chroot_tmpdir_rel
= os
.path
.relpath(chroot_tmpdir
, os
.sep
)
1158 base_tmpdir
= os
.path
.join(chroot
, chroot_tmpdir_rel
)
1160 base_tmpdir
= config
.get("text", "tmpdir", None)
1161 self
.tmpdir
= tempfile
.mkdtemp(prefix
="pyx", dir=base_tmpdir
)
1162 atexit
.register(self
._cleanup
)
1163 for usefile
in self
.usefiles
:
1164 extpos
= usefile
.rfind(".")
1166 os
.rename(usefile
, os
.path
.join(self
.tmpdir
, "texput" + usefile
[extpos
:]))
1170 tex_tmpdir
= os
.sep
+ os
.path
.relpath(self
.tmpdir
, chroot
)
1172 tex_tmpdir
= self
.tmpdir
1173 cmd
= self
.cmd
+ ['--output-directory', tex_tmpdir
]
1176 self
.popen
= config
.Popen(cmd
, stdin
=config
.PIPE
, stdout
=config
.PIPE
, stderr
=config
.STDOUT
, bufsize
=0)
1177 self
.texinput
= io
.TextIOWrapper(self
.popen
.stdin
, encoding
=self
.texenc
)
1180 self
.copyinput
.write
1181 except AttributeError:
1182 self
.texinput
= Tee(open(self
.copyinput
, "w", encoding
=self
.texenc
), self
.texinput
)
1184 self
.texinput
= Tee(self
.copyinput
, self
.texinput
)
1185 self
.texoutput
= MonitorOutput(self
.name
, io
.TextIOWrapper(self
.popen
.stdout
, encoding
=self
.texenc
))
1186 self
._execute
("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1187 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1188 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
1189 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1190 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1191 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1192 "\\newdimen\\PyXDimenHAlignRT%\n" +
1193 _textattrspreamble
+ # insert preambles for textattrs macros
1194 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1195 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1196 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1197 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1198 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1199 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
1200 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1201 "lt=\\the\\PyXDimenHAlignLT,"
1202 "rt=\\the\\PyXDimenHAlignRT,"
1203 "ht=\\the\\ht\\PyXBox,"
1204 "dp=\\the\\dp\\PyXBox:}%\n"
1205 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1206 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1207 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1208 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
1209 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
1210 self
.texmessages_start_default
+ self
.texmessages_start
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1212 def do_preamble(self
, expr
, texmessages
):
1213 """Ensure preamble mode and execute expr."""
1214 if self
.state
< STATE_PREAMBLE
:
1216 self
._execute
(expr
, texmessages
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1218 def do_typeset(self
, expr
, texmessages
):
1219 """Ensure typeset mode and typeset expr."""
1220 if self
.state
< STATE_PREAMBLE
:
1222 if self
.state
< STATE_TYPESET
:
1224 return self
._execute
(expr
, texmessages
, STATE_TYPESET
, STATE_TYPESET
)
1226 def do_finish(self
, cleanup
=True):
1227 """Teardown TeX interpreter and cleanup environment.
1229 :param bool cleanup: use _cleanup regularly/explicitly (not via atexit)
1231 if self
.state
== STATE_DONE
:
1233 if self
.state
< STATE_TYPESET
:
1236 assert self
.state
== STATE_DONE
1237 self
.texinput
.close() # close the input queue and
1238 self
.texoutput
.done() # wait for finish of the output
1240 if self
.needdvitextboxes
:
1241 dvifilename
= os
.path
.join(self
.tmpdir
, "texput.dvi")
1242 self
.dvifile
= dvifile
.DVIfile(dvifilename
, debug
=self
.dvitype
)
1244 for box
in self
.needdvitextboxes
:
1245 box
.readdvipage(self
.dvifile
, page
)
1247 if self
.dvifile
is not None and self
.dvifile
.readpage(None) is not None:
1248 raise ValueError("end of dvifile expected but further pages follow")
1250 atexit
.unregister(self
._cleanup
)
1253 def preamble(self
, expr
, texmessages
=[]):
1254 """Execute a preamble.
1256 :param str expr: expression to be executed
1257 :param texmessages: additional message parsers
1258 :type texmessages: list of :class:`texmessage` parsers
1260 Preambles must not generate output, but are used to load files, perform
1261 settings, define macros, *etc*. In LaTeX mode, preambles are executed
1262 before ``\\begin{document}``. The method can be called multiple times,
1263 but only prior to :meth:`SingleRunner.text` and
1264 :meth:`SingleRunner.text_pt`.
1267 texmessages
= self
.texmessages_preamble_default
+ self
.texmessages_preamble
+ texmessages
1268 self
.do_preamble(expr
, texmessages
)
1270 def text_pt(self
, x_pt
, y_pt
, expr
, textattrs
=[], texmessages
=[], fontmap
=None, singlecharmode
=False):
1273 :param float x_pt: x position in pts
1274 :param float y_pt: y position in pts
1275 :param expr: text to be typeset
1276 :type expr: str or :class:`MultiEngineText`
1277 :param textattrs: styles and attributes to be applied to the text
1278 :type textattrs: list of :class:`textattr, :class:`trafo.trafo_pt`,
1279 and :class:`style.fillstyle`
1280 :param texmessages: additional message parsers
1281 :type texmessages: list of :class:`texmessage` parsers
1282 :param fontmap: force a fontmap to be used (instead of the default
1283 depending on the output format)
1284 :type fontmap: None or fontmap
1285 :param bool singlecharmode: position each character separately
1286 :returns: text output insertable into a canvas.
1287 :rtype: :class:`textextbox_pt`
1288 :raises: :exc:`TexDoneError`: when the TeX interpreter has been
1292 if self
.state
== STATE_DONE
:
1293 raise TexDoneError("typesetting process was terminated already")
1294 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1295 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1296 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1297 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1298 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1299 if isinstance(expr
, MultiEngineText
):
1301 for ta
in textattrs
[::-1]:
1302 expr
= ta
.apply(expr
)
1303 first
= self
.state
< STATE_TYPESET
1304 left_pt
, right_pt
, height_pt
, depth_pt
= self
.do_typeset(expr
, self
.texmessages_run_default
+ self
.texmessages_run
+ texmessages
)
1305 if self
.texipc
and first
:
1306 self
.dvifile
= dvifile
.DVIfile(os
.path
.join(self
.tmpdir
, "texput.dvi"), debug
=self
.dvitype
)
1307 box
= textextbox_pt(x_pt
, y_pt
, left_pt
, right_pt
, height_pt
, depth_pt
, self
.do_finish
, fontmap
, singlecharmode
, fillstyles
)
1309 box
.reltransform(t
) # TODO: should trafos really use reltransform???
1310 # this is quite different from what we do elsewhere!!!
1311 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1313 box
.readdvipage(self
.dvifile
, self
.page
)
1315 self
.needdvitextboxes
.append(box
)
1318 def text(self
, x
, y
, *args
, **kwargs
):
1321 This method is identical to :meth:`text_pt` with the only difference of
1322 using PyX lengths to position the output.
1324 :param x: x position
1326 :param y: y position
1330 return self
.text_pt(unit
.topt(x
), unit
.topt(y
), *args
, **kwargs
)
1333 class SingleTexRunner(SingleRunner
):
1335 def __init__(self
, cmd
=config
.getlist("text", "tex", ["tex"]), lfs
="10pt", **kwargs
):
1336 """Plain TeX interface.
1338 This class adjusts the :class:`SingleRunner` to use plain TeX.
1340 :param cmd: command and arguments to start the TeX interpreter
1341 :type cmd: list of str
1342 :param lfs: resemble LaTeX font settings within plain TeX by loading a
1344 :type lfs: str or None
1345 :param kwargs: additional arguments passed to :class:`SingleRunner`
1347 An lfs-file is a file defining a set of font commands like ``\\normalsize``
1348 by font selection commands in plain TeX. Several of those files
1349 resembling standard settings of LaTeX are distributed along with PyX in
1350 the ``pyx/data/lfs`` directory. This directory also contains a LaTeX
1351 file to create lfs files for different settings (LaTeX class, class
1352 options, and style files).
1355 super().__init
__(cmd
=cmd
, **kwargs
)
1359 def go_typeset(self
):
1360 assert self
.state
== STATE_PREAMBLE
1361 self
.state
= STATE_TYPESET
1363 def go_finish(self
):
1364 self
._execute
("\\end%\n", self
.texmessages_end_default
+ self
.texmessages_end
, STATE_TYPESET
, STATE_DONE
)
1366 def force_done(self
):
1367 self
.texinput
.write("\n\\end\n")
1372 if not self
.lfs
.endswith(".lfs"):
1373 self
.lfs
= "%s.lfs" % self
.lfs
1374 with config
.open(self
.lfs
, []) as lfsfile
:
1375 lfsdef
= lfsfile
.read().decode("ascii")
1376 self
._execute
(lfsdef
, [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1377 self
._execute
("\\normalsize%\n", [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1378 self
._execute
("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1381 class SingleLatexRunner(SingleRunner
):
1383 #: default :class:`texmessage` parsers at LaTeX class loading
1384 texmessages_docclass_default
= [texmessage
.load
]
1385 #: default :class:`texmessage` parsers at ``\begin{document}``
1386 texmessages_begindoc_default
= [texmessage
.load
, texmessage
.no_aux
]
1388 def __init__(self
, cmd
=config
.getlist("text", "latex", ["latex"]),
1389 docclass
="article", docopt
=None, pyxgraphics
=True,
1390 texmessages_docclass
=[], texmessages_begindoc
=[], **kwargs
):
1393 This class adjusts the :class:`SingleRunner` to use LaTeX.
1395 :param cmd: command and arguments to start the TeX interpreter
1397 :type cmd: list of str
1398 :param str docclass: document class
1399 :param docopt: document loading options
1400 :type docopt: str or None
1401 :param bool pyxgraphics: activate graphics bundle support, see
1403 :param texmessages_docclass: additional message parsers at LaTeX class
1405 :type texmessages_docclass: list of :class:`texmessage` parsers
1406 :param texmessages_begindoc: additional message parsers at
1407 ``\\begin{document}``
1408 :type texmessages_begindoc: list of :class:`texmessage` parsers
1409 :param kwargs: additional arguments passed to :class:`SingleRunner`
1412 super().__init
__(cmd
=cmd
, **kwargs
)
1413 self
.docclass
= docclass
1414 self
.docopt
= docopt
1415 self
.pyxgraphics
= pyxgraphics
1416 self
.texmessages_docclass
= texmessages_docclass
1417 self
.texmessages_begindoc
= texmessages_begindoc
1420 def go_typeset(self
):
1421 self
._execute
("\\begin{document}", self
.texmessages_begindoc_default
+ self
.texmessages_begindoc
, STATE_PREAMBLE
, STATE_TYPESET
)
1423 def go_finish(self
):
1424 self
._execute
("\\end{document}%\n", self
.texmessages_end_default
+ self
.texmessages_end
, STATE_TYPESET
, STATE_DONE
)
1426 def force_done(self
):
1427 self
.texinput
.write("\n\\catcode`\\@11\\relax\\@@end\n")
1431 if self
.pyxgraphics
:
1432 with config
.open("pyx.def", []) as source
, open(os
.path
.join(self
.tmpdir
, "pyx.def"), "wb") as dest
:
1433 dest
.write(source
.read())
1434 self
._execute
("\\makeatletter%\n"
1435 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
1436 "\\def\\ProcessOptions{%\n"
1437 "\\def\\Gin@driver{" + self
.tmpdir
.replace(os
.sep
, "/") + "/pyx.def}%\n"
1438 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
1439 "\\saveProcessOptions}%\n"
1441 [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1442 if self
.docopt
is not None:
1443 self
._execute
("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
1444 self
.texmessages_docclass_default
+ self
.texmessages_docclass
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1446 self
._execute
("\\documentclass{%s}" % self
.docclass
,
1447 self
.texmessages_docclass_default
+ self
.texmessages_docclass
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1450 def reset_for_tex_done(f
):
1452 def wrapped(self
, *args
, **kwargs
):
1454 return f(self
, *args
, **kwargs
)
1455 except TexDoneError
:
1456 self
.reset(reinit
=True)
1457 return f(self
, *args
, **kwargs
)
1463 def __init__(self
, cls
, *args
, **kwargs
):
1464 """A restartable :class:`SingleRunner` class
1466 :param cls: the class being wrapped
1467 :type cls: :class:`SingleRunner` class
1468 :param list args: args at class instantiation
1469 :param dict kwargs: keyword args at at class instantiation
1474 self
.kwargs
= kwargs
1477 def preamble(self
, expr
, texmessages
=[]):
1478 "resembles :meth:`SingleRunner.preamble`"
1479 self
.preambles
.append((expr
, texmessages
))
1480 self
.instance
.preamble(expr
, texmessages
)
1483 def text_pt(self
, *args
, **kwargs
):
1484 "resembles :meth:`SingleRunner.text_pt`"
1485 return self
.instance
.text_pt(*args
, **kwargs
)
1488 def text(self
, *args
, **kwargs
):
1489 "resembles :meth:`SingleRunner.text`"
1490 return self
.instance
.text(*args
, **kwargs
)
1492 def reset(self
, reinit
=False):
1493 """Start a new :class:`SingleRunner` instance
1495 :param bool reinit: replay :meth:`preamble` calls on the new instance
1497 After executing this function further preamble calls are allowed,
1498 whereas once a text output has been created, :meth:`preamble` calls are
1502 self
.instance
= self
.cls(*self
.args
, **self
.kwargs
)
1504 for expr
, texmessages
in self
.preambles
:
1505 self
.instance
.preamble(expr
, texmessages
)
1510 class TexEngine(MultiRunner
):
1512 def __init__(self
, *args
, **kwargs
):
1513 """A restartable :class:`SingleTexRunner` class
1515 :param list args: args at class instantiation
1516 :param dict kwargs: keyword args at at class instantiation
1519 super().__init
__(SingleTexRunner
, *args
, **kwargs
)
1522 class LatexEngine(MultiRunner
):
1524 def __init__(self
, *args
, **kwargs
):
1525 """A restartable :class:`SingleLatexRunner` class
1527 :param list args: args at class instantiation
1528 :param dict kwargs: keyword args at at class instantiation
1531 super().__init
__(SingleLatexRunner
, *args
, **kwargs
)
1534 from pyx
import deco
1535 from pyx
.font
import T1font
1536 from pyx
.font
.t1file
import T1File
1537 from pyx
.font
.afmfile
import AFMfile
1540 class MultiEngineText
:
1542 def __init__(self
, tex
, unicode):
1544 self
.unicode = unicode
1549 def __init__(self
, text
, scale
=1, shift
=0):
1557 def __init__(self
, texts
, frac
=False, align
=0):
1558 assert not frac
or len(texts
) == 2
1564 class unicodetextbox_pt(textbox_pt
):
1566 def __init__(self
, x_pt
, y_pt
, texts
, font
, size
, mathmode
=False):
1569 self
.canvas
= canvas
.canvas()
1570 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated_pt(x_pt
, y_pt
)
1572 if isinstance(texts
, (str, Text
, StackedText
)):
1577 if isinstance(text
, str):
1579 if isinstance(text
, Text
):
1581 text_fragments
= text
.text
.split('-')
1583 text_fragments
= [text
.text
]
1584 for i
, text_fragment
in enumerate(text_fragments
):
1586 self
.canvas
.fill(path
.rect_pt(x_pt
+0.5*(self
.minuswidth_pt
-self
.minuslength_pt
), self
.mathaxis_pt
-0.5*self
.minusthickness_pt
, self
.minuslength_pt
, self
.minusthickness_pt
))
1587 x_pt
+= self
.minuswidth_pt
1590 t
= self
.font
.text_pt(x_pt
, text
.shift
*self
.size
, text_fragment
, text
.scale
*self
.size
)
1591 x_pt
+= t
.bbox().width_pt()
1593 assert '·' in text_fragment
1594 t
= self
.font
.text_pt(x_pt
, text
.shift
*self
.size
, text_fragment
.replace('·', 'x'), text
.scale
*self
.size
)
1595 x_pt
+= t
.bbox().width_pt()
1596 self
.canvas
.insert(t
)
1598 assert isinstance(text
, StackedText
)
1599 shift
= self
.mathaxis_pt
if text
.frac
else 0
1600 ts
= [self
.font
.text_pt(x_pt
, text
.shift
*self
.size
+shift
, text
.text
, text
.scale
*self
.size
)
1601 for text
in text
.texts
]
1602 width_pt
= max(t
.bbox().width_pt() for t
in ts
)
1604 self
.canvas
.fill(path
.rect_pt(x_pt
, self
.mathaxis_pt
-0.5*self
.minusthickness_pt
, width_pt
, self
.minusthickness_pt
))
1606 self
.canvas
.insert(t
, [trafo
.translate_pt(text
.align
*(width_pt
-t
.bbox().width_pt()), 0)] if text
.align
else [])
1609 bbox
= self
.canvas
.bbox()
1610 bbox
.includepoint_pt(0, 0)
1611 bbox
.includepoint_pt(x_pt
, 0)
1612 box
.rect_pt
.__init
__(self
, bbox
.llx_pt
, bbox
.lly_pt
, bbox
.urx_pt
-bbox
.llx_pt
, bbox
.ury_pt
-bbox
.lly_pt
, abscenter_pt
= (0, 0))
1613 box
.rect
.transform(self
, self
.texttrafo
)
1615 def _extract_minus_properties(self
):
1616 minus_path
= self
.font
.text_pt(0, 0, '=', 1).textpath().normpath()
1617 minus_path
.normsubpaths
= [normsubpath
for normsubpath
in minus_path
.normsubpaths
if normsubpath
]
1618 self
.font
.minusthickness_pt
= max(normsubpath
.bbox().height_pt() for normsubpath
in minus_path
.normsubpaths
)
1619 self
.font
.halfminuswidth_pt
, self
.font
.mathaxis_pt
= minus_path
.bbox().center_pt()
1620 self
.font
.minuslength_pt
= minus_path
.bbox().width_pt()
1623 def mathaxis_pt(self
):
1624 if not hasattr(self
.font
, "mathaxis_pt"):
1625 self
._extract
_minus
_properties
()
1626 return self
.font
.mathaxis_pt
*self
.size
1629 def minuswidth_pt(self
):
1630 if not hasattr(self
.font
, "halfminuswidth_pt"):
1631 self
._extract
_minus
_properties
()
1632 return 2*self
.font
.halfminuswidth_pt
*self
.size
1635 def minuslength_pt(self
):
1636 if not hasattr(self
.font
, "minuslength_pt"):
1637 self
._extract
_minus
_properties
()
1638 return self
.font
.minuslength_pt
*self
.size
1641 def minusthickness_pt(self
):
1642 if not hasattr(self
.font
, "minusthickness_pt"):
1643 self
._extract
_minus
_properties
()
1644 return self
.font
.minusthickness_pt
*self
.size
1646 def transform(self
, *trafos
, keep_anchor
=False):
1647 box
.rect
.transform(self
, *trafos
, keep_anchor
=keep_anchor
)
1648 for trafo
in trafos
:
1649 self
.texttrafo
= trafo
* self
.texttrafo
1652 return self
.canvas
.bbox().transformed(self
.texttrafo
)
1656 for item
in self
.canvas
.items
:
1657 if isinstance(item
, canvas
.canvas
):
1658 for subitem
in item
.items
:
1659 r
+= subitem
.textpath().transformed(item
.trafo
)
1660 elif isinstance(item
, deco
.decoratedpath
):
1663 r
+= item
.textpath()
1664 return r
.transformed(self
.texttrafo
)
1666 def processPS(self
, file, writer
, context
, registry
, bbox
):
1667 c
= canvas
.canvas([self
.texttrafo
])
1668 c
.insert(self
.canvas
)
1669 c
.processPS(file, writer
, context
, registry
, bbox
)
1671 def processPDF(self
, file, writer
, context
, registry
, bbox
):
1672 c
= canvas
.canvas([self
.texttrafo
])
1673 c
.insert(self
.canvas
)
1674 c
.processPDF(file, writer
, context
, registry
, bbox
)
1676 def processSVG(self
, xml
, writer
, context
, registry
, bbox
):
1677 c
= canvas
.canvas([self
.texttrafo
])
1678 c
.insert(self
.canvas
)
1679 c
.processSVG(xml
, writer
, context
, registry
, bbox
)
1682 class UnicodeEngine
:
1684 def __init__(self
, fontname
="cmr10", size
=10):
1685 self
.font
= T1font(T1File
.from_PF_bytes(config
.open(fontname
, [config
.format
.type1
]).read()),
1686 AFMfile(config
.open(fontname
, [config
.format
.afm
], ascii
=True)))
1690 raise NotImplemented()
1693 raise NotImplemented()
1695 def text_pt(self
, x_pt
, y_pt
, text
, textattrs
=[], texmessages
=[], fontmap
=None, singlecharmode
=False):
1697 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1698 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1699 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1700 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1701 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1702 mathmode
= bool(attr
.getattrs(textattrs
, [_mathmode
]))
1704 if isinstance(text
, MultiEngineText
):
1706 output
= unicodetextbox_pt(x_pt
, y_pt
, text
, self
.font
, self
.size
, mathmode
=mathmode
)
1707 for ta
in textattrs
: # reverse?!
1708 ta
.apply_trafo(output
)
1712 def text(self
, x
, y
, *args
, **kwargs
):
1713 return self
.text_pt(unit
.topt(x
), unit
.topt(y
), *args
, **kwargs
)
1716 # from pyx.font.otffile import OpenTypeFont
1718 # class OTFUnicodeText:
1720 # def __init__(self, fontname, size=10):
1721 # self.font = OpenTypeFont(config.open(fontname, [config.format.ttf]))
1724 # def preamble(self):
1725 # raise NotImplemented()
1728 # raise NotImplemented()
1730 # def text_pt(self, x_pt, y_pt, text, textattrs=[], texmessages=[], fontmap=None, singlecharmode=False):
1731 # # def text_pt(self, x_pt, y_pt, text, *args, **kwargs):
1732 # return unicodetextbox_pt(x_pt, y_pt, text, self.font, self.size)
1734 # def text(self, x, y, *args, **kwargs):
1735 # return self.text_pt(unit.topt(x), unit.topt(y), *args, **kwargs)
1738 # old, deprecated names:
1739 texrunner
= TexRunner
= TexEngine
1740 latexrunner
= LatexRunner
= LatexEngine
1742 # module level interface documentation for autodoc
1743 # the actual values are setup by the set function
1745 #: the current :class:`MultiRunner` instance for the module level functions
1746 default_runner
= None
1748 #: default_runner.preamble (bound method)
1751 #: default_runner.text_pt (bound method)
1754 #: default_runner.text (bound method)
1757 #: default_runner.reset (bound method)
1760 def set(engine
=None, cls
=None, mode
=None, *args
, **kwargs
):
1761 """Setup a new module level :class:`MultiRunner`
1763 :param cls: the module level :class:`MultiRunner` to be used, i.e.
1764 :class:`TexRunner` or :class:`LatexRunner`
1765 :type cls: :class:`MultiRunner` object, not instance
1766 :param mode: ``"tex"`` for :class:`TexRunner` or ``"latex"`` for
1767 :class:`LatexRunner` with arbitraty capitalization, overwriting the cls
1770 :deprecated: use the cls argument instead
1771 :type mode: str or None
1772 :param list args: args at class instantiation
1773 :param dict kwargs: keyword args at at class instantiation
1776 # note: default_runner and defaulttexrunner are deprecated
1777 global default_engine
, default_runner
, defaulttexrunner
, reset
, preamble
, text
, text_pt
1778 if mode
is not None:
1779 logger
.warning("mode setting is deprecated, use the engine argument instead")
1781 assert engine
is None
1782 engine
= {"tex": TexEngine
, "latex": LatexEngine
}[mode
.lower()]
1783 elif cls
is not None:
1784 logger
.warning("cls setting is deprecated, use the engine argument instead")
1787 default_engine
= default_runner
= defaulttexrunner
= engine(*args
, **kwargs
)
1788 preamble
= default_runner
.preamble
1789 text_pt
= default_runner
.text_pt
1790 text
= default_runner
.text
1791 reset
= default_runner
.reset
1793 # initialize default_runner
1794 set({"TexEngine": TexEngine
, "LatexEngine": LatexEngine
, "UnicodeEngine": UnicodeEngine
}[config
.get('text', 'default_engine', 'TexEngine')])
1797 def escapestring(s
, replace
={" ": "~",
1809 "\\": "{$\setminus$}",
1811 "Escapes ASCII characters such that they can be typeset by TeX/LaTeX"""
1814 if not 32 <= ord(s[i]) < 127:
1815 raise ValueError("escapestring function handles ascii strings only
")
1822 s = s[:i] + r + s[i+1:]