##// END OF EJS Templates
Fix IPython.lib.latextools for Python 3
Thomas Kluyver -
Show More
@@ -1,250 +1,251 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Tools for handling LaTeX.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2010 IPython Development Team.
10 10 #
11 11 # Distributed under the terms of the Modified BSD License.
12 12 #
13 13 # The full license is in the file COPYING.txt, distributed with this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 from StringIO import StringIO
20 from io import BytesIO
21 21 from base64 import encodestring
22 22 import os
23 23 import tempfile
24 24 import shutil
25 25 import subprocess
26 26
27 27 from IPython.utils.process import find_cmd, FindCmdError
28 28 from IPython.config.configurable import SingletonConfigurable
29 29 from IPython.utils.traitlets import Instance, List, CBool, CUnicode
30 from IPython.utils.py3compat import bytes_to_str
30 31
31 32 #-----------------------------------------------------------------------------
32 33 # Tools
33 34 #-----------------------------------------------------------------------------
34 35
35 36
36 37 class LaTeXTool(SingletonConfigurable):
37 38 """An object to store configuration of the LaTeX tool."""
38 39
39 40 backends = List(
40 41 CUnicode, ["matplotlib", "dvipng"],
41 42 help="Preferred backend to draw LaTeX math equations. "
42 43 "Backends in the list are checked one by one and the first "
43 44 "usable one is used. Note that `matplotlib` backend "
44 45 "is usable only for inline style equations. To draw "
45 46 "display style equations, `dvipng` backend must be specified. ",
46 47 # It is a List instead of Enum, to make configuration more
47 48 # flexible. For example, to use matplotlib mainly but dvipng
48 49 # for display style, the default ["matplotlib", "dvipng"] can
49 50 # be used. To NOT use dvipng so that other repr such as
50 51 # unicode pretty printing is used, you can use ["matplotlib"].
51 52 config=True)
52 53
53 54 use_breqn = CBool(
54 55 True,
55 56 help="Use breqn.sty to automatically break long equations. "
56 57 "This configuration takes effect only for dvipng backend.",
57 58 config=True)
58 59
59 60 packages = List(
60 61 ['amsmath', 'amsthm', 'amssymb', 'bm'],
61 62 help="A list of packages to use for dvipng backend. "
62 63 "'breqn' will be automatically appended when use_breqn=True.",
63 64 config=True)
64 65
65 66 preamble = CUnicode(
66 67 help="Additional preamble to use when generating LaTeX source "
67 68 "for dvipng backend.",
68 69 config=True)
69 70
70 71
71 72 def latex_to_png(s, encode=False, backend=None, wrap=False):
72 73 """Render a LaTeX string to PNG.
73 74
74 75 Parameters
75 76 ----------
76 77 s : str
77 78 The raw string containing valid inline LaTeX.
78 79 encode : bool, optional
79 80 Should the PNG data bebase64 encoded to make it JSON'able.
80 81 backend : {matplotlib, dvipng}
81 82 Backend for producing PNG data.
82 83 wrap : bool
83 84 If true, Automatically wrap `s` as a LaTeX equation.
84 85
85 86 None is returned when the backend cannot be used.
86 87
87 88 """
88 89 allowed_backends = LaTeXTool.instance().backends
89 90 if backend is None:
90 91 backend = allowed_backends[0]
91 92 if backend not in allowed_backends:
92 93 return None
93 94 if backend == 'matplotlib':
94 95 f = latex_to_png_mpl
95 96 elif backend == 'dvipng':
96 97 f = latex_to_png_dvipng
97 98 else:
98 99 raise ValueError('No such backend {0}'.format(backend))
99 100 bin_data = f(s, wrap)
100 101 if encode and bin_data:
101 102 bin_data = encodestring(bin_data)
102 103 return bin_data
103 104
104 105
105 106 def latex_to_png_mpl(s, wrap):
106 107 try:
107 108 from matplotlib import mathtext
108 109 except ImportError:
109 110 return None
110 111
111 112 if wrap:
112 113 s = '${0}$'.format(s)
113 114 mt = mathtext.MathTextParser('bitmap')
114 f = StringIO()
115 f = BytesIO()
115 116 mt.to_png(f, s, fontsize=12)
116 117 return f.getvalue()
117 118
118 119
119 120 def latex_to_png_dvipng(s, wrap):
120 121 try:
121 122 find_cmd('latex')
122 123 find_cmd('dvipng')
123 124 except FindCmdError:
124 125 return None
125 126 try:
126 127 workdir = tempfile.mkdtemp()
127 128 tmpfile = os.path.join(workdir, "tmp.tex")
128 129 dvifile = os.path.join(workdir, "tmp.dvi")
129 130 outfile = os.path.join(workdir, "tmp.png")
130 131
131 132 with open(tmpfile, "w") as f:
132 133 f.writelines(genelatex(s, wrap))
133 134
134 135 subprocess.check_call(
135 136 ["latex", "-halt-on-errror", tmpfile], cwd=workdir,
136 137 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
137 138
138 139 subprocess.check_call(
139 140 ["dvipng", "-T", "tight", "-x", "1500", "-z", "9",
140 141 "-bg", "transparent", "-o", outfile, dvifile], cwd=workdir,
141 142 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
142 143
143 with open(outfile) as f:
144 with open(outfile, "rb") as f:
144 145 bin_data = f.read()
145 146 finally:
146 147 shutil.rmtree(workdir)
147 148 return bin_data
148 149
149 150
150 151 def kpsewhich(filename):
151 152 """Invoke kpsewhich command with an argument `filename`."""
152 153 try:
153 154 find_cmd("kpsewhich")
154 155 proc = subprocess.Popen(
155 156 ["kpsewhich", filename],
156 157 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
157 158 (stdout, stderr) = proc.communicate()
158 159 return stdout.strip()
159 160 except FindCmdError:
160 161 pass
161 162
162 163
163 164 def genelatex(body, wrap):
164 165 """Generate LaTeX document for dvipng backend."""
165 166 lt = LaTeXTool.instance()
166 167 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
167 168 yield r'\documentclass{article}'
168 169 packages = lt.packages
169 170 if breqn:
170 171 packages = packages + ['breqn']
171 172 for pack in packages:
172 173 yield r'\usepackage{{{0}}}'.format(pack)
173 174 yield r'\pagestyle{empty}'
174 175 if lt.preamble:
175 176 yield lt.preamble
176 177 yield r'\begin{document}'
177 178 if breqn:
178 179 yield r'\begin{dmath*}'
179 180 yield body
180 181 yield r'\end{dmath*}'
181 182 elif wrap:
182 183 yield '$${0}$$'.format(body)
183 184 else:
184 185 yield body
185 186 yield r'\end{document}'
186 187
187 188
188 189 _data_uri_template_png = """<img src="data:image/png;base64,%s" alt=%s />"""
189 190
190 191 def latex_to_html(s, alt='image'):
191 192 """Render LaTeX to HTML with embedded PNG data using data URIs.
192 193
193 194 Parameters
194 195 ----------
195 196 s : str
196 197 The raw string containing valid inline LateX.
197 198 alt : str
198 199 The alt text to use for the HTML.
199 200 """
200 base64_data = latex_to_png(s, encode=True)
201 base64_data = bytes_to_str(latex_to_png(s, encode=True), 'ascii')
201 202 if base64_data:
202 203 return _data_uri_template_png % (base64_data, alt)
203 204
204 205
205 206 # From matplotlib, thanks to mdboom. Once this is in matplotlib releases, we
206 207 # will remove.
207 208 def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None):
208 209 """
209 210 Given a math expression, renders it in a closely-clipped bounding
210 211 box to an image file.
211 212
212 213 *s*
213 214 A math expression. The math portion should be enclosed in
214 215 dollar signs.
215 216
216 217 *filename_or_obj*
217 218 A filepath or writable file-like object to write the image data
218 219 to.
219 220
220 221 *prop*
221 222 If provided, a FontProperties() object describing the size and
222 223 style of the text.
223 224
224 225 *dpi*
225 226 Override the output dpi, otherwise use the default associated
226 227 with the output format.
227 228
228 229 *format*
229 230 The output format, eg. 'svg', 'pdf', 'ps' or 'png'. If not
230 231 provided, will be deduced from the filename.
231 232 """
232 233 from matplotlib import figure
233 234 # backend_agg supports all of the core output formats
234 235 from matplotlib.backends import backend_agg
235 236 from matplotlib.font_manager import FontProperties
236 237 from matplotlib.mathtext import MathTextParser
237 238
238 239 if prop is None:
239 240 prop = FontProperties()
240 241
241 242 parser = MathTextParser('path')
242 243 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
243 244
244 245 fig = figure.Figure(figsize=(width / 72.0, height / 72.0))
245 246 fig.text(0, depth/height, s, fontproperties=prop)
246 247 backend_agg.FigureCanvasAgg(fig)
247 248 fig.savefig(filename_or_obj, dpi=dpi, format=format)
248 249
249 250 return depth
250 251
@@ -1,119 +1,139 b''
1 1 # encoding: utf-8
2 2 """Tests for IPython.utils.path.py"""
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (C) 2008-2011 The IPython Development Team
6 6 #
7 7 # Distributed under the terms of the BSD License. The full license is in
8 8 # the file COPYING, distributed as part of this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 import nose.tools as nt
12 12
13 13 from IPython.lib import latextools
14 from IPython.testing.decorators import onlyif_cmds_exist
14 from IPython.testing.decorators import onlyif_cmds_exist, skipif_not_matplotlib
15 15 from IPython.testing.tools import monkeypatch
16 16 from IPython.utils.process import FindCmdError
17 17
18 18
19 19 def test_latex_to_png_dvipng_fails_when_no_cmd():
20 20 """
21 21 `latex_to_png_dvipng` should return None when there is no required command
22 22 """
23 23 for command in ['latex', 'dvipng']:
24 24 yield (check_latex_to_png_dvipng_fails_when_no_cmd, command)
25 25
26 26
27 27 def check_latex_to_png_dvipng_fails_when_no_cmd(command):
28 28 def mock_find_cmd(arg):
29 29 if arg == command:
30 30 raise FindCmdError
31 31
32 32 with monkeypatch(latextools, "find_cmd", mock_find_cmd):
33 33 nt.assert_equals(latextools.latex_to_png_dvipng("whatever", True),
34 34 None)
35 35
36 36
37 37 @onlyif_cmds_exist('latex', 'dvipng')
38 38 def test_latex_to_png_dvipng_runs():
39 39 """
40 40 Test that latex_to_png_dvipng just runs without error.
41 41 """
42 42 def mock_kpsewhich(filename):
43 43 nt.assert_equals(filename, "breqn.sty")
44 44 return None
45 45
46 46 for (s, wrap) in [("$$x^2$$", False), ("x^2", True)]:
47 47 yield (latextools.latex_to_png_dvipng, s, wrap)
48 48
49 49 with monkeypatch(latextools, "kpsewhich", mock_kpsewhich):
50 50 yield (latextools.latex_to_png_dvipng, s, wrap)
51 51
52 @skipif_not_matplotlib
53 def test_latex_to_png_mpl_runs():
54 """
55 Test that latex_to_png_mpl just runs without error.
56 """
57 def mock_kpsewhich(filename):
58 nt.assert_equals(filename, "breqn.sty")
59 return None
60
61 for (s, wrap) in [("$x^2$", False), ("x^2", True)]:
62 yield (latextools.latex_to_png_mpl, s, wrap)
63
64 with monkeypatch(latextools, "kpsewhich", mock_kpsewhich):
65 yield (latextools.latex_to_png_mpl, s, wrap)
66
67 @skipif_not_matplotlib
68 def test_latex_to_html():
69 img = latextools.latex_to_html("$x^2$")
70 nt.assert_in("data:image/png;base64,iVBOR", img)
71
52 72
53 73 def test_genelatex_no_wrap():
54 74 """
55 75 Test genelatex with wrap=False.
56 76 """
57 77 def mock_kpsewhich(filename):
58 78 assert False, ("kpsewhich should not be called "
59 79 "(called with {0})".format(filename))
60 80
61 81 with monkeypatch(latextools, "kpsewhich", mock_kpsewhich):
62 82 nt.assert_equals(
63 83 '\n'.join(latextools.genelatex("body text", False)),
64 84 r'''\documentclass{article}
65 85 \usepackage{amsmath}
66 86 \usepackage{amsthm}
67 87 \usepackage{amssymb}
68 88 \usepackage{bm}
69 89 \pagestyle{empty}
70 90 \begin{document}
71 91 body text
72 92 \end{document}''')
73 93
74 94
75 95 def test_genelatex_wrap_with_breqn():
76 96 """
77 97 Test genelatex with wrap=True for the case breqn.sty is installed.
78 98 """
79 99 def mock_kpsewhich(filename):
80 100 nt.assert_equals(filename, "breqn.sty")
81 101 return "path/to/breqn.sty"
82 102
83 103 with monkeypatch(latextools, "kpsewhich", mock_kpsewhich):
84 104 nt.assert_equals(
85 105 '\n'.join(latextools.genelatex("x^2", True)),
86 106 r'''\documentclass{article}
87 107 \usepackage{amsmath}
88 108 \usepackage{amsthm}
89 109 \usepackage{amssymb}
90 110 \usepackage{bm}
91 111 \usepackage{breqn}
92 112 \pagestyle{empty}
93 113 \begin{document}
94 114 \begin{dmath*}
95 115 x^2
96 116 \end{dmath*}
97 117 \end{document}''')
98 118
99 119
100 120 def test_genelatex_wrap_without_breqn():
101 121 """
102 122 Test genelatex with wrap=True for the case breqn.sty is not installed.
103 123 """
104 124 def mock_kpsewhich(filename):
105 125 nt.assert_equals(filename, "breqn.sty")
106 126 return None
107 127
108 128 with monkeypatch(latextools, "kpsewhich", mock_kpsewhich):
109 129 nt.assert_equals(
110 130 '\n'.join(latextools.genelatex("x^2", True)),
111 131 r'''\documentclass{article}
112 132 \usepackage{amsmath}
113 133 \usepackage{amsthm}
114 134 \usepackage{amssymb}
115 135 \usepackage{bm}
116 136 \pagestyle{empty}
117 137 \begin{document}
118 138 $$x^2$$
119 139 \end{document}''')
General Comments 0
You need to be logged in to leave comments. Login now