rich_ipython_widget.py
249 lines
| 9.8 KiB
| text/x-python
|
PythonLexer
Evan Patterson
|
r3304 | # Standard libary imports. | ||
from base64 import decodestring | ||||
MinRK
|
r3154 | import os | ||
import re | ||||
Evan Patterson
|
r3304 | |||
# System libary imports. | ||||
from IPython.external.qt import QtCore, QtGui | ||||
epatters
|
r2758 | |||
# Local imports | ||||
epatters
|
r2765 | from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image | ||
epatters
|
r2758 | from ipython_widget import IPythonWidget | ||
class RichIPythonWidget(IPythonWidget): | ||||
""" An IPythonWidget that supports rich text, including lists, images, and | ||||
tables. Note that raw performance will be reduced compared to the plain | ||||
text version. | ||||
""" | ||||
epatters
|
r2835 | # RichIPythonWidget protected class variables. | ||
_payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload' | ||||
epatters
|
r2765 | |||
epatters
|
r2758 | #--------------------------------------------------------------------------- | ||
epatters
|
r2776 | # 'object' interface | ||
epatters
|
r2758 | #--------------------------------------------------------------------------- | ||
epatters
|
r2776 | def __init__(self, *args, **kw): | ||
epatters
|
r2758 | """ Create a RichIPythonWidget. | ||
""" | ||||
epatters
|
r2776 | kw['kind'] = 'rich' | ||
super(RichIPythonWidget, self).__init__(*args, **kw) | ||||
epatters
|
r3361 | |||
# Configure the ConsoleWidget HTML exporter for our formats. | ||||
self._html_exporter.image_tag = self._get_image_tag | ||||
epatters
|
r3364 | # Dictionary for resolving document resource names to SVG data. | ||
self._name_to_svg_map = {} | ||||
epatters
|
r2765 | |||
#--------------------------------------------------------------------------- | ||||
# 'ConsoleWidget' protected interface | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2990 | def _context_menu_make(self, pos): | ||
""" Reimplemented to return a custom context menu for images. | ||||
epatters
|
r2765 | """ | ||
format = self._control.cursorForPosition(pos).charFormat() | ||||
name = format.stringProperty(QtGui.QTextFormat.ImageName) | ||||
epatters
|
r3307 | if name: | ||
epatters
|
r2765 | menu = QtGui.QMenu() | ||
menu.addAction('Copy Image', lambda: self._copy_image(name)) | ||||
menu.addAction('Save Image As...', lambda: self._save_image(name)) | ||||
menu.addSeparator() | ||||
epatters
|
r3364 | svg = self._name_to_svg_map.get(name, None) | ||
if svg is not None: | ||||
epatters
|
r2765 | menu.addSeparator() | ||
menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg)) | ||||
menu.addAction('Save SVG As...', | ||||
lambda: save_svg(svg, self._control)) | ||||
epatters
|
r3307 | else: | ||
menu = super(RichIPythonWidget, self)._context_menu_make(pos) | ||||
epatters
|
r2990 | return menu | ||
Brian Granger
|
r3277 | |||
#--------------------------------------------------------------------------- | ||||
# 'BaseFrontendMixin' abstract interface | ||||
#--------------------------------------------------------------------------- | ||||
Brian Granger
|
r3278 | def _handle_pyout(self, msg): | ||
""" Overridden to handle rich data types, like SVG. | ||||
""" | ||||
if not self._hidden and self._is_from_this_session(msg): | ||||
content = msg['content'] | ||||
prompt_number = content['execution_count'] | ||||
data = content['data'] | ||||
if data.has_key('image/svg+xml'): | ||||
self._append_plain_text(self.output_sep) | ||||
self._append_html(self._make_out_prompt(prompt_number)) | ||||
# TODO: try/except this call. | ||||
self._append_svg(data['image/svg+xml']) | ||||
self._append_html(self.output_sep2) | ||||
Brian Granger
|
r3279 | elif data.has_key('image/png'): | ||
self._append_plain_text(self.output_sep) | ||||
self._append_html(self._make_out_prompt(prompt_number)) | ||||
Brian Granger
|
r3284 | # This helps the output to look nice. | ||
self._append_plain_text('\n') | ||||
Brian Granger
|
r3279 | # TODO: try/except these calls | ||
png = decodestring(data['image/png']) | ||||
self._append_png(png) | ||||
self._append_html(self.output_sep2) | ||||
Brian Granger
|
r3278 | else: | ||
# Default back to the plain text representation. | ||||
return super(RichIPythonWidget, self)._handle_pyout(msg) | ||||
Brian Granger
|
r3277 | def _handle_display_data(self, msg): | ||
Brian Granger
|
r3278 | """ Overridden to handle rich data types, like SVG. | ||
Brian Granger
|
r3277 | """ | ||
if not self._hidden and self._is_from_this_session(msg): | ||||
source = msg['content']['source'] | ||||
data = msg['content']['data'] | ||||
metadata = msg['content']['metadata'] | ||||
# Try to use the svg or html representations. | ||||
# FIXME: Is this the right ordering of things to try? | ||||
if data.has_key('image/svg+xml'): | ||||
svg = data['image/svg+xml'] | ||||
# TODO: try/except this call. | ||||
self._append_svg(svg) | ||||
Brian Granger
|
r3279 | elif data.has_key('image/png'): | ||
# TODO: try/except these calls | ||||
# PNG data is base64 encoded as it passes over the network | ||||
# in a JSON structure so we decode it. | ||||
png = decodestring(data['image/png']) | ||||
self._append_png(png) | ||||
Brian Granger
|
r3277 | else: | ||
# Default back to the plain text representation. | ||||
return super(RichIPythonWidget, self)._handle_display_data(msg) | ||||
epatters
|
r2758 | #--------------------------------------------------------------------------- | ||
epatters
|
r2765 | # 'RichIPythonWidget' protected interface | ||
#--------------------------------------------------------------------------- | ||||
Brian Granger
|
r3277 | def _append_svg(self, svg): | ||
""" Append raw svg data to the widget. | ||||
""" | ||||
try: | ||||
image = svg_to_image(svg) | ||||
except ValueError: | ||||
self._append_plain_text('Received invalid plot data.') | ||||
else: | ||||
format = self._add_image(image) | ||||
epatters
|
r3364 | self._name_to_svg_map[format.name()] = svg | ||
Brian Granger
|
r3277 | cursor = self._get_end_cursor() | ||
cursor.insertBlock() | ||||
cursor.insertImage(format) | ||||
Brian Granger
|
r3279 | cursor.insertBlock() | ||
def _append_png(self, png): | ||||
""" Append raw svg data to the widget. | ||||
""" | ||||
try: | ||||
image = QtGui.QImage() | ||||
image.loadFromData(png, 'PNG') | ||||
except ValueError: | ||||
self._append_plain_text('Received invalid plot data.') | ||||
else: | ||||
format = self._add_image(image) | ||||
cursor = self._get_end_cursor() | ||||
cursor.insertBlock() | ||||
cursor.insertImage(format) | ||||
Brian Granger
|
r3277 | cursor.insertBlock() | ||
epatters
|
r2765 | def _add_image(self, image): | ||
""" Adds the specified QImage to the document and returns a | ||||
QTextImageFormat that references it. | ||||
""" | ||||
document = self._control.document() | ||||
Evan Patterson
|
r3304 | name = str(image.cacheKey()) | ||
epatters
|
r2765 | document.addResource(QtGui.QTextDocument.ImageResource, | ||
QtCore.QUrl(name), image) | ||||
format = QtGui.QTextImageFormat() | ||||
format.setName(name) | ||||
return format | ||||
def _copy_image(self, name): | ||||
""" Copies the ImageResource with 'name' to the clipboard. | ||||
""" | ||||
image = self._get_image(name) | ||||
QtGui.QApplication.clipboard().setImage(image) | ||||
def _get_image(self, name): | ||||
""" Returns the QImage stored as the ImageResource with 'name'. | ||||
""" | ||||
document = self._control.document() | ||||
epatters
|
r3364 | image = document.resource(QtGui.QTextDocument.ImageResource, | ||
QtCore.QUrl(name)) | ||||
return image | ||||
epatters
|
r2765 | |||
epatters
|
r3361 | def _get_image_tag(self, match, path = None, format = "png"): | ||
Mark Voorhies
|
r3132 | """ Return (X)HTML mark-up for the image-tag given by match. | ||
Parameters | ||||
---------- | ||||
epatters
|
r3361 | match : re.SRE_Match | ||
Mark Voorhies
|
r3132 | A match to an HTML image tag as exported by Qt, with | ||
match.group("Name") containing the matched image ID. | ||||
path : string|None, optional [default None] | ||||
epatters
|
r3361 | If not None, specifies a path to which supporting files may be | ||
written (e.g., for linked images). If None, all images are to be | ||||
included inline. | ||||
Mark Voorhies
|
r3132 | |||
format : "png"|"svg", optional [default "png"] | ||||
Format for returned or referenced images. | ||||
""" | ||||
epatters
|
r3361 | if format == "png": | ||
Mark Voorhies
|
r3125 | try: | ||
image = self._get_image(match.group("name")) | ||||
except KeyError: | ||||
return "<b>Couldn't find image %s</b>" % match.group("name") | ||||
epatters
|
r3361 | if path is not None: | ||
MinRK
|
r3154 | if not os.path.exists(path): | ||
os.mkdir(path) | ||||
relpath = os.path.basename(path) | ||||
epatters
|
r3361 | if image.save("%s/qt_img%s.png" % (path,match.group("name")), | ||
"PNG"): | ||||
Mark Voorhies
|
r3125 | return '<img src="%s/qt_img%s.png">' % (relpath, | ||
match.group("name")) | ||||
else: | ||||
return "<b>Couldn't save image!</b>" | ||||
else: | ||||
ba = QtCore.QByteArray() | ||||
buffer_ = QtCore.QBuffer(ba) | ||||
buffer_.open(QtCore.QIODevice.WriteOnly) | ||||
image.save(buffer_, "PNG") | ||||
buffer_.close() | ||||
return '<img src="data:image/png;base64,\n%s\n" />' % ( | ||||
re.sub(r'(.{60})',r'\1\n',str(ba.toBase64()))) | ||||
epatters
|
r3361 | elif format == "svg": | ||
Mark Voorhies
|
r3125 | try: | ||
epatters
|
r3364 | svg = str(self._name_to_svg_map[match.group("name")]) | ||
Mark Voorhies
|
r3125 | except KeyError: | ||
return "<b>Couldn't find image %s</b>" % match.group("name") | ||||
# Not currently checking path, because it's tricky to find a | ||||
# cross-browser way to embed external SVG images (e.g., via | ||||
# object or embed tags). | ||||
# Chop stand-alone header from matplotlib SVG | ||||
offset = svg.find("<svg") | ||||
assert(offset > -1) | ||||
return svg[offset:] | ||||
else: | ||||
return '<b>Unrecognized image format</b>' | ||||
Brian Granger
|
r3277 | |||
epatters
|
r3361 | def _save_image(self, name, format='PNG'): | ||
""" Shows a save dialog for the ImageResource with 'name'. | ||||
""" | ||||
dialog = QtGui.QFileDialog(self._control, 'Save Image') | ||||
dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) | ||||
dialog.setDefaultSuffix(format.lower()) | ||||
dialog.setNameFilter('%s file (*.%s)' % (format, format.lower())) | ||||
if dialog.exec_(): | ||||
filename = dialog.selectedFiles()[0] | ||||
image = self._get_image(name) | ||||
image.save(filename, format) | ||||