##// END OF EJS Templates
latex_to_png: avoid deprecated matplotlib functions
Jake VanderPlas -
Show More
@@ -1,222 +1,230 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, PurePath
15 from pathlib import Path, PurePath
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
80
81 None is returned when the backend cannot be used.
81 None is returned when the backend cannot be used.
82
82
83 """
83 """
84 s = cast_unicode(s)
84 s = cast_unicode(s)
85 allowed_backends = LaTeXTool.instance().backends
85 allowed_backends = LaTeXTool.instance().backends
86 if backend is None:
86 if backend is None:
87 backend = allowed_backends[0]
87 backend = allowed_backends[0]
88 if backend not in allowed_backends:
88 if backend not in allowed_backends:
89 return None
89 return None
90 if backend == 'matplotlib':
90 if backend == 'matplotlib':
91 f = latex_to_png_mpl
91 f = latex_to_png_mpl
92 elif backend == 'dvipng':
92 elif backend == 'dvipng':
93 f = latex_to_png_dvipng
93 f = latex_to_png_dvipng
94 if color.startswith('#'):
94 if color.startswith('#'):
95 # Convert hex RGB color to LaTeX RGB color.
95 # Convert hex RGB color to LaTeX RGB color.
96 if len(color) == 7:
96 if len(color) == 7:
97 try:
97 try:
98 color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
98 color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
99 textwrap.wrap(color[1:], 2)]))
99 textwrap.wrap(color[1:], 2)]))
100 except ValueError as e:
100 except ValueError as e:
101 raise ValueError('Invalid color specification {}.'.format(color)) from e
101 raise ValueError('Invalid color specification {}.'.format(color)) from e
102 else:
102 else:
103 raise ValueError('Invalid color specification {}.'.format(color))
103 raise ValueError('Invalid color specification {}.'.format(color))
104 else:
104 else:
105 raise ValueError('No such backend {0}'.format(backend))
105 raise ValueError('No such backend {0}'.format(backend))
106 bin_data = f(s, wrap, color, scale)
106 bin_data = f(s, wrap, color, scale)
107 if encode and bin_data:
107 if encode and bin_data:
108 bin_data = encodebytes(bin_data)
108 bin_data = encodebytes(bin_data)
109 return bin_data
109 return bin_data
110
110
111
111
112 def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
112 def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
113 try:
113 try:
114 from matplotlib import mathtext
114 from matplotlib import figure, font_manager, mathtext
115 from matplotlib.backends import backend_agg
115 from pyparsing import ParseFatalException
116 from pyparsing import ParseFatalException
116 except ImportError:
117 except ImportError:
117 return None
118 return None
118
119
119 # mpl mathtext doesn't support display math, force inline
120 # mpl mathtext doesn't support display math, force inline
120 s = s.replace('$$', '$')
121 s = s.replace('$$', '$')
121 if wrap:
122 if wrap:
122 s = u'${0}$'.format(s)
123 s = u'${0}$'.format(s)
123
124
124 try:
125 try:
125 mt = mathtext.MathTextParser('bitmap')
126 prop = font_manager.FontProperties(size=12)
126 f = BytesIO()
127 dpi = 120 * scale
127 dpi = 120*scale
128 buffer = BytesIO()
128 mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
129
129 return f.getvalue()
130 # Adapted from mathtext.math_to_image
131 parser = mathtext.MathTextParser("path")
132 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
133 fig = figure.Figure(figsize=(width / 72, height / 72))
134 fig.text(0, depth / height, s, fontproperties=prop, color=color)
135 backend_agg.FigureCanvasAgg(fig)
136 fig.savefig(buffer, dpi=dpi, format="png", transparent=True)
137 return buffer.getvalue()
130 except (ValueError, RuntimeError, ParseFatalException):
138 except (ValueError, RuntimeError, ParseFatalException):
131 return None
139 return None
132
140
133
141
134 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
142 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
135 try:
143 try:
136 find_cmd('latex')
144 find_cmd('latex')
137 find_cmd('dvipng')
145 find_cmd('dvipng')
138 except FindCmdError:
146 except FindCmdError:
139 return None
147 return None
140 try:
148 try:
141 workdir = Path(tempfile.mkdtemp())
149 workdir = Path(tempfile.mkdtemp())
142 tmpfile = workdir.joinpath("tmp.tex")
150 tmpfile = workdir.joinpath("tmp.tex")
143 dvifile = workdir.joinpath("tmp.dvi")
151 dvifile = workdir.joinpath("tmp.dvi")
144 outfile = workdir.joinpath("tmp.png")
152 outfile = workdir.joinpath("tmp.png")
145
153
146 with tmpfile.open("w", encoding="utf8") as f:
154 with tmpfile.open("w", encoding="utf8") as f:
147 f.writelines(genelatex(s, wrap))
155 f.writelines(genelatex(s, wrap))
148
156
149 with open(os.devnull, 'wb') as devnull:
157 with open(os.devnull, 'wb') as devnull:
150 subprocess.check_call(
158 subprocess.check_call(
151 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
159 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
152 cwd=workdir, stdout=devnull, stderr=devnull)
160 cwd=workdir, stdout=devnull, stderr=devnull)
153
161
154 resolution = round(150*scale)
162 resolution = round(150*scale)
155 subprocess.check_call(
163 subprocess.check_call(
156 ["dvipng", "-T", "tight", "-D", str(resolution), "-z", "9",
164 ["dvipng", "-T", "tight", "-D", str(resolution), "-z", "9",
157 "-bg", "transparent", "-o", outfile, dvifile, "-fg", color],
165 "-bg", "transparent", "-o", outfile, dvifile, "-fg", color],
158 cwd=workdir, stdout=devnull, stderr=devnull)
166 cwd=workdir, stdout=devnull, stderr=devnull)
159
167
160 with outfile.open("rb") as f:
168 with outfile.open("rb") as f:
161 return f.read()
169 return f.read()
162 except subprocess.CalledProcessError:
170 except subprocess.CalledProcessError:
163 return None
171 return None
164 finally:
172 finally:
165 shutil.rmtree(workdir)
173 shutil.rmtree(workdir)
166
174
167
175
168 def kpsewhich(filename):
176 def kpsewhich(filename):
169 """Invoke kpsewhich command with an argument `filename`."""
177 """Invoke kpsewhich command with an argument `filename`."""
170 try:
178 try:
171 find_cmd("kpsewhich")
179 find_cmd("kpsewhich")
172 proc = subprocess.Popen(
180 proc = subprocess.Popen(
173 ["kpsewhich", filename],
181 ["kpsewhich", filename],
174 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
182 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
175 (stdout, stderr) = proc.communicate()
183 (stdout, stderr) = proc.communicate()
176 return stdout.strip().decode('utf8', 'replace')
184 return stdout.strip().decode('utf8', 'replace')
177 except FindCmdError:
185 except FindCmdError:
178 pass
186 pass
179
187
180
188
181 def genelatex(body, wrap):
189 def genelatex(body, wrap):
182 """Generate LaTeX document for dvipng backend."""
190 """Generate LaTeX document for dvipng backend."""
183 lt = LaTeXTool.instance()
191 lt = LaTeXTool.instance()
184 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
192 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
185 yield r'\documentclass{article}'
193 yield r'\documentclass{article}'
186 packages = lt.packages
194 packages = lt.packages
187 if breqn:
195 if breqn:
188 packages = packages + ['breqn']
196 packages = packages + ['breqn']
189 for pack in packages:
197 for pack in packages:
190 yield r'\usepackage{{{0}}}'.format(pack)
198 yield r'\usepackage{{{0}}}'.format(pack)
191 yield r'\pagestyle{empty}'
199 yield r'\pagestyle{empty}'
192 if lt.preamble:
200 if lt.preamble:
193 yield lt.preamble
201 yield lt.preamble
194 yield r'\begin{document}'
202 yield r'\begin{document}'
195 if breqn:
203 if breqn:
196 yield r'\begin{dmath*}'
204 yield r'\begin{dmath*}'
197 yield body
205 yield body
198 yield r'\end{dmath*}'
206 yield r'\end{dmath*}'
199 elif wrap:
207 elif wrap:
200 yield u'$${0}$$'.format(body)
208 yield u'$${0}$$'.format(body)
201 else:
209 else:
202 yield body
210 yield body
203 yield u'\\end{document}'
211 yield u'\\end{document}'
204
212
205
213
206 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
214 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
207
215
208 def latex_to_html(s, alt='image'):
216 def latex_to_html(s, alt='image'):
209 """Render LaTeX to HTML with embedded PNG data using data URIs.
217 """Render LaTeX to HTML with embedded PNG data using data URIs.
210
218
211 Parameters
219 Parameters
212 ----------
220 ----------
213 s : str
221 s : str
214 The raw string containing valid inline LateX.
222 The raw string containing valid inline LateX.
215 alt : str
223 alt : str
216 The alt text to use for the HTML.
224 The alt text to use for the HTML.
217 """
225 """
218 base64_data = latex_to_png(s, encode=True).decode('ascii')
226 base64_data = latex_to_png(s, encode=True).decode('ascii')
219 if base64_data:
227 if base64_data:
220 return _data_uri_template_png % (base64_data, alt)
228 return _data_uri_template_png % (base64_data, alt)
221
229
222
230
General Comments 0
You need to be logged in to leave comments. Login now