# System library imports import os import re from PyQt4 import QtCore, QtGui # Local imports from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image 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. """ # RichIPythonWidget protected class variables. _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload' _svg_text_format_property = 1 #--------------------------------------------------------------------------- # 'object' interface #--------------------------------------------------------------------------- def __init__(self, *args, **kw): """ Create a RichIPythonWidget. """ kw['kind'] = 'rich' super(RichIPythonWidget, self).__init__(*args, **kw) # Dictionary for resolving Qt names to images when # generating XHTML output self._name_to_svg = {} #--------------------------------------------------------------------------- # 'ConsoleWidget' protected interface #--------------------------------------------------------------------------- def _context_menu_make(self, pos): """ Reimplemented to return a custom context menu for images. """ format = self._control.cursorForPosition(pos).charFormat() name = format.stringProperty(QtGui.QTextFormat.ImageName) if name.isEmpty(): menu = super(RichIPythonWidget, self)._context_menu_make(pos) else: menu = QtGui.QMenu() menu.addAction('Copy Image', lambda: self._copy_image(name)) menu.addAction('Save Image As...', lambda: self._save_image(name)) menu.addSeparator() svg = format.stringProperty(self._svg_text_format_property) if not svg.isEmpty(): menu.addSeparator() menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg)) menu.addAction('Save SVG As...', lambda: save_svg(svg, self._control)) return menu #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface #--------------------------------------------------------------------------- def _handle_display_data(self, msg): """ A handler for ``display_data`` message that handles html and svg. """ 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) elif data.has_key('text/html'): html = data['text/html'] self._append_html(html) else: # Default back to the plain text representation. return super(RichIPythonWidget, self)._handle_display_data(msg) #--------------------------------------------------------------------------- # 'FrontendWidget' protected interface #--------------------------------------------------------------------------- def _process_execute_payload(self, item): """ Reimplemented to handle matplotlib plot payloads. """ if item['source'] == self._payload_source_plot: # TODO: remove this as all plot data is coming back through the # display_data message type. if item['format'] == 'svg': svg = item['data'] self._append_svg(svg) return True else: # Add other plot formats here! return False else: return super(RichIPythonWidget, self)._process_execute_payload(item) #--------------------------------------------------------------------------- # 'RichIPythonWidget' protected interface #--------------------------------------------------------------------------- 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) self._name_to_svg[str(format.name())] = svg format.setProperty(self._svg_text_format_property, svg) cursor = self._get_end_cursor() cursor.insertBlock() cursor.insertImage(format) cursor.insertBlock() def _add_image(self, image): """ Adds the specified QImage to the document and returns a QTextImageFormat that references it. """ document = self._control.document() name = QtCore.QString.number(image.cacheKey()) 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() variant = document.resource(QtGui.QTextDocument.ImageResource, QtCore.QUrl(name)) return variant.toPyObject() 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) def image_tag(self, match, path = None, format = "png"): """ Return (X)HTML mark-up for the image-tag given by match. Parameters ---------- match : re.SRE_Match 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] 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. format : "png"|"svg", optional [default "png"] Format for returned or referenced images. Subclasses supporting image display should override this method. """ if(format == "png"): try: image = self._get_image(match.group("name")) except KeyError: return "Couldn't find image %s" % match.group("name") if(path is not None): if not os.path.exists(path): os.mkdir(path) relpath = os.path.basename(path) if(image.save("%s/qt_img%s.png" % (path,match.group("name")), "PNG")): return '' % (relpath, match.group("name")) else: return "Couldn't save image!" else: ba = QtCore.QByteArray() buffer_ = QtCore.QBuffer(ba) buffer_.open(QtCore.QIODevice.WriteOnly) image.save(buffer_, "PNG") buffer_.close() return '' % ( re.sub(r'(.{60})',r'\1\n',str(ba.toBase64()))) elif(format == "svg"): try: svg = str(self._name_to_svg[match.group("name")]) except KeyError: return "Couldn't find image %s" % 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(" -1) return svg[offset:] else: return 'Unrecognized image format'