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