##// END OF EJS Templates
Replace usage of os.devnull with subprocess.DEVNULL
Yann Pellegrini -
Show More
@@ -1,258 +1,257 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Tools for handling LaTeX."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from io import BytesIO, open
8 8 import os
9 9 import tempfile
10 10 import shutil
11 11 import subprocess
12 12 from base64 import encodebytes
13 13 import textwrap
14 14
15 15 from pathlib import Path
16 16
17 17 from IPython.utils.process import find_cmd, FindCmdError
18 18 from traitlets.config import get_config
19 19 from traitlets.config.configurable import SingletonConfigurable
20 20 from traitlets import List, Bool, Unicode
21 21 from IPython.utils.py3compat import cast_unicode
22 22
23 23
24 24 class LaTeXTool(SingletonConfigurable):
25 25 """An object to store configuration of the LaTeX tool."""
26 26 def _config_default(self):
27 27 return get_config()
28 28
29 29 backends = List(
30 30 Unicode(), ["matplotlib", "dvipng"],
31 31 help="Preferred backend to draw LaTeX math equations. "
32 32 "Backends in the list are checked one by one and the first "
33 33 "usable one is used. Note that `matplotlib` backend "
34 34 "is usable only for inline style equations. To draw "
35 35 "display style equations, `dvipng` backend must be specified. ",
36 36 # It is a List instead of Enum, to make configuration more
37 37 # flexible. For example, to use matplotlib mainly but dvipng
38 38 # for display style, the default ["matplotlib", "dvipng"] can
39 39 # be used. To NOT use dvipng so that other repr such as
40 40 # unicode pretty printing is used, you can use ["matplotlib"].
41 41 ).tag(config=True)
42 42
43 43 use_breqn = Bool(
44 44 True,
45 45 help="Use breqn.sty to automatically break long equations. "
46 46 "This configuration takes effect only for dvipng backend.",
47 47 ).tag(config=True)
48 48
49 49 packages = List(
50 50 ['amsmath', 'amsthm', 'amssymb', 'bm'],
51 51 help="A list of packages to use for dvipng backend. "
52 52 "'breqn' will be automatically appended when use_breqn=True.",
53 53 ).tag(config=True)
54 54
55 55 preamble = Unicode(
56 56 help="Additional preamble to use when generating LaTeX source "
57 57 "for dvipng backend.",
58 58 ).tag(config=True)
59 59
60 60
61 61 def latex_to_png(s, encode=False, backend=None, wrap=False, color='Black',
62 62 scale=1.0):
63 63 """Render a LaTeX string to PNG.
64 64
65 65 Parameters
66 66 ----------
67 67 s : str
68 68 The raw string containing valid inline LaTeX.
69 69 encode : bool, optional
70 70 Should the PNG data base64 encoded to make it JSON'able.
71 71 backend : {matplotlib, dvipng}
72 72 Backend for producing PNG data.
73 73 wrap : bool
74 74 If true, Automatically wrap `s` as a LaTeX equation.
75 75 color : string
76 76 Foreground color name among dvipsnames, e.g. 'Maroon' or on hex RGB
77 77 format, e.g. '#AA20FA'.
78 78 scale : float
79 79 Scale factor for the resulting PNG.
80 80 None is returned when the backend cannot be used.
81 81
82 82 """
83 83 s = cast_unicode(s)
84 84 allowed_backends = LaTeXTool.instance().backends
85 85 if backend is None:
86 86 backend = allowed_backends[0]
87 87 if backend not in allowed_backends:
88 88 return None
89 89 if backend == 'matplotlib':
90 90 f = latex_to_png_mpl
91 91 elif backend == 'dvipng':
92 92 f = latex_to_png_dvipng
93 93 if color.startswith('#'):
94 94 # Convert hex RGB color to LaTeX RGB color.
95 95 if len(color) == 7:
96 96 try:
97 97 color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
98 98 textwrap.wrap(color[1:], 2)]))
99 99 except ValueError as e:
100 100 raise ValueError('Invalid color specification {}.'.format(color)) from e
101 101 else:
102 102 raise ValueError('Invalid color specification {}.'.format(color))
103 103 else:
104 104 raise ValueError('No such backend {0}'.format(backend))
105 105 bin_data = f(s, wrap, color, scale)
106 106 if encode and bin_data:
107 107 bin_data = encodebytes(bin_data)
108 108 return bin_data
109 109
110 110
111 111 def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
112 112 try:
113 113 from matplotlib import figure, font_manager, mathtext
114 114 from matplotlib.backends import backend_agg
115 115 from pyparsing import ParseFatalException
116 116 except ImportError:
117 117 return None
118 118
119 119 # mpl mathtext doesn't support display math, force inline
120 120 s = s.replace('$$', '$')
121 121 if wrap:
122 122 s = u'${0}$'.format(s)
123 123
124 124 try:
125 125 prop = font_manager.FontProperties(size=12)
126 126 dpi = 120 * scale
127 127 buffer = BytesIO()
128 128
129 129 # Adapted from mathtext.math_to_image
130 130 parser = mathtext.MathTextParser("path")
131 131 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
132 132 fig = figure.Figure(figsize=(width / 72, height / 72))
133 133 fig.text(0, depth / height, s, fontproperties=prop, color=color)
134 134 backend_agg.FigureCanvasAgg(fig)
135 135 fig.savefig(buffer, dpi=dpi, format="png", transparent=True)
136 136 return buffer.getvalue()
137 137 except (ValueError, RuntimeError, ParseFatalException):
138 138 return None
139 139
140 140
141 141 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
142 142 try:
143 143 find_cmd('latex')
144 144 find_cmd('dvipng')
145 145 except FindCmdError:
146 146 return None
147 147
148 148 startupinfo = None
149 149 if os.name == "nt":
150 150 # prevent popup-windows
151 151 startupinfo = subprocess.STARTUPINFO()
152 152 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
153 153
154 154 try:
155 155 workdir = Path(tempfile.mkdtemp())
156 156 tmpfile = "tmp.tex"
157 157 dvifile = "tmp.dvi"
158 158 outfile = "tmp.png"
159 159
160 160 with workdir.joinpath(tmpfile).open("w", encoding="utf8") as f:
161 161 f.writelines(genelatex(s, wrap))
162 162
163 with open(os.devnull, 'wb') as devnull:
164 subprocess.check_call(
165 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
166 cwd=workdir,
167 stdout=devnull,
168 stderr=devnull,
169 startupinfo=startupinfo,
170 )
171
172 resolution = round(150*scale)
173 subprocess.check_call(
174 [
175 "dvipng",
176 "-T",
177 "tight",
178 "-D",
179 str(resolution),
180 "-z",
181 "9",
182 "-bg",
183 "Transparent",
184 "-o",
185 outfile,
186 dvifile,
187 "-fg",
188 color,
189 ],
190 cwd=workdir,
191 stdout=devnull,
192 stderr=devnull,
193 startupinfo=startupinfo,
194 )
163 subprocess.check_call(
164 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
165 cwd=workdir,
166 stdout=subprocess.DEVNULL,
167 stderr=subprocess.DEVNULL,
168 startupinfo=startupinfo,
169 )
170
171 resolution = round(150 * scale)
172 subprocess.check_call(
173 [
174 "dvipng",
175 "-T",
176 "tight",
177 "-D",
178 str(resolution),
179 "-z",
180 "9",
181 "-bg",
182 "Transparent",
183 "-o",
184 outfile,
185 dvifile,
186 "-fg",
187 color,
188 ],
189 cwd=workdir,
190 stdout=subprocess.DEVNULL,
191 stderr=subprocess.DEVNULL,
192 startupinfo=startupinfo,
193 )
195 194
196 195 with workdir.joinpath(outfile).open("rb") as f:
197 196 return f.read()
198 197 except subprocess.CalledProcessError:
199 198 return None
200 199 finally:
201 200 shutil.rmtree(workdir)
202 201
203 202
204 203 def kpsewhich(filename):
205 204 """Invoke kpsewhich command with an argument `filename`."""
206 205 try:
207 206 find_cmd("kpsewhich")
208 207 proc = subprocess.Popen(
209 208 ["kpsewhich", filename],
210 209 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
211 210 (stdout, stderr) = proc.communicate()
212 211 return stdout.strip().decode('utf8', 'replace')
213 212 except FindCmdError:
214 213 pass
215 214
216 215
217 216 def genelatex(body, wrap):
218 217 """Generate LaTeX document for dvipng backend."""
219 218 lt = LaTeXTool.instance()
220 219 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
221 220 yield r'\documentclass{article}'
222 221 packages = lt.packages
223 222 if breqn:
224 223 packages = packages + ['breqn']
225 224 for pack in packages:
226 225 yield r'\usepackage{{{0}}}'.format(pack)
227 226 yield r'\pagestyle{empty}'
228 227 if lt.preamble:
229 228 yield lt.preamble
230 229 yield r'\begin{document}'
231 230 if breqn:
232 231 yield r'\begin{dmath*}'
233 232 yield body
234 233 yield r'\end{dmath*}'
235 234 elif wrap:
236 235 yield u'$${0}$$'.format(body)
237 236 else:
238 237 yield body
239 238 yield u'\\end{document}'
240 239
241 240
242 241 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
243 242
244 243 def latex_to_html(s, alt='image'):
245 244 """Render LaTeX to HTML with embedded PNG data using data URIs.
246 245
247 246 Parameters
248 247 ----------
249 248 s : str
250 249 The raw string containing valid inline LateX.
251 250 alt : str
252 251 The alt text to use for the HTML.
253 252 """
254 253 base64_data = latex_to_png(s, encode=True).decode('ascii')
255 254 if base64_data:
256 255 return _data_uri_template_png % (base64_data, alt)
257 256
258 257
General Comments 0
You need to be logged in to leave comments. Login now