123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- 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 sympy.utilities.misc import debug
- 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 system_default_viewer(fname, fmt):
- """ Open fname with the default system viewer.
- In practice, it is impossible for python to know when the system viewer is
- done. For this reason, we ensure the passed file will not be deleted under
- it, and this function does not attempt to block.
- """
- # copy to a new temporary file that will not be deleted
- with tempfile.NamedTemporaryFile(prefix='sympy-preview-',
- suffix=os.path.splitext(fname)[1],
- delete=False) as temp_f:
- with open(fname, 'rb') as f:
- shutil.copyfileobj(f, temp_f)
- import platform
- if platform.system() == 'Darwin':
- import subprocess
- subprocess.call(('open', temp_f.name))
- elif platform.system() == 'Windows':
- os.startfile(temp_f.name)
- else:
- import subprocess
- subprocess.call(('xdg-open', temp_f.name))
- def pyglet_viewer(fname, fmt):
- try:
- from pyglet import window, image, gl
- from pyglet.window import key
- from pyglet.image.codecs import ImageDecodeException
- except ImportError:
- raise ImportError("pyglet is required for preview.\n visit https://pyglet.org/")
- 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()
- def _get_latex_main(expr, *, preamble=None, packages=(), extra_preamble=None,
- euler=True, fontsize=None, **latex_settings):
- """
- Generate string of a LaTeX document rendering ``expr``.
- """
- 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])
- if extra_preamble:
- package_includes += extra_preamble
- if not fontsize:
- fontsize = "12pt"
- elif isinstance(fontsize, int):
- fontsize = "{}pt".format(fontsize)
- preamble = r"""\documentclass[varwidth,%s]{standalone}
- %s
- \begin{document}
- """ % (fontsize, package_includes)
- else:
- if packages or extra_preamble:
- raise ValueError("The \"packages\" or \"extra_preamble\" keywords"
- "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) +
- '$')
- return preamble + '\n' + latex_string + '\n\n' + r"\end{document}"
- @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, extra_preamble=None, fontsize=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, it will fall back to using a system file
- association (via ``open`` and ``xdg-open``). To always use your system file
- association without searching for the above readers, use
- >>> from sympy.printing.preview import system_default_viewer
- >>> preview(x + y, output='dvi', viewer=system_default_viewer)
- If this still does not find the viewer you want, it can be set 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 generated
- file to this filename instead.
- There is also support for writing to a ``io.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)
- It is also possible to use the standard preamble and provide additional
- information to the preamble using the ``extra_preamble`` keyword argument.
- >>> from sympy import sin
- >>> extra_preamble = "\\renewcommand{\\sin}{\\cos}"
- >>> preview(sin(x), output='png', extra_preamble=extra_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 :func:`~sympy.printing.latex.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")
- """
- # pyglet is the default for png
- if viewer is None and output == "png":
- try:
- import pyglet # noqa: F401
- except ImportError:
- pass
- else:
- viewer = pyglet_viewer
- # look up a known application
- if viewer is None:
- # sorted in order from most pretty to most ugly
- # very discussable, but indeed 'gv' looks awful :)
- candidates = {
- "dvi": [ "evince", "okular", "kdvi", "xdvi" ],
- "ps": [ "evince", "okular", "gsview", "gv" ],
- "pdf": [ "evince", "okular", "kpdf", "acroread", "xpdf", "gv" ],
- }
- for candidate in candidates.get(output, []):
- path = shutil.which(candidate)
- if path is not None:
- viewer = path
- break
- # otherwise, use the system default for file association
- if viewer is None:
- viewer = system_default_viewer
- 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 not callable(viewer) and not shutil.which(viewer):
- raise OSError("Unrecognized viewer: %s" % viewer)
- latex_main = _get_latex_main(expr, preamble=preamble, packages=packages,
- euler=euler, extra_preamble=extra_preamble,
- fontsize=fontsize, **latex_settings)
- debug("Latex code:")
- debug(latex_main)
- 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 callable(viewer):
- viewer(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))
|