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