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