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