diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index 08620f1..d4f939e 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -507,6 +507,136 @@ class ConsoleWidget(Configurable, QtGui.QWidget): return self._control.print_(printer) + def export_html_inline(self, parent = None): + """ Export the contents of the ConsoleWidget as HTML with inline PNGs. + """ + self.export_html(parent, inline = True) + + def export_html(self, parent = None, inline = False): + """ Export the contents of the ConsoleWidget as HTML. + + Parameters: + ----------- + inline : bool, optional [default True] + + If True, include images as inline PNGs. Otherwise, + include them as links to external PNG files, mimicking + 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'') + html = self.fix_html_encoding( + str(self._control.toHtml().toUtf8())) + f.write(img_re.sub( + lambda x: self.image_tag(x, path = path, format = "png"), + html)) + finally: + f.close() + return filename + return None + + def export_xhtml(self, parent = None): + """ Export the contents of the ConsoleWidget as XHTML with inline SVGs. + """ + 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:]) + # And now declare UTF-8 encoding + html = self.fix_html_encoding(html) + f.write(img_re.sub( + lambda x: self.image_tag(x, path = None, format = "svg"), + html)) + finally: + f.close() + return filename + return None + + def fix_html_encoding(self, html): + """ Return html string, with a UTF-8 declaration added to . + + Assumes that html is Qt generated and has already been UTF-8 encoded + and coerced to a python string. If the expected head element is + not found, the given object is returned unmodified. + + This patching is needed for proper rendering of some characters + (e.g., indented commands) when viewing exported HTML on a local + system (i.e., without seeing an encoding declaration in an HTTP + header). + + C.f. http://www.w3.org/International/O-charset for details. + """ + offset = html.find("") + if(offset > -1): + html = (html[:offset+6]+ + '\n\n'+ + html[offset+6:]) + + return html + + 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. + """ + + # 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 +874,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.export_html) + html_action.setEnabled(True) + html_inline_action = menu.addAction('Export HTML (inline PNGs)', + self.export_html_inline) + html_inline_action.setEnabled(True) + xhtml_action = menu.addAction('Export XHTML (inline SVGs)', + self.export_xhtml) + 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..0b6e69d 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._name_to_svg = {} #--------------------------------------------------------------------------- # '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._name_to_svg[str(format.name())] = svg format.setProperty(self._svg_text_format_property, svg) cursor = self._get_end_cursor() cursor.insertBlock() @@ -121,3 +125,68 @@ class RichIPythonWidget(IPythonWidget): 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): + 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._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' +