##// END OF EJS Templates
Fixed #735. More useful error message in html output
Ian Murray -
Show More
@@ -1,295 +1,295 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 #---------------------------------------------------------------------------
32 #---------------------------------------------------------------------------
33 # 'object' interface
33 # 'object' interface
34 #---------------------------------------------------------------------------
34 #---------------------------------------------------------------------------
35
35
36 def __init__(self, *args, **kw):
36 def __init__(self, *args, **kw):
37 """ Create a RichIPythonWidget.
37 """ Create a RichIPythonWidget.
38 """
38 """
39 kw['kind'] = 'rich'
39 kw['kind'] = 'rich'
40 super(RichIPythonWidget, self).__init__(*args, **kw)
40 super(RichIPythonWidget, self).__init__(*args, **kw)
41
41
42 # Configure the ConsoleWidget HTML exporter for our formats.
42 # Configure the ConsoleWidget HTML exporter for our formats.
43 self._html_exporter.image_tag = self._get_image_tag
43 self._html_exporter.image_tag = self._get_image_tag
44
44
45 # Dictionary for resolving document resource names to SVG data.
45 # Dictionary for resolving document resource names to SVG data.
46 self._name_to_svg_map = {}
46 self._name_to_svg_map = {}
47
47
48 # Do we support jpg ?
48 # Do we support jpg ?
49 # it seems that sometime jpg support is a plugin of QT, so try to assume
49 # it seems that sometime jpg support is a plugin of QT, so try to assume
50 # it is not always supported.
50 # it is not always supported.
51 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
51 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
52 self._jpg_supported = 'jpeg' in _supported_format
52 self._jpg_supported = 'jpeg' in _supported_format
53
53
54
54
55 #---------------------------------------------------------------------------
55 #---------------------------------------------------------------------------
56 # 'ConsoleWidget' protected interface
56 # 'ConsoleWidget' protected interface
57 #---------------------------------------------------------------------------
57 #---------------------------------------------------------------------------
58
58
59 def _context_menu_make(self, pos):
59 def _context_menu_make(self, pos):
60 """ Reimplemented to return a custom context menu for images.
60 """ Reimplemented to return a custom context menu for images.
61 """
61 """
62 format = self._control.cursorForPosition(pos).charFormat()
62 format = self._control.cursorForPosition(pos).charFormat()
63 name = format.stringProperty(QtGui.QTextFormat.ImageName)
63 name = format.stringProperty(QtGui.QTextFormat.ImageName)
64 if name:
64 if name:
65 menu = QtGui.QMenu()
65 menu = QtGui.QMenu()
66
66
67 menu.addAction('Copy Image', lambda: self._copy_image(name))
67 menu.addAction('Copy Image', lambda: self._copy_image(name))
68 menu.addAction('Save Image As...', lambda: self._save_image(name))
68 menu.addAction('Save Image As...', lambda: self._save_image(name))
69 menu.addSeparator()
69 menu.addSeparator()
70
70
71 svg = self._name_to_svg_map.get(name, None)
71 svg = self._name_to_svg_map.get(name, None)
72 if svg is not None:
72 if svg is not None:
73 menu.addSeparator()
73 menu.addSeparator()
74 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
74 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
75 menu.addAction('Save SVG As...',
75 menu.addAction('Save SVG As...',
76 lambda: save_svg(svg, self._control))
76 lambda: save_svg(svg, self._control))
77 else:
77 else:
78 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
78 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
79 return menu
79 return menu
80
80
81 #---------------------------------------------------------------------------
81 #---------------------------------------------------------------------------
82 # 'BaseFrontendMixin' abstract interface
82 # 'BaseFrontendMixin' abstract interface
83 #---------------------------------------------------------------------------
83 #---------------------------------------------------------------------------
84 def _pre_image_append(self, msg, prompt_number):
84 def _pre_image_append(self, msg, prompt_number):
85 """ Append the Out[] prompt and make the output nicer
85 """ Append the Out[] prompt and make the output nicer
86
86
87 Shared code for some the following if statement
87 Shared code for some the following if statement
88 """
88 """
89 self.log.debug("pyout: %s", msg.get('content', ''))
89 self.log.debug("pyout: %s", msg.get('content', ''))
90 self._append_plain_text(self.output_sep, True)
90 self._append_plain_text(self.output_sep, True)
91 self._append_html(self._make_out_prompt(prompt_number), True)
91 self._append_html(self._make_out_prompt(prompt_number), True)
92 self._append_plain_text('\n', True)
92 self._append_plain_text('\n', True)
93
93
94 def _handle_pyout(self, msg):
94 def _handle_pyout(self, msg):
95 """ Overridden to handle rich data types, like SVG.
95 """ Overridden to handle rich data types, like SVG.
96 """
96 """
97 if not self._hidden and self._is_from_this_session(msg):
97 if not self._hidden and self._is_from_this_session(msg):
98 content = msg['content']
98 content = msg['content']
99 prompt_number = content.get('execution_count', 0)
99 prompt_number = content.get('execution_count', 0)
100 data = content['data']
100 data = content['data']
101 if data.has_key('image/svg+xml'):
101 if data.has_key('image/svg+xml'):
102 self._pre_image_append(msg, prompt_number)
102 self._pre_image_append(msg, prompt_number)
103 self._append_svg(data['image/svg+xml'], True)
103 self._append_svg(data['image/svg+xml'], True)
104 self._append_html(self.output_sep2, True)
104 self._append_html(self.output_sep2, True)
105 elif data.has_key('image/png'):
105 elif data.has_key('image/png'):
106 self._pre_image_append(msg, prompt_number)
106 self._pre_image_append(msg, prompt_number)
107 self._append_png(decodestring(data['image/png'].encode('ascii')), True)
107 self._append_png(decodestring(data['image/png'].encode('ascii')), True)
108 self._append_html(self.output_sep2, True)
108 self._append_html(self.output_sep2, True)
109 elif data.has_key('image/jpeg') and self._jpg_supported:
109 elif data.has_key('image/jpeg') and self._jpg_supported:
110 self._pre_image_append(msg, prompt_number)
110 self._pre_image_append(msg, prompt_number)
111 self._append_jpg(decodestring(data['image/jpeg'].encode('ascii')), True)
111 self._append_jpg(decodestring(data['image/jpeg'].encode('ascii')), True)
112 self._append_html(self.output_sep2, True)
112 self._append_html(self.output_sep2, True)
113 else:
113 else:
114 # Default back to the plain text representation.
114 # Default back to the plain text representation.
115 return super(RichIPythonWidget, self)._handle_pyout(msg)
115 return super(RichIPythonWidget, self)._handle_pyout(msg)
116
116
117 def _handle_display_data(self, msg):
117 def _handle_display_data(self, msg):
118 """ Overridden to handle rich data types, like SVG.
118 """ Overridden to handle rich data types, like SVG.
119 """
119 """
120 if not self._hidden and self._is_from_this_session(msg):
120 if not self._hidden and self._is_from_this_session(msg):
121 source = msg['content']['source']
121 source = msg['content']['source']
122 data = msg['content']['data']
122 data = msg['content']['data']
123 metadata = msg['content']['metadata']
123 metadata = msg['content']['metadata']
124 # Try to use the svg or html representations.
124 # Try to use the svg or html representations.
125 # FIXME: Is this the right ordering of things to try?
125 # FIXME: Is this the right ordering of things to try?
126 if data.has_key('image/svg+xml'):
126 if data.has_key('image/svg+xml'):
127 self.log.debug("display: %s", msg.get('content', ''))
127 self.log.debug("display: %s", msg.get('content', ''))
128 svg = data['image/svg+xml']
128 svg = data['image/svg+xml']
129 self._append_svg(svg, True)
129 self._append_svg(svg, True)
130 elif data.has_key('image/png'):
130 elif data.has_key('image/png'):
131 self.log.debug("display: %s", msg.get('content', ''))
131 self.log.debug("display: %s", msg.get('content', ''))
132 # PNG data is base64 encoded as it passes over the network
132 # PNG data is base64 encoded as it passes over the network
133 # in a JSON structure so we decode it.
133 # in a JSON structure so we decode it.
134 png = decodestring(data['image/png'].encode('ascii'))
134 png = decodestring(data['image/png'].encode('ascii'))
135 self._append_png(png, True)
135 self._append_png(png, True)
136 elif data.has_key('image/jpeg') and self._jpg_supported:
136 elif data.has_key('image/jpeg') and self._jpg_supported:
137 self.log.debug("display: %s", msg.get('content', ''))
137 self.log.debug("display: %s", msg.get('content', ''))
138 jpg = decodestring(data['image/jpeg'].encode('ascii'))
138 jpg = decodestring(data['image/jpeg'].encode('ascii'))
139 self._append_jpg(jpg, True)
139 self._append_jpg(jpg, True)
140 else:
140 else:
141 # Default back to the plain text representation.
141 # Default back to the plain text representation.
142 return super(RichIPythonWidget, self)._handle_display_data(msg)
142 return super(RichIPythonWidget, self)._handle_display_data(msg)
143
143
144 #---------------------------------------------------------------------------
144 #---------------------------------------------------------------------------
145 # 'RichIPythonWidget' protected interface
145 # 'RichIPythonWidget' protected interface
146 #---------------------------------------------------------------------------
146 #---------------------------------------------------------------------------
147
147
148 def _append_jpg(self, jpg, before_prompt=False):
148 def _append_jpg(self, jpg, before_prompt=False):
149 """ Append raw JPG data to the widget."""
149 """ Append raw JPG data to the widget."""
150 self._append_custom(self._insert_jpg, jpg, before_prompt)
150 self._append_custom(self._insert_jpg, jpg, before_prompt)
151
151
152 def _append_png(self, png, before_prompt=False):
152 def _append_png(self, png, before_prompt=False):
153 """ Append raw PNG data to the widget.
153 """ Append raw PNG data to the widget.
154 """
154 """
155 self._append_custom(self._insert_png, png, before_prompt)
155 self._append_custom(self._insert_png, png, before_prompt)
156
156
157 def _append_svg(self, svg, before_prompt=False):
157 def _append_svg(self, svg, before_prompt=False):
158 """ Append raw SVG data to the widget.
158 """ Append raw SVG data to the widget.
159 """
159 """
160 self._append_custom(self._insert_svg, svg, before_prompt)
160 self._append_custom(self._insert_svg, svg, before_prompt)
161
161
162 def _add_image(self, image):
162 def _add_image(self, image):
163 """ Adds the specified QImage to the document and returns a
163 """ Adds the specified QImage to the document and returns a
164 QTextImageFormat that references it.
164 QTextImageFormat that references it.
165 """
165 """
166 document = self._control.document()
166 document = self._control.document()
167 name = str(image.cacheKey())
167 name = str(image.cacheKey())
168 document.addResource(QtGui.QTextDocument.ImageResource,
168 document.addResource(QtGui.QTextDocument.ImageResource,
169 QtCore.QUrl(name), image)
169 QtCore.QUrl(name), image)
170 format = QtGui.QTextImageFormat()
170 format = QtGui.QTextImageFormat()
171 format.setName(name)
171 format.setName(name)
172 return format
172 return format
173
173
174 def _copy_image(self, name):
174 def _copy_image(self, name):
175 """ Copies the ImageResource with 'name' to the clipboard.
175 """ Copies the ImageResource with 'name' to the clipboard.
176 """
176 """
177 image = self._get_image(name)
177 image = self._get_image(name)
178 QtGui.QApplication.clipboard().setImage(image)
178 QtGui.QApplication.clipboard().setImage(image)
179
179
180 def _get_image(self, name):
180 def _get_image(self, name):
181 """ Returns the QImage stored as the ImageResource with 'name'.
181 """ Returns the QImage stored as the ImageResource with 'name'.
182 """
182 """
183 document = self._control.document()
183 document = self._control.document()
184 image = document.resource(QtGui.QTextDocument.ImageResource,
184 image = document.resource(QtGui.QTextDocument.ImageResource,
185 QtCore.QUrl(name))
185 QtCore.QUrl(name))
186 return image
186 return image
187
187
188 def _get_image_tag(self, match, path = None, format = "png"):
188 def _get_image_tag(self, match, path = None, format = "png"):
189 """ Return (X)HTML mark-up for the image-tag given by match.
189 """ Return (X)HTML mark-up for the image-tag given by match.
190
190
191 Parameters
191 Parameters
192 ----------
192 ----------
193 match : re.SRE_Match
193 match : re.SRE_Match
194 A match to an HTML image tag as exported by Qt, with
194 A match to an HTML image tag as exported by Qt, with
195 match.group("Name") containing the matched image ID.
195 match.group("Name") containing the matched image ID.
196
196
197 path : string|None, optional [default None]
197 path : string|None, optional [default None]
198 If not None, specifies a path to which supporting files may be
198 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
199 written (e.g., for linked images). If None, all images are to be
200 included inline.
200 included inline.
201
201
202 format : "png"|"svg"|"jpg", optional [default "png"]
202 format : "png"|"svg"|"jpg", optional [default "png"]
203 Format for returned or referenced images.
203 Format for returned or referenced images.
204 """
204 """
205 if format in ("png","jpg"):
205 if format in ("png","jpg"):
206 try:
206 try:
207 image = self._get_image(match.group("name"))
207 image = self._get_image(match.group("name"))
208 except KeyError:
208 except KeyError:
209 return "<b>Couldn't find image %s</b>" % match.group("name")
209 return "<b>Couldn't find image %s</b>" % match.group("name")
210
210
211 if path is not None:
211 if path is not None:
212 if not os.path.exists(path):
212 if not os.path.exists(path):
213 os.mkdir(path)
213 os.mkdir(path)
214 relpath = os.path.basename(path)
214 relpath = os.path.basename(path)
215 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
215 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
216 "PNG"):
216 "PNG"):
217 return '<img src="%s/qt_img%s.%s">' % (relpath,
217 return '<img src="%s/qt_img%s.%s">' % (relpath,
218 match.group("name"),format)
218 match.group("name"),format)
219 else:
219 else:
220 return "<b>Couldn't save image!</b>"
220 return "<b>Couldn't save image!</b>"
221 else:
221 else:
222 ba = QtCore.QByteArray()
222 ba = QtCore.QByteArray()
223 buffer_ = QtCore.QBuffer(ba)
223 buffer_ = QtCore.QBuffer(ba)
224 buffer_.open(QtCore.QIODevice.WriteOnly)
224 buffer_.open(QtCore.QIODevice.WriteOnly)
225 image.save(buffer_, format.upper())
225 image.save(buffer_, format.upper())
226 buffer_.close()
226 buffer_.close()
227 return '<img src="data:image/%s;base64,\n%s\n" />' % (
227 return '<img src="data:image/%s;base64,\n%s\n" />' % (
228 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
228 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
229
229
230 elif format == "svg":
230 elif format == "svg":
231 try:
231 try:
232 svg = str(self._name_to_svg_map[match.group("name")])
232 svg = str(self._name_to_svg_map[match.group("name")])
233 except KeyError:
233 except KeyError:
234 return "<b>Couldn't find image %s</b>" % match.group("name")
234 return "<b>Cannot convert a PNG to SVG. </b>To fix this, add this to your config: <span>c.InlineBackendConfig.figure_format = 'svg'</span>"
235
235
236 # Not currently checking path, because it's tricky to find a
236 # Not currently checking path, because it's tricky to find a
237 # cross-browser way to embed external SVG images (e.g., via
237 # cross-browser way to embed external SVG images (e.g., via
238 # object or embed tags).
238 # object or embed tags).
239
239
240 # Chop stand-alone header from matplotlib SVG
240 # Chop stand-alone header from matplotlib SVG
241 offset = svg.find("<svg")
241 offset = svg.find("<svg")
242 assert(offset > -1)
242 assert(offset > -1)
243
243
244 return svg[offset:]
244 return svg[offset:]
245
245
246 else:
246 else:
247 return '<b>Unrecognized image format</b>'
247 return '<b>Unrecognized image format</b>'
248
248
249 def _insert_jpg(self, cursor, jpg):
249 def _insert_jpg(self, cursor, jpg):
250 """ Insert raw PNG data into the widget."""
250 """ Insert raw PNG data into the widget."""
251 self._insert_img(cursor, jpg, 'jpg')
251 self._insert_img(cursor, jpg, 'jpg')
252
252
253 def _insert_png(self, cursor, png):
253 def _insert_png(self, cursor, png):
254 """ Insert raw PNG data into the widget.
254 """ Insert raw PNG data into the widget.
255 """
255 """
256 self._insert_img(cursor, png, 'png')
256 self._insert_img(cursor, png, 'png')
257
257
258 def _insert_img(self, cursor, img, fmt):
258 def _insert_img(self, cursor, img, fmt):
259 """ insert a raw image, jpg or png """
259 """ insert a raw image, jpg or png """
260 try:
260 try:
261 image = QtGui.QImage()
261 image = QtGui.QImage()
262 image.loadFromData(img, fmt.upper())
262 image.loadFromData(img, fmt.upper())
263 except ValueError:
263 except ValueError:
264 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
264 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
265 else:
265 else:
266 format = self._add_image(image)
266 format = self._add_image(image)
267 cursor.insertBlock()
267 cursor.insertBlock()
268 cursor.insertImage(format)
268 cursor.insertImage(format)
269 cursor.insertBlock()
269 cursor.insertBlock()
270
270
271 def _insert_svg(self, cursor, svg):
271 def _insert_svg(self, cursor, svg):
272 """ Insert raw SVG data into the widet.
272 """ Insert raw SVG data into the widet.
273 """
273 """
274 try:
274 try:
275 image = svg_to_image(svg)
275 image = svg_to_image(svg)
276 except ValueError:
276 except ValueError:
277 self._insert_plain_text(cursor, 'Received invalid SVG data.')
277 self._insert_plain_text(cursor, 'Received invalid SVG data.')
278 else:
278 else:
279 format = self._add_image(image)
279 format = self._add_image(image)
280 self._name_to_svg_map[format.name()] = svg
280 self._name_to_svg_map[format.name()] = svg
281 cursor.insertBlock()
281 cursor.insertBlock()
282 cursor.insertImage(format)
282 cursor.insertImage(format)
283 cursor.insertBlock()
283 cursor.insertBlock()
284
284
285 def _save_image(self, name, format='PNG'):
285 def _save_image(self, name, format='PNG'):
286 """ Shows a save dialog for the ImageResource with 'name'.
286 """ Shows a save dialog for the ImageResource with 'name'.
287 """
287 """
288 dialog = QtGui.QFileDialog(self._control, 'Save Image')
288 dialog = QtGui.QFileDialog(self._control, 'Save Image')
289 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
289 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
290 dialog.setDefaultSuffix(format.lower())
290 dialog.setDefaultSuffix(format.lower())
291 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
291 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
292 if dialog.exec_():
292 if dialog.exec_():
293 filename = dialog.selectedFiles()[0]
293 filename = dialog.selectedFiles()[0]
294 image = self._get_image(name)
294 image = self._get_image(name)
295 image.save(filename, format)
295 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now