preview.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import os
  2. from os.path import join
  3. import shutil
  4. import tempfile
  5. try:
  6. from subprocess import STDOUT, CalledProcessError, check_output
  7. except ImportError:
  8. pass
  9. from sympy.utilities.decorator import doctest_depends_on
  10. from .latex import latex
  11. __doctest_requires__ = {('preview',): ['pyglet']}
  12. def _check_output_no_window(*args, **kwargs):
  13. # Avoid showing a cmd.exe window when running this
  14. # on Windows
  15. if os.name == 'nt':
  16. creation_flag = 0x08000000 # CREATE_NO_WINDOW
  17. else:
  18. creation_flag = 0 # Default value
  19. return check_output(*args, creationflags=creation_flag, **kwargs)
  20. def _run_pyglet(fname, fmt):
  21. from pyglet import window, image, gl
  22. from pyglet.window import key
  23. from pyglet.image.codecs import ImageDecodeException
  24. try:
  25. img = image.load(fname)
  26. except ImageDecodeException:
  27. raise ValueError("pyglet preview does not work for '{}' files.".format(fmt))
  28. offset = 25
  29. config = gl.Config(double_buffer=False)
  30. win = window.Window(
  31. width=img.width + 2*offset,
  32. height=img.height + 2*offset,
  33. caption="sympy",
  34. resizable=False,
  35. config=config
  36. )
  37. win.set_vsync(False)
  38. try:
  39. def on_close():
  40. win.has_exit = True
  41. win.on_close = on_close
  42. def on_key_press(symbol, modifiers):
  43. if symbol in [key.Q, key.ESCAPE]:
  44. on_close()
  45. win.on_key_press = on_key_press
  46. def on_expose():
  47. gl.glClearColor(1.0, 1.0, 1.0, 1.0)
  48. gl.glClear(gl.GL_COLOR_BUFFER_BIT)
  49. img.blit(
  50. (win.width - img.width) / 2,
  51. (win.height - img.height) / 2
  52. )
  53. win.on_expose = on_expose
  54. while not win.has_exit:
  55. win.dispatch_events()
  56. win.flip()
  57. except KeyboardInterrupt:
  58. pass
  59. win.close()
  60. @doctest_depends_on(exe=('latex', 'dvipng'), modules=('pyglet',),
  61. disable_viewers=('evince', 'gimp', 'superior-dvi-viewer'))
  62. def preview(expr, output='png', viewer=None, euler=True, packages=(),
  63. filename=None, outputbuffer=None, preamble=None, dvioptions=None,
  64. outputTexFile=None, **latex_settings):
  65. r"""
  66. View expression or LaTeX markup in PNG, DVI, PostScript or PDF form.
  67. If the expr argument is an expression, it will be exported to LaTeX and
  68. then compiled using the available TeX distribution. The first argument,
  69. 'expr', may also be a LaTeX string. The function will then run the
  70. appropriate viewer for the given output format or use the user defined
  71. one. By default png output is generated.
  72. By default pretty Euler fonts are used for typesetting (they were used to
  73. typeset the well known "Concrete Mathematics" book). For that to work, you
  74. need the 'eulervm.sty' LaTeX style (in Debian/Ubuntu, install the
  75. texlive-fonts-extra package). If you prefer default AMS fonts or your
  76. system lacks 'eulervm' LaTeX package then unset the 'euler' keyword
  77. argument.
  78. To use viewer auto-detection, lets say for 'png' output, issue
  79. >>> from sympy import symbols, preview, Symbol
  80. >>> x, y = symbols("x,y")
  81. >>> preview(x + y, output='png')
  82. This will choose 'pyglet' by default. To select a different one, do
  83. >>> preview(x + y, output='png', viewer='gimp')
  84. The 'png' format is considered special. For all other formats the rules
  85. are slightly different. As an example we will take 'dvi' output format. If
  86. you would run
  87. >>> preview(x + y, output='dvi')
  88. then 'view' will look for available 'dvi' viewers on your system
  89. (predefined in the function, so it will try evince, first, then kdvi and
  90. xdvi). If nothing is found you will need to set the viewer explicitly.
  91. >>> preview(x + y, output='dvi', viewer='superior-dvi-viewer')
  92. This will skip auto-detection and will run user specified
  93. 'superior-dvi-viewer'. If 'view' fails to find it on your system it will
  94. gracefully raise an exception.
  95. You may also enter 'file' for the viewer argument. Doing so will cause
  96. this function to return a file object in read-only mode, if 'filename'
  97. is unset. However, if it was set, then 'preview' writes the genereted
  98. file to this filename instead.
  99. There is also support for writing to a BytesIO like object, which needs
  100. to be passed to the 'outputbuffer' argument.
  101. >>> from io import BytesIO
  102. >>> obj = BytesIO()
  103. >>> preview(x + y, output='png', viewer='BytesIO',
  104. ... outputbuffer=obj)
  105. The LaTeX preamble can be customized by setting the 'preamble' keyword
  106. argument. This can be used, e.g., to set a different font size, use a
  107. custom documentclass or import certain set of LaTeX packages.
  108. >>> preamble = "\\documentclass[10pt]{article}\n" \
  109. ... "\\usepackage{amsmath,amsfonts}\\begin{document}"
  110. >>> preview(x + y, output='png', preamble=preamble)
  111. If the value of 'output' is different from 'dvi' then command line
  112. options can be set ('dvioptions' argument) for the execution of the
  113. 'dvi'+output conversion tool. These options have to be in the form of a
  114. list of strings (see subprocess.Popen).
  115. Additional keyword args will be passed to the latex call, e.g., the
  116. symbol_names flag.
  117. >>> phidd = Symbol('phidd')
  118. >>> preview(phidd, symbol_names={phidd:r'\ddot{\varphi}'})
  119. For post-processing the generated TeX File can be written to a file by
  120. passing the desired filename to the 'outputTexFile' keyword
  121. argument. To write the TeX code to a file named
  122. "sample.tex" and run the default png viewer to display the resulting
  123. bitmap, do
  124. >>> preview(x + y, outputTexFile="sample.tex")
  125. """
  126. special = [ 'pyglet' ]
  127. if viewer is None:
  128. if output == "png":
  129. viewer = "pyglet"
  130. else:
  131. # sorted in order from most pretty to most ugly
  132. # very discussable, but indeed 'gv' looks awful :)
  133. # TODO add candidates for windows to list
  134. candidates = {
  135. "dvi": [ "evince", "okular", "kdvi", "xdvi" ],
  136. "ps": [ "evince", "okular", "gsview", "gv" ],
  137. "pdf": [ "evince", "okular", "kpdf", "acroread", "xpdf", "gv" ],
  138. }
  139. try:
  140. candidate_viewers = candidates[output]
  141. except KeyError:
  142. raise ValueError("Invalid output format: %s" % output) from None
  143. for candidate in candidate_viewers:
  144. path = shutil.which(candidate)
  145. if path is not None:
  146. viewer = path
  147. break
  148. else:
  149. raise OSError(
  150. "No viewers found for '%s' output format." % output)
  151. else:
  152. if viewer == "file":
  153. if filename is None:
  154. raise ValueError("filename has to be specified if viewer=\"file\"")
  155. elif viewer == "BytesIO":
  156. if outputbuffer is None:
  157. raise ValueError("outputbuffer has to be a BytesIO "
  158. "compatible object if viewer=\"BytesIO\"")
  159. elif viewer not in special and not shutil.which(viewer):
  160. raise OSError("Unrecognized viewer: %s" % viewer)
  161. if preamble is None:
  162. actual_packages = packages + ("amsmath", "amsfonts")
  163. if euler:
  164. actual_packages += ("euler",)
  165. package_includes = "\n" + "\n".join(["\\usepackage{%s}" % p
  166. for p in actual_packages])
  167. preamble = r"""\documentclass[varwidth,12pt]{standalone}
  168. %s
  169. \begin{document}
  170. """ % (package_includes)
  171. else:
  172. if packages:
  173. raise ValueError("The \"packages\" keyword must not be set if a "
  174. "custom LaTeX preamble was specified")
  175. if isinstance(expr, str):
  176. latex_string = expr
  177. else:
  178. latex_string = ('$\\displaystyle ' +
  179. latex(expr, mode='plain', **latex_settings) +
  180. '$')
  181. latex_main = preamble + '\n' + latex_string + '\n\n' + r"\end{document}"
  182. with tempfile.TemporaryDirectory() as workdir:
  183. with open(join(workdir, 'texput.tex'), 'w', encoding='utf-8') as fh:
  184. fh.write(latex_main)
  185. if outputTexFile is not None:
  186. shutil.copyfile(join(workdir, 'texput.tex'), outputTexFile)
  187. if not shutil.which('latex'):
  188. raise RuntimeError("latex program is not installed")
  189. try:
  190. _check_output_no_window(
  191. ['latex', '-halt-on-error', '-interaction=nonstopmode',
  192. 'texput.tex'],
  193. cwd=workdir,
  194. stderr=STDOUT)
  195. except CalledProcessError as e:
  196. raise RuntimeError(
  197. "'latex' exited abnormally with the following output:\n%s" %
  198. e.output)
  199. src = "texput.%s" % (output)
  200. if output != "dvi":
  201. # in order of preference
  202. commandnames = {
  203. "ps": ["dvips"],
  204. "pdf": ["dvipdfmx", "dvipdfm", "dvipdf"],
  205. "png": ["dvipng"],
  206. "svg": ["dvisvgm"],
  207. }
  208. try:
  209. cmd_variants = commandnames[output]
  210. except KeyError:
  211. raise ValueError("Invalid output format: %s" % output) from None
  212. # find an appropriate command
  213. for cmd_variant in cmd_variants:
  214. cmd_path = shutil.which(cmd_variant)
  215. if cmd_path:
  216. cmd = [cmd_path]
  217. break
  218. else:
  219. if len(cmd_variants) > 1:
  220. raise RuntimeError("None of %s are installed" % ", ".join(cmd_variants))
  221. else:
  222. raise RuntimeError("%s is not installed" % cmd_variants[0])
  223. defaultoptions = {
  224. "dvipng": ["-T", "tight", "-z", "9", "--truecolor"],
  225. "dvisvgm": ["--no-fonts"],
  226. }
  227. commandend = {
  228. "dvips": ["-o", src, "texput.dvi"],
  229. "dvipdf": ["texput.dvi", src],
  230. "dvipdfm": ["-o", src, "texput.dvi"],
  231. "dvipdfmx": ["-o", src, "texput.dvi"],
  232. "dvipng": ["-o", src, "texput.dvi"],
  233. "dvisvgm": ["-o", src, "texput.dvi"],
  234. }
  235. if dvioptions is not None:
  236. cmd.extend(dvioptions)
  237. else:
  238. cmd.extend(defaultoptions.get(cmd_variant, []))
  239. cmd.extend(commandend[cmd_variant])
  240. try:
  241. _check_output_no_window(cmd, cwd=workdir, stderr=STDOUT)
  242. except CalledProcessError as e:
  243. raise RuntimeError(
  244. "'%s' exited abnormally with the following output:\n%s" %
  245. (' '.join(cmd), e.output))
  246. if viewer == "file":
  247. shutil.move(join(workdir, src), filename)
  248. elif viewer == "BytesIO":
  249. with open(join(workdir, src), 'rb') as fh:
  250. outputbuffer.write(fh.read())
  251. elif viewer == "pyglet":
  252. try:
  253. import pyglet # noqa: F401
  254. except ImportError:
  255. raise ImportError("pyglet is required for preview.\n visit http://www.pyglet.org/")
  256. return _run_pyglet(join(workdir, src), fmt=output)
  257. else:
  258. try:
  259. _check_output_no_window(
  260. [viewer, src], cwd=workdir, stderr=STDOUT)
  261. except CalledProcessError as e:
  262. raise RuntimeError(
  263. "'%s %s' exited abnormally with the following output:\n%s" %
  264. (viewer, src, e.output))