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

1import os 

2from os.path import join 

3import shutil 

4import tempfile 

5 

6try: 

7 from subprocess import STDOUT, CalledProcessError, check_output 

8except ImportError: 

9 pass 

10 

11from sympy.utilities.decorator import doctest_depends_on 

12from sympy.utilities.misc import debug 

13from .latex import latex 

14 

15__doctest_requires__ = {('preview',): ['pyglet']} 

16 

17 

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) 

26 

27 

28def system_default_viewer(fname, fmt): 

29 """ Open fname with the default system viewer. 

30 

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) 

41 

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)) 

51 

52 

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/") 

60 

61 try: 

62 img = image.load(fname) 

63 except ImageDecodeException: 

64 raise ValueError("pyglet preview does not work for '{}' files.".format(fmt)) 

65 

66 offset = 25 

67 

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 ) 

76 

77 win.set_vsync(False) 

78 

79 try: 

80 def on_close(): 

81 win.has_exit = True 

82 

83 win.on_close = on_close 

84 

85 def on_key_press(symbol, modifiers): 

86 if symbol in [key.Q, key.ESCAPE]: 

87 on_close() 

88 

89 win.on_key_press = on_key_press 

90 

91 def on_expose(): 

92 gl.glClearColor(1.0, 1.0, 1.0, 1.0) 

93 gl.glClear(gl.GL_COLOR_BUFFER_BIT) 

94 

95 img.blit( 

96 (win.width - img.width) / 2, 

97 (win.height - img.height) / 2 

98 ) 

99 

100 win.on_expose = on_expose 

101 

102 while not win.has_exit: 

103 win.dispatch_events() 

104 win.flip() 

105 except KeyboardInterrupt: 

106 pass 

107 

108 win.close() 

109 

110 

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 

124 

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 

131 

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") 

139 

140 if isinstance(expr, str): 

141 latex_string = expr 

142 else: 

143 latex_string = ('$\\displaystyle ' + 

144 latex(expr, mode='plain', **latex_settings) + 

145 '$') 

146 

147 return preamble + '\n' + latex_string + '\n\n' + r"\end{document}" 

148 

149 

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. 

158 

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. 

164 

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. 

171 

172 To use viewer auto-detection, lets say for 'png' output, issue 

173 

174 >>> from sympy import symbols, preview, Symbol 

175 >>> x, y = symbols("x,y") 

176 

177 >>> preview(x + y, output='png') 

178 

179 This will choose 'pyglet' by default. To select a different one, do 

180 

181 >>> preview(x + y, output='png', viewer='gimp') 

182 

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 

186 

187 >>> preview(x + y, output='dvi') 

188 

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 

194 

195 >>> from sympy.printing.preview import system_default_viewer 

196 >>> preview(x + y, output='dvi', viewer=system_default_viewer) 

197 

198 If this still does not find the viewer you want, it can be set explicitly. 

199 

200 >>> preview(x + y, output='dvi', viewer='superior-dvi-viewer') 

201 

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. 

205 

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. 

210 

211 There is also support for writing to a ``io.BytesIO`` like object, which 

212 needs to be passed to the ``outputbuffer`` argument. 

213 

214 >>> from io import BytesIO 

215 >>> obj = BytesIO() 

216 >>> preview(x + y, output='png', viewer='BytesIO', 

217 ... outputbuffer=obj) 

218 

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. 

222 

223 >>> preamble = "\\documentclass[10pt]{article}\n" \ 

224 ... "\\usepackage{amsmath,amsfonts}\\begin{document}" 

225 >>> preview(x + y, output='png', preamble=preamble) 

226 

227 It is also possible to use the standard preamble and provide additional 

228 information to the preamble using the ``extra_preamble`` keyword argument. 

229 

230 >>> from sympy import sin 

231 >>> extra_preamble = "\\renewcommand{\\sin}{\\cos}" 

232 >>> preview(sin(x), output='png', extra_preamble=extra_preamble) 

233 

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``). 

238 

239 Additional keyword args will be passed to the :func:`~sympy.printing.latex.latex` call, 

240 e.g., the ``symbol_names`` flag. 

241 

242 >>> phidd = Symbol('phidd') 

243 >>> preview(phidd, symbol_names={phidd: r'\ddot{\varphi}'}) 

244 

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 

250 

251 >>> preview(x + y, outputTexFile="sample.tex") 

252 

253 

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 

263 

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 } 

273 

274 for candidate in candidates.get(output, []): 

275 path = shutil.which(candidate) 

276 if path is not None: 

277 viewer = path 

278 break 

279 

280 # otherwise, use the system default for file association 

281 if viewer is None: 

282 viewer = system_default_viewer 

283 

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) 

293 

294 latex_main = _get_latex_main(expr, preamble=preamble, packages=packages, 

295 euler=euler, extra_preamble=extra_preamble, 

296 fontsize=fontsize, **latex_settings) 

297 

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) 

303 

304 if outputTexFile is not None: 

305 shutil.copyfile(join(workdir, 'texput.tex'), outputTexFile) 

306 

307 if not shutil.which('latex'): 

308 raise RuntimeError("latex program is not installed") 

309 

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) 

320 

321 src = "texput.%s" % (output) 

322 

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 

335 

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]) 

347 

348 defaultoptions = { 

349 "dvipng": ["-T", "tight", "-z", "9", "--truecolor"], 

350 "dvisvgm": ["--no-fonts"], 

351 } 

352 

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 } 

361 

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]) 

367 

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)) 

374 

375 

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))