Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Added HTML export #167

Closed
wants to merge 5 commits into from

3 participants

@markvoorhies

This is my first pass at adding HTML export to ipython. Here's the description that I'm
sending to ipython-dev:

I tried three approaches (available as three context menu options):

1) Export HTML (external PNGs):
This mimics Firefox's "Save as Web Page, complete" behavior.
Saving "mypath/test.htm" gives an HTML file with links to PNGs in
"mypath/test_files/". The PNGs are named relative to format.name()
to avoid collisions.

Works in Firefox 3.6.10, Konqueror 4.4.2/KHTML, and Konqueror 4.4.2/WebKit

2) Export HTML (inline PNGs):
Saves a single HTML file with images as inline base64 PNGs
(c.f. http://en.wikipedia.org/wiki/Data_URI_scheme#HTML)

Works in Firefox 3.6.10, Konqueror 4.4.2/KHTML, and Konqueror 4.4.2/WebKit

3) Export XHTML (inline SVGs):
Saves a single XHTML file with images as inline SVG. The "XML" is generated
by overwriting the Qt-generated document header, so it is not guaranteed to
be valid XML (but Firefox does validate my test case).

Works in Firefox 3.6.10 and Konqueror 4.4.2/WebKit.
Image placement is incorrect for Konqueror 4.4.2/KHTML.

(all tests run on a Dell Latitude D630 w/ Kubuntu Lucid:
mvoorhie@virgil:~$ uname -a
Linux virgil 2.6.32-24-generic #43-Ubuntu SMP Thu Sep 16 14:17:33 UTC 2010 i686 GNU/Linux)

It may be possible to link external SVG images via an or tag,
but I couldn't find a clean/portable way to do this.

Current issues:

  • I'm doing lots of string coercion in order to use re.sub on Qt's HTML. I mostly
    get away with it, but we wind up with a few bad character encodings in the
    output (e.g., the tabs for multi-line inputs). Would be good for someone who
    knows more about unicode to take a look at this...

  • The file name generation for "Export HTML (external PNGs)" is a bit hacky. Should
    probably be rewritten to use os.path.

  • Haven't tested with anything other than the Qt front end. In theory, other front
    ends will export HTML with the images stripped, unless they implement their own
    version of imagetag().

Feel free to take/hack what you like and ditch the rest.

Happy hacking,

--Mark

@markvoorhies markvoorhies Add HTML export options
Three export options (HTML w/ external PNGs, HTML w/ inline PNGs,
and XHTML w/ inline SVGs) are added to the context menu.
badeab1
@fperez

Is this method inherited from the parent Qt widget? If not, it should be named_with_underscores. We use PEP-8 names everywhere, except when overriding parent methods whose name we don't control.

Same applies to the rest below, I won't repeat it.

@fperez

Name image_tag? And does the format need to be all uppercased? If it's case insensitive, I'd prefer using 'png' for the format spec.

@fperez
Owner

Thanks a lot, Mark! I made some minor form comments, but I won't have time to give you more meaningful feedback or testing for a couple of days. But this looks great, and I promise to get back to it and test it.

One minor note: you may want to make a feature branch for this, in case it grows and your master picks up other things, so the html-print-specific work stays contained and we can review it in isolation. I think you can edit the source branch for the pull request after the fact. If not don't worry for this one, just keep it in mind for future proposals.

@ellisonbg
Owner

Mark, this is a really nice go at this. I tested on my Mac with Firefox, Chrome and Safari and all worked fine for all formats. I do think we want all of these choices. The docstrings need to be cleaned up into our standard format. For examples, see a file like IPython/zmq/kernelmanager.py.

@markvoorhies

"One minor note: you may want to make a feature branch for this"

I've added a feature branch (htmlexport), but can't figure out how to switch the branch for the pull request
after the fact. I'll go ahead and have my master track htmlexport until this pull request is resolved (I'll
keep unrelated pulls on a separate local branch until then).

@markvoorhies markvoorhies Specify character encoding in HTML HEAD
This adds an explicit declaration of the UTF-8 character encoding
to the Qt generated HTML dump (since we've just explicitly requested
UTF-8 conversion from QString, this declaration should be correct),
c.f. http://www.w3.org/International/O-charset

