##// END OF EJS Templates
Backport PR #9203: Latextools: Make latex_to_png_mpl not fail on errors...
Min RK -
Show More
@@ -1,199 +1,205
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
12
13 from IPython.utils.process import find_cmd, FindCmdError
13 from IPython.utils.process import find_cmd, FindCmdError
14 from traitlets.config import get_config
14 from traitlets.config import get_config
15 from traitlets.config.configurable import SingletonConfigurable
15 from traitlets.config.configurable import SingletonConfigurable
16 from traitlets import List, Bool, Unicode
16 from traitlets import List, Bool, Unicode
17 from IPython.utils.py3compat import cast_unicode, cast_unicode_py2 as u, PY3
17 from IPython.utils.py3compat import cast_unicode, cast_unicode_py2 as u, PY3
18
18
19 try: # Py3
19 try: # Py3
20 from base64 import encodebytes
20 from base64 import encodebytes
21 except ImportError: # Py2
21 except ImportError: # Py2
22 from base64 import encodestring as encodebytes
22 from base64 import encodestring as encodebytes
23
23
24
24
25 class LaTeXTool(SingletonConfigurable):
25 class LaTeXTool(SingletonConfigurable):
26 """An object to store configuration of the LaTeX tool."""
26 """An object to store configuration of the LaTeX tool."""
27 def _config_default(self):
27 def _config_default(self):
28 return get_config()
28 return get_config()
29
29
30 backends = List(
30 backends = List(
31 Unicode(), ["matplotlib", "dvipng"],
31 Unicode(), ["matplotlib", "dvipng"],
32 help="Preferred backend to draw LaTeX math equations. "
32 help="Preferred backend to draw LaTeX math equations. "
33 "Backends in the list are checked one by one and the first "
33 "Backends in the list are checked one by one and the first "
34 "usable one is used. Note that `matplotlib` backend "
34 "usable one is used. Note that `matplotlib` backend "
35 "is usable only for inline style equations. To draw "
35 "is usable only for inline style equations. To draw "
36 "display style equations, `dvipng` backend must be specified. ",
36 "display style equations, `dvipng` backend must be specified. ",
37 # It is a List instead of Enum, to make configuration more
37 # It is a List instead of Enum, to make configuration more
38 # flexible. For example, to use matplotlib mainly but dvipng
38 # flexible. For example, to use matplotlib mainly but dvipng
39 # for display style, the default ["matplotlib", "dvipng"] can
39 # for display style, the default ["matplotlib", "dvipng"] can
40 # be used. To NOT use dvipng so that other repr such as
40 # be used. To NOT use dvipng so that other repr such as
41 # unicode pretty printing is used, you can use ["matplotlib"].
41 # unicode pretty printing is used, you can use ["matplotlib"].
42 config=True)
42 config=True)
43
43
44 use_breqn = Bool(
44 use_breqn = Bool(
45 True,
45 True,
46 help="Use breqn.sty to automatically break long equations. "
46 help="Use breqn.sty to automatically break long equations. "
47 "This configuration takes effect only for dvipng backend.",
47 "This configuration takes effect only for dvipng backend.",
48 config=True)
48 config=True)
49
49
50 packages = List(
50 packages = List(
51 ['amsmath', 'amsthm', 'amssymb', 'bm'],
51 ['amsmath', 'amsthm', 'amssymb', 'bm'],
52 help="A list of packages to use for dvipng backend. "
52 help="A list of packages to use for dvipng backend. "
53 "'breqn' will be automatically appended when use_breqn=True.",
53 "'breqn' will be automatically appended when use_breqn=True.",
54 config=True)
54 config=True)
55
55
56 preamble = Unicode(
56 preamble = Unicode(
57 help="Additional preamble to use when generating LaTeX source "
57 help="Additional preamble to use when generating LaTeX source "
58 "for dvipng backend.",
58 "for dvipng backend.",
59 config=True)
59 config=True)
60
60
61
61
62 def latex_to_png(s, encode=False, backend=None, wrap=False):
62 def latex_to_png(s, encode=False, backend=None, wrap=False):
63 """Render a LaTeX string to PNG.
63 """Render a LaTeX string to PNG.
64
64
65 Parameters
65 Parameters
66 ----------
66 ----------
67 s : text
67 s : text
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
75
76 None is returned when the backend cannot be used.
76 None is returned when the backend cannot be used.
77
77
78 """
78 """
79 s = cast_unicode(s)
79 s = cast_unicode(s)
80 allowed_backends = LaTeXTool.instance().backends
80 allowed_backends = LaTeXTool.instance().backends
81 if backend is None:
81 if backend is None:
82 backend = allowed_backends[0]
82 backend = allowed_backends[0]
83 if backend not in allowed_backends:
83 if backend not in allowed_backends:
84 return None
84 return None
85 if backend == 'matplotlib':
85 if backend == 'matplotlib':
86 f = latex_to_png_mpl
86 f = latex_to_png_mpl
87 elif backend == 'dvipng':
87 elif backend == 'dvipng':
88 f = latex_to_png_dvipng
88 f = latex_to_png_dvipng
89 else:
89 else:
90 raise ValueError('No such backend {0}'.format(backend))
90 raise ValueError('No such backend {0}'.format(backend))
91 bin_data = f(s, wrap)
91 bin_data = f(s, wrap)
92 if encode and bin_data:
92 if encode and bin_data:
93 bin_data = encodebytes(bin_data)
93 bin_data = encodebytes(bin_data)
94 return bin_data
94 return bin_data
95
95
96
96
97 def latex_to_png_mpl(s, wrap):
97 def latex_to_png_mpl(s, wrap):
98 try:
98 try:
99 from matplotlib import mathtext
99 from matplotlib import mathtext
100 from pyparsing import ParseFatalException
100 except ImportError:
101 except ImportError:
101 return None
102 return None
102
103
103 # mpl mathtext doesn't support display math, force inline
104 # mpl mathtext doesn't support display math, force inline
104 s = s.replace('$$', '$')
105 s = s.replace('$$', '$')
105 if wrap:
106 if wrap:
106 s = u'${0}$'.format(s)
107 s = u'${0}$'.format(s)
107
108
109 try:
108 mt = mathtext.MathTextParser('bitmap')
110 mt = mathtext.MathTextParser('bitmap')
109 f = BytesIO()
111 f = BytesIO()
110 mt.to_png(f, s, fontsize=12)
112 mt.to_png(f, s, fontsize=12)
111 return f.getvalue()
113 return f.getvalue()
114 except (ValueError, RuntimeError, ParseFatalException):
115 return None
112
116
113
117
114 def latex_to_png_dvipng(s, wrap):
118 def latex_to_png_dvipng(s, wrap):
115 try:
119 try:
116 find_cmd('latex')
120 find_cmd('latex')
117 find_cmd('dvipng')
121 find_cmd('dvipng')
118 except FindCmdError:
122 except FindCmdError:
119 return None
123 return None
120 try:
124 try:
121 workdir = tempfile.mkdtemp()
125 workdir = tempfile.mkdtemp()
122 tmpfile = os.path.join(workdir, "tmp.tex")
126 tmpfile = os.path.join(workdir, "tmp.tex")
123 dvifile = os.path.join(workdir, "tmp.dvi")
127 dvifile = os.path.join(workdir, "tmp.dvi")
124 outfile = os.path.join(workdir, "tmp.png")
128 outfile = os.path.join(workdir, "tmp.png")
125
129
126 with open(tmpfile, "w", encoding='utf8') as f:
130 with open(tmpfile, "w", encoding='utf8') as f:
127 f.writelines(genelatex(s, wrap))
131 f.writelines(genelatex(s, wrap))
128
132
129 with open(os.devnull, 'wb') as devnull:
133 with open(os.devnull, 'wb') as devnull:
130 subprocess.check_call(
134 subprocess.check_call(
131 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
135 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
132 cwd=workdir, stdout=devnull, stderr=devnull)
136 cwd=workdir, stdout=devnull, stderr=devnull)
133
137
134 subprocess.check_call(
138 subprocess.check_call(
135 ["dvipng", "-T", "tight", "-x", "1500", "-z", "9",
139 ["dvipng", "-T", "tight", "-x", "1500", "-z", "9",
136 "-bg", "transparent", "-o", outfile, dvifile], cwd=workdir,
140 "-bg", "transparent", "-o", outfile, dvifile], cwd=workdir,
137 stdout=devnull, stderr=devnull)
141 stdout=devnull, stderr=devnull)
138
142
139 with open(outfile, "rb") as f:
143 with open(outfile, "rb") as f:
140 return f.read()
144 return f.read()
145 except subprocess.CalledProcessError:
146 return None
141 finally:
147 finally:
142 shutil.rmtree(workdir)
148 shutil.rmtree(workdir)
143
149
144
150
145 def kpsewhich(filename):
151 def kpsewhich(filename):
146 """Invoke kpsewhich command with an argument `filename`."""
152 """Invoke kpsewhich command with an argument `filename`."""
147 try:
153 try:
148 find_cmd("kpsewhich")
154 find_cmd("kpsewhich")
149 proc = subprocess.Popen(
155 proc = subprocess.Popen(
150 ["kpsewhich", filename],
156 ["kpsewhich", filename],
151 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
157 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
152 (stdout, stderr) = proc.communicate()
158 (stdout, stderr) = proc.communicate()
153 return stdout.strip().decode('utf8', 'replace')
159 return stdout.strip().decode('utf8', 'replace')
154 except FindCmdError:
160 except FindCmdError:
155 pass
161 pass
156
162
157
163
158 def genelatex(body, wrap):
164 def genelatex(body, wrap):
159 """Generate LaTeX document for dvipng backend."""
165 """Generate LaTeX document for dvipng backend."""
160 lt = LaTeXTool.instance()
166 lt = LaTeXTool.instance()
161 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
167 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
162 yield u(r'\documentclass{article}')
168 yield u(r'\documentclass{article}')
163 packages = lt.packages
169 packages = lt.packages
164 if breqn:
170 if breqn:
165 packages = packages + ['breqn']
171 packages = packages + ['breqn']
166 for pack in packages:
172 for pack in packages:
167 yield u(r'\usepackage{{{0}}}'.format(pack))
173 yield u(r'\usepackage{{{0}}}'.format(pack))
168 yield u(r'\pagestyle{empty}')
174 yield u(r'\pagestyle{empty}')
169 if lt.preamble:
175 if lt.preamble:
170 yield lt.preamble
176 yield lt.preamble
171 yield u(r'\begin{document}')
177 yield u(r'\begin{document}')
172 if breqn:
178 if breqn:
173 yield u(r'\begin{dmath*}')
179 yield u(r'\begin{dmath*}')
174 yield body
180 yield body
175 yield u(r'\end{dmath*}')
181 yield u(r'\end{dmath*}')
176 elif wrap:
182 elif wrap:
177 yield u'$${0}$$'.format(body)
183 yield u'$${0}$$'.format(body)
178 else:
184 else:
179 yield body
185 yield body
180 yield u'\end{document}'
186 yield u'\end{document}'
181
187
182
188
183 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
189 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
184
190
185 def latex_to_html(s, alt='image'):
191 def latex_to_html(s, alt='image'):
186 """Render LaTeX to HTML with embedded PNG data using data URIs.
192 """Render LaTeX to HTML with embedded PNG data using data URIs.
187
193
188 Parameters
194 Parameters
189 ----------
195 ----------
190 s : str
196 s : str
191 The raw string containing valid inline LateX.
197 The raw string containing valid inline LateX.
192 alt : str
198 alt : str
193 The alt text to use for the HTML.
199 The alt text to use for the HTML.
194 """
200 """
195 base64_data = latex_to_png(s, encode=True).decode('ascii')
201 base64_data = latex_to_png(s, encode=True).decode('ascii')
196 if base64_data:
202 if base64_data:
197 return _data_uri_template_png % (base64_data, alt)
203 return _data_uri_template_png % (base64_data, alt)
198
204
199
205
General Comments 0
You need to be logged in to leave comments. Login now