Show More
@@ -507,6 +507,136 b' class ConsoleWidget(Configurable, QtGui.QWidget):' | |||||
507 | return |
|
507 | return | |
508 | self._control.print_(printer) |
|
508 | self._control.print_(printer) | |
509 |
|
509 | |||
|
510 | def export_html_inline(self, parent = None): | |||
|
511 | """ Export the contents of the ConsoleWidget as HTML with inline PNGs. | |||
|
512 | """ | |||
|
513 | self.export_html(parent, inline = True) | |||
|
514 | ||||
|
515 | def export_html(self, parent = None, inline = False): | |||
|
516 | """ Export the contents of the ConsoleWidget as HTML. | |||
|
517 | ||||
|
518 | Parameters: | |||
|
519 | ----------- | |||
|
520 | inline : bool, optional [default True] | |||
|
521 | ||||
|
522 | If True, include images as inline PNGs. Otherwise, | |||
|
523 | include them as links to external PNG files, mimicking | |||
|
524 | Firefox's "Web Page, complete" behavior. | |||
|
525 | """ | |||
|
526 | dialog = QtGui.QFileDialog(parent, 'Save HTML Document') | |||
|
527 | dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) | |||
|
528 | dialog.setDefaultSuffix('htm') | |||
|
529 | dialog.setNameFilter('HTML document (*.htm)') | |||
|
530 | if dialog.exec_(): | |||
|
531 | filename = str(dialog.selectedFiles()[0]) | |||
|
532 | if(inline): | |||
|
533 | path = None | |||
|
534 | else: | |||
|
535 | offset = filename.rfind(".") | |||
|
536 | if(offset > 0): | |||
|
537 | path = filename[:offset]+"_files" | |||
|
538 | else: | |||
|
539 | path = filename+"_files" | |||
|
540 | import os | |||
|
541 | try: | |||
|
542 | os.mkdir(path) | |||
|
543 | except OSError: | |||
|
544 | # TODO: check that this is an "already exists" error | |||
|
545 | pass | |||
|
546 | ||||
|
547 | f = open(filename, 'w') | |||
|
548 | try: | |||
|
549 | # N.B. this is overly restrictive, but Qt's output is | |||
|
550 | # predictable... | |||
|
551 | img_re = re.compile(r'<img src="(?P<name>[\d]+)" />') | |||
|
552 | html = self.fix_html_encoding( | |||
|
553 | str(self._control.toHtml().toUtf8())) | |||
|
554 | f.write(img_re.sub( | |||
|
555 | lambda x: self.image_tag(x, path = path, format = "png"), | |||
|
556 | html)) | |||
|
557 | finally: | |||
|
558 | f.close() | |||
|
559 | return filename | |||
|
560 | return None | |||
|
561 | ||||
|
562 | def export_xhtml(self, parent = None): | |||
|
563 | """ Export the contents of the ConsoleWidget as XHTML with inline SVGs. | |||
|
564 | """ | |||
|
565 | dialog = QtGui.QFileDialog(parent, 'Save XHTML Document') | |||
|
566 | dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) | |||
|
567 | dialog.setDefaultSuffix('xml') | |||
|
568 | dialog.setNameFilter('XHTML document (*.xml)') | |||
|
569 | if dialog.exec_(): | |||
|
570 | filename = str(dialog.selectedFiles()[0]) | |||
|
571 | f = open(filename, 'w') | |||
|
572 | try: | |||
|
573 | # N.B. this is overly restrictive, but Qt's output is | |||
|
574 | # predictable... | |||
|
575 | img_re = re.compile(r'<img src="(?P<name>[\d]+)" />') | |||
|
576 | html = str(self._control.toHtml().toUtf8()) | |||
|
577 | # Hack to make xhtml header -- note that we are not doing | |||
|
578 | # any check for valid xml | |||
|
579 | offset = html.find("<html>") | |||
|
580 | assert(offset > -1) | |||
|
581 | html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+ | |||
|
582 | html[offset+6:]) | |||
|
583 | # And now declare UTF-8 encoding | |||
|
584 | html = self.fix_html_encoding(html) | |||
|
585 | f.write(img_re.sub( | |||
|
586 | lambda x: self.image_tag(x, path = None, format = "svg"), | |||
|
587 | html)) | |||
|
588 | finally: | |||
|
589 | f.close() | |||
|
590 | return filename | |||
|
591 | return None | |||
|
592 | ||||
|
593 | def fix_html_encoding(self, html): | |||
|
594 | """ Return html string, with a UTF-8 declaration added to <HEAD>. | |||
|
595 | ||||
|
596 | Assumes that html is Qt generated and has already been UTF-8 encoded | |||
|
597 | and coerced to a python string. If the expected head element is | |||
|
598 | not found, the given object is returned unmodified. | |||
|
599 | ||||
|
600 | This patching is needed for proper rendering of some characters | |||
|
601 | (e.g., indented commands) when viewing exported HTML on a local | |||
|
602 | system (i.e., without seeing an encoding declaration in an HTTP | |||
|
603 | header). | |||
|
604 | ||||
|
605 | C.f. http://www.w3.org/International/O-charset for details. | |||
|
606 | """ | |||
|
607 | offset = html.find("<head>") | |||
|
608 | if(offset > -1): | |||
|
609 | html = (html[:offset+6]+ | |||
|
610 | '\n<meta http-equiv="Content-Type" '+ | |||
|
611 | 'content="text/html; charset=utf-8" />\n'+ | |||
|
612 | html[offset+6:]) | |||
|
613 | ||||
|
614 | return html | |||
|
615 | ||||
|
616 | def image_tag(self, match, path = None, format = "png"): | |||
|
617 | """ Return (X)HTML mark-up for the image-tag given by match. | |||
|
618 | ||||
|
619 | Parameters | |||
|
620 | ---------- | |||
|
621 | match : re.SRE_Match | |||
|
622 | A match to an HTML image tag as exported by Qt, with | |||
|
623 | match.group("Name") containing the matched image ID. | |||
|
624 | ||||
|
625 | path : string|None, optional [default None] | |||
|
626 | If not None, specifies a path to which supporting files | |||
|
627 | may be written (e.g., for linked images). | |||
|
628 | If None, all images are to be included inline. | |||
|
629 | ||||
|
630 | format : "png"|"svg", optional [default "png"] | |||
|
631 | Format for returned or referenced images. | |||
|
632 | ||||
|
633 | Subclasses supporting image display should override this | |||
|
634 | method. | |||
|
635 | """ | |||
|
636 | ||||
|
637 | # Default case -- not enough information to generate tag | |||
|
638 | return "" | |||
|
639 | ||||
510 | def prompt_to_top(self): |
|
640 | def prompt_to_top(self): | |
511 | """ Moves the prompt to the top of the viewport. |
|
641 | """ Moves the prompt to the top of the viewport. | |
512 | """ |
|
642 | """ | |
@@ -744,7 +874,15 b' class ConsoleWidget(Configurable, QtGui.QWidget):' | |||||
744 | menu.addSeparator() |
|
874 | menu.addSeparator() | |
745 | print_action = menu.addAction('Print', self.print_) |
|
875 | print_action = menu.addAction('Print', self.print_) | |
746 | print_action.setEnabled(True) |
|
876 | print_action.setEnabled(True) | |
747 |
|
877 | html_action = menu.addAction('Export HTML (external PNGs)', | ||
|
878 | self.export_html) | |||
|
879 | html_action.setEnabled(True) | |||
|
880 | html_inline_action = menu.addAction('Export HTML (inline PNGs)', | |||
|
881 | self.export_html_inline) | |||
|
882 | html_inline_action.setEnabled(True) | |||
|
883 | xhtml_action = menu.addAction('Export XHTML (inline SVGs)', | |||
|
884 | self.export_xhtml) | |||
|
885 | xhtml_action.setEnabled(True) | |||
748 | return menu |
|
886 | return menu | |
749 |
|
887 | |||
750 | def _control_key_down(self, modifiers, include_command=True): |
|
888 | def _control_key_down(self, modifiers, include_command=True): |
@@ -25,6 +25,9 b' class RichIPythonWidget(IPythonWidget):' | |||||
25 | """ |
|
25 | """ | |
26 | kw['kind'] = 'rich' |
|
26 | kw['kind'] = 'rich' | |
27 | super(RichIPythonWidget, self).__init__(*args, **kw) |
|
27 | super(RichIPythonWidget, self).__init__(*args, **kw) | |
|
28 | # Dictionary for resolving Qt names to images when | |||
|
29 | # generating XHTML output | |||
|
30 | self._name_to_svg = {} | |||
28 |
|
31 | |||
29 | #--------------------------------------------------------------------------- |
|
32 | #--------------------------------------------------------------------------- | |
30 | # 'ConsoleWidget' protected interface |
|
33 | # 'ConsoleWidget' protected interface | |
@@ -68,6 +71,7 b' class RichIPythonWidget(IPythonWidget):' | |||||
68 | self._append_plain_text('Received invalid plot data.') |
|
71 | self._append_plain_text('Received invalid plot data.') | |
69 | else: |
|
72 | else: | |
70 | format = self._add_image(image) |
|
73 | format = self._add_image(image) | |
|
74 | self._name_to_svg[str(format.name())] = svg | |||
71 | format.setProperty(self._svg_text_format_property, svg) |
|
75 | format.setProperty(self._svg_text_format_property, svg) | |
72 | cursor = self._get_end_cursor() |
|
76 | cursor = self._get_end_cursor() | |
73 | cursor.insertBlock() |
|
77 | cursor.insertBlock() | |
@@ -121,3 +125,68 b' class RichIPythonWidget(IPythonWidget):' | |||||
121 | filename = dialog.selectedFiles()[0] |
|
125 | filename = dialog.selectedFiles()[0] | |
122 | image = self._get_image(name) |
|
126 | image = self._get_image(name) | |
123 | image.save(filename, format) |
|
127 | image.save(filename, format) | |
|
128 | ||||
|
129 | def image_tag(self, match, path = None, format = "png"): | |||
|
130 | """ Return (X)HTML mark-up for the image-tag given by match. | |||
|
131 | ||||
|
132 | Parameters | |||
|
133 | ---------- | |||
|
134 | match : re.SRE_Match | |||
|
135 | A match to an HTML image tag as exported by Qt, with | |||
|
136 | match.group("Name") containing the matched image ID. | |||
|
137 | ||||
|
138 | path : string|None, optional [default None] | |||
|
139 | If not None, specifies a path to which supporting files | |||
|
140 | may be written (e.g., for linked images). | |||
|
141 | If None, all images are to be included inline. | |||
|
142 | ||||
|
143 | format : "png"|"svg", optional [default "png"] | |||
|
144 | Format for returned or referenced images. | |||
|
145 | ||||
|
146 | Subclasses supporting image display should override this | |||
|
147 | method. | |||
|
148 | """ | |||
|
149 | ||||
|
150 | if(format == "png"): | |||
|
151 | try: | |||
|
152 | image = self._get_image(match.group("name")) | |||
|
153 | except KeyError: | |||
|
154 | return "<b>Couldn't find image %s</b>" % match.group("name") | |||
|
155 | ||||
|
156 | if(path is not None): | |||
|
157 | relpath = path[path.rfind("/")+1:] | |||
|
158 | if(image.save("%s/qt_img%s.png" % (path,match.group("name")), | |||
|
159 | "PNG")): | |||
|
160 | return '<img src="%s/qt_img%s.png">' % (relpath, | |||
|
161 | match.group("name")) | |||
|
162 | else: | |||
|
163 | return "<b>Couldn't save image!</b>" | |||
|
164 | else: | |||
|
165 | ba = QtCore.QByteArray() | |||
|
166 | buffer_ = QtCore.QBuffer(ba) | |||
|
167 | buffer_.open(QtCore.QIODevice.WriteOnly) | |||
|
168 | image.save(buffer_, "PNG") | |||
|
169 | buffer_.close() | |||
|
170 | import re | |||
|
171 | return '<img src="data:image/png;base64,\n%s\n" />' % ( | |||
|
172 | re.sub(r'(.{60})',r'\1\n',str(ba.toBase64()))) | |||
|
173 | ||||
|
174 | elif(format == "svg"): | |||
|
175 | try: | |||
|
176 | svg = str(self._name_to_svg[match.group("name")]) | |||
|
177 | except KeyError: | |||
|
178 | return "<b>Couldn't find image %s</b>" % match.group("name") | |||
|
179 | ||||
|
180 | # Not currently checking path, because it's tricky to find a | |||
|
181 | # cross-browser way to embed external SVG images (e.g., via | |||
|
182 | # object or embed tags). | |||
|
183 | ||||
|
184 | # Chop stand-alone header from matplotlib SVG | |||
|
185 | offset = svg.find("<svg") | |||
|
186 | assert(offset > -1) | |||
|
187 | ||||
|
188 | return svg[offset:] | |||
|
189 | ||||
|
190 | else: | |||
|
191 | return '<b>Unrecognized image format</b>' | |||
|
192 |
General Comments 0
You need to be logged in to leave comments.
Login now