##// END OF EJS Templates
make lib.latextools accept unicode...
Min RK -
Show More
@@ -1,251 +1,190 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
24 import shutil
11 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.application import Application
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
40 backends = List(
24 backends = List(
41 CUnicode, ["matplotlib", "dvipng"],
25 Unicode, ["matplotlib", "dvipng"],
42 help="Preferred backend to draw LaTeX math equations. "
26 help="Preferred backend to draw LaTeX math equations. "
43 "Backends in the list are checked one by one and the first "
27 "Backends in the list are checked one by one and the first "
44 "usable one is used. Note that `matplotlib` backend "
28 "usable one is used. Note that `matplotlib` backend "
45 "is usable only for inline style equations. To draw "
29 "is usable only for inline style equations. To draw "
46 "display style equations, `dvipng` backend must be specified. ",
30 "display style equations, `dvipng` backend must be specified. ",
47 # It is a List instead of Enum, to make configuration more
31 # It is a List instead of Enum, to make configuration more
48 # flexible. For example, to use matplotlib mainly but dvipng
32 # flexible. For example, to use matplotlib mainly but dvipng
49 # for display style, the default ["matplotlib", "dvipng"] can
33 # for display style, the default ["matplotlib", "dvipng"] can
50 # be used. To NOT use dvipng so that other repr such as
34 # be used. To NOT use dvipng so that other repr such as
51 # unicode pretty printing is used, you can use ["matplotlib"].
35 # unicode pretty printing is used, you can use ["matplotlib"].
52 config=True)
36 config=True)
53
37
54 use_breqn = CBool(
38 use_breqn = Bool(
55 True,
39 True,
56 help="Use breqn.sty to automatically break long equations. "
40 help="Use breqn.sty to automatically break long equations. "
57 "This configuration takes effect only for dvipng backend.",
41 "This configuration takes effect only for dvipng backend.",
58 config=True)
42 config=True)
59
43
60 packages = List(
44 packages = List(
61 ['amsmath', 'amsthm', 'amssymb', 'bm'],
45 ['amsmath', 'amsthm', 'amssymb', 'bm'],
62 help="A list of packages to use for dvipng backend. "
46 help="A list of packages to use for dvipng backend. "
63 "'breqn' will be automatically appended when use_breqn=True.",
47 "'breqn' will be automatically appended when use_breqn=True.",
64 config=True)
48 config=True)
65
49
66 preamble = CUnicode(
50 preamble = Unicode(
67 help="Additional preamble to use when generating LaTeX source "
51 help="Additional preamble to use when generating LaTeX source "
68 "for dvipng backend.",
52 "for dvipng backend.",
69 config=True)
53 config=True)
70
54
71
55
72 def latex_to_png(s, encode=False, backend=None, wrap=False):
56 def latex_to_png(s, encode=False, backend=None, wrap=False):
73 """Render a LaTeX string to PNG.
57 """Render a LaTeX string to PNG.
74
58
75 Parameters
59 Parameters
76 ----------
60 ----------
77 s : str
61 s : text
78 The raw string containing valid inline LaTeX.
62 The raw string containing valid inline LaTeX.
79 encode : bool, optional
63 encode : bool, optional
80 Should the PNG data bebase64 encoded to make it JSON'able.
64 Should the PNG data base64 encoded to make it JSON'able.
81 backend : {matplotlib, dvipng}
65 backend : {matplotlib, dvipng}
82 Backend for producing PNG data.
66 Backend for producing PNG data.
83 wrap : bool
67 wrap : bool
84 If true, Automatically wrap `s` as a LaTeX equation.
68 If true, Automatically wrap `s` as a LaTeX equation.
85
69
86 None is returned when the backend cannot be used.
70 None is returned when the backend cannot be used.
87
71
88 """
72 """
73 s = cast_unicode(s)
89 allowed_backends = LaTeXTool.instance().backends
74 allowed_backends = LaTeXTool.instance().backends
90 if backend is None:
75 if backend is None:
91 backend = allowed_backends[0]
76 backend = allowed_backends[0]
92 if backend not in allowed_backends:
77 if backend not in allowed_backends:
93 return None
78 return None
94 if backend == 'matplotlib':
79 if backend == 'matplotlib':
95 f = latex_to_png_mpl
80 f = latex_to_png_mpl
96 elif backend == 'dvipng':
81 elif backend == 'dvipng':
97 f = latex_to_png_dvipng
82 f = latex_to_png_dvipng
98 else:
83 else:
99 raise ValueError('No such backend {0}'.format(backend))
84 raise ValueError('No such backend {0}'.format(backend))
100 bin_data = f(s, wrap)
85 bin_data = f(s, wrap)
101 if encode and bin_data:
86 if encode and bin_data:
102 bin_data = encodestring(bin_data)
87 bin_data = encodestring(bin_data)
103 return bin_data
88 return bin_data
104
89
105
90
106 def latex_to_png_mpl(s, wrap):
91 def latex_to_png_mpl(s, wrap):
107 try:
92 try:
108 from matplotlib import mathtext
93 from matplotlib import mathtext
109 except ImportError:
94 except ImportError:
110 return None
95 return None
111
96
112 if wrap:
97 if wrap:
113 s = '${0}$'.format(s)
98 s = u'${0}$'.format(s)
114 mt = mathtext.MathTextParser('bitmap')
99 mt = mathtext.MathTextParser('bitmap')
115 f = BytesIO()
100 f = BytesIO()
116 mt.to_png(f, s, fontsize=12)
101 mt.to_png(f, s, fontsize=12)
117 return f.getvalue()
102 return f.getvalue()
118
103
119
104
120 def latex_to_png_dvipng(s, wrap):
105 def latex_to_png_dvipng(s, wrap):
121 try:
106 try:
122 find_cmd('latex')
107 find_cmd('latex')
123 find_cmd('dvipng')
108 find_cmd('dvipng')
124 except FindCmdError:
109 except FindCmdError:
125 return None
110 return None
126 try:
111 try:
127 workdir = tempfile.mkdtemp()
112 workdir = tempfile.mkdtemp()
128 tmpfile = os.path.join(workdir, "tmp.tex")
113 tmpfile = os.path.join(workdir, "tmp.tex")
129 dvifile = os.path.join(workdir, "tmp.dvi")
114 dvifile = os.path.join(workdir, "tmp.dvi")
130 outfile = os.path.join(workdir, "tmp.png")
115 outfile = os.path.join(workdir, "tmp.png")
131
116
132 with open(tmpfile, "w") as f:
117 with open(tmpfile, "w", encoding='utf8') as f:
133 f.writelines(genelatex(s, wrap))
118 f.writelines(genelatex(s, wrap))
134
119
135 with open(os.devnull, 'w') as devnull:
120 with open(os.devnull, 'wb') as devnull:
136 subprocess.check_call(
121 subprocess.check_call(
137 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
122 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
138 cwd=workdir, stdout=devnull, stderr=devnull)
123 cwd=workdir, stdout=devnull, stderr=devnull)
139
124
140 subprocess.check_call(
125 subprocess.check_call(
141 ["dvipng", "-T", "tight", "-x", "1500", "-z", "9",
126 ["dvipng", "-T", "tight", "-x", "1500", "-z", "9",
142 "-bg", "transparent", "-o", outfile, dvifile], cwd=workdir,
127 "-bg", "transparent", "-o", outfile, dvifile], cwd=workdir,
143 stdout=devnull, stderr=devnull)
128 stdout=devnull, stderr=devnull)
144
129
145 with open(outfile, "rb") as f:
130 with open(outfile, "rb") as f:
146 return f.read()
131 return f.read()
147 finally:
132 finally:
148 shutil.rmtree(workdir)
133 shutil.rmtree(workdir)
149
134
150
135
151 def kpsewhich(filename):
136 def kpsewhich(filename):
152 """Invoke kpsewhich command with an argument `filename`."""
137 """Invoke kpsewhich command with an argument `filename`."""
153 try:
138 try:
154 find_cmd("kpsewhich")
139 find_cmd("kpsewhich")
155 proc = subprocess.Popen(
140 proc = subprocess.Popen(
156 ["kpsewhich", filename],
141 ["kpsewhich", filename],
157 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
142 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
158 (stdout, stderr) = proc.communicate()
143 (stdout, stderr) = proc.communicate()
159 return stdout.strip()
144 return stdout.strip().decode('utf8', 'replace')
160 except FindCmdError:
145 except FindCmdError:
161 pass
146 pass
162
147
163
148
164 def genelatex(body, wrap):
149 def genelatex(body, wrap):
165 """Generate LaTeX document for dvipng backend."""
150 """Generate LaTeX document for dvipng backend."""
166 lt = LaTeXTool.instance()
151 lt = LaTeXTool.instance()
167 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
152 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
168 yield r'\documentclass{article}'
153 yield u(r'\documentclass{article}')
169 packages = lt.packages
154 packages = lt.packages
170 if breqn:
155 if breqn:
171 packages = packages + ['breqn']
156 packages = packages + ['breqn']
172 for pack in packages:
157 for pack in packages:
173 yield r'\usepackage{{{0}}}'.format(pack)
158 yield u(r'\usepackage{{{0}}}'.format(pack))
174 yield r'\pagestyle{empty}'
159 yield u(r'\pagestyle{empty}')
175 if lt.preamble:
160 if lt.preamble:
176 yield lt.preamble
161 yield lt.preamble
177 yield r'\begin{document}'
162 yield u(r'\begin{document}')
178 if breqn:
163 if breqn:
179 yield r'\begin{dmath*}'
164 yield u(r'\begin{dmath*}')
180 yield body
165 yield body
181 yield r'\end{dmath*}'
166 yield u(r'\end{dmath*}')
182 elif wrap:
167 elif wrap:
183 yield '$${0}$$'.format(body)
168 yield u'$${0}$$'.format(body)
184 else:
169 else:
185 yield body
170 yield body
186 yield r'\end{document}'
171 yield u'\end{document}'
187
172
188
173
189 _data_uri_template_png = """<img src="data:image/png;base64,%s" alt=%s />"""
174 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
190
175
191 def latex_to_html(s, alt='image'):
176 def latex_to_html(s, alt='image'):
192 """Render LaTeX to HTML with embedded PNG data using data URIs.
177 """Render LaTeX to HTML with embedded PNG data using data URIs.
193
178
194 Parameters
179 Parameters
195 ----------
180 ----------
196 s : str
181 s : str
197 The raw string containing valid inline LateX.
182 The raw string containing valid inline LateX.
198 alt : str
183 alt : str
199 The alt text to use for the HTML.
184 The alt text to use for the HTML.
200 """
185 """
201 base64_data = bytes_to_str(latex_to_png(s, encode=True), 'ascii')
186 base64_data = latex_to_png(s, encode=True).decode('ascii')
202 if base64_data:
187 if base64_data:
203 return _data_uri_template_png % (base64_data, alt)
188 return _data_uri_template_png % (base64_data, alt)
204
189
205
190
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
@@ -1,347 +1,346 b''
1 # Copyright (c) IPython Development Team.
1 # Copyright (c) IPython Development Team.
2 # Distributed under the terms of the Modified BSD License.
2 # Distributed under the terms of the Modified BSD License.
3
3
4 from base64 import decodestring
4 from base64 import decodestring
5 import os
5 import os
6 import re
6 import re
7
7
8 from IPython.external.qt import QtCore, QtGui
8 from IPython.external.qt import QtCore, QtGui
9
9
10 from IPython.lib.latextools import latex_to_png
10 from IPython.lib.latextools import latex_to_png
11 from IPython.utils.path import ensure_dir_exists
11 from IPython.utils.path import ensure_dir_exists
12 from IPython.utils.traitlets import Bool
12 from IPython.utils.traitlets import Bool
13 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
13 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
14 from .ipython_widget import IPythonWidget
14 from .ipython_widget import IPythonWidget
15
15
16
16
17 class RichIPythonWidget(IPythonWidget):
17 class RichIPythonWidget(IPythonWidget):
18 """ An IPythonWidget that supports rich text, including lists, images, and
18 """ An IPythonWidget that supports rich text, including lists, images, and
19 tables. Note that raw performance will be reduced compared to the plain
19 tables. Note that raw performance will be reduced compared to the plain
20 text version.
20 text version.
21 """
21 """
22
22
23 # RichIPythonWidget protected class variables.
23 # RichIPythonWidget protected class variables.
24 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
24 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
25 _jpg_supported = Bool(False)
25 _jpg_supported = Bool(False)
26
26
27 # Used to determine whether a given html export attempt has already
27 # Used to determine whether a given html export attempt has already
28 # displayed a warning about being unable to convert a png to svg.
28 # displayed a warning about being unable to convert a png to svg.
29 _svg_warning_displayed = False
29 _svg_warning_displayed = False
30
30
31 #---------------------------------------------------------------------------
31 #---------------------------------------------------------------------------
32 # 'object' interface
32 # 'object' interface
33 #---------------------------------------------------------------------------
33 #---------------------------------------------------------------------------
34
34
35 def __init__(self, *args, **kw):
35 def __init__(self, *args, **kw):
36 """ Create a RichIPythonWidget.
36 """ Create a RichIPythonWidget.
37 """
37 """
38 kw['kind'] = 'rich'
38 kw['kind'] = 'rich'
39 super(RichIPythonWidget, self).__init__(*args, **kw)
39 super(RichIPythonWidget, self).__init__(*args, **kw)
40
40
41 # Configure the ConsoleWidget HTML exporter for our formats.
41 # Configure the ConsoleWidget HTML exporter for our formats.
42 self._html_exporter.image_tag = self._get_image_tag
42 self._html_exporter.image_tag = self._get_image_tag
43
43
44 # Dictionary for resolving document resource names to SVG data.
44 # Dictionary for resolving document resource names to SVG data.
45 self._name_to_svg_map = {}
45 self._name_to_svg_map = {}
46
46
47 # Do we support jpg ?
47 # Do we support jpg ?
48 # it seems that sometime jpg support is a plugin of QT, so try to assume
48 # it seems that sometime jpg support is a plugin of QT, so try to assume
49 # it is not always supported.
49 # it is not always supported.
50 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
50 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
51 self._jpg_supported = 'jpeg' in _supported_format
51 self._jpg_supported = 'jpeg' in _supported_format
52
52
53
53
54 #---------------------------------------------------------------------------
54 #---------------------------------------------------------------------------
55 # 'ConsoleWidget' public interface overides
55 # 'ConsoleWidget' public interface overides
56 #---------------------------------------------------------------------------
56 #---------------------------------------------------------------------------
57
57
58 def export_html(self):
58 def export_html(self):
59 """ Shows a dialog to export HTML/XML in various formats.
59 """ Shows a dialog to export HTML/XML in various formats.
60
60
61 Overridden in order to reset the _svg_warning_displayed flag prior
61 Overridden in order to reset the _svg_warning_displayed flag prior
62 to the export running.
62 to the export running.
63 """
63 """
64 self._svg_warning_displayed = False
64 self._svg_warning_displayed = False
65 super(RichIPythonWidget, self).export_html()
65 super(RichIPythonWidget, self).export_html()
66
66
67
67
68 #---------------------------------------------------------------------------
68 #---------------------------------------------------------------------------
69 # 'ConsoleWidget' protected interface
69 # 'ConsoleWidget' protected interface
70 #---------------------------------------------------------------------------
70 #---------------------------------------------------------------------------
71
71
72 def _context_menu_make(self, pos):
72 def _context_menu_make(self, pos):
73 """ Reimplemented to return a custom context menu for images.
73 """ Reimplemented to return a custom context menu for images.
74 """
74 """
75 format = self._control.cursorForPosition(pos).charFormat()
75 format = self._control.cursorForPosition(pos).charFormat()
76 name = format.stringProperty(QtGui.QTextFormat.ImageName)
76 name = format.stringProperty(QtGui.QTextFormat.ImageName)
77 if name:
77 if name:
78 menu = QtGui.QMenu()
78 menu = QtGui.QMenu()
79
79
80 menu.addAction('Copy Image', lambda: self._copy_image(name))
80 menu.addAction('Copy Image', lambda: self._copy_image(name))
81 menu.addAction('Save Image As...', lambda: self._save_image(name))
81 menu.addAction('Save Image As...', lambda: self._save_image(name))
82 menu.addSeparator()
82 menu.addSeparator()
83
83
84 svg = self._name_to_svg_map.get(name, None)
84 svg = self._name_to_svg_map.get(name, None)
85 if svg is not None:
85 if svg is not None:
86 menu.addSeparator()
86 menu.addSeparator()
87 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
87 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
88 menu.addAction('Save SVG As...',
88 menu.addAction('Save SVG As...',
89 lambda: save_svg(svg, self._control))
89 lambda: save_svg(svg, self._control))
90 else:
90 else:
91 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
91 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
92 return menu
92 return menu
93
93
94 #---------------------------------------------------------------------------
94 #---------------------------------------------------------------------------
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', ''))
102 self.log.debug("execute_result: %s", msg.get('content', ''))
103 self._append_plain_text(self.output_sep, True)
103 self._append_plain_text(self.output_sep, True)
104 self._append_html(self._make_out_prompt(prompt_number), True)
104 self._append_html(self._make_out_prompt(prompt_number), True)
105 self._append_plain_text('\n', True)
105 self._append_plain_text('\n', True)
106
106
107 def _handle_execute_result(self, msg):
107 def _handle_execute_result(self, msg):
108 """ Overridden to handle rich data types, like SVG.
108 """ Overridden to handle rich data types, like SVG.
109 """
109 """
110 if self.include_output(msg):
110 if self.include_output(msg):
111 self.flush_clearoutput()
111 self.flush_clearoutput()
112 content = msg['content']
112 content = msg['content']
113 prompt_number = content.get('execution_count', 0)
113 prompt_number = content.get('execution_count', 0)
114 data = content['data']
114 data = content['data']
115 metadata = msg['content']['metadata']
115 metadata = msg['content']['metadata']
116 if 'image/svg+xml' in data:
116 if 'image/svg+xml' in data:
117 self._pre_image_append(msg, prompt_number)
117 self._pre_image_append(msg, prompt_number)
118 self._append_svg(data['image/svg+xml'], True)
118 self._append_svg(data['image/svg+xml'], True)
119 self._append_html(self.output_sep2, True)
119 self._append_html(self.output_sep2, True)
120 elif 'image/png' in data:
120 elif 'image/png' in data:
121 self._pre_image_append(msg, prompt_number)
121 self._pre_image_append(msg, prompt_number)
122 png = decodestring(data['image/png'].encode('ascii'))
122 png = decodestring(data['image/png'].encode('ascii'))
123 self._append_png(png, True, metadata=metadata.get('image/png', None))
123 self._append_png(png, True, metadata=metadata.get('image/png', None))
124 self._append_html(self.output_sep2, True)
124 self._append_html(self.output_sep2, True)
125 elif 'image/jpeg' in data and self._jpg_supported:
125 elif 'image/jpeg' in data and self._jpg_supported:
126 self._pre_image_append(msg, prompt_number)
126 self._pre_image_append(msg, prompt_number)
127 jpg = decodestring(data['image/jpeg'].encode('ascii'))
127 jpg = decodestring(data['image/jpeg'].encode('ascii'))
128 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
128 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
129 self._append_html(self.output_sep2, True)
129 self._append_html(self.output_sep2, True)
130 elif 'text/latex' in data:
130 elif 'text/latex' in data:
131 self._pre_image_append(msg, prompt_number)
131 self._pre_image_append(msg, prompt_number)
132 latex = data['text/latex'].encode('ascii')
133 # latex_to_png takes care of handling $
132 # latex_to_png takes care of handling $
134 latex = latex.strip('$')
133 latex = latex.strip('$')
135 png = latex_to_png(latex, wrap=True)
134 png = latex_to_png(latex, wrap=True)
136 if png is not None:
135 if png is not None:
137 self._append_png(png, True)
136 self._append_png(png, True)
138 self._append_html(self.output_sep2, True)
137 self._append_html(self.output_sep2, True)
139 else:
138 else:
140 # Print plain text if png can't be generated
139 # Print plain text if png can't be generated
141 return super(RichIPythonWidget, self)._handle_execute_result(msg)
140 return super(RichIPythonWidget, self)._handle_execute_result(msg)
142 else:
141 else:
143 # Default back to the plain text representation.
142 # Default back to the plain text representation.
144 return super(RichIPythonWidget, self)._handle_execute_result(msg)
143 return super(RichIPythonWidget, self)._handle_execute_result(msg)
145
144
146 def _handle_display_data(self, msg):
145 def _handle_display_data(self, msg):
147 """ Overridden to handle rich data types, like SVG.
146 """ Overridden to handle rich data types, like SVG.
148 """
147 """
149 if self.include_output(msg):
148 if self.include_output(msg):
150 self.flush_clearoutput()
149 self.flush_clearoutput()
151 data = msg['content']['data']
150 data = msg['content']['data']
152 metadata = msg['content']['metadata']
151 metadata = msg['content']['metadata']
153 # Try to use the svg or html representations.
152 # Try to use the svg or html representations.
154 # FIXME: Is this the right ordering of things to try?
153 # FIXME: Is this the right ordering of things to try?
155 if 'image/svg+xml' in data:
154 if 'image/svg+xml' in data:
156 self.log.debug("display: %s", msg.get('content', ''))
155 self.log.debug("display: %s", msg.get('content', ''))
157 svg = data['image/svg+xml']
156 svg = data['image/svg+xml']
158 self._append_svg(svg, True)
157 self._append_svg(svg, True)
159 elif 'image/png' in data:
158 elif 'image/png' in data:
160 self.log.debug("display: %s", msg.get('content', ''))
159 self.log.debug("display: %s", msg.get('content', ''))
161 # PNG data is base64 encoded as it passes over the network
160 # PNG data is base64 encoded as it passes over the network
162 # in a JSON structure so we decode it.
161 # in a JSON structure so we decode it.
163 png = decodestring(data['image/png'].encode('ascii'))
162 png = decodestring(data['image/png'].encode('ascii'))
164 self._append_png(png, True, metadata=metadata.get('image/png', None))
163 self._append_png(png, True, metadata=metadata.get('image/png', None))
165 elif 'image/jpeg' in data and self._jpg_supported:
164 elif 'image/jpeg' in data and self._jpg_supported:
166 self.log.debug("display: %s", msg.get('content', ''))
165 self.log.debug("display: %s", msg.get('content', ''))
167 jpg = decodestring(data['image/jpeg'].encode('ascii'))
166 jpg = decodestring(data['image/jpeg'].encode('ascii'))
168 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
167 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
169 else:
168 else:
170 # Default back to the plain text representation.
169 # Default back to the plain text representation.
171 return super(RichIPythonWidget, self)._handle_display_data(msg)
170 return super(RichIPythonWidget, self)._handle_display_data(msg)
172
171
173 #---------------------------------------------------------------------------
172 #---------------------------------------------------------------------------
174 # 'RichIPythonWidget' protected interface
173 # 'RichIPythonWidget' protected interface
175 #---------------------------------------------------------------------------
174 #---------------------------------------------------------------------------
176
175
177 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
176 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
178 """ Append raw JPG data to the widget."""
177 """ Append raw JPG data to the widget."""
179 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
178 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
180
179
181 def _append_png(self, png, before_prompt=False, metadata=None):
180 def _append_png(self, png, before_prompt=False, metadata=None):
182 """ Append raw PNG data to the widget.
181 """ Append raw PNG data to the widget.
183 """
182 """
184 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
183 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
185
184
186 def _append_svg(self, svg, before_prompt=False):
185 def _append_svg(self, svg, before_prompt=False):
187 """ Append raw SVG data to the widget.
186 """ Append raw SVG data to the widget.
188 """
187 """
189 self._append_custom(self._insert_svg, svg, before_prompt)
188 self._append_custom(self._insert_svg, svg, before_prompt)
190
189
191 def _add_image(self, image):
190 def _add_image(self, image):
192 """ Adds the specified QImage to the document and returns a
191 """ Adds the specified QImage to the document and returns a
193 QTextImageFormat that references it.
192 QTextImageFormat that references it.
194 """
193 """
195 document = self._control.document()
194 document = self._control.document()
196 name = str(image.cacheKey())
195 name = str(image.cacheKey())
197 document.addResource(QtGui.QTextDocument.ImageResource,
196 document.addResource(QtGui.QTextDocument.ImageResource,
198 QtCore.QUrl(name), image)
197 QtCore.QUrl(name), image)
199 format = QtGui.QTextImageFormat()
198 format = QtGui.QTextImageFormat()
200 format.setName(name)
199 format.setName(name)
201 return format
200 return format
202
201
203 def _copy_image(self, name):
202 def _copy_image(self, name):
204 """ Copies the ImageResource with 'name' to the clipboard.
203 """ Copies the ImageResource with 'name' to the clipboard.
205 """
204 """
206 image = self._get_image(name)
205 image = self._get_image(name)
207 QtGui.QApplication.clipboard().setImage(image)
206 QtGui.QApplication.clipboard().setImage(image)
208
207
209 def _get_image(self, name):
208 def _get_image(self, name):
210 """ Returns the QImage stored as the ImageResource with 'name'.
209 """ Returns the QImage stored as the ImageResource with 'name'.
211 """
210 """
212 document = self._control.document()
211 document = self._control.document()
213 image = document.resource(QtGui.QTextDocument.ImageResource,
212 image = document.resource(QtGui.QTextDocument.ImageResource,
214 QtCore.QUrl(name))
213 QtCore.QUrl(name))
215 return image
214 return image
216
215
217 def _get_image_tag(self, match, path = None, format = "png"):
216 def _get_image_tag(self, match, path = None, format = "png"):
218 """ Return (X)HTML mark-up for the image-tag given by match.
217 """ Return (X)HTML mark-up for the image-tag given by match.
219
218
220 Parameters
219 Parameters
221 ----------
220 ----------
222 match : re.SRE_Match
221 match : re.SRE_Match
223 A match to an HTML image tag as exported by Qt, with
222 A match to an HTML image tag as exported by Qt, with
224 match.group("Name") containing the matched image ID.
223 match.group("Name") containing the matched image ID.
225
224
226 path : string|None, optional [default None]
225 path : string|None, optional [default None]
227 If not None, specifies a path to which supporting files may be
226 If not None, specifies a path to which supporting files may be
228 written (e.g., for linked images). If None, all images are to be
227 written (e.g., for linked images). If None, all images are to be
229 included inline.
228 included inline.
230
229
231 format : "png"|"svg"|"jpg", optional [default "png"]
230 format : "png"|"svg"|"jpg", optional [default "png"]
232 Format for returned or referenced images.
231 Format for returned or referenced images.
233 """
232 """
234 if format in ("png","jpg"):
233 if format in ("png","jpg"):
235 try:
234 try:
236 image = self._get_image(match.group("name"))
235 image = self._get_image(match.group("name"))
237 except KeyError:
236 except KeyError:
238 return "<b>Couldn't find image %s</b>" % match.group("name")
237 return "<b>Couldn't find image %s</b>" % match.group("name")
239
238
240 if path is not None:
239 if path is not None:
241 ensure_dir_exists(path)
240 ensure_dir_exists(path)
242 relpath = os.path.basename(path)
241 relpath = os.path.basename(path)
243 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
242 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
244 "PNG"):
243 "PNG"):
245 return '<img src="%s/qt_img%s.%s">' % (relpath,
244 return '<img src="%s/qt_img%s.%s">' % (relpath,
246 match.group("name"),format)
245 match.group("name"),format)
247 else:
246 else:
248 return "<b>Couldn't save image!</b>"
247 return "<b>Couldn't save image!</b>"
249 else:
248 else:
250 ba = QtCore.QByteArray()
249 ba = QtCore.QByteArray()
251 buffer_ = QtCore.QBuffer(ba)
250 buffer_ = QtCore.QBuffer(ba)
252 buffer_.open(QtCore.QIODevice.WriteOnly)
251 buffer_.open(QtCore.QIODevice.WriteOnly)
253 image.save(buffer_, format.upper())
252 image.save(buffer_, format.upper())
254 buffer_.close()
253 buffer_.close()
255 return '<img src="data:image/%s;base64,\n%s\n" />' % (
254 return '<img src="data:image/%s;base64,\n%s\n" />' % (
256 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
255 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
257
256
258 elif format == "svg":
257 elif format == "svg":
259 try:
258 try:
260 svg = str(self._name_to_svg_map[match.group("name")])
259 svg = str(self._name_to_svg_map[match.group("name")])
261 except KeyError:
260 except KeyError:
262 if not self._svg_warning_displayed:
261 if not self._svg_warning_displayed:
263 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
262 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
264 'Cannot convert PNG images to SVG, export with PNG figures instead. '
263 'Cannot convert PNG images to SVG, export with PNG figures instead. '
265 'If you want to export matplotlib figures as SVG, add '
264 'If you want to export matplotlib figures as SVG, add '
266 'to your ipython config:\n\n'
265 'to your ipython config:\n\n'
267 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
266 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
268 'And regenerate the figures.',
267 'And regenerate the figures.',
269 QtGui.QMessageBox.Ok)
268 QtGui.QMessageBox.Ok)
270 self._svg_warning_displayed = True
269 self._svg_warning_displayed = True
271 return ("<b>Cannot convert PNG images to SVG.</b> "
270 return ("<b>Cannot convert PNG images to SVG.</b> "
272 "You must export this session with PNG images. "
271 "You must export this session with PNG images. "
273 "If you want to export matplotlib figures as SVG, add to your config "
272 "If you want to export matplotlib figures as SVG, add to your config "
274 "<span>c.InlineBackend.figure_format = 'svg'</span> "
273 "<span>c.InlineBackend.figure_format = 'svg'</span> "
275 "and regenerate the figures.")
274 "and regenerate the figures.")
276
275
277 # Not currently checking path, because it's tricky to find a
276 # Not currently checking path, because it's tricky to find a
278 # cross-browser way to embed external SVG images (e.g., via
277 # cross-browser way to embed external SVG images (e.g., via
279 # object or embed tags).
278 # object or embed tags).
280
279
281 # Chop stand-alone header from matplotlib SVG
280 # Chop stand-alone header from matplotlib SVG
282 offset = svg.find("<svg")
281 offset = svg.find("<svg")
283 assert(offset > -1)
282 assert(offset > -1)
284
283
285 return svg[offset:]
284 return svg[offset:]
286
285
287 else:
286 else:
288 return '<b>Unrecognized image format</b>'
287 return '<b>Unrecognized image format</b>'
289
288
290 def _insert_jpg(self, cursor, jpg, metadata=None):
289 def _insert_jpg(self, cursor, jpg, metadata=None):
291 """ Insert raw PNG data into the widget."""
290 """ Insert raw PNG data into the widget."""
292 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
291 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
293
292
294 def _insert_png(self, cursor, png, metadata=None):
293 def _insert_png(self, cursor, png, metadata=None):
295 """ Insert raw PNG data into the widget.
294 """ Insert raw PNG data into the widget.
296 """
295 """
297 self._insert_img(cursor, png, 'png', metadata=metadata)
296 self._insert_img(cursor, png, 'png', metadata=metadata)
298
297
299 def _insert_img(self, cursor, img, fmt, metadata=None):
298 def _insert_img(self, cursor, img, fmt, metadata=None):
300 """ insert a raw image, jpg or png """
299 """ insert a raw image, jpg or png """
301 if metadata:
300 if metadata:
302 width = metadata.get('width', None)
301 width = metadata.get('width', None)
303 height = metadata.get('height', None)
302 height = metadata.get('height', None)
304 else:
303 else:
305 width = height = None
304 width = height = None
306 try:
305 try:
307 image = QtGui.QImage()
306 image = QtGui.QImage()
308 image.loadFromData(img, fmt.upper())
307 image.loadFromData(img, fmt.upper())
309 if width and height:
308 if width and height:
310 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
309 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
311 elif width and not height:
310 elif width and not height:
312 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
311 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
313 elif height and not width:
312 elif height and not width:
314 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
313 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
315 except ValueError:
314 except ValueError:
316 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
315 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
317 else:
316 else:
318 format = self._add_image(image)
317 format = self._add_image(image)
319 cursor.insertBlock()
318 cursor.insertBlock()
320 cursor.insertImage(format)
319 cursor.insertImage(format)
321 cursor.insertBlock()
320 cursor.insertBlock()
322
321
323 def _insert_svg(self, cursor, svg):
322 def _insert_svg(self, cursor, svg):
324 """ Insert raw SVG data into the widet.
323 """ Insert raw SVG data into the widet.
325 """
324 """
326 try:
325 try:
327 image = svg_to_image(svg)
326 image = svg_to_image(svg)
328 except ValueError:
327 except ValueError:
329 self._insert_plain_text(cursor, 'Received invalid SVG data.')
328 self._insert_plain_text(cursor, 'Received invalid SVG data.')
330 else:
329 else:
331 format = self._add_image(image)
330 format = self._add_image(image)
332 self._name_to_svg_map[format.name()] = svg
331 self._name_to_svg_map[format.name()] = svg
333 cursor.insertBlock()
332 cursor.insertBlock()
334 cursor.insertImage(format)
333 cursor.insertImage(format)
335 cursor.insertBlock()
334 cursor.insertBlock()
336
335
337 def _save_image(self, name, format='PNG'):
336 def _save_image(self, name, format='PNG'):
338 """ Shows a save dialog for the ImageResource with 'name'.
337 """ Shows a save dialog for the ImageResource with 'name'.
339 """
338 """
340 dialog = QtGui.QFileDialog(self._control, 'Save Image')
339 dialog = QtGui.QFileDialog(self._control, 'Save Image')
341 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
340 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
342 dialog.setDefaultSuffix(format.lower())
341 dialog.setDefaultSuffix(format.lower())
343 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
342 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
344 if dialog.exec_():
343 if dialog.exec_():
345 filename = dialog.selectedFiles()[0]
344 filename = dialog.selectedFiles()[0]
346 image = self._get_image(name)
345 image = self._get_image(name)
347 image.save(filename, format)
346 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now