123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- import os
- from os.path import join
- import shutil
- import tempfile
- try:
- from subprocess import STDOUT, CalledProcessError, check_output
- except ImportError:
- pass
- from sympy.utilities.decorator import doctest_depends_on
- from .latex import latex
- __doctest_requires__ = {('preview',): ['pyglet']}
- def _check_output_no_window(*args, **kwargs):
- # Avoid showing a cmd.exe window when running this
- # on Windows
- if os.name == 'nt':
- creation_flag = 0x08000000 # CREATE_NO_WINDOW
- else:
- creation_flag = 0 # Default value
- return check_output(*args, creationflags=creation_flag, **kwargs)
- def _run_pyglet(fname, fmt):
- from pyglet import window, image, gl
- from pyglet.window import key
- from pyglet.image.codecs import ImageDecodeException
- try:
- img = image.load(fname)
- except ImageDecodeException:
- raise ValueError("pyglet preview does not work for '{}' files.".format(fmt))
- offset = 25
- config = gl.Config(double_buffer=False)
- win = window.Window(
- width=img.width + 2*offset,
- height=img.height + 2*offset,
- caption="sympy",
- resizable=False,
- config=config
- )
- win.set_vsync(False)
- try:
- def on_close():
- win.has_exit = True
- win.on_close = on_close
- def on_key_press(symbol, modifiers):
- if symbol in [key.Q, key.ESCAPE]:
- on_close()
- win.on_key_press = on_key_press
- def on_expose():
- gl.glClearColor(1.0, 1.0, 1.0, 1.0)
- gl.glClear(gl.GL_COLOR_BUFFER_BIT)
- img.blit(
- (win.width - img.width) / 2,
- (win.height - img.height) / 2
- )
- win.on_expose = on_expose
- while not win.has_exit:
- win.dispatch_events()
- win.flip()
- except KeyboardInterrupt:
- pass
- win.close()
- @doctest_depends_on(exe=('latex', 'dvipng'), modules=('pyglet',),
- disable_viewers=('evince', 'gimp', 'superior-dvi-viewer'))
- def preview(expr, output='png', viewer=None, euler=True, packages=(),
- filename=None, outputbuffer=None, preamble=None, dvioptions=None,
- outputTexFile=None, **latex_settings):
- r"""
- View expression or LaTeX markup in PNG, DVI, PostScript or PDF form.
- If the expr argument is an expression, it will be exported to LaTeX and
- then compiled using the available TeX distribution. The first argument,
- 'expr', may also be a LaTeX string. The function will then run the
- appropriate viewer for the given output format or use the user defined
- one. By default png output is generated.
- By default pretty Euler fonts are used for typesetting (they were used to
- typeset the well known "Concrete Mathematics" book). For that to work, you
- need the 'eulervm.sty' LaTeX style (in Debian/Ubuntu, install the
- texlive-fonts-extra package). If you prefer default AMS fonts or your
- system lacks 'eulervm' LaTeX package then unset the 'euler' keyword
- argument.
- To use viewer auto-detection, lets say for 'png' output, issue
- >>> from sympy import symbols, preview, Symbol
- >>> x, y = symbols("x,y")
- >>> preview(x + y, output='png')
- This will choose 'pyglet' by default. To select a different one, do
- >>> preview(x + y, output='png', viewer='gimp')
- The 'png' format is considered special. For all other formats the rules
- are slightly different. As an example we will take 'dvi' output format. If
- you would run
- >>> preview(x + y, output='dvi')
- then 'view' will look for available 'dvi' viewers on your system
- (predefined in the function, so it will try evince, first, then kdvi and
- xdvi). If nothing is found you will need to set the viewer explicitly.
- >>> preview(x + y, output='dvi', viewer='superior-dvi-viewer')
- This will skip auto-detection and will run user specified
- 'superior-dvi-viewer'. If 'view' fails to find it on your system it will
- gracefully raise an exception.
- You may also enter 'file' for the viewer argument. Doing so will cause
- this function to return a file object in read-only mode, if 'filename'
- is unset. However, if it was set, then 'preview' writes the genereted
- file to this filename instead.
- There is also support for writing to a BytesIO like object, which needs
- to be passed to the 'outputbuffer' argument.
- >>> from io import BytesIO
- >>> obj = BytesIO()
- >>> preview(x + y, output='png', viewer='BytesIO',
- ... outputbuffer=obj)
- The LaTeX preamble can be customized by setting the 'preamble' keyword
- argument. This can be used, e.g., to set a different font size, use a
- custom documentclass or import certain set of LaTeX packages.
- >>> preamble = "\\documentclass[10pt]{article}\n" \
- ... "\\usepackage{amsmath,amsfonts}\\begin{document}"
- >>> preview(x + y, output='png', preamble=preamble)
- If the value of 'output' is different from 'dvi' then command line
- options can be set ('dvioptions' argument) for the execution of the
- 'dvi'+output conversion tool. These options have to be in the form of a
- list of strings (see subprocess.Popen).
- Additional keyword args will be passed to the latex call, e.g., the
- symbol_names flag.
- >>> phidd = Symbol('phidd')
- >>> preview(phidd, symbol_names={phidd:r'\ddot{\varphi}'})
- For post-processing the generated TeX File can be written to a file by
- passing the desired filename to the 'outputTexFile' keyword
- argument. To write the TeX code to a file named
- "sample.tex" and run the default png viewer to display the resulting
- bitmap, do
- >>> preview(x + y, outputTexFile="sample.tex")
- """
- special = [ 'pyglet' ]
- if viewer is None:
- if output == "png":
- viewer = "pyglet"
- else:
- # sorted in order from most pretty to most ugly
- # very discussable, but indeed 'gv' looks awful :)
- # TODO add candidates for windows to list
- candidates = {
- "dvi": [ "evince", "okular", "kdvi", "xdvi" ],
- "ps": [ "evince", "okular", "gsview", "gv" ],
- "pdf": [ "evince", "okular", "kpdf", "acroread", "xpdf", "gv" ],
- }
- try:
- candidate_viewers = candidates[output]
- except KeyError:
- raise ValueError("Invalid output format: %s" % output) from None
- for candidate in candidate_viewers:
- path = shutil.which(candidate)
- if path is not None:
- viewer = path
- break
- else:
- raise OSError(
- "No viewers found for '%s' output format." % output)
- else:
- if viewer == "file":
- if filename is None:
- raise ValueError("filename has to be specified if viewer=\"file\"")
- elif viewer == "BytesIO":
- if outputbuffer is None:
- raise ValueError("outputbuffer has to be a BytesIO "
- "compatible object if viewer=\"BytesIO\"")
- elif viewer not in special and not shutil.which(viewer):
- raise OSError("Unrecognized viewer: %s" % viewer)
- if preamble is None:
- actual_packages = packages + ("amsmath", "amsfonts")
- if euler:
- actual_packages += ("euler",)
- package_includes = "\n" + "\n".join(["\\usepackage{%s}" % p
- for p in actual_packages])
- preamble = r"""\documentclass[varwidth,12pt]{standalone}
- %s
- \begin{document}
- """ % (package_includes)
- else:
- if packages:
- raise ValueError("The \"packages\" keyword must not be set if a "
- "custom LaTeX preamble was specified")
- if isinstance(expr, str):
- latex_string = expr
- else:
- latex_string = ('$\\displaystyle ' +
- latex(expr, mode='plain', **latex_settings) +
- '$')
- latex_main = preamble + '\n' + latex_string + '\n\n' + r"\end{document}"
- with tempfile.TemporaryDirectory() as workdir:
- with open(join(workdir, 'texput.tex'), 'w', encoding='utf-8') as fh:
- fh.write(latex_main)
- if outputTexFile is not None:
- shutil.copyfile(join(workdir, 'texput.tex'), outputTexFile)
- if not shutil.which('latex'):
- raise RuntimeError("latex program is not installed")
- try:
- _check_output_no_window(
- ['latex', '-halt-on-error', '-interaction=nonstopmode',
- 'texput.tex'],
- cwd=workdir,
- stderr=STDOUT)
- except CalledProcessError as e:
- raise RuntimeError(
- "'latex' exited abnormally with the following output:\n%s" %
- e.output)
- src = "texput.%s" % (output)
- if output != "dvi":
- # in order of preference
- commandnames = {
- "ps": ["dvips"],
- "pdf": ["dvipdfmx", "dvipdfm", "dvipdf"],
- "png": ["dvipng"],
- "svg": ["dvisvgm"],
- }
- try:
- cmd_variants = commandnames[output]
- except KeyError:
- raise ValueError("Invalid output format: %s" % output) from None
- # find an appropriate command
- for cmd_variant in cmd_variants:
- cmd_path = shutil.which(cmd_variant)
- if cmd_path:
- cmd = [cmd_path]
- break
- else:
- if len(cmd_variants) > 1:
- raise RuntimeError("None of %s are installed" % ", ".join(cmd_variants))
- else:
- raise RuntimeError("%s is not installed" % cmd_variants[0])
- defaultoptions = {
- "dvipng": ["-T", "tight", "-z", "9", "--truecolor"],
- "dvisvgm": ["--no-fonts"],
- }
- commandend = {
- "dvips": ["-o", src, "texput.dvi"],
- "dvipdf": ["texput.dvi", src],
- "dvipdfm": ["-o", src, "texput.dvi"],
- "dvipdfmx": ["-o", src, "texput.dvi"],
- "dvipng": ["-o", src, "texput.dvi"],
- "dvisvgm": ["-o", src, "texput.dvi"],
- }
- if dvioptions is not None:
- cmd.extend(dvioptions)
- else:
- cmd.extend(defaultoptions.get(cmd_variant, []))
- cmd.extend(commandend[cmd_variant])
- try:
- _check_output_no_window(cmd, cwd=workdir, stderr=STDOUT)
- except CalledProcessError as e:
- raise RuntimeError(
- "'%s' exited abnormally with the following output:\n%s" %
- (' '.join(cmd), e.output))
- if viewer == "file":
- shutil.move(join(workdir, src), filename)
- elif viewer == "BytesIO":
- with open(join(workdir, src), 'rb') as fh:
- outputbuffer.write(fh.read())
- elif viewer == "pyglet":
- try:
- import pyglet # noqa: F401
- except ImportError:
- raise ImportError("pyglet is required for preview.\n visit http://www.pyglet.org/")
- return _run_pyglet(join(workdir, src), fmt=output)
- else:
- try:
- _check_output_no_window(
- [viewer, src], cwd=workdir, stderr=STDOUT)
- except CalledProcessError as e:
- raise RuntimeError(
- "'%s %s' exited abnormally with the following output:\n%s" %
- (viewer, src, e.output))
|