##// END OF EJS Templates
support latex in display messages in qt
Min RK -
Show More
@@ -1,348 +1,357 b''
1 1 # Copyright (c) IPython Development Team.
2 2 # Distributed under the terms of the Modified BSD License.
3 3
4 4 from base64 import decodestring
5 5 import os
6 6 import re
7 7
8 8 from IPython.external.qt import QtCore, QtGui
9 9
10 10 from IPython.lib.latextools import latex_to_png
11 11 from IPython.utils.path import ensure_dir_exists
12 12 from IPython.utils.traitlets import Bool
13 13 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
14 14 from .ipython_widget import IPythonWidget
15 15
16 16
17 17 class RichIPythonWidget(IPythonWidget):
18 18 """ An IPythonWidget that supports rich text, including lists, images, and
19 19 tables. Note that raw performance will be reduced compared to the plain
20 20 text version.
21 21 """
22 22
23 23 # RichIPythonWidget protected class variables.
24 24 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
25 25 _jpg_supported = Bool(False)
26 26
27 27 # Used to determine whether a given html export attempt has already
28 28 # displayed a warning about being unable to convert a png to svg.
29 29 _svg_warning_displayed = False
30 30
31 31 #---------------------------------------------------------------------------
32 32 # 'object' interface
33 33 #---------------------------------------------------------------------------
34 34
35 35 def __init__(self, *args, **kw):
36 36 """ Create a RichIPythonWidget.
37 37 """
38 38 kw['kind'] = 'rich'
39 39 super(RichIPythonWidget, self).__init__(*args, **kw)
40 40
41 41 # Configure the ConsoleWidget HTML exporter for our formats.
42 42 self._html_exporter.image_tag = self._get_image_tag
43 43
44 44 # Dictionary for resolving document resource names to SVG data.
45 45 self._name_to_svg_map = {}
46 46
47 47 # Do we support jpg ?
48 48 # it seems that sometime jpg support is a plugin of QT, so try to assume
49 49 # it is not always supported.
50 50 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
51 51 self._jpg_supported = 'jpeg' in _supported_format
52 52
53 53
54 54 #---------------------------------------------------------------------------
55 55 # 'ConsoleWidget' public interface overides
56 56 #---------------------------------------------------------------------------
57 57
58 58 def export_html(self):
59 59 """ Shows a dialog to export HTML/XML in various formats.
60 60
61 61 Overridden in order to reset the _svg_warning_displayed flag prior
62 62 to the export running.
63 63 """
64 64 self._svg_warning_displayed = False
65 65 super(RichIPythonWidget, self).export_html()
66 66
67 67
68 68 #---------------------------------------------------------------------------
69 69 # 'ConsoleWidget' protected interface
70 70 #---------------------------------------------------------------------------
71 71
72 72 def _context_menu_make(self, pos):
73 73 """ Reimplemented to return a custom context menu for images.
74 74 """
75 75 format = self._control.cursorForPosition(pos).charFormat()
76 76 name = format.stringProperty(QtGui.QTextFormat.ImageName)
77 77 if name:
78 78 menu = QtGui.QMenu()
79 79
80 80 menu.addAction('Copy Image', lambda: self._copy_image(name))
81 81 menu.addAction('Save Image As...', lambda: self._save_image(name))
82 82 menu.addSeparator()
83 83
84 84 svg = self._name_to_svg_map.get(name, None)
85 85 if svg is not None:
86 86 menu.addSeparator()
87 87 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
88 88 menu.addAction('Save SVG As...',
89 89 lambda: save_svg(svg, self._control))
90 90 else:
91 91 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
92 92 return menu
93 93
94 94 #---------------------------------------------------------------------------
95 95 # 'BaseFrontendMixin' abstract interface
96 96 #---------------------------------------------------------------------------
97 97 def _pre_image_append(self, msg, prompt_number):
98 98 """ Append the Out[] prompt and make the output nicer
99 99
100 100 Shared code for some the following if statement
101 101 """
102 102 self.log.debug("execute_result: %s", msg.get('content', ''))
103 103 self._append_plain_text(self.output_sep, True)
104 104 self._append_html(self._make_out_prompt(prompt_number), True)
105 105 self._append_plain_text('\n', True)
106 106
107 107 def _handle_execute_result(self, msg):
108 108 """ Overridden to handle rich data types, like SVG.
109 109 """
110 110 if self.include_output(msg):
111 111 self.flush_clearoutput()
112 112 content = msg['content']
113 113 prompt_number = content.get('execution_count', 0)
114 114 data = content['data']
115 115 metadata = msg['content']['metadata']
116 116 if 'image/svg+xml' in data:
117 117 self._pre_image_append(msg, prompt_number)
118 118 self._append_svg(data['image/svg+xml'], True)
119 119 self._append_html(self.output_sep2, True)
120 120 elif 'image/png' in data:
121 121 self._pre_image_append(msg, prompt_number)
122 122 png = decodestring(data['image/png'].encode('ascii'))
123 123 self._append_png(png, True, metadata=metadata.get('image/png', None))
124 124 self._append_html(self.output_sep2, True)
125 125 elif 'image/jpeg' in data and self._jpg_supported:
126 126 self._pre_image_append(msg, prompt_number)
127 127 jpg = decodestring(data['image/jpeg'].encode('ascii'))
128 128 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
129 129 self._append_html(self.output_sep2, True)
130 130 elif 'text/latex' in data:
131 131 self._pre_image_append(msg, prompt_number)
132 132 try:
133 133 png = latex_to_png(data['text/latex'], wrap=False)
134 134 except Exception:
135 135 self.log.error("Failed to render latex: %r", data['text/latex'], exc_info=True)
136 136 png = None
137 137 if png is not None:
138 138 self._append_png(png, True)
139 139 self._append_html(self.output_sep2, True)
140 140 else:
141 141 # Print plain text if png can't be generated
142 142 return super(RichIPythonWidget, self)._handle_execute_result(msg)
143 143 else:
144 144 # Default back to the plain text representation.
145 145 return super(RichIPythonWidget, self)._handle_execute_result(msg)
146 146
147 147 def _handle_display_data(self, msg):
148 148 """ Overridden to handle rich data types, like SVG.
149 149 """
150 150 if self.include_output(msg):
151 151 self.flush_clearoutput()
152 152 data = msg['content']['data']
153 153 metadata = msg['content']['metadata']
154 154 # Try to use the svg or html representations.
155 155 # FIXME: Is this the right ordering of things to try?
156 self.log.debug("display: %s", msg.get('content', ''))
156 157 if 'image/svg+xml' in data:
157 self.log.debug("display: %s", msg.get('content', ''))
158 158 svg = data['image/svg+xml']
159 159 self._append_svg(svg, True)
160 160 elif 'image/png' in data:
161 self.log.debug("display: %s", msg.get('content', ''))
162 161 # PNG data is base64 encoded as it passes over the network
163 162 # in a JSON structure so we decode it.
164 163 png = decodestring(data['image/png'].encode('ascii'))
165 164 self._append_png(png, True, metadata=metadata.get('image/png', None))
166 165 elif 'image/jpeg' in data and self._jpg_supported:
167 self.log.debug("display: %s", msg.get('content', ''))
168 166 jpg = decodestring(data['image/jpeg'].encode('ascii'))
169 167 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
168 elif 'text/latex' in data:
169 try:
170 png = latex_to_png(data['text/latex'], wrap=False)
171 except Exception:
172 self.log.error("Failed to render latex: %r", data['text/latex'], exc_info=True)
173 png = None
174 if png is not None:
175 self._append_png(png, True)
176 else:
177 # Print plain text if png can't be generated
178 return super(RichIPythonWidget, self)._handle_display_data(msg)
170 179 else:
171 180 # Default back to the plain text representation.
172 181 return super(RichIPythonWidget, self)._handle_display_data(msg)
173 182
174 183 #---------------------------------------------------------------------------
175 184 # 'RichIPythonWidget' protected interface
176 185 #---------------------------------------------------------------------------
177 186
178 187 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
179 188 """ Append raw JPG data to the widget."""
180 189 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
181 190
182 191 def _append_png(self, png, before_prompt=False, metadata=None):
183 192 """ Append raw PNG data to the widget.
184 193 """
185 194 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
186 195
187 196 def _append_svg(self, svg, before_prompt=False):
188 197 """ Append raw SVG data to the widget.
189 198 """
190 199 self._append_custom(self._insert_svg, svg, before_prompt)
191 200
192 201 def _add_image(self, image):
193 202 """ Adds the specified QImage to the document and returns a
194 203 QTextImageFormat that references it.
195 204 """
196 205 document = self._control.document()
197 206 name = str(image.cacheKey())
198 207 document.addResource(QtGui.QTextDocument.ImageResource,
199 208 QtCore.QUrl(name), image)
200 209 format = QtGui.QTextImageFormat()
201 210 format.setName(name)
202 211 return format
203 212
204 213 def _copy_image(self, name):
205 214 """ Copies the ImageResource with 'name' to the clipboard.
206 215 """
207 216 image = self._get_image(name)
208 217 QtGui.QApplication.clipboard().setImage(image)
209 218
210 219 def _get_image(self, name):
211 220 """ Returns the QImage stored as the ImageResource with 'name'.
212 221 """
213 222 document = self._control.document()
214 223 image = document.resource(QtGui.QTextDocument.ImageResource,
215 224 QtCore.QUrl(name))
216 225 return image
217 226
218 227 def _get_image_tag(self, match, path = None, format = "png"):
219 228 """ Return (X)HTML mark-up for the image-tag given by match.
220 229
221 230 Parameters
222 231 ----------
223 232 match : re.SRE_Match
224 233 A match to an HTML image tag as exported by Qt, with
225 234 match.group("Name") containing the matched image ID.
226 235
227 236 path : string|None, optional [default None]
228 237 If not None, specifies a path to which supporting files may be
229 238 written (e.g., for linked images). If None, all images are to be
230 239 included inline.
231 240
232 241 format : "png"|"svg"|"jpg", optional [default "png"]
233 242 Format for returned or referenced images.
234 243 """
235 244 if format in ("png","jpg"):
236 245 try:
237 246 image = self._get_image(match.group("name"))
238 247 except KeyError:
239 248 return "<b>Couldn't find image %s</b>" % match.group("name")
240 249
241 250 if path is not None:
242 251 ensure_dir_exists(path)
243 252 relpath = os.path.basename(path)
244 253 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
245 254 "PNG"):
246 255 return '<img src="%s/qt_img%s.%s">' % (relpath,
247 256 match.group("name"),format)
248 257 else:
249 258 return "<b>Couldn't save image!</b>"
250 259 else:
251 260 ba = QtCore.QByteArray()
252 261 buffer_ = QtCore.QBuffer(ba)
253 262 buffer_.open(QtCore.QIODevice.WriteOnly)
254 263 image.save(buffer_, format.upper())
255 264 buffer_.close()
256 265 return '<img src="data:image/%s;base64,\n%s\n" />' % (
257 266 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
258 267
259 268 elif format == "svg":
260 269 try:
261 270 svg = str(self._name_to_svg_map[match.group("name")])
262 271 except KeyError:
263 272 if not self._svg_warning_displayed:
264 273 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
265 274 'Cannot convert PNG images to SVG, export with PNG figures instead. '
266 275 'If you want to export matplotlib figures as SVG, add '
267 276 'to your ipython config:\n\n'
268 277 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
269 278 'And regenerate the figures.',
270 279 QtGui.QMessageBox.Ok)
271 280 self._svg_warning_displayed = True
272 281 return ("<b>Cannot convert PNG images to SVG.</b> "
273 282 "You must export this session with PNG images. "
274 283 "If you want to export matplotlib figures as SVG, add to your config "
275 284 "<span>c.InlineBackend.figure_format = 'svg'</span> "
276 285 "and regenerate the figures.")
277 286
278 287 # Not currently checking path, because it's tricky to find a
279 288 # cross-browser way to embed external SVG images (e.g., via
280 289 # object or embed tags).
281 290
282 291 # Chop stand-alone header from matplotlib SVG
283 292 offset = svg.find("<svg")
284 293 assert(offset > -1)
285 294
286 295 return svg[offset:]
287 296
288 297 else:
289 298 return '<b>Unrecognized image format</b>'
290 299
291 300 def _insert_jpg(self, cursor, jpg, metadata=None):
292 301 """ Insert raw PNG data into the widget."""
293 302 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
294 303
295 304 def _insert_png(self, cursor, png, metadata=None):
296 305 """ Insert raw PNG data into the widget.
297 306 """
298 307 self._insert_img(cursor, png, 'png', metadata=metadata)
299 308
300 309 def _insert_img(self, cursor, img, fmt, metadata=None):
301 310 """ insert a raw image, jpg or png """
302 311 if metadata:
303 312 width = metadata.get('width', None)
304 313 height = metadata.get('height', None)
305 314 else:
306 315 width = height = None
307 316 try:
308 317 image = QtGui.QImage()
309 318 image.loadFromData(img, fmt.upper())
310 319 if width and height:
311 320 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
312 321 elif width and not height:
313 322 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
314 323 elif height and not width:
315 324 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
316 325 except ValueError:
317 326 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
318 327 else:
319 328 format = self._add_image(image)
320 329 cursor.insertBlock()
321 330 cursor.insertImage(format)
322 331 cursor.insertBlock()
323 332
324 333 def _insert_svg(self, cursor, svg):
325 334 """ Insert raw SVG data into the widet.
326 335 """
327 336 try:
328 337 image = svg_to_image(svg)
329 338 except ValueError:
330 339 self._insert_plain_text(cursor, 'Received invalid SVG data.')
331 340 else:
332 341 format = self._add_image(image)
333 342 self._name_to_svg_map[format.name()] = svg
334 343 cursor.insertBlock()
335 344 cursor.insertImage(format)
336 345 cursor.insertBlock()
337 346
338 347 def _save_image(self, name, format='PNG'):
339 348 """ Shows a save dialog for the ImageResource with 'name'.
340 349 """
341 350 dialog = QtGui.QFileDialog(self._control, 'Save Image')
342 351 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
343 352 dialog.setDefaultSuffix(format.lower())
344 353 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
345 354 if dialog.exec_():
346 355 filename = dialog.selectedFiles()[0]
347 356 image = self._get_image(name)
348 357 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now