diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index 08620f1..7993ca8 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -507,6 +507,89 @@ class ConsoleWidget(Configurable, QtGui.QWidget): return self._control.print_(printer) + def exportHtmlInline(self, parent = None): + self.exportHtml(parent, inline = True) + + def exportHtml(self, parent = None, inline = False): + """ Export the contents of the ConsoleWidget as an HTML file. + + If inline == True, include images as inline PNGs. Otherwise, + include them as links to external PNG files, mimicking the + Firefox's "Web Page, complete" behavior. + """ + dialog = QtGui.QFileDialog(parent, 'Save HTML Document') + dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + dialog.setDefaultSuffix('htm') + dialog.setNameFilter('HTML document (*.htm)') + if dialog.exec_(): + filename = str(dialog.selectedFiles()[0]) + if(inline): + path = None + else: + offset = filename.rfind(".") + if(offset > 0): + path = filename[:offset]+"_files" + else: + path = filename+"_files" + import os + try: + os.mkdir(path) + except OSError: + # TODO: check that this is an "already exists" error + pass + + f = open(filename, 'w') + try: + # N.B. this is overly restrictive, but Qt's output is + # predictable... + img_re = re.compile(r'') + f.write(img_re.sub( + lambda x: self.imagetag(x, path = path, format = "PNG"), + str(self._control.toHtml().toUtf8()))) + finally: + f.close() + return filename + return None + + def exportXhtml(self, parent = None): + """ Export the contents of the ConsoleWidget as an XHTML file + with figures as inline SVG. + """ + dialog = QtGui.QFileDialog(parent, 'Save XHTML Document') + dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + dialog.setDefaultSuffix('xml') + dialog.setNameFilter('XHTML document (*.xml)') + if dialog.exec_(): + filename = str(dialog.selectedFiles()[0]) + f = open(filename, 'w') + try: + # N.B. this is overly restrictive, but Qt's output is + # predictable... + img_re = re.compile(r'') + html = str(self._control.toHtml().toUtf8()) + # Hack to make xhtml header -- note that we are not doing + # any check for valid xml + offset = html.find("") + assert(offset > -1) + html = ('\n'+ + html[offset+6:]) + f.write(img_re.sub( + lambda x: self.imagetag(x, path = None, format = "SVG"), + html)) + finally: + f.close() + return filename + return None + + def imagetag(self, match, path = None): + """ Given an re.match object matching an image name in an HTML export, + return an appropriate substitution string for the image tag + (e.g., link, embedded image, ...). As a side effect, files may + be generated in the directory given by path.""" + + # Default case -- not enough information to generate tag + return "" + def prompt_to_top(self): """ Moves the prompt to the top of the viewport. """ @@ -744,7 +827,15 @@ class ConsoleWidget(Configurable, QtGui.QWidget): menu.addSeparator() print_action = menu.addAction('Print', self.print_) print_action.setEnabled(True) - + html_action = menu.addAction('Export HTML (external PNGs)', + self.exportHtml) + html_action.setEnabled(True) + html_inline_action = menu.addAction('Export HTML (inline PNGs)', + self.exportHtmlInline) + html_inline_action.setEnabled(True) + xhtml_action = menu.addAction('Export XHTML (inline SVGs)', + self.exportXhtml) + xhtml_action.setEnabled(True) return menu def _control_key_down(self, modifiers, include_command=True): diff --git a/IPython/frontend/qt/console/rich_ipython_widget.py b/IPython/frontend/qt/console/rich_ipython_widget.py index 5a74eb7..7b18733 100644 --- a/IPython/frontend/qt/console/rich_ipython_widget.py +++ b/IPython/frontend/qt/console/rich_ipython_widget.py @@ -25,6 +25,9 @@ class RichIPythonWidget(IPythonWidget): """ kw['kind'] = 'rich' super(RichIPythonWidget, self).__init__(*args, **kw) + # Dictionary for resolving Qt names to images when + # generating XHTML output + self._name2svg = {} #--------------------------------------------------------------------------- # 'ConsoleWidget' protected interface @@ -68,6 +71,7 @@ class RichIPythonWidget(IPythonWidget): self._append_plain_text('Received invalid plot data.') else: format = self._add_image(image) + self._name2svg[str(format.name())] = svg format.setProperty(self._svg_text_format_property, svg) cursor = self._get_end_cursor() cursor.insertBlock() @@ -121,3 +125,53 @@ class RichIPythonWidget(IPythonWidget): filename = dialog.selectedFiles()[0] image = self._get_image(name) image.save(filename, format) + + def imagetag(self, match, path = None, format = "PNG"): + """ Given an re.match object matching an image name in an HTML dump, + return an appropriate substitution string for the image tag + (e.g., link, embedded image, ...). As a side effect, files may + be generated in the directory given by path.""" + + 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): + relpath = path[path.rfind("/")+1:] + 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() + import re + return '' % ( + re.sub(r'(.{60})',r'\1\n',str(ba.toBase64()))) + + elif(format == "SVG"): + try: + svg = str(self._name2svg[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' +