This patch fixes incorrect characters (e.g., A-hat for tab) in, e.g.,
Firefox's default rendering of exported HTML.

Applying the same fix to both HTML and XHTML export even though
Firefox and WebKit appear to assume UTF-8 for XHTML even without
an explicit declaration.
f467f96
@markvoorhies

Hi Fernando,
I just pushed the UTF-8 fix that we discussed (f467f96). With this commit, the state of the pull request
branch (master == htmlexport) matches what I showed you at the Py4sci meeting.

@fperez
Owner

Merge branch 'master' of http://github.com/markvoorhies/ipython into markvoorhies-master

This adds HTML export of the entire buffer of the Qt console, with
option to save images either as inline PNG, PNGs in a separate folder,
or as inline SVG with all original metadata the SVG could have had
still intact.

Closed by aae8597 (pull request).

@jtriley jtriley referenced this pull request from a commit in jtriley/ipython
@fperez fperez Merge branch 'master' of http://github.com/markvoorhies/ipython into …
…markvoorhies-master

This adds HTML export of the entire buffer of the Qt console, with
option to save images either as inline PNG, PNGs in a separate folder,
or as inline SVG with all original metadata the SVG could have had
still intact.

Closes gh-167 (pull request).
aae8597
@fperez fperez referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@damianavila damianavila referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@mattvonrocketstein mattvonrocketstein referenced this pull request from a commit in mattvonrocketstein/ipython
@fperez fperez Merge branch 'master' of http://github.com/markvoorhies/ipython into …
…markvoorhies-master

This adds HTML export of the entire buffer of the Qt console, with
option to save images either as inline PNG, PNGs in a separate folder,
or as inline SVG with all original metadata the SVG could have had
still intact.

Closes gh-167 (pull request).
09f0134
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 11, 2010
  1. @markvoorhies

    Add HTML export options

    markvoorhies authored
    Three export options (HTML w/ external PNGs, HTML w/ inline PNGs,
    and XHTML w/ inline SVGs) are added to the context menu.
Commits on Oct 12, 2010
  1. @markvoorhies
  2. @markvoorhies
  3. @markvoorhies
Commits on Oct 13, 2010
  1. @markvoorhies

    Specify character encoding in HTML HEAD

    markvoorhies authored
    This adds an explicit declaration of the UTF-8 character encoding
    to the Qt generated HTML dump (since we've just explicitly requested
    UTF-8 conversion from QString, this declaration should be correct),
    c.f. http://www.w3.org/International/O-charset
    
    This patch fixes incorrect characters (e.g., A-hat for tab) in, e.g.,
    Firefox's default rendering of exported HTML.
    
    Applying the same fix to both HTML and XHTML export even though
    Firefox and WebKit appear to assume UTF-8 for XHTML even without
    an explicit declaration.
This page is out of date. Refresh to see the latest.
View
140 IPython/frontend/qt/console/console_widget.py
@@ -507,6 +507,136 @@ def print_(self, printer = None):
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'<img src="(?P<name>[\d]+)" />')
+ 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'<img src="(?P<name>[\d]+)" />')
+ 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("<html>")
+ assert(offset > -1)
+ html = ('<html xmlns="http://www.w3.org/1999/xhtml">\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 <HEAD>.
+
+ 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("<head>")
+ if(offset > -1):
+ html = (html[:offset+6]+
+ '\n<meta http-equiv="Content-Type" '+
+ 'content="text/html; charset=utf-8" />\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 @@ def _context_menu_make(self, pos):
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):
View
69 IPython/frontend/qt/console/rich_ipython_widget.py
@@ -25,6 +25,9 @@ def __init__(self, *args, **kw):
"""
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 @@ def _process_execute_payload(self, item):
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 @@ def _save_image(self, name, format='PNG'):
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 "<b>Couldn't find image %s</b>" % 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 '<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()
+ import re
+ return '<img src="data:image/png;base64,\n%s\n" />' % (
+ 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 "<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>'
+
Something went wrong with that request. Please try again.