Coverage for /usr/lib/python3/dist-packages/sympy/printing/preview.py: 10%
157 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1import os
2from os.path import join
3import shutil
4import tempfile
6try:
7 from subprocess import STDOUT, CalledProcessError, check_output
8except ImportError:
9 pass
11from sympy.utilities.decorator import doctest_depends_on
12from sympy.utilities.misc import debug
13from .latex import latex
15__doctest_requires__ = {('preview',): ['pyglet']}
18def _check_output_no_window(*args, **kwargs):
19 # Avoid showing a cmd.exe window when running this
20 # on Windows
21 if os.name == 'nt':
22 creation_flag = 0x08000000 # CREATE_NO_WINDOW
23 else:
24 creation_flag = 0 # Default value
25 return check_output(*args, creationflags=creation_flag, **kwargs)
28def system_default_viewer(fname, fmt):
29 """ Open fname with the default system viewer.
31 In practice, it is impossible for python to know when the system viewer is
32 done. For this reason, we ensure the passed file will not be deleted under
33 it, and this function does not attempt to block.
34 """
35 # copy to a new temporary file that will not be deleted
36 with tempfile.NamedTemporaryFile(prefix='sympy-preview-',
37 suffix=os.path.splitext(fname)[1],
38 delete=False) as temp_f:
39 with open(fname, 'rb') as f:
40 shutil.copyfileobj(f, temp_f)
42 import platform
43 if platform.system() == 'Darwin':
44 import subprocess
45 subprocess.call(('open', temp_f.name))
46 elif platform.system() == 'Windows':
47 os.startfile(temp_f.name)
48 else:
49 import subprocess
50 subprocess.call(('xdg-open', temp_f.name))
53def pyglet_viewer(fname, fmt):
54 try:
55 from pyglet import window, image, gl
56 from pyglet.window import key
57 from pyglet.image.codecs import ImageDecodeException
58 except ImportError:
59 raise ImportError("pyglet is required for preview.\n visit https://pyglet.org/")
61 try:
62 img = image.load(fname)
63 except ImageDecodeException:
64 raise ValueError("pyglet preview does not work for '{}' files.".format(fmt))
66 offset = 25
68 config = gl.Config(double_buffer=False)
69 win = window.Window(
70 width=img.width + 2*offset,
71 height=img.height + 2*offset,
72 caption="SymPy",
73 resizable=False,
74 config=config
75 )
77 win.set_vsync(False)
79 try:
80 def on_close():
81 win.has_exit = True
83 win.on_close = on_close
85 def on_key_press(symbol, modifiers):
86 if symbol in [key.Q, key.ESCAPE]:
87 on_close()
89 win.on_key_press = on_key_press
91 def on_expose():
92 gl.glClearColor(1.0, 1.0, 1.0, 1.0)
93 gl.glClear(gl.GL_COLOR_BUFFER_BIT)
95 img.blit(
96 (win.width - img.width) / 2,
97 (win.height - img.height) / 2
98 )
100 win.on_expose = on_expose
102 while not win.has_exit:
103 win.dispatch_events()
104 win.flip()
105 except KeyboardInterrupt:
106 pass
108 win.close()
111def _get_latex_main(expr, *, preamble=None, packages=(), extra_preamble=None,
112 euler=True, fontsize=None, **latex_settings):
113 """
114 Generate string of a LaTeX document rendering ``expr``.
115 """
116 if preamble is None:
117 actual_packages = packages + ("amsmath", "amsfonts")
118 if euler:
119 actual_packages += ("euler",)
120 package_includes = "\n" + "\n".join(["\\usepackage{%s}" % p
121 for p in actual_packages])
122 if extra_preamble:
123 package_includes += extra_preamble
125 if not fontsize:
126 fontsize = "12pt"
127 elif isinstance(fontsize, int):
128 fontsize = "{}pt".format(fontsize)
129 preamble = r"""\documentclass[varwidth,%s]{standalone}
130%s
132\begin{document}
133""" % (fontsize, package_includes)
134 else:
135 if packages or extra_preamble:
136 raise ValueError("The \"packages\" or \"extra_preamble\" keywords"
137 "must not be set if a "
138 "custom LaTeX preamble was specified")
140 if isinstance(expr, str):
141 latex_string = expr
142 else:
143 latex_string = ('$\\displaystyle ' +
144 latex(expr, mode='plain', **latex_settings) +
145 '$')
147 return preamble + '\n' + latex_string + '\n\n' + r"\end{document}"
150@doctest_depends_on(exe=('latex', 'dvipng'), modules=('pyglet',),
151 disable_viewers=('evince', 'gimp', 'superior-dvi-viewer'))
152def preview(expr, output='png', viewer=None, euler=True, packages=(),
153 filename=None, outputbuffer=None, preamble=None, dvioptions=None,
154 outputTexFile=None, extra_preamble=None, fontsize=None,
155 **latex_settings):
156 r"""
157 View expression or LaTeX markup in PNG, DVI, PostScript or PDF form.
159 If the expr argument is an expression, it will be exported to LaTeX and
160 then compiled using the available TeX distribution. The first argument,
161 'expr', may also be a LaTeX string. The function will then run the
162 appropriate viewer for the given output format or use the user defined
163 one. By default png output is generated.
165 By default pretty Euler fonts are used for typesetting (they were used to
166 typeset the well known "Concrete Mathematics" book). For that to work, you
167 need the 'eulervm.sty' LaTeX style (in Debian/Ubuntu, install the
168 texlive-fonts-extra package). If you prefer default AMS fonts or your
169 system lacks 'eulervm' LaTeX package then unset the 'euler' keyword
170 argument.
172 To use viewer auto-detection, lets say for 'png' output, issue
174 >>> from sympy import symbols, preview, Symbol
175 >>> x, y = symbols("x,y")
177 >>> preview(x + y, output='png')
179 This will choose 'pyglet' by default. To select a different one, do
181 >>> preview(x + y, output='png', viewer='gimp')
183 The 'png' format is considered special. For all other formats the rules
184 are slightly different. As an example we will take 'dvi' output format. If
185 you would run
187 >>> preview(x + y, output='dvi')
189 then 'view' will look for available 'dvi' viewers on your system
190 (predefined in the function, so it will try evince, first, then kdvi and
191 xdvi). If nothing is found, it will fall back to using a system file
192 association (via ``open`` and ``xdg-open``). To always use your system file
193 association without searching for the above readers, use
195 >>> from sympy.printing.preview import system_default_viewer
196 >>> preview(x + y, output='dvi', viewer=system_default_viewer)
198 If this still does not find the viewer you want, it can be set explicitly.
200 >>> preview(x + y, output='dvi', viewer='superior-dvi-viewer')
202 This will skip auto-detection and will run user specified
203 'superior-dvi-viewer'. If ``view`` fails to find it on your system it will
204 gracefully raise an exception.
206 You may also enter ``'file'`` for the viewer argument. Doing so will cause
207 this function to return a file object in read-only mode, if ``filename``
208 is unset. However, if it was set, then 'preview' writes the generated
209 file to this filename instead.
211 There is also support for writing to a ``io.BytesIO`` like object, which
212 needs to be passed to the ``outputbuffer`` argument.
214 >>> from io import BytesIO
215 >>> obj = BytesIO()
216 >>> preview(x + y, output='png', viewer='BytesIO',
217 ... outputbuffer=obj)
219 The LaTeX preamble can be customized by setting the 'preamble' keyword
220 argument. This can be used, e.g., to set a different font size, use a
221 custom documentclass or import certain set of LaTeX packages.
223 >>> preamble = "\\documentclass[10pt]{article}\n" \
224 ... "\\usepackage{amsmath,amsfonts}\\begin{document}"
225 >>> preview(x + y, output='png', preamble=preamble)
227 It is also possible to use the standard preamble and provide additional
228 information to the preamble using the ``extra_preamble`` keyword argument.
230 >>> from sympy import sin
231 >>> extra_preamble = "\\renewcommand{\\sin}{\\cos}"
232 >>> preview(sin(x), output='png', extra_preamble=extra_preamble)
234 If the value of 'output' is different from 'dvi' then command line
235 options can be set ('dvioptions' argument) for the execution of the
236 'dvi'+output conversion tool. These options have to be in the form of a
237 list of strings (see ``subprocess.Popen``).
239 Additional keyword args will be passed to the :func:`~sympy.printing.latex.latex` call,
240 e.g., the ``symbol_names`` flag.
242 >>> phidd = Symbol('phidd')
243 >>> preview(phidd, symbol_names={phidd: r'\ddot{\varphi}'})
245 For post-processing the generated TeX File can be written to a file by
246 passing the desired filename to the 'outputTexFile' keyword
247 argument. To write the TeX code to a file named
248 ``"sample.tex"`` and run the default png viewer to display the resulting
249 bitmap, do
251 >>> preview(x + y, outputTexFile="sample.tex")
254 """
255 # pyglet is the default for png
256 if viewer is None and output == "png":
257 try:
258 import pyglet # noqa: F401
259 except ImportError:
260 pass
261 else:
262 viewer = pyglet_viewer
264 # look up a known application
265 if viewer is None:
266 # sorted in order from most pretty to most ugly
267 # very discussable, but indeed 'gv' looks awful :)
268 candidates = {
269 "dvi": [ "evince", "okular", "kdvi", "xdvi" ],
270 "ps": [ "evince", "okular", "gsview", "gv" ],
271 "pdf": [ "evince", "okular", "kpdf", "acroread", "xpdf", "gv" ],
272 }
274 for candidate in candidates.get(output, []):
275 path = shutil.which(candidate)
276 if path is not None:
277 viewer = path
278 break
280 # otherwise, use the system default for file association
281 if viewer is None:
282 viewer = system_default_viewer
284 if viewer == "file":
285 if filename is None:
286 raise ValueError("filename has to be specified if viewer=\"file\"")
287 elif viewer == "BytesIO":
288 if outputbuffer is None:
289 raise ValueError("outputbuffer has to be a BytesIO "
290 "compatible object if viewer=\"BytesIO\"")
291 elif not callable(viewer) and not shutil.which(viewer):
292 raise OSError("Unrecognized viewer: %s" % viewer)
294 latex_main = _get_latex_main(expr, preamble=preamble, packages=packages,
295 euler=euler, extra_preamble=extra_preamble,
296 fontsize=fontsize, **latex_settings)
298 debug("Latex code:")
299 debug(latex_main)
300 with tempfile.TemporaryDirectory() as workdir:
301 with open(join(workdir, 'texput.tex'), 'w', encoding='utf-8') as fh:
302 fh.write(latex_main)
304 if outputTexFile is not None:
305 shutil.copyfile(join(workdir, 'texput.tex'), outputTexFile)
307 if not shutil.which('latex'):
308 raise RuntimeError("latex program is not installed")
310 try:
311 _check_output_no_window(
312 ['latex', '-halt-on-error', '-interaction=nonstopmode',
313 'texput.tex'],
314 cwd=workdir,
315 stderr=STDOUT)
316 except CalledProcessError as e:
317 raise RuntimeError(
318 "'latex' exited abnormally with the following output:\n%s" %
319 e.output)
321 src = "texput.%s" % (output)
323 if output != "dvi":
324 # in order of preference
325 commandnames = {
326 "ps": ["dvips"],
327 "pdf": ["dvipdfmx", "dvipdfm", "dvipdf"],
328 "png": ["dvipng"],
329 "svg": ["dvisvgm"],
330 }
331 try:
332 cmd_variants = commandnames[output]
333 except KeyError:
334 raise ValueError("Invalid output format: %s" % output) from None
336 # find an appropriate command
337 for cmd_variant in cmd_variants:
338 cmd_path = shutil.which(cmd_variant)
339 if cmd_path:
340 cmd = [cmd_path]
341 break
342 else:
343 if len(cmd_variants) > 1:
344 raise RuntimeError("None of %s are installed" % ", ".join(cmd_variants))
345 else:
346 raise RuntimeError("%s is not installed" % cmd_variants[0])
348 defaultoptions = {
349 "dvipng": ["-T", "tight", "-z", "9", "--truecolor"],
350 "dvisvgm": ["--no-fonts"],
351 }
353 commandend = {
354 "dvips": ["-o", src, "texput.dvi"],
355 "dvipdf": ["texput.dvi", src],
356 "dvipdfm": ["-o", src, "texput.dvi"],
357 "dvipdfmx": ["-o", src, "texput.dvi"],
358 "dvipng": ["-o", src, "texput.dvi"],
359 "dvisvgm": ["-o", src, "texput.dvi"],
360 }
362 if dvioptions is not None:
363 cmd.extend(dvioptions)
364 else:
365 cmd.extend(defaultoptions.get(cmd_variant, []))
366 cmd.extend(commandend[cmd_variant])
368 try:
369 _check_output_no_window(cmd, cwd=workdir, stderr=STDOUT)
370 except CalledProcessError as e:
371 raise RuntimeError(
372 "'%s' exited abnormally with the following output:\n%s" %
373 (' '.join(cmd), e.output))
376 if viewer == "file":
377 shutil.move(join(workdir, src), filename)
378 elif viewer == "BytesIO":
379 with open(join(workdir, src), 'rb') as fh:
380 outputbuffer.write(fh.read())
381 elif callable(viewer):
382 viewer(join(workdir, src), fmt=output)
383 else:
384 try:
385 _check_output_no_window(
386 [viewer, src], cwd=workdir, stderr=STDOUT)
387 except CalledProcessError as e:
388 raise RuntimeError(
389 "'%s %s' exited abnormally with the following output:\n%s" %
390 (viewer, src, e.output))