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