##// END OF EJS Templates
Merge pull request #4054 from minrk/qtunicodeagain...
Thomas Kluyver -
r12306:98d37b14 merge
parent child Browse files
Show More
@@ -1,339 +1,341 b''
1 #-----------------------------------------------------------------------------
1 #-----------------------------------------------------------------------------
2 # Copyright (c) 2010, IPython Development Team.
2 # Copyright (c) 2010, IPython Development Team.
3 #
3 #
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5 #
5 #
6 # The full license is in the file COPYING.txt, distributed with this software.
6 # The full license is in the file COPYING.txt, distributed with this software.
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard libary imports.
9 # Standard libary imports.
10 from base64 import decodestring
10 from base64 import decodestring
11 import os
11 import os
12 import re
12 import re
13
13
14 # System libary imports.
14 # System libary imports.
15 from IPython.external.qt import QtCore, QtGui
15 from IPython.external.qt import QtCore, QtGui
16
16
17 # Local imports
17 # Local imports
18 from IPython.utils.traitlets import Bool
18 from IPython.utils.traitlets import Bool
19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
20 from ipython_widget import IPythonWidget
20 from ipython_widget import IPythonWidget
21
21
22
22
23 class RichIPythonWidget(IPythonWidget):
23 class RichIPythonWidget(IPythonWidget):
24 """ An IPythonWidget that supports rich text, including lists, images, and
24 """ An IPythonWidget that supports rich text, including lists, images, and
25 tables. Note that raw performance will be reduced compared to the plain
25 tables. Note that raw performance will be reduced compared to the plain
26 text version.
26 text version.
27 """
27 """
28
28
29 # RichIPythonWidget protected class variables.
29 # RichIPythonWidget protected class variables.
30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
31 _jpg_supported = Bool(False)
31 _jpg_supported = Bool(False)
32
32
33 # Used to determine whether a given html export attempt has already
33 # Used to determine whether a given html export attempt has already
34 # displayed a warning about being unable to convert a png to svg.
34 # displayed a warning about being unable to convert a png to svg.
35 _svg_warning_displayed = False
35 _svg_warning_displayed = False
36
36
37 #---------------------------------------------------------------------------
37 #---------------------------------------------------------------------------
38 # 'object' interface
38 # 'object' interface
39 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
40
40
41 def __init__(self, *args, **kw):
41 def __init__(self, *args, **kw):
42 """ Create a RichIPythonWidget.
42 """ Create a RichIPythonWidget.
43 """
43 """
44 kw['kind'] = 'rich'
44 kw['kind'] = 'rich'
45 super(RichIPythonWidget, self).__init__(*args, **kw)
45 super(RichIPythonWidget, self).__init__(*args, **kw)
46
46
47 # Configure the ConsoleWidget HTML exporter for our formats.
47 # Configure the ConsoleWidget HTML exporter for our formats.
48 self._html_exporter.image_tag = self._get_image_tag
48 self._html_exporter.image_tag = self._get_image_tag
49
49
50 # Dictionary for resolving document resource names to SVG data.
50 # Dictionary for resolving document resource names to SVG data.
51 self._name_to_svg_map = {}
51 self._name_to_svg_map = {}
52
52
53 # Do we support jpg ?
53 # Do we support jpg ?
54 # it seems that sometime jpg support is a plugin of QT, so try to assume
54 # it seems that sometime jpg support is a plugin of QT, so try to assume
55 # it is not always supported.
55 # it is not always supported.
56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
57 self._jpg_supported = 'jpeg' in _supported_format
57 self._jpg_supported = 'jpeg' in _supported_format
58
58
59
59
60 #---------------------------------------------------------------------------
60 #---------------------------------------------------------------------------
61 # 'ConsoleWidget' public interface overides
61 # 'ConsoleWidget' public interface overides
62 #---------------------------------------------------------------------------
62 #---------------------------------------------------------------------------
63
63
64 def export_html(self):
64 def export_html(self):
65 """ Shows a dialog to export HTML/XML in various formats.
65 """ Shows a dialog to export HTML/XML in various formats.
66
66
67 Overridden in order to reset the _svg_warning_displayed flag prior
67 Overridden in order to reset the _svg_warning_displayed flag prior
68 to the export running.
68 to the export running.
69 """
69 """
70 self._svg_warning_displayed = False
70 self._svg_warning_displayed = False
71 super(RichIPythonWidget, self).export_html()
71 super(RichIPythonWidget, self).export_html()
72
72
73
73
74 #---------------------------------------------------------------------------
74 #---------------------------------------------------------------------------
75 # 'ConsoleWidget' protected interface
75 # 'ConsoleWidget' protected interface
76 #---------------------------------------------------------------------------
76 #---------------------------------------------------------------------------
77
77
78 def _context_menu_make(self, pos):
78 def _context_menu_make(self, pos):
79 """ Reimplemented to return a custom context menu for images.
79 """ Reimplemented to return a custom context menu for images.
80 """
80 """
81 format = self._control.cursorForPosition(pos).charFormat()
81 format = self._control.cursorForPosition(pos).charFormat()
82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
83 if name:
83 if name:
84 menu = QtGui.QMenu()
84 menu = QtGui.QMenu()
85
85
86 menu.addAction('Copy Image', lambda: self._copy_image(name))
86 menu.addAction('Copy Image', lambda: self._copy_image(name))
87 menu.addAction('Save Image As...', lambda: self._save_image(name))
87 menu.addAction('Save Image As...', lambda: self._save_image(name))
88 menu.addSeparator()
88 menu.addSeparator()
89
89
90 svg = self._name_to_svg_map.get(name, None)
90 svg = self._name_to_svg_map.get(name, None)
91 if svg is not None:
91 if svg is not None:
92 menu.addSeparator()
92 menu.addSeparator()
93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
94 menu.addAction('Save SVG As...',
94 menu.addAction('Save SVG As...',
95 lambda: save_svg(svg, self._control))
95 lambda: save_svg(svg, self._control))
96 else:
96 else:
97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
98 return menu
98 return menu
99
99
100 #---------------------------------------------------------------------------
100 #---------------------------------------------------------------------------
101 # 'BaseFrontendMixin' abstract interface
101 # 'BaseFrontendMixin' abstract interface
102 #---------------------------------------------------------------------------
102 #---------------------------------------------------------------------------
103 def _pre_image_append(self, msg, prompt_number):
103 def _pre_image_append(self, msg, prompt_number):
104 """ Append the Out[] prompt and make the output nicer
104 """ Append the Out[] prompt and make the output nicer
105
105
106 Shared code for some the following if statement
106 Shared code for some the following if statement
107 """
107 """
108 self.log.debug("pyout: %s", msg.get('content', ''))
108 self.log.debug("pyout: %s", msg.get('content', ''))
109 self._append_plain_text(self.output_sep, True)
109 self._append_plain_text(self.output_sep, True)
110 self._append_html(self._make_out_prompt(prompt_number), True)
110 self._append_html(self._make_out_prompt(prompt_number), True)
111 self._append_plain_text('\n', True)
111 self._append_plain_text('\n', True)
112
112
113 def _handle_pyout(self, msg):
113 def _handle_pyout(self, msg):
114 """ Overridden to handle rich data types, like SVG.
114 """ Overridden to handle rich data types, like SVG.
115 """
115 """
116 if not self._hidden and self._is_from_this_session(msg):
116 if not self._hidden and self._is_from_this_session(msg):
117 content = msg['content']
117 content = msg['content']
118 prompt_number = content.get('execution_count', 0)
118 prompt_number = content.get('execution_count', 0)
119 data = content['data']
119 data = content['data']
120 metadata = msg['content']['metadata']
120 metadata = msg['content']['metadata']
121 if 'image/svg+xml' in data:
121 if 'image/svg+xml' in data:
122 self._pre_image_append(msg, prompt_number)
122 self._pre_image_append(msg, prompt_number)
123 self._append_svg(data['image/svg+xml'], True)
123 self._append_svg(data['image/svg+xml'], True)
124 self._append_html(self.output_sep2, True)
124 self._append_html(self.output_sep2, True)
125 elif 'image/png' in data:
125 elif 'image/png' in data:
126 self._pre_image_append(msg, prompt_number)
126 self._pre_image_append(msg, prompt_number)
127 png = decodestring(data['image/png'].encode('ascii'))
127 png = decodestring(data['image/png'].encode('ascii'))
128 self._append_png(png, True, metadata=metadata.get('image/png', None))
128 self._append_png(png, True, metadata=metadata.get('image/png', None))
129 self._append_html(self.output_sep2, True)
129 self._append_html(self.output_sep2, True)
130 elif 'image/jpeg' in data and self._jpg_supported:
130 elif 'image/jpeg' in data and self._jpg_supported:
131 self._pre_image_append(msg, prompt_number)
131 self._pre_image_append(msg, prompt_number)
132 jpg = decodestring(data['image/jpeg'].encode('ascii'))
132 jpg = decodestring(data['image/jpeg'].encode('ascii'))
133 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
133 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
134 self._append_html(self.output_sep2, True)
134 self._append_html(self.output_sep2, True)
135 else:
135 else:
136 # Default back to the plain text representation.
136 # Default back to the plain text representation.
137 return super(RichIPythonWidget, self)._handle_pyout(msg)
137 return super(RichIPythonWidget, self)._handle_pyout(msg)
138
138
139 def _handle_display_data(self, msg):
139 def _handle_display_data(self, msg):
140 """ Overridden to handle rich data types, like SVG.
140 """ Overridden to handle rich data types, like SVG.
141 """
141 """
142 if not self._hidden and self._is_from_this_session(msg):
142 if not self._hidden and self._is_from_this_session(msg):
143 source = msg['content']['source']
143 source = msg['content']['source']
144 data = msg['content']['data']
144 data = msg['content']['data']
145 metadata = msg['content']['metadata']
145 metadata = msg['content']['metadata']
146 # Try to use the svg or html representations.
146 # Try to use the svg or html representations.
147 # FIXME: Is this the right ordering of things to try?
147 # FIXME: Is this the right ordering of things to try?
148 if 'image/svg+xml' in data:
148 if 'image/svg+xml' in data:
149 self.log.debug("display: %s", msg.get('content', ''))
149 self.log.debug("display: %s", msg.get('content', ''))
150 svg = data['image/svg+xml']
150 svg = data['image/svg+xml']
151 self._append_svg(svg, True)
151 self._append_svg(svg, True)
152 elif 'image/png' in data:
152 elif 'image/png' in data:
153 self.log.debug("display: %s", msg.get('content', ''))
153 self.log.debug("display: %s", msg.get('content', ''))
154 # PNG data is base64 encoded as it passes over the network
154 # PNG data is base64 encoded as it passes over the network
155 # in a JSON structure so we decode it.
155 # in a JSON structure so we decode it.
156 png = decodestring(data['image/png'].encode('ascii'))
156 png = decodestring(data['image/png'].encode('ascii'))
157 self._append_png(png, True, metadata=metadata.get('image/png', None))
157 self._append_png(png, True, metadata=metadata.get('image/png', None))
158 elif 'image/jpeg' in data and self._jpg_supported:
158 elif 'image/jpeg' in data and self._jpg_supported:
159 self.log.debug("display: %s", msg.get('content', ''))
159 self.log.debug("display: %s", msg.get('content', ''))
160 jpg = decodestring(data['image/jpeg'].encode('ascii'))
160 jpg = decodestring(data['image/jpeg'].encode('ascii'))
161 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
161 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
162 else:
162 else:
163 # Default back to the plain text representation.
163 # Default back to the plain text representation.
164 return super(RichIPythonWidget, self)._handle_display_data(msg)
164 return super(RichIPythonWidget, self)._handle_display_data(msg)
165
165
166 #---------------------------------------------------------------------------
166 #---------------------------------------------------------------------------
167 # 'RichIPythonWidget' protected interface
167 # 'RichIPythonWidget' protected interface
168 #---------------------------------------------------------------------------
168 #---------------------------------------------------------------------------
169
169
170 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
170 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
171 """ Append raw JPG data to the widget."""
171 """ Append raw JPG data to the widget."""
172 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
172 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
173
173
174 def _append_png(self, png, before_prompt=False, metadata=None):
174 def _append_png(self, png, before_prompt=False, metadata=None):
175 """ Append raw PNG data to the widget.
175 """ Append raw PNG data to the widget.
176 """
176 """
177 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
177 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
178
178
179 def _append_svg(self, svg, before_prompt=False):
179 def _append_svg(self, svg, before_prompt=False):
180 """ Append raw SVG data to the widget.
180 """ Append raw SVG data to the widget.
181 """
181 """
182 self._append_custom(self._insert_svg, svg, before_prompt)
182 self._append_custom(self._insert_svg, svg, before_prompt)
183
183
184 def _add_image(self, image):
184 def _add_image(self, image):
185 """ Adds the specified QImage to the document and returns a
185 """ Adds the specified QImage to the document and returns a
186 QTextImageFormat that references it.
186 QTextImageFormat that references it.
187 """
187 """
188 document = self._control.document()
188 document = self._control.document()
189 name = str(image.cacheKey())
189 name = str(image.cacheKey())
190 document.addResource(QtGui.QTextDocument.ImageResource,
190 document.addResource(QtGui.QTextDocument.ImageResource,
191 QtCore.QUrl(name), image)
191 QtCore.QUrl(name), image)
192 format = QtGui.QTextImageFormat()
192 format = QtGui.QTextImageFormat()
193 format.setName(name)
193 format.setName(name)
194 return format
194 return format
195
195
196 def _copy_image(self, name):
196 def _copy_image(self, name):
197 """ Copies the ImageResource with 'name' to the clipboard.
197 """ Copies the ImageResource with 'name' to the clipboard.
198 """
198 """
199 image = self._get_image(name)
199 image = self._get_image(name)
200 QtGui.QApplication.clipboard().setImage(image)
200 QtGui.QApplication.clipboard().setImage(image)
201
201
202 def _get_image(self, name):
202 def _get_image(self, name):
203 """ Returns the QImage stored as the ImageResource with 'name'.
203 """ Returns the QImage stored as the ImageResource with 'name'.
204 """
204 """
205 document = self._control.document()
205 document = self._control.document()
206 image = document.resource(QtGui.QTextDocument.ImageResource,
206 image = document.resource(QtGui.QTextDocument.ImageResource,
207 QtCore.QUrl(name))
207 QtCore.QUrl(name))
208 return image
208 return image
209
209
210 def _get_image_tag(self, match, path = None, format = "png"):
210 def _get_image_tag(self, match, path = None, format = "png"):
211 """ Return (X)HTML mark-up for the image-tag given by match.
211 """ Return (X)HTML mark-up for the image-tag given by match.
212
212
213 Parameters
213 Parameters
214 ----------
214 ----------
215 match : re.SRE_Match
215 match : re.SRE_Match
216 A match to an HTML image tag as exported by Qt, with
216 A match to an HTML image tag as exported by Qt, with
217 match.group("Name") containing the matched image ID.
217 match.group("Name") containing the matched image ID.
218
218
219 path : string|None, optional [default None]
219 path : string|None, optional [default None]
220 If not None, specifies a path to which supporting files may be
220 If not None, specifies a path to which supporting files may be
221 written (e.g., for linked images). If None, all images are to be
221 written (e.g., for linked images). If None, all images are to be
222 included inline.
222 included inline.
223
223
224 format : "png"|"svg"|"jpg", optional [default "png"]
224 format : "png"|"svg"|"jpg", optional [default "png"]
225 Format for returned or referenced images.
225 Format for returned or referenced images.
226 """
226 """
227 if format in ("png","jpg"):
227 if format in ("png","jpg"):
228 try:
228 try:
229 image = self._get_image(match.group("name"))
229 image = self._get_image(match.group("name"))
230 except KeyError:
230 except KeyError:
231 return "<b>Couldn't find image %s</b>" % match.group("name")
231 return "<b>Couldn't find image %s</b>" % match.group("name")
232
232
233 if path is not None:
233 if path is not None:
234 if not os.path.exists(path):
234 if not os.path.exists(path):
235 os.mkdir(path)
235 os.mkdir(path)
236 relpath = os.path.basename(path)
236 relpath = os.path.basename(path)
237 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
237 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
238 "PNG"):
238 "PNG"):
239 return '<img src="%s/qt_img%s.%s">' % (relpath,
239 return '<img src="%s/qt_img%s.%s">' % (relpath,
240 match.group("name"),format)
240 match.group("name"),format)
241 else:
241 else:
242 return "<b>Couldn't save image!</b>"
242 return "<b>Couldn't save image!</b>"
243 else:
243 else:
244 ba = QtCore.QByteArray()
244 ba = QtCore.QByteArray()
245 buffer_ = QtCore.QBuffer(ba)
245 buffer_ = QtCore.QBuffer(ba)
246 buffer_.open(QtCore.QIODevice.WriteOnly)
246 buffer_.open(QtCore.QIODevice.WriteOnly)
247 image.save(buffer_, format.upper())
247 image.save(buffer_, format.upper())
248 buffer_.close()
248 buffer_.close()
249 return '<img src="data:image/%s;base64,\n%s\n" />' % (
249 return '<img src="data:image/%s;base64,\n%s\n" />' % (
250 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
250 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
251
251
252 elif format == "svg":
252 elif format == "svg":
253 try:
253 try:
254 svg = str(self._name_to_svg_map[match.group("name")])
254 svg = str(self._name_to_svg_map[match.group("name")])
255 except KeyError:
255 except KeyError:
256 if not self._svg_warning_displayed:
256 if not self._svg_warning_displayed:
257 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
257 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
258 'Cannot convert a PNG to SVG. To fix this, add this '
258 'Cannot convert PNG images to SVG, export with PNG figures instead. '
259 'If you want to export matplotlib figures as SVG, add '
259 'to your ipython config:\n\n'
260 'to your ipython config:\n\n'
260 '\tc.InlineBackendConfig.figure_format = \'svg\'\n\n'
261 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
261 'And regenerate the figures.',
262 'And regenerate the figures.',
262 QtGui.QMessageBox.Ok)
263 QtGui.QMessageBox.Ok)
263 self._svg_warning_displayed = True
264 self._svg_warning_displayed = True
264 return ("<b>Cannot convert a PNG to SVG.</b> "
265 return ("<b>Cannot convert PNG images to SVG.</b> "
265 "To fix this, add this to your config: "
266 "You must export this session with PNG images. "
266 "<span>c.InlineBackendConfig.figure_format = 'svg'</span> "
267 "If you want to export matplotlib figures as SVG, add to your config "
268 "<span>c.InlineBackend.figure_format = 'svg'</span> "
267 "and regenerate the figures.")
269 "and regenerate the figures.")
268
270
269 # Not currently checking path, because it's tricky to find a
271 # Not currently checking path, because it's tricky to find a
270 # cross-browser way to embed external SVG images (e.g., via
272 # cross-browser way to embed external SVG images (e.g., via
271 # object or embed tags).
273 # object or embed tags).
272
274
273 # Chop stand-alone header from matplotlib SVG
275 # Chop stand-alone header from matplotlib SVG
274 offset = svg.find("<svg")
276 offset = svg.find("<svg")
275 assert(offset > -1)
277 assert(offset > -1)
276
278
277 return svg[offset:]
279 return svg[offset:]
278
280
279 else:
281 else:
280 return '<b>Unrecognized image format</b>'
282 return '<b>Unrecognized image format</b>'
281
283
282 def _insert_jpg(self, cursor, jpg, metadata=None):
284 def _insert_jpg(self, cursor, jpg, metadata=None):
283 """ Insert raw PNG data into the widget."""
285 """ Insert raw PNG data into the widget."""
284 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
286 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
285
287
286 def _insert_png(self, cursor, png, metadata=None):
288 def _insert_png(self, cursor, png, metadata=None):
287 """ Insert raw PNG data into the widget.
289 """ Insert raw PNG data into the widget.
288 """
290 """
289 self._insert_img(cursor, png, 'png', metadata=metadata)
291 self._insert_img(cursor, png, 'png', metadata=metadata)
290
292
291 def _insert_img(self, cursor, img, fmt, metadata=None):
293 def _insert_img(self, cursor, img, fmt, metadata=None):
292 """ insert a raw image, jpg or png """
294 """ insert a raw image, jpg or png """
293 if metadata:
295 if metadata:
294 width = metadata.get('width', None)
296 width = metadata.get('width', None)
295 height = metadata.get('height', None)
297 height = metadata.get('height', None)
296 else:
298 else:
297 width = height = None
299 width = height = None
298 try:
300 try:
299 image = QtGui.QImage()
301 image = QtGui.QImage()
300 image.loadFromData(img, fmt.upper())
302 image.loadFromData(img, fmt.upper())
301 if width and height:
303 if width and height:
302 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
304 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
303 elif width and not height:
305 elif width and not height:
304 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
306 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
305 elif height and not width:
307 elif height and not width:
306 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
308 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
307 except ValueError:
309 except ValueError:
308 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
310 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
309 else:
311 else:
310 format = self._add_image(image)
312 format = self._add_image(image)
311 cursor.insertBlock()
313 cursor.insertBlock()
312 cursor.insertImage(format)
314 cursor.insertImage(format)
313 cursor.insertBlock()
315 cursor.insertBlock()
314
316
315 def _insert_svg(self, cursor, svg):
317 def _insert_svg(self, cursor, svg):
316 """ Insert raw SVG data into the widet.
318 """ Insert raw SVG data into the widet.
317 """
319 """
318 try:
320 try:
319 image = svg_to_image(svg)
321 image = svg_to_image(svg)
320 except ValueError:
322 except ValueError:
321 self._insert_plain_text(cursor, 'Received invalid SVG data.')
323 self._insert_plain_text(cursor, 'Received invalid SVG data.')
322 else:
324 else:
323 format = self._add_image(image)
325 format = self._add_image(image)
324 self._name_to_svg_map[format.name()] = svg
326 self._name_to_svg_map[format.name()] = svg
325 cursor.insertBlock()
327 cursor.insertBlock()
326 cursor.insertImage(format)
328 cursor.insertImage(format)
327 cursor.insertBlock()
329 cursor.insertBlock()
328
330
329 def _save_image(self, name, format='PNG'):
331 def _save_image(self, name, format='PNG'):
330 """ Shows a save dialog for the ImageResource with 'name'.
332 """ Shows a save dialog for the ImageResource with 'name'.
331 """
333 """
332 dialog = QtGui.QFileDialog(self._control, 'Save Image')
334 dialog = QtGui.QFileDialog(self._control, 'Save Image')
333 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
335 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
334 dialog.setDefaultSuffix(format.lower())
336 dialog.setDefaultSuffix(format.lower())
335 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
337 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
336 if dialog.exec_():
338 if dialog.exec_():
337 filename = dialog.selectedFiles()[0]
339 filename = dialog.selectedFiles()[0]
338 image = self._get_image(name)
340 image = self._get_image(name)
339 image.save(filename, format)
341 image.save(filename, format)
@@ -1,255 +1,238 b''
1 """ Defines classes and functions for working with Qt's rich text system.
1 """ Defines classes and functions for working with Qt's rich text system.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports.
7 # Standard library imports
8 import io
8 import os
9 import os
9 import re
10 import re
10
11
11 # System library imports.
12 # System library imports
12 from IPython.external.qt import QtGui
13 from IPython.external.qt import QtGui
13
14
14 # IPython imports
15 # IPython imports
15 from IPython.utils import py3compat
16 from IPython.utils import py3compat
16
17
17 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
18 # Constants
19 # Constants
19 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
20
21
21 # A regular expression for an HTML paragraph with no content.
22 # A regular expression for an HTML paragraph with no content.
22 EMPTY_P_RE = re.compile(r'<p[^/>]*>\s*</p>')
23 EMPTY_P_RE = re.compile(r'<p[^/>]*>\s*</p>')
23
24
24 # A regular expression for matching images in rich text HTML.
25 # A regular expression for matching images in rich text HTML.
25 # Note that this is overly restrictive, but Qt's output is predictable...
26 # Note that this is overly restrictive, but Qt's output is predictable...
26 IMG_RE = re.compile(r'<img src="(?P<name>[\d]+)" />')
27 IMG_RE = re.compile(r'<img src="(?P<name>[\d]+)" />')
27
28
28 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
29 # Classes
30 # Classes
30 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
31
32
32 class HtmlExporter(object):
33 class HtmlExporter(object):
33 """ A stateful HTML exporter for a Q(Plain)TextEdit.
34 """ A stateful HTML exporter for a Q(Plain)TextEdit.
34
35
35 This class is designed for convenient user interaction.
36 This class is designed for convenient user interaction.
36 """
37 """
37
38
38 def __init__(self, control):
39 def __init__(self, control):
39 """ Creates an HtmlExporter for the given Q(Plain)TextEdit.
40 """ Creates an HtmlExporter for the given Q(Plain)TextEdit.
40 """
41 """
41 assert isinstance(control, (QtGui.QPlainTextEdit, QtGui.QTextEdit))
42 assert isinstance(control, (QtGui.QPlainTextEdit, QtGui.QTextEdit))
42 self.control = control
43 self.control = control
43 self.filename = 'ipython.html'
44 self.filename = 'ipython.html'
44 self.image_tag = None
45 self.image_tag = None
45 self.inline_png = None
46 self.inline_png = None
46
47
47 def export(self):
48 def export(self):
48 """ Displays a dialog for exporting HTML generated by Qt's rich text
49 """ Displays a dialog for exporting HTML generated by Qt's rich text
49 system.
50 system.
50
51
51 Returns
52 Returns
52 -------
53 -------
53 The name of the file that was saved, or None if no file was saved.
54 The name of the file that was saved, or None if no file was saved.
54 """
55 """
55 parent = self.control.window()
56 parent = self.control.window()
56 dialog = QtGui.QFileDialog(parent, 'Save as...')
57 dialog = QtGui.QFileDialog(parent, 'Save as...')
57 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
58 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
58 filters = [
59 filters = [
59 'HTML with PNG figures (*.html *.htm)',
60 'HTML with PNG figures (*.html *.htm)',
60 'XHTML with inline SVG figures (*.xhtml *.xml)'
61 'XHTML with inline SVG figures (*.xhtml *.xml)'
61 ]
62 ]
62 dialog.setNameFilters(filters)
63 dialog.setNameFilters(filters)
63 if self.filename:
64 if self.filename:
64 dialog.selectFile(self.filename)
65 dialog.selectFile(self.filename)
65 root,ext = os.path.splitext(self.filename)
66 root,ext = os.path.splitext(self.filename)
66 if ext.lower() in ('.xml', '.xhtml'):
67 if ext.lower() in ('.xml', '.xhtml'):
67 dialog.selectNameFilter(filters[-1])
68 dialog.selectNameFilter(filters[-1])
68
69
69 if dialog.exec_():
70 if dialog.exec_():
70 self.filename = dialog.selectedFiles()[0]
71 self.filename = dialog.selectedFiles()[0]
71 choice = dialog.selectedNameFilter()
72 choice = dialog.selectedNameFilter()
72 html = self.control.document().toHtml().encode('utf-8')
73 html = py3compat.cast_unicode(self.control.document().toHtml())
73
74
74 # Configure the exporter.
75 # Configure the exporter.
75 if choice.startswith('XHTML'):
76 if choice.startswith('XHTML'):
76 exporter = export_xhtml
77 exporter = export_xhtml
77 else:
78 else:
78 # If there are PNGs, decide how to export them.
79 # If there are PNGs, decide how to export them.
79 inline = self.inline_png
80 inline = self.inline_png
80 if inline is None and IMG_RE.search(html):
81 if inline is None and IMG_RE.search(html):
81 dialog = QtGui.QDialog(parent)
82 dialog = QtGui.QDialog(parent)
82 dialog.setWindowTitle('Save as...')
83 dialog.setWindowTitle('Save as...')
83 layout = QtGui.QVBoxLayout(dialog)
84 layout = QtGui.QVBoxLayout(dialog)
84 msg = "Exporting HTML with PNGs"
85 msg = "Exporting HTML with PNGs"
85 info = "Would you like inline PNGs (single large html " \
86 info = "Would you like inline PNGs (single large html " \
86 "file) or external image files?"
87 "file) or external image files?"
87 checkbox = QtGui.QCheckBox("&Don't ask again")
88 checkbox = QtGui.QCheckBox("&Don't ask again")
88 checkbox.setShortcut('D')
89 checkbox.setShortcut('D')
89 ib = QtGui.QPushButton("&Inline")
90 ib = QtGui.QPushButton("&Inline")
90 ib.setShortcut('I')
91 ib.setShortcut('I')
91 eb = QtGui.QPushButton("&External")
92 eb = QtGui.QPushButton("&External")
92 eb.setShortcut('E')
93 eb.setShortcut('E')
93 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
94 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
94 dialog.windowTitle(), msg)
95 dialog.windowTitle(), msg)
95 box.setInformativeText(info)
96 box.setInformativeText(info)
96 box.addButton(ib, QtGui.QMessageBox.NoRole)
97 box.addButton(ib, QtGui.QMessageBox.NoRole)
97 box.addButton(eb, QtGui.QMessageBox.YesRole)
98 box.addButton(eb, QtGui.QMessageBox.YesRole)
98 layout.setSpacing(0)
99 layout.setSpacing(0)
99 layout.addWidget(box)
100 layout.addWidget(box)
100 layout.addWidget(checkbox)
101 layout.addWidget(checkbox)
101 dialog.setLayout(layout)
102 dialog.setLayout(layout)
102 dialog.show()
103 dialog.show()
103 reply = box.exec_()
104 reply = box.exec_()
104 dialog.hide()
105 dialog.hide()
105 inline = (reply == 0)
106 inline = (reply == 0)
106 if checkbox.checkState():
107 if checkbox.checkState():
107 # Don't ask anymore; always use this choice.
108 # Don't ask anymore; always use this choice.
108 self.inline_png = inline
109 self.inline_png = inline
109 exporter = lambda h, f, i: export_html(h, f, i, inline)
110 exporter = lambda h, f, i: export_html(h, f, i, inline)
110
111
111 # Perform the export!
112 # Perform the export!
112 try:
113 try:
113 return exporter(html, self.filename, self.image_tag)
114 return exporter(html, self.filename, self.image_tag)
114 except Exception as e:
115 except Exception as e:
115 msg = "Error exporting HTML to %s\n" % self.filename + str(e)
116 msg = "Error exporting HTML to %s\n" % self.filename + str(e)
116 reply = QtGui.QMessageBox.warning(parent, 'Error', msg,
117 reply = QtGui.QMessageBox.warning(parent, 'Error', msg,
117 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
118 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
118
119
119 return None
120 return None
120
121
121 #-----------------------------------------------------------------------------
122 #-----------------------------------------------------------------------------
122 # Functions
123 # Functions
123 #-----------------------------------------------------------------------------
124 #-----------------------------------------------------------------------------
124
125
125 def export_html(html, filename, image_tag = None, inline = True):
126 def export_html(html, filename, image_tag = None, inline = True):
126 """ Export the contents of the ConsoleWidget as HTML.
127 """ Export the contents of the ConsoleWidget as HTML.
127
128
128 Parameters:
129 Parameters:
129 -----------
130 -----------
130 html : str,
131 html : unicode,
131 A utf-8 encoded Python string containing the Qt HTML to export.
132 A Python unicode string containing the Qt HTML to export.
132
133
133 filename : str
134 filename : str
134 The file to be saved.
135 The file to be saved.
135
136
136 image_tag : callable, optional (default None)
137 image_tag : callable, optional (default None)
137 Used to convert images. See ``default_image_tag()`` for information.
138 Used to convert images. See ``default_image_tag()`` for information.
138
139
139 inline : bool, optional [default True]
140 inline : bool, optional [default True]
140 If True, include images as inline PNGs. Otherwise, include them as
141 If True, include images as inline PNGs. Otherwise, include them as
141 links to external PNG files, mimicking web browsers' "Web Page,
142 links to external PNG files, mimicking web browsers' "Web Page,
142 Complete" behavior.
143 Complete" behavior.
143 """
144 """
144 if image_tag is None:
145 if image_tag is None:
145 image_tag = default_image_tag
146 image_tag = default_image_tag
146 else:
147 image_tag = ensure_utf8(image_tag)
148
147
149 if inline:
148 if inline:
150 path = None
149 path = None
151 else:
150 else:
152 root,ext = os.path.splitext(filename)
151 root,ext = os.path.splitext(filename)
153 path = root + "_files"
152 path = root + "_files"
154 if os.path.isfile(path):
153 if os.path.isfile(path):
155 raise OSError("%s exists, but is not a directory." % path)
154 raise OSError("%s exists, but is not a directory." % path)
156
155
157 with open(filename, 'w') as f:
156 with io.open(filename, 'w', encoding='utf-8') as f:
158 html = fix_html(html)
157 html = fix_html(html)
159 f.write(IMG_RE.sub(lambda x: image_tag(x, path = path, format = "png"),
158 f.write(IMG_RE.sub(lambda x: image_tag(x, path = path, format = "png"),
160 html))
159 html))
161
160
162
161
163 def export_xhtml(html, filename, image_tag=None):
162 def export_xhtml(html, filename, image_tag=None):
164 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
163 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
165
164
166 Parameters:
165 Parameters:
167 -----------
166 -----------
168 html : str,
167 html : unicode,
169 A utf-8 encoded Python string containing the Qt HTML to export.
168 A Python unicode string containing the Qt HTML to export.
170
169
171 filename : str
170 filename : str
172 The file to be saved.
171 The file to be saved.
173
172
174 image_tag : callable, optional (default None)
173 image_tag : callable, optional (default None)
175 Used to convert images. See ``default_image_tag()`` for information.
174 Used to convert images. See ``default_image_tag()`` for information.
176 """
175 """
177 if image_tag is None:
176 if image_tag is None:
178 image_tag = default_image_tag
177 image_tag = default_image_tag
179 else:
180 image_tag = ensure_utf8(image_tag)
181
178
182 with open(filename, 'w') as f:
179 with io.open(filename, 'w', encoding='utf-8') as f:
183 # Hack to make xhtml header -- note that we are not doing any check for
180 # Hack to make xhtml header -- note that we are not doing any check for
184 # valid XML.
181 # valid XML.
185 offset = html.find("<html>")
182 offset = html.find("<html>")
186 assert offset > -1, 'Invalid HTML string: no <html> tag.'
183 assert offset > -1, 'Invalid HTML string: no <html> tag.'
187 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
184 html = (u'<html xmlns="http://www.w3.org/1999/xhtml">\n'+
188 html[offset+6:])
185 html[offset+6:])
189
186
190 html = fix_html(html)
187 html = fix_html(html)
191 f.write(IMG_RE.sub(lambda x: image_tag(x, path = None, format = "svg"),
188 f.write(IMG_RE.sub(lambda x: image_tag(x, path = None, format = "svg"),
192 html))
189 html))
193
190
194
191
195 def default_image_tag(match, path = None, format = "png"):
192 def default_image_tag(match, path = None, format = "png"):
196 """ Return (X)HTML mark-up for the image-tag given by match.
193 """ Return (X)HTML mark-up for the image-tag given by match.
197
194
198 This default implementation merely removes the image, and exists mostly
195 This default implementation merely removes the image, and exists mostly
199 for documentation purposes. More information than is present in the Qt
196 for documentation purposes. More information than is present in the Qt
200 HTML is required to supply the images.
197 HTML is required to supply the images.
201
198
202 Parameters
199 Parameters
203 ----------
200 ----------
204 match : re.SRE_Match
201 match : re.SRE_Match
205 A match to an HTML image tag as exported by Qt, with match.group("Name")
202 A match to an HTML image tag as exported by Qt, with match.group("Name")
206 containing the matched image ID.
203 containing the matched image ID.
207
204
208 path : string|None, optional [default None]
205 path : string|None, optional [default None]
209 If not None, specifies a path to which supporting files may be written
206 If not None, specifies a path to which supporting files may be written
210 (e.g., for linked images). If None, all images are to be included
207 (e.g., for linked images). If None, all images are to be included
211 inline.
208 inline.
212
209
213 format : "png"|"svg", optional [default "png"]
210 format : "png"|"svg", optional [default "png"]
214 Format for returned or referenced images.
211 Format for returned or referenced images.
215 """
212 """
216 return ''
213 return u''
217
218
219 def ensure_utf8(image_tag):
220 """wrapper for ensuring image_tag returns utf8-encoded str on Python 2"""
221 if py3compat.PY3:
222 # nothing to do on Python 3
223 return image_tag
224
225 def utf8_image_tag(*args, **kwargs):
226 s = image_tag(*args, **kwargs)
227 if isinstance(s, unicode):
228 s = s.encode('utf8')
229 return s
230 return utf8_image_tag
231
214
232
215
233 def fix_html(html):
216 def fix_html(html):
234 """ Transforms a Qt-generated HTML string into a standards-compliant one.
217 """ Transforms a Qt-generated HTML string into a standards-compliant one.
235
218
236 Parameters:
219 Parameters:
237 -----------
220 -----------
238 html : str,
221 html : unicode,
239 A utf-8 encoded Python string containing the Qt HTML.
222 A Python unicode string containing the Qt HTML.
240 """
223 """
241 # A UTF-8 declaration is needed for proper rendering of some characters
224 # A UTF-8 declaration is needed for proper rendering of some characters
242 # (e.g., indented commands) when viewing exported HTML on a local system
225 # (e.g., indented commands) when viewing exported HTML on a local system
243 # (i.e., without seeing an encoding declaration in an HTTP header).
226 # (i.e., without seeing an encoding declaration in an HTTP header).
244 # C.f. http://www.w3.org/International/O-charset for details.
227 # C.f. http://www.w3.org/International/O-charset for details.
245 offset = html.find('<head>')
228 offset = html.find('<head>')
246 if offset > -1:
229 if offset > -1:
247 html = (html[:offset+6]+
230 html = (html[:offset+6]+
248 '\n<meta http-equiv="Content-Type" '+
231 '\n<meta http-equiv="Content-Type" '+
249 'content="text/html; charset=utf-8" />\n'+
232 'content="text/html; charset=utf-8" />\n'+
250 html[offset+6:])
233 html[offset+6:])
251
234
252 # Replace empty paragraphs tags with line breaks.
235 # Replace empty paragraphs tags with line breaks.
253 html = re.sub(EMPTY_P_RE, '<br/>', html)
236 html = re.sub(EMPTY_P_RE, '<br/>', html)
254
237
255 return html
238 return html
General Comments 0
You need to be logged in to leave comments. Login now