##// END OF EJS Templates
Merge pull request #1449 from icmurray/master...
Thomas Kluyver -
r6867:b4a3ed44 merge
parent child Browse files
Show More
@@ -1,295 +1,325 b''
1 #-----------------------------------------------------------------------------
1 #-----------------------------------------------------------------------------
2 # Copyright (c) 2010, IPython Development Team.
2 # Copyright (c) 2010, IPython Development Team.
3 #
3 #
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5 #
5 #
6 # The full license is in the file COPYING.txt, distributed with this software.
6 # The full license is in the file COPYING.txt, distributed with this software.
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard libary imports.
9 # Standard libary imports.
10 from base64 import decodestring
10 from base64 import decodestring
11 import os
11 import os
12 import re
12 import re
13
13
14 # System libary imports.
14 # System libary imports.
15 from IPython.external.qt import QtCore, QtGui
15 from IPython.external.qt import QtCore, QtGui
16
16
17 # Local imports
17 # Local imports
18 from IPython.utils.traitlets import Bool
18 from IPython.utils.traitlets import Bool
19 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
19 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
20 from ipython_widget import IPythonWidget
20 from ipython_widget import IPythonWidget
21
21
22
22
23 class RichIPythonWidget(IPythonWidget):
23 class RichIPythonWidget(IPythonWidget):
24 """ An IPythonWidget that supports rich text, including lists, images, and
24 """ An IPythonWidget that supports rich text, including lists, images, and
25 tables. Note that raw performance will be reduced compared to the plain
25 tables. Note that raw performance will be reduced compared to the plain
26 text version.
26 text version.
27 """
27 """
28
28
29 # RichIPythonWidget protected class variables.
29 # RichIPythonWidget protected class variables.
30 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
30 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
31 _jpg_supported = Bool(False)
31 _jpg_supported = Bool(False)
32
33 # Used to determine whether a given html export attempt has already
34 # displayed a warning about being unable to convert a png to svg.
35 _svg_warning_displayed = False
36
32 #---------------------------------------------------------------------------
37 #---------------------------------------------------------------------------
33 # 'object' interface
38 # 'object' interface
34 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
35
40
36 def __init__(self, *args, **kw):
41 def __init__(self, *args, **kw):
37 """ Create a RichIPythonWidget.
42 """ Create a RichIPythonWidget.
38 """
43 """
39 kw['kind'] = 'rich'
44 kw['kind'] = 'rich'
40 super(RichIPythonWidget, self).__init__(*args, **kw)
45 super(RichIPythonWidget, self).__init__(*args, **kw)
41
46
42 # Configure the ConsoleWidget HTML exporter for our formats.
47 # Configure the ConsoleWidget HTML exporter for our formats.
43 self._html_exporter.image_tag = self._get_image_tag
48 self._html_exporter.image_tag = self._get_image_tag
44
49
45 # Dictionary for resolving document resource names to SVG data.
50 # Dictionary for resolving document resource names to SVG data.
46 self._name_to_svg_map = {}
51 self._name_to_svg_map = {}
47
52
48 # Do we support jpg ?
53 # Do we support jpg ?
49 # it seems that sometime jpg support is a plugin of QT, so try to assume
54 # it seems that sometime jpg support is a plugin of QT, so try to assume
50 # it is not always supported.
55 # it is not always supported.
51 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
52 self._jpg_supported = 'jpeg' in _supported_format
57 self._jpg_supported = 'jpeg' in _supported_format
53
58
54
59
55 #---------------------------------------------------------------------------
60 #---------------------------------------------------------------------------
61 # 'ConsoleWidget' public interface overides
62 #---------------------------------------------------------------------------
63
64 def export_html(self):
65 """ Shows a dialog to export HTML/XML in various formats.
66
67 Overridden in order to reset the _svg_warning_displayed flag prior
68 to the export running.
69 """
70 self._svg_warning_displayed = False
71 super(RichIPythonWidget, self).export_html()
72
73
74 #---------------------------------------------------------------------------
56 # 'ConsoleWidget' protected interface
75 # 'ConsoleWidget' protected interface
57 #---------------------------------------------------------------------------
76 #---------------------------------------------------------------------------
58
77
59 def _context_menu_make(self, pos):
78 def _context_menu_make(self, pos):
60 """ Reimplemented to return a custom context menu for images.
79 """ Reimplemented to return a custom context menu for images.
61 """
80 """
62 format = self._control.cursorForPosition(pos).charFormat()
81 format = self._control.cursorForPosition(pos).charFormat()
63 name = format.stringProperty(QtGui.QTextFormat.ImageName)
82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
64 if name:
83 if name:
65 menu = QtGui.QMenu()
84 menu = QtGui.QMenu()
66
85
67 menu.addAction('Copy Image', lambda: self._copy_image(name))
86 menu.addAction('Copy Image', lambda: self._copy_image(name))
68 menu.addAction('Save Image As...', lambda: self._save_image(name))
87 menu.addAction('Save Image As...', lambda: self._save_image(name))
69 menu.addSeparator()
88 menu.addSeparator()
70
89
71 svg = self._name_to_svg_map.get(name, None)
90 svg = self._name_to_svg_map.get(name, None)
72 if svg is not None:
91 if svg is not None:
73 menu.addSeparator()
92 menu.addSeparator()
74 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
75 menu.addAction('Save SVG As...',
94 menu.addAction('Save SVG As...',
76 lambda: save_svg(svg, self._control))
95 lambda: save_svg(svg, self._control))
77 else:
96 else:
78 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
79 return menu
98 return menu
80
99
81 #---------------------------------------------------------------------------
100 #---------------------------------------------------------------------------
82 # 'BaseFrontendMixin' abstract interface
101 # 'BaseFrontendMixin' abstract interface
83 #---------------------------------------------------------------------------
102 #---------------------------------------------------------------------------
84 def _pre_image_append(self, msg, prompt_number):
103 def _pre_image_append(self, msg, prompt_number):
85 """ Append the Out[] prompt and make the output nicer
104 """ Append the Out[] prompt and make the output nicer
86
105
87 Shared code for some the following if statement
106 Shared code for some the following if statement
88 """
107 """
89 self.log.debug("pyout: %s", msg.get('content', ''))
108 self.log.debug("pyout: %s", msg.get('content', ''))
90 self._append_plain_text(self.output_sep, True)
109 self._append_plain_text(self.output_sep, True)
91 self._append_html(self._make_out_prompt(prompt_number), True)
110 self._append_html(self._make_out_prompt(prompt_number), True)
92 self._append_plain_text('\n', True)
111 self._append_plain_text('\n', True)
93
112
94 def _handle_pyout(self, msg):
113 def _handle_pyout(self, msg):
95 """ Overridden to handle rich data types, like SVG.
114 """ Overridden to handle rich data types, like SVG.
96 """
115 """
97 if not self._hidden and self._is_from_this_session(msg):
116 if not self._hidden and self._is_from_this_session(msg):
98 content = msg['content']
117 content = msg['content']
99 prompt_number = content.get('execution_count', 0)
118 prompt_number = content.get('execution_count', 0)
100 data = content['data']
119 data = content['data']
101 if data.has_key('image/svg+xml'):
120 if data.has_key('image/svg+xml'):
102 self._pre_image_append(msg, prompt_number)
121 self._pre_image_append(msg, prompt_number)
103 self._append_svg(data['image/svg+xml'], True)
122 self._append_svg(data['image/svg+xml'], True)
104 self._append_html(self.output_sep2, True)
123 self._append_html(self.output_sep2, True)
105 elif data.has_key('image/png'):
124 elif data.has_key('image/png'):
106 self._pre_image_append(msg, prompt_number)
125 self._pre_image_append(msg, prompt_number)
107 self._append_png(decodestring(data['image/png'].encode('ascii')), True)
126 self._append_png(decodestring(data['image/png'].encode('ascii')), True)
108 self._append_html(self.output_sep2, True)
127 self._append_html(self.output_sep2, True)
109 elif data.has_key('image/jpeg') and self._jpg_supported:
128 elif data.has_key('image/jpeg') and self._jpg_supported:
110 self._pre_image_append(msg, prompt_number)
129 self._pre_image_append(msg, prompt_number)
111 self._append_jpg(decodestring(data['image/jpeg'].encode('ascii')), True)
130 self._append_jpg(decodestring(data['image/jpeg'].encode('ascii')), True)
112 self._append_html(self.output_sep2, True)
131 self._append_html(self.output_sep2, True)
113 else:
132 else:
114 # Default back to the plain text representation.
133 # Default back to the plain text representation.
115 return super(RichIPythonWidget, self)._handle_pyout(msg)
134 return super(RichIPythonWidget, self)._handle_pyout(msg)
116
135
117 def _handle_display_data(self, msg):
136 def _handle_display_data(self, msg):
118 """ Overridden to handle rich data types, like SVG.
137 """ Overridden to handle rich data types, like SVG.
119 """
138 """
120 if not self._hidden and self._is_from_this_session(msg):
139 if not self._hidden and self._is_from_this_session(msg):
121 source = msg['content']['source']
140 source = msg['content']['source']
122 data = msg['content']['data']
141 data = msg['content']['data']
123 metadata = msg['content']['metadata']
142 metadata = msg['content']['metadata']
124 # Try to use the svg or html representations.
143 # Try to use the svg or html representations.
125 # FIXME: Is this the right ordering of things to try?
144 # FIXME: Is this the right ordering of things to try?
126 if data.has_key('image/svg+xml'):
145 if data.has_key('image/svg+xml'):
127 self.log.debug("display: %s", msg.get('content', ''))
146 self.log.debug("display: %s", msg.get('content', ''))
128 svg = data['image/svg+xml']
147 svg = data['image/svg+xml']
129 self._append_svg(svg, True)
148 self._append_svg(svg, True)
130 elif data.has_key('image/png'):
149 elif data.has_key('image/png'):
131 self.log.debug("display: %s", msg.get('content', ''))
150 self.log.debug("display: %s", msg.get('content', ''))
132 # PNG data is base64 encoded as it passes over the network
151 # PNG data is base64 encoded as it passes over the network
133 # in a JSON structure so we decode it.
152 # in a JSON structure so we decode it.
134 png = decodestring(data['image/png'].encode('ascii'))
153 png = decodestring(data['image/png'].encode('ascii'))
135 self._append_png(png, True)
154 self._append_png(png, True)
136 elif data.has_key('image/jpeg') and self._jpg_supported:
155 elif data.has_key('image/jpeg') and self._jpg_supported:
137 self.log.debug("display: %s", msg.get('content', ''))
156 self.log.debug("display: %s", msg.get('content', ''))
138 jpg = decodestring(data['image/jpeg'].encode('ascii'))
157 jpg = decodestring(data['image/jpeg'].encode('ascii'))
139 self._append_jpg(jpg, True)
158 self._append_jpg(jpg, True)
140 else:
159 else:
141 # Default back to the plain text representation.
160 # Default back to the plain text representation.
142 return super(RichIPythonWidget, self)._handle_display_data(msg)
161 return super(RichIPythonWidget, self)._handle_display_data(msg)
143
162
144 #---------------------------------------------------------------------------
163 #---------------------------------------------------------------------------
145 # 'RichIPythonWidget' protected interface
164 # 'RichIPythonWidget' protected interface
146 #---------------------------------------------------------------------------
165 #---------------------------------------------------------------------------
147
166
148 def _append_jpg(self, jpg, before_prompt=False):
167 def _append_jpg(self, jpg, before_prompt=False):
149 """ Append raw JPG data to the widget."""
168 """ Append raw JPG data to the widget."""
150 self._append_custom(self._insert_jpg, jpg, before_prompt)
169 self._append_custom(self._insert_jpg, jpg, before_prompt)
151
170
152 def _append_png(self, png, before_prompt=False):
171 def _append_png(self, png, before_prompt=False):
153 """ Append raw PNG data to the widget.
172 """ Append raw PNG data to the widget.
154 """
173 """
155 self._append_custom(self._insert_png, png, before_prompt)
174 self._append_custom(self._insert_png, png, before_prompt)
156
175
157 def _append_svg(self, svg, before_prompt=False):
176 def _append_svg(self, svg, before_prompt=False):
158 """ Append raw SVG data to the widget.
177 """ Append raw SVG data to the widget.
159 """
178 """
160 self._append_custom(self._insert_svg, svg, before_prompt)
179 self._append_custom(self._insert_svg, svg, before_prompt)
161
180
162 def _add_image(self, image):
181 def _add_image(self, image):
163 """ Adds the specified QImage to the document and returns a
182 """ Adds the specified QImage to the document and returns a
164 QTextImageFormat that references it.
183 QTextImageFormat that references it.
165 """
184 """
166 document = self._control.document()
185 document = self._control.document()
167 name = str(image.cacheKey())
186 name = str(image.cacheKey())
168 document.addResource(QtGui.QTextDocument.ImageResource,
187 document.addResource(QtGui.QTextDocument.ImageResource,
169 QtCore.QUrl(name), image)
188 QtCore.QUrl(name), image)
170 format = QtGui.QTextImageFormat()
189 format = QtGui.QTextImageFormat()
171 format.setName(name)
190 format.setName(name)
172 return format
191 return format
173
192
174 def _copy_image(self, name):
193 def _copy_image(self, name):
175 """ Copies the ImageResource with 'name' to the clipboard.
194 """ Copies the ImageResource with 'name' to the clipboard.
176 """
195 """
177 image = self._get_image(name)
196 image = self._get_image(name)
178 QtGui.QApplication.clipboard().setImage(image)
197 QtGui.QApplication.clipboard().setImage(image)
179
198
180 def _get_image(self, name):
199 def _get_image(self, name):
181 """ Returns the QImage stored as the ImageResource with 'name'.
200 """ Returns the QImage stored as the ImageResource with 'name'.
182 """
201 """
183 document = self._control.document()
202 document = self._control.document()
184 image = document.resource(QtGui.QTextDocument.ImageResource,
203 image = document.resource(QtGui.QTextDocument.ImageResource,
185 QtCore.QUrl(name))
204 QtCore.QUrl(name))
186 return image
205 return image
187
206
188 def _get_image_tag(self, match, path = None, format = "png"):
207 def _get_image_tag(self, match, path = None, format = "png"):
189 """ Return (X)HTML mark-up for the image-tag given by match.
208 """ Return (X)HTML mark-up for the image-tag given by match.
190
209
191 Parameters
210 Parameters
192 ----------
211 ----------
193 match : re.SRE_Match
212 match : re.SRE_Match
194 A match to an HTML image tag as exported by Qt, with
213 A match to an HTML image tag as exported by Qt, with
195 match.group("Name") containing the matched image ID.
214 match.group("Name") containing the matched image ID.
196
215
197 path : string|None, optional [default None]
216 path : string|None, optional [default None]
198 If not None, specifies a path to which supporting files may be
217 If not None, specifies a path to which supporting files may be
199 written (e.g., for linked images). If None, all images are to be
218 written (e.g., for linked images). If None, all images are to be
200 included inline.
219 included inline.
201
220
202 format : "png"|"svg"|"jpg", optional [default "png"]
221 format : "png"|"svg"|"jpg", optional [default "png"]
203 Format for returned or referenced images.
222 Format for returned or referenced images.
204 """
223 """
205 if format in ("png","jpg"):
224 if format in ("png","jpg"):
206 try:
225 try:
207 image = self._get_image(match.group("name"))
226 image = self._get_image(match.group("name"))
208 except KeyError:
227 except KeyError:
209 return "<b>Couldn't find image %s</b>" % match.group("name")
228 return "<b>Couldn't find image %s</b>" % match.group("name")
210
229
211 if path is not None:
230 if path is not None:
212 if not os.path.exists(path):
231 if not os.path.exists(path):
213 os.mkdir(path)
232 os.mkdir(path)
214 relpath = os.path.basename(path)
233 relpath = os.path.basename(path)
215 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
234 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
216 "PNG"):
235 "PNG"):
217 return '<img src="%s/qt_img%s.%s">' % (relpath,
236 return '<img src="%s/qt_img%s.%s">' % (relpath,
218 match.group("name"),format)
237 match.group("name"),format)
219 else:
238 else:
220 return "<b>Couldn't save image!</b>"
239 return "<b>Couldn't save image!</b>"
221 else:
240 else:
222 ba = QtCore.QByteArray()
241 ba = QtCore.QByteArray()
223 buffer_ = QtCore.QBuffer(ba)
242 buffer_ = QtCore.QBuffer(ba)
224 buffer_.open(QtCore.QIODevice.WriteOnly)
243 buffer_.open(QtCore.QIODevice.WriteOnly)
225 image.save(buffer_, format.upper())
244 image.save(buffer_, format.upper())
226 buffer_.close()
245 buffer_.close()
227 return '<img src="data:image/%s;base64,\n%s\n" />' % (
246 return '<img src="data:image/%s;base64,\n%s\n" />' % (
228 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
247 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
229
248
230 elif format == "svg":
249 elif format == "svg":
231 try:
250 try:
232 svg = str(self._name_to_svg_map[match.group("name")])
251 svg = str(self._name_to_svg_map[match.group("name")])
233 except KeyError:
252 except KeyError:
234 return "<b>Couldn't find image %s</b>" % match.group("name")
253 if not self._svg_warning_displayed:
254 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
255 'Cannot convert a PNG to SVG. To fix this, add this '
256 'to your ipython config:\n\n'
257 '\tc.InlineBackendConfig.figure_format = \'svg\'\n\n'
258 'And regenerate the figures.',
259 QtGui.QMessageBox.Ok)
260 self._svg_warning_displayed = True
261 return ("<b>Cannot convert a PNG to SVG.</b> "
262 "To fix this, add this to your config: "
263 "<span>c.InlineBackendConfig.figure_format = 'svg'</span> "
264 "and regenerate the figures.")
235
265
236 # Not currently checking path, because it's tricky to find a
266 # Not currently checking path, because it's tricky to find a
237 # cross-browser way to embed external SVG images (e.g., via
267 # cross-browser way to embed external SVG images (e.g., via
238 # object or embed tags).
268 # object or embed tags).
239
269
240 # Chop stand-alone header from matplotlib SVG
270 # Chop stand-alone header from matplotlib SVG
241 offset = svg.find("<svg")
271 offset = svg.find("<svg")
242 assert(offset > -1)
272 assert(offset > -1)
243
273
244 return svg[offset:]
274 return svg[offset:]
245
275
246 else:
276 else:
247 return '<b>Unrecognized image format</b>'
277 return '<b>Unrecognized image format</b>'
248
278
249 def _insert_jpg(self, cursor, jpg):
279 def _insert_jpg(self, cursor, jpg):
250 """ Insert raw PNG data into the widget."""
280 """ Insert raw PNG data into the widget."""
251 self._insert_img(cursor, jpg, 'jpg')
281 self._insert_img(cursor, jpg, 'jpg')
252
282
253 def _insert_png(self, cursor, png):
283 def _insert_png(self, cursor, png):
254 """ Insert raw PNG data into the widget.
284 """ Insert raw PNG data into the widget.
255 """
285 """
256 self._insert_img(cursor, png, 'png')
286 self._insert_img(cursor, png, 'png')
257
287
258 def _insert_img(self, cursor, img, fmt):
288 def _insert_img(self, cursor, img, fmt):
259 """ insert a raw image, jpg or png """
289 """ insert a raw image, jpg or png """
260 try:
290 try:
261 image = QtGui.QImage()
291 image = QtGui.QImage()
262 image.loadFromData(img, fmt.upper())
292 image.loadFromData(img, fmt.upper())
263 except ValueError:
293 except ValueError:
264 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
294 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
265 else:
295 else:
266 format = self._add_image(image)
296 format = self._add_image(image)
267 cursor.insertBlock()
297 cursor.insertBlock()
268 cursor.insertImage(format)
298 cursor.insertImage(format)
269 cursor.insertBlock()
299 cursor.insertBlock()
270
300
271 def _insert_svg(self, cursor, svg):
301 def _insert_svg(self, cursor, svg):
272 """ Insert raw SVG data into the widet.
302 """ Insert raw SVG data into the widet.
273 """
303 """
274 try:
304 try:
275 image = svg_to_image(svg)
305 image = svg_to_image(svg)
276 except ValueError:
306 except ValueError:
277 self._insert_plain_text(cursor, 'Received invalid SVG data.')
307 self._insert_plain_text(cursor, 'Received invalid SVG data.')
278 else:
308 else:
279 format = self._add_image(image)
309 format = self._add_image(image)
280 self._name_to_svg_map[format.name()] = svg
310 self._name_to_svg_map[format.name()] = svg
281 cursor.insertBlock()
311 cursor.insertBlock()
282 cursor.insertImage(format)
312 cursor.insertImage(format)
283 cursor.insertBlock()
313 cursor.insertBlock()
284
314
285 def _save_image(self, name, format='PNG'):
315 def _save_image(self, name, format='PNG'):
286 """ Shows a save dialog for the ImageResource with 'name'.
316 """ Shows a save dialog for the ImageResource with 'name'.
287 """
317 """
288 dialog = QtGui.QFileDialog(self._control, 'Save Image')
318 dialog = QtGui.QFileDialog(self._control, 'Save Image')
289 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
319 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
290 dialog.setDefaultSuffix(format.lower())
320 dialog.setDefaultSuffix(format.lower())
291 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
321 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
292 if dialog.exec_():
322 if dialog.exec_():
293 filename = dialog.selectedFiles()[0]
323 filename = dialog.selectedFiles()[0]
294 image = self._get_image(name)
324 image = self._get_image(name)
295 image.save(filename, format)
325 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now