##// END OF EJS Templates
Merge pull request #7184 from minrk/latextools...
Matthias Bussonnier -
r19784:080fb686 merge
parent child Browse files
Show More
@@ -54,7 +54,6 b' from IPython.core.prefilter import PrefilterManager'
54 from IPython.core.profiledir import ProfileDir
54 from IPython.core.profiledir import ProfileDir
55 from IPython.core.prompts import PromptManager
55 from IPython.core.prompts import PromptManager
56 from IPython.core.usage import default_banner
56 from IPython.core.usage import default_banner
57 from IPython.lib.latextools import LaTeXTool
58 from IPython.testing.skipdoctest import skip_doctest
57 from IPython.testing.skipdoctest import skip_doctest
59 from IPython.utils import PyColorize
58 from IPython.utils import PyColorize
60 from IPython.utils import io
59 from IPython.utils import io
@@ -533,7 +532,6 b' class InteractiveShell(SingletonConfigurable):'
533 self.init_display_pub()
532 self.init_display_pub()
534 self.init_data_pub()
533 self.init_data_pub()
535 self.init_displayhook()
534 self.init_displayhook()
536 self.init_latextool()
537 self.init_magics()
535 self.init_magics()
538 self.init_alias()
536 self.init_alias()
539 self.init_logstart()
537 self.init_logstart()
@@ -727,12 +725,6 b' class InteractiveShell(SingletonConfigurable):'
727 # the appropriate time.
725 # the appropriate time.
728 self.display_trap = DisplayTrap(hook=self.displayhook)
726 self.display_trap = DisplayTrap(hook=self.displayhook)
729
727
730 def init_latextool(self):
731 """Configure LaTeXTool."""
732 cfg = LaTeXTool.instance(parent=self)
733 if cfg not in self.configurables:
734 self.configurables.append(cfg)
735
736 def init_virtualenv(self):
728 def init_virtualenv(self):
737 """Add a virtualenv to sys.path so the user can import modules from it.
729 """Add a virtualenv to sys.path so the user can import modules from it.
738 This isn't perfect: it doesn't use the Python interpreter with which the
730 This isn't perfect: it doesn't use the Python interpreter with which the
@@ -1,23 +1,10 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Tools for handling LaTeX.
2 """Tools for handling LaTeX."""
3
3
4 Authors:
4 # Copyright (c) IPython Development Team.
5
6 * Brian Granger
7 """
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2010 IPython Development Team.
10 #
11 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
12 #
13 # The full license is in the file COPYING.txt, distributed with this software.
14 #-----------------------------------------------------------------------------
15
16 #-----------------------------------------------------------------------------
17 # Imports
18 #-----------------------------------------------------------------------------
19
6
20 from io import BytesIO
7 from io import BytesIO, open
21 from base64 import encodestring
8 from base64 import encodestring
22 import os
9 import os
23 import tempfile
10 import tempfile
@@ -25,20 +12,19 b' import shutil'
25 import subprocess
12 import subprocess
26
13
27 from IPython.utils.process import find_cmd, FindCmdError
14 from IPython.utils.process import find_cmd, FindCmdError
15 from IPython.config import get_config
28 from IPython.config.configurable import SingletonConfigurable
16 from IPython.config.configurable import SingletonConfigurable
29 from IPython.utils.traitlets import List, CBool, CUnicode
17 from IPython.utils.traitlets import List, Bool, Unicode
30 from IPython.utils.py3compat import bytes_to_str
18 from IPython.utils.py3compat import cast_unicode, cast_unicode_py2 as u
31
32 #-----------------------------------------------------------------------------
33 # Tools
34 #-----------------------------------------------------------------------------
35
19
36
20
37 class LaTeXTool(SingletonConfigurable):
21 class LaTeXTool(SingletonConfigurable):
38 """An object to store configuration of the LaTeX tool."""
22 """An object to store configuration of the LaTeX tool."""
39
23 def _config_default(self):
24 return get_config()
25
40 backends = List(
26 backends = List(
41 CUnicode, ["matplotlib", "dvipng"],
27 Unicode, ["matplotlib", "dvipng"],
42 help="Preferred backend to draw LaTeX math equations. "
28 help="Preferred backend to draw LaTeX math equations. "
43 "Backends in the list are checked one by one and the first "
29 "Backends in the list are checked one by one and the first "
44 "usable one is used. Note that `matplotlib` backend "
30 "usable one is used. Note that `matplotlib` backend "
@@ -51,7 +37,7 b' class LaTeXTool(SingletonConfigurable):'
51 # unicode pretty printing is used, you can use ["matplotlib"].
37 # unicode pretty printing is used, you can use ["matplotlib"].
52 config=True)
38 config=True)
53
39
54 use_breqn = CBool(
40 use_breqn = Bool(
55 True,
41 True,
56 help="Use breqn.sty to automatically break long equations. "
42 help="Use breqn.sty to automatically break long equations. "
57 "This configuration takes effect only for dvipng backend.",
43 "This configuration takes effect only for dvipng backend.",
@@ -63,7 +49,7 b' class LaTeXTool(SingletonConfigurable):'
63 "'breqn' will be automatically appended when use_breqn=True.",
49 "'breqn' will be automatically appended when use_breqn=True.",
64 config=True)
50 config=True)
65
51
66 preamble = CUnicode(
52 preamble = Unicode(
67 help="Additional preamble to use when generating LaTeX source "
53 help="Additional preamble to use when generating LaTeX source "
68 "for dvipng backend.",
54 "for dvipng backend.",
69 config=True)
55 config=True)
@@ -74,10 +60,10 b' def latex_to_png(s, encode=False, backend=None, wrap=False):'
74
60
75 Parameters
61 Parameters
76 ----------
62 ----------
77 s : str
63 s : text
78 The raw string containing valid inline LaTeX.
64 The raw string containing valid inline LaTeX.
79 encode : bool, optional
65 encode : bool, optional
80 Should the PNG data bebase64 encoded to make it JSON'able.
66 Should the PNG data base64 encoded to make it JSON'able.
81 backend : {matplotlib, dvipng}
67 backend : {matplotlib, dvipng}
82 Backend for producing PNG data.
68 Backend for producing PNG data.
83 wrap : bool
69 wrap : bool
@@ -86,6 +72,7 b' def latex_to_png(s, encode=False, backend=None, wrap=False):'
86 None is returned when the backend cannot be used.
72 None is returned when the backend cannot be used.
87
73
88 """
74 """
75 s = cast_unicode(s)
89 allowed_backends = LaTeXTool.instance().backends
76 allowed_backends = LaTeXTool.instance().backends
90 if backend is None:
77 if backend is None:
91 backend = allowed_backends[0]
78 backend = allowed_backends[0]
@@ -108,9 +95,12 b' def latex_to_png_mpl(s, wrap):'
108 from matplotlib import mathtext
95 from matplotlib import mathtext
109 except ImportError:
96 except ImportError:
110 return None
97 return None
111
98
99 # mpl mathtext doesn't support display math, force inline
100 s = s.replace('$$', '$')
112 if wrap:
101 if wrap:
113 s = '${0}$'.format(s)
102 s = u'${0}$'.format(s)
103
114 mt = mathtext.MathTextParser('bitmap')
104 mt = mathtext.MathTextParser('bitmap')
115 f = BytesIO()
105 f = BytesIO()
116 mt.to_png(f, s, fontsize=12)
106 mt.to_png(f, s, fontsize=12)
@@ -129,10 +119,10 b' def latex_to_png_dvipng(s, wrap):'
129 dvifile = os.path.join(workdir, "tmp.dvi")
119 dvifile = os.path.join(workdir, "tmp.dvi")
130 outfile = os.path.join(workdir, "tmp.png")
120 outfile = os.path.join(workdir, "tmp.png")
131
121
132 with open(tmpfile, "w") as f:
122 with open(tmpfile, "w", encoding='utf8') as f:
133 f.writelines(genelatex(s, wrap))
123 f.writelines(genelatex(s, wrap))
134
124
135 with open(os.devnull, 'w') as devnull:
125 with open(os.devnull, 'wb') as devnull:
136 subprocess.check_call(
126 subprocess.check_call(
137 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
127 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
138 cwd=workdir, stdout=devnull, stderr=devnull)
128 cwd=workdir, stdout=devnull, stderr=devnull)
@@ -156,7 +146,7 b' def kpsewhich(filename):'
156 ["kpsewhich", filename],
146 ["kpsewhich", filename],
157 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
147 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
158 (stdout, stderr) = proc.communicate()
148 (stdout, stderr) = proc.communicate()
159 return stdout.strip()
149 return stdout.strip().decode('utf8', 'replace')
160 except FindCmdError:
150 except FindCmdError:
161 pass
151 pass
162
152
@@ -165,28 +155,28 b' def genelatex(body, wrap):'
165 """Generate LaTeX document for dvipng backend."""
155 """Generate LaTeX document for dvipng backend."""
166 lt = LaTeXTool.instance()
156 lt = LaTeXTool.instance()
167 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
157 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
168 yield r'\documentclass{article}'
158 yield u(r'\documentclass{article}')
169 packages = lt.packages
159 packages = lt.packages
170 if breqn:
160 if breqn:
171 packages = packages + ['breqn']
161 packages = packages + ['breqn']
172 for pack in packages:
162 for pack in packages:
173 yield r'\usepackage{{{0}}}'.format(pack)
163 yield u(r'\usepackage{{{0}}}'.format(pack))
174 yield r'\pagestyle{empty}'
164 yield u(r'\pagestyle{empty}')
175 if lt.preamble:
165 if lt.preamble:
176 yield lt.preamble
166 yield lt.preamble
177 yield r'\begin{document}'
167 yield u(r'\begin{document}')
178 if breqn:
168 if breqn:
179 yield r'\begin{dmath*}'
169 yield u(r'\begin{dmath*}')
180 yield body
170 yield body
181 yield r'\end{dmath*}'
171 yield u(r'\end{dmath*}')
182 elif wrap:
172 elif wrap:
183 yield '$${0}$$'.format(body)
173 yield u'$${0}$$'.format(body)
184 else:
174 else:
185 yield body
175 yield body
186 yield r'\end{document}'
176 yield u'\end{document}'
187
177
188
178
189 _data_uri_template_png = """<img src="data:image/png;base64,%s" alt=%s />"""
179 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
190
180
191 def latex_to_html(s, alt='image'):
181 def latex_to_html(s, alt='image'):
192 """Render LaTeX to HTML with embedded PNG data using data URIs.
182 """Render LaTeX to HTML with embedded PNG data using data URIs.
@@ -198,54 +188,8 b" def latex_to_html(s, alt='image'):"
198 alt : str
188 alt : str
199 The alt text to use for the HTML.
189 The alt text to use for the HTML.
200 """
190 """
201 base64_data = bytes_to_str(latex_to_png(s, encode=True), 'ascii')
191 base64_data = latex_to_png(s, encode=True).decode('ascii')
202 if base64_data:
192 if base64_data:
203 return _data_uri_template_png % (base64_data, alt)
193 return _data_uri_template_png % (base64_data, alt)
204
194
205
195
206 # From matplotlib, thanks to mdboom. Once this is in matplotlib releases, we
207 # will remove.
208 def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None):
209 """
210 Given a math expression, renders it in a closely-clipped bounding
211 box to an image file.
212
213 *s*
214 A math expression. The math portion should be enclosed in
215 dollar signs.
216
217 *filename_or_obj*
218 A filepath or writable file-like object to write the image data
219 to.
220
221 *prop*
222 If provided, a FontProperties() object describing the size and
223 style of the text.
224
225 *dpi*
226 Override the output dpi, otherwise use the default associated
227 with the output format.
228
229 *format*
230 The output format, eg. 'svg', 'pdf', 'ps' or 'png'. If not
231 provided, will be deduced from the filename.
232 """
233 from matplotlib import figure
234 # backend_agg supports all of the core output formats
235 from matplotlib.backends import backend_agg
236 from matplotlib.font_manager import FontProperties
237 from matplotlib.mathtext import MathTextParser
238
239 if prop is None:
240 prop = FontProperties()
241
242 parser = MathTextParser('path')
243 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
244
245 fig = figure.Figure(figsize=(width / 72.0, height / 72.0))
246 fig.text(0, depth/height, s, fontproperties=prop)
247 backend_agg.FigureCanvasAgg(fig)
248 fig.savefig(filename_or_obj, dpi=dpi, format=format)
249
250 return depth
251
@@ -239,9 +239,7 b' class IPythonWidget(FrontendWidget):'
239
239
240
240
241 def _handle_execute_result(self, msg):
241 def _handle_execute_result(self, msg):
242 """ Reimplemented for IPython-style "display hook".
242 """Reimplemented for IPython-style "display hook"."""
243 """
244 self.log.debug("execute_result: %s", msg.get('content', ''))
245 if self.include_output(msg):
243 if self.include_output(msg):
246 self.flush_clearoutput()
244 self.flush_clearoutput()
247 content = msg['content']
245 content = msg['content']
@@ -258,9 +256,7 b' class IPythonWidget(FrontendWidget):'
258 self._append_plain_text(text + self.output_sep2, True)
256 self._append_plain_text(text + self.output_sep2, True)
259
257
260 def _handle_display_data(self, msg):
258 def _handle_display_data(self, msg):
261 """ The base handler for the ``display_data`` message.
259 """The base handler for the ``display_data`` message."""
262 """
263 self.log.debug("display: %s", msg.get('content', ''))
264 # For now, we don't display data from other frontends, but we
260 # For now, we don't display data from other frontends, but we
265 # eventually will as this allows all frontends to monitor the display
261 # eventually will as this allows all frontends to monitor the display
266 # data. But we need to figure out how to handle this in the GUI.
262 # data. But we need to figure out how to handle this in the GUI.
@@ -95,18 +95,17 b' class RichIPythonWidget(IPythonWidget):'
95 # 'BaseFrontendMixin' abstract interface
95 # 'BaseFrontendMixin' abstract interface
96 #---------------------------------------------------------------------------
96 #---------------------------------------------------------------------------
97 def _pre_image_append(self, msg, prompt_number):
97 def _pre_image_append(self, msg, prompt_number):
98 """ Append the Out[] prompt and make the output nicer
98 """Append the Out[] prompt and make the output nicer
99
99
100 Shared code for some the following if statement
100 Shared code for some the following if statement
101 """
101 """
102 self.log.debug("execute_result: %s", msg.get('content', ''))
103 self._append_plain_text(self.output_sep, True)
102 self._append_plain_text(self.output_sep, True)
104 self._append_html(self._make_out_prompt(prompt_number), True)
103 self._append_html(self._make_out_prompt(prompt_number), True)
105 self._append_plain_text('\n', True)
104 self._append_plain_text('\n', True)
106
105
107 def _handle_execute_result(self, msg):
106 def _handle_execute_result(self, msg):
108 """ Overridden to handle rich data types, like SVG.
107 """Overridden to handle rich data types, like SVG."""
109 """
108 self.log.debug("execute_result: %s", msg.get('content', ''))
110 if self.include_output(msg):
109 if self.include_output(msg):
111 self.flush_clearoutput()
110 self.flush_clearoutput()
112 content = msg['content']
111 content = msg['content']
@@ -129,43 +128,35 b' class RichIPythonWidget(IPythonWidget):'
129 self._append_html(self.output_sep2, True)
128 self._append_html(self.output_sep2, True)
130 elif 'text/latex' in data:
129 elif 'text/latex' in data:
131 self._pre_image_append(msg, prompt_number)
130 self._pre_image_append(msg, prompt_number)
132 latex = data['text/latex'].encode('ascii')
131 self._append_latex(data['text/latex'], True)
133 # latex_to_png takes care of handling $
132 self._append_html(self.output_sep2, True)
134 latex = latex.strip('$')
135 png = latex_to_png(latex, wrap=True)
136 if png is not None:
137 self._append_png(png, True)
138 self._append_html(self.output_sep2, True)
139 else:
140 # Print plain text if png can't be generated
141 return super(RichIPythonWidget, self)._handle_execute_result(msg)
142 else:
133 else:
143 # Default back to the plain text representation.
134 # Default back to the plain text representation.
144 return super(RichIPythonWidget, self)._handle_execute_result(msg)
135 return super(RichIPythonWidget, self)._handle_execute_result(msg)
145
136
146 def _handle_display_data(self, msg):
137 def _handle_display_data(self, msg):
147 """ Overridden to handle rich data types, like SVG.
138 """Overridden to handle rich data types, like SVG."""
148 """
139 self.log.debug("display_data: %s", msg.get('content', ''))
149 if self.include_output(msg):
140 if self.include_output(msg):
150 self.flush_clearoutput()
141 self.flush_clearoutput()
151 data = msg['content']['data']
142 data = msg['content']['data']
152 metadata = msg['content']['metadata']
143 metadata = msg['content']['metadata']
153 # Try to use the svg or html representations.
144 # Try to use the svg or html representations.
154 # FIXME: Is this the right ordering of things to try?
145 # FIXME: Is this the right ordering of things to try?
146 self.log.debug("display: %s", msg.get('content', ''))
155 if 'image/svg+xml' in data:
147 if 'image/svg+xml' in data:
156 self.log.debug("display: %s", msg.get('content', ''))
157 svg = data['image/svg+xml']
148 svg = data['image/svg+xml']
158 self._append_svg(svg, True)
149 self._append_svg(svg, True)
159 elif 'image/png' in data:
150 elif 'image/png' in data:
160 self.log.debug("display: %s", msg.get('content', ''))
161 # PNG data is base64 encoded as it passes over the network
151 # PNG data is base64 encoded as it passes over the network
162 # in a JSON structure so we decode it.
152 # in a JSON structure so we decode it.
163 png = decodestring(data['image/png'].encode('ascii'))
153 png = decodestring(data['image/png'].encode('ascii'))
164 self._append_png(png, True, metadata=metadata.get('image/png', None))
154 self._append_png(png, True, metadata=metadata.get('image/png', None))
165 elif 'image/jpeg' in data and self._jpg_supported:
155 elif 'image/jpeg' in data and self._jpg_supported:
166 self.log.debug("display: %s", msg.get('content', ''))
167 jpg = decodestring(data['image/jpeg'].encode('ascii'))
156 jpg = decodestring(data['image/jpeg'].encode('ascii'))
168 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
157 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
158 elif 'text/latex' in data:
159 self._append_latex(data['text/latex'], True)
169 else:
160 else:
170 # Default back to the plain text representation.
161 # Default back to the plain text representation.
171 return super(RichIPythonWidget, self)._handle_display_data(msg)
162 return super(RichIPythonWidget, self)._handle_display_data(msg)
@@ -174,6 +165,16 b' class RichIPythonWidget(IPythonWidget):'
174 # 'RichIPythonWidget' protected interface
165 # 'RichIPythonWidget' protected interface
175 #---------------------------------------------------------------------------
166 #---------------------------------------------------------------------------
176
167
168 def _append_latex(self, latex, before_prompt=False, metadata=None):
169 """ Append latex data to the widget."""
170 try:
171 png = latex_to_png(latex, wrap=False)
172 except Exception as e:
173 self.log.error("Failed to render latex: '%s'", latex, exc_info=True)
174 self._append_plain_text("Failed to render latex: %s" % e, before_prompt)
175 else:
176 self._append_png(png, before_prompt, metadata)
177
177 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
178 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
178 """ Append raw JPG data to the widget."""
179 """ Append raw JPG data to the widget."""
179 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
180 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
General Comments 0
You need to be logged in to leave comments. Login now