1
# Standard libary imports.
2
from base64 import decodestring
6
# System libary imports.
7
from IPython.external.qt import QtCore, QtGui
10
from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
11
from ipython_widget import IPythonWidget
14
class RichIPythonWidget(IPythonWidget):
15
""" An IPythonWidget that supports rich text, including lists, images, and
16
tables. Note that raw performance will be reduced compared to the plain
20
# RichIPythonWidget protected class variables.
21
_payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
23
#---------------------------------------------------------------------------
25
#---------------------------------------------------------------------------
27
def __init__(self, *args, **kw):
28
""" Create a RichIPythonWidget.
31
super(RichIPythonWidget, self).__init__(*args, **kw)
33
# Configure the ConsoleWidget HTML exporter for our formats.
34
self._html_exporter.image_tag = self._get_image_tag
36
# Dictionary for resolving document resource names to SVG data.
37
self._name_to_svg_map = {}
39
#---------------------------------------------------------------------------
40
# 'ConsoleWidget' protected interface
41
#---------------------------------------------------------------------------
43
def _context_menu_make(self, pos):
44
""" Reimplemented to return a custom context menu for images.
46
format = self._control.cursorForPosition(pos).charFormat()
47
name = format.stringProperty(QtGui.QTextFormat.ImageName)
51
menu.addAction('Copy Image', lambda: self._copy_image(name))
52
menu.addAction('Save Image As...', lambda: self._save_image(name))
55
svg = self._name_to_svg_map.get(name, None)
58
menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
59
menu.addAction('Save SVG As...',
60
lambda: save_svg(svg, self._control))
62
menu = super(RichIPythonWidget, self)._context_menu_make(pos)
65
#---------------------------------------------------------------------------
66
# 'BaseFrontendMixin' abstract interface
67
#---------------------------------------------------------------------------
69
def _handle_pyout(self, msg):
70
""" Overridden to handle rich data types, like SVG.
72
if not self._hidden and self._is_from_this_session(msg):
73
content = msg['content']
74
prompt_number = content['execution_count']
75
data = content['data']
76
if data.has_key('image/svg+xml'):
77
self._append_plain_text(self.output_sep, True)
78
self._append_html(self._make_out_prompt(prompt_number), True)
79
self._append_svg(data['image/svg+xml'], True)
80
self._append_html(self.output_sep2, True)
81
elif data.has_key('image/png'):
82
self._append_plain_text(self.output_sep, True)
83
self._append_html(self._make_out_prompt(prompt_number), True)
84
# This helps the output to look nice.
85
self._append_plain_text('\n', True)
86
self._append_png(decodestring(data['image/png']), True)
87
self._append_html(self.output_sep2, True)
89
# Default back to the plain text representation.
90
return super(RichIPythonWidget, self)._handle_pyout(msg)
92
def _handle_display_data(self, msg):
93
""" Overridden to handle rich data types, like SVG.
95
if not self._hidden and self._is_from_this_session(msg):
96
source = msg['content']['source']
97
data = msg['content']['data']
98
metadata = msg['content']['metadata']
99
# Try to use the svg or html representations.
100
# FIXME: Is this the right ordering of things to try?
101
if data.has_key('image/svg+xml'):
102
svg = data['image/svg+xml']
103
self._append_svg(svg, True)
104
elif data.has_key('image/png'):
105
# PNG data is base64 encoded as it passes over the network
106
# in a JSON structure so we decode it.
107
png = decodestring(data['image/png'])
108
self._append_png(png, True)
110
# Default back to the plain text representation.
111
return super(RichIPythonWidget, self)._handle_display_data(msg)
113
#---------------------------------------------------------------------------
114
# 'RichIPythonWidget' protected interface
115
#---------------------------------------------------------------------------
117
def _append_png(self, png, before_prompt=False):
118
""" Append raw PNG data to the widget.
120
self._append_custom(self._insert_png, png, before_prompt)
122
def _append_svg(self, svg, before_prompt=False):
123
""" Append raw SVG data to the widget.
125
self._append_custom(self._insert_svg, svg, before_prompt)
127
def _add_image(self, image):
128
""" Adds the specified QImage to the document and returns a
129
QTextImageFormat that references it.
131
document = self._control.document()
132
name = str(image.cacheKey())
133
document.addResource(QtGui.QTextDocument.ImageResource,
134
QtCore.QUrl(name), image)
135
format = QtGui.QTextImageFormat()
139
def _copy_image(self, name):
140
""" Copies the ImageResource with 'name' to the clipboard.
142
image = self._get_image(name)
143
QtGui.QApplication.clipboard().setImage(image)
145
def _get_image(self, name):
146
""" Returns the QImage stored as the ImageResource with 'name'.
148
document = self._control.document()
149
image = document.resource(QtGui.QTextDocument.ImageResource,
153
def _get_image_tag(self, match, path = None, format = "png"):
154
""" Return (X)HTML mark-up for the image-tag given by match.
159
A match to an HTML image tag as exported by Qt, with
160
match.group("Name") containing the matched image ID.
162
path : string|None, optional [default None]
163
If not None, specifies a path to which supporting files may be
164
written (e.g., for linked images). If None, all images are to be
167
format : "png"|"svg", optional [default "png"]
168
Format for returned or referenced images.
172
image = self._get_image(match.group("name"))
174
return "<b>Couldn't find image %s</b>" % match.group("name")
177
if not os.path.exists(path):
179
relpath = os.path.basename(path)
180
if image.save("%s/qt_img%s.png" % (path,match.group("name")),
182
return '<img src="%s/qt_img%s.png">' % (relpath,
185
return "<b>Couldn't save image!</b>"
187
ba = QtCore.QByteArray()
188
buffer_ = QtCore.QBuffer(ba)
189
buffer_.open(QtCore.QIODevice.WriteOnly)
190
image.save(buffer_, "PNG")
192
return '<img src="data:image/png;base64,\n%s\n" />' % (
193
re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
195
elif format == "svg":
197
svg = str(self._name_to_svg_map[match.group("name")])
199
return "<b>Couldn't find image %s</b>" % match.group("name")
201
# Not currently checking path, because it's tricky to find a
202
# cross-browser way to embed external SVG images (e.g., via
203
# object or embed tags).
205
# Chop stand-alone header from matplotlib SVG
206
offset = svg.find("<svg")
212
return '<b>Unrecognized image format</b>'
214
def _insert_png(self, cursor, png):
215
""" Insert raw PNG data into the widget.
218
image = QtGui.QImage()
219
image.loadFromData(png, 'PNG')
221
self._insert_plain_text(cursor, 'Received invalid PNG data.')
223
format = self._add_image(image)
225
cursor.insertImage(format)
228
def _insert_svg(self, cursor, svg):
229
""" Insert raw SVG data into the widet.
232
image = svg_to_image(svg)
234
self._insert_plain_text(cursor, 'Received invalid SVG data.')
236
format = self._add_image(image)
237
self._name_to_svg_map[format.name()] = svg
239
cursor.insertImage(format)
242
def _save_image(self, name, format='PNG'):
243
""" Shows a save dialog for the ImageResource with 'name'.
245
dialog = QtGui.QFileDialog(self._control, 'Save Image')
246
dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
247
dialog.setDefaultSuffix(format.lower())
248
dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
250
filename = dialog.selectedFiles()[0]
251
image = self._get_image(name)
252
image.save(filename, format)