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