##// END OF EJS Templates
add RichIPythonWidget._append_latex...
Min RK -
Show More
@@ -1,357 +1,350 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 """ Append the Out[] prompt and make the output nicer
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 self.log.debug("execute_result: %s", msg.get('content', ''))
103 102 self._append_plain_text(self.output_sep, True)
104 103 self._append_html(self._make_out_prompt(prompt_number), True)
105 104 self._append_plain_text('\n', True)
106 105
107 106 def _handle_execute_result(self, msg):
108 """ Overridden to handle rich data types, like SVG.
109 """
107 """Overridden to handle rich data types, like SVG."""
108 self.log.debug("execute_result: %s", msg.get('content', ''))
110 109 if self.include_output(msg):
111 110 self.flush_clearoutput()
112 111 content = msg['content']
113 112 prompt_number = content.get('execution_count', 0)
114 113 data = content['data']
115 114 metadata = msg['content']['metadata']
116 115 if 'image/svg+xml' in data:
117 116 self._pre_image_append(msg, prompt_number)
118 117 self._append_svg(data['image/svg+xml'], True)
119 118 self._append_html(self.output_sep2, True)
120 119 elif 'image/png' in data:
121 120 self._pre_image_append(msg, prompt_number)
122 121 png = decodestring(data['image/png'].encode('ascii'))
123 122 self._append_png(png, True, metadata=metadata.get('image/png', None))
124 123 self._append_html(self.output_sep2, True)
125 124 elif 'image/jpeg' in data and self._jpg_supported:
126 125 self._pre_image_append(msg, prompt_number)
127 126 jpg = decodestring(data['image/jpeg'].encode('ascii'))
128 127 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
129 128 self._append_html(self.output_sep2, True)
130 129 elif 'text/latex' in data:
131 130 self._pre_image_append(msg, prompt_number)
132 try:
133 png = latex_to_png(data['text/latex'], wrap=False)
134 except Exception:
135 self.log.error("Failed to render latex: %r", data['text/latex'], exc_info=True)
136 png = None
137 if png is not None:
138 self._append_png(png, True)
139 self._append_html(self.output_sep2, True)
140 else:
141 # Print plain text if png can't be generated
142 return super(RichIPythonWidget, self)._handle_execute_result(msg)
131 self._append_latex(data['text/latex'], True)
132 self._append_html(self.output_sep2, True)
143 133 else:
144 134 # Default back to the plain text representation.
145 135 return super(RichIPythonWidget, self)._handle_execute_result(msg)
146 136
147 137 def _handle_display_data(self, msg):
148 """ Overridden to handle rich data types, like SVG.
149 """
138 """Overridden to handle rich data types, like SVG."""
139 self.log.debug("display_data: %s", msg.get('content', ''))
150 140 if self.include_output(msg):
151 141 self.flush_clearoutput()
152 142 data = msg['content']['data']
153 143 metadata = msg['content']['metadata']
154 144 # Try to use the svg or html representations.
155 145 # FIXME: Is this the right ordering of things to try?
156 146 self.log.debug("display: %s", msg.get('content', ''))
157 147 if 'image/svg+xml' in data:
158 148 svg = data['image/svg+xml']
159 149 self._append_svg(svg, True)
160 150 elif 'image/png' in data:
161 151 # PNG data is base64 encoded as it passes over the network
162 152 # in a JSON structure so we decode it.
163 153 png = decodestring(data['image/png'].encode('ascii'))
164 154 self._append_png(png, True, metadata=metadata.get('image/png', None))
165 155 elif 'image/jpeg' in data and self._jpg_supported:
166 156 jpg = decodestring(data['image/jpeg'].encode('ascii'))
167 157 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
168 158 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)
159 self._append_latex(data['text/latex'], True)
179 160 else:
180 161 # Default back to the plain text representation.
181 162 return super(RichIPythonWidget, self)._handle_display_data(msg)
182 163
183 164 #---------------------------------------------------------------------------
184 165 # 'RichIPythonWidget' protected interface
185 166 #---------------------------------------------------------------------------
186 167
168 def _append_latex(self, latex, before_prompt=False, metadata=None):
169 """ Append latex data to the widget."""
170 try:
171 png = latex_to_png(latex, wrap=False)
172 except Exception as e:
173 self.log.error("Failed to render latex: '%s'", latex, exc_info=True)
174 self._append_plain_text("Failed to render latex:\n%s\nError was: %s" % (
175 latex, e,
176 ), before_prompt)
177 else:
178 self._append_png(png, before_prompt, metadata)
179
187 180 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
188 181 """ Append raw JPG data to the widget."""
189 182 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
190 183
191 184 def _append_png(self, png, before_prompt=False, metadata=None):
192 185 """ Append raw PNG data to the widget.
193 186 """
194 187 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
195 188
196 189 def _append_svg(self, svg, before_prompt=False):
197 190 """ Append raw SVG data to the widget.
198 191 """
199 192 self._append_custom(self._insert_svg, svg, before_prompt)
200 193
201 194 def _add_image(self, image):
202 195 """ Adds the specified QImage to the document and returns a
203 196 QTextImageFormat that references it.
204 197 """
205 198 document = self._control.document()
206 199 name = str(image.cacheKey())
207 200 document.addResource(QtGui.QTextDocument.ImageResource,
208 201 QtCore.QUrl(name), image)
209 202 format = QtGui.QTextImageFormat()
210 203 format.setName(name)
211 204 return format
212 205
213 206 def _copy_image(self, name):
214 207 """ Copies the ImageResource with 'name' to the clipboard.
215 208 """
216 209 image = self._get_image(name)
217 210 QtGui.QApplication.clipboard().setImage(image)
218 211
219 212 def _get_image(self, name):
220 213 """ Returns the QImage stored as the ImageResource with 'name'.
221 214 """
222 215 document = self._control.document()
223 216 image = document.resource(QtGui.QTextDocument.ImageResource,
224 217 QtCore.QUrl(name))
225 218 return image
226 219
227 220 def _get_image_tag(self, match, path = None, format = "png"):
228 221 """ Return (X)HTML mark-up for the image-tag given by match.
229 222
230 223 Parameters
231 224 ----------
232 225 match : re.SRE_Match
233 226 A match to an HTML image tag as exported by Qt, with
234 227 match.group("Name") containing the matched image ID.
235 228
236 229 path : string|None, optional [default None]
237 230 If not None, specifies a path to which supporting files may be
238 231 written (e.g., for linked images). If None, all images are to be
239 232 included inline.
240 233
241 234 format : "png"|"svg"|"jpg", optional [default "png"]
242 235 Format for returned or referenced images.
243 236 """
244 237 if format in ("png","jpg"):
245 238 try:
246 239 image = self._get_image(match.group("name"))
247 240 except KeyError:
248 241 return "<b>Couldn't find image %s</b>" % match.group("name")
249 242
250 243 if path is not None:
251 244 ensure_dir_exists(path)
252 245 relpath = os.path.basename(path)
253 246 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
254 247 "PNG"):
255 248 return '<img src="%s/qt_img%s.%s">' % (relpath,
256 249 match.group("name"),format)
257 250 else:
258 251 return "<b>Couldn't save image!</b>"
259 252 else:
260 253 ba = QtCore.QByteArray()
261 254 buffer_ = QtCore.QBuffer(ba)
262 255 buffer_.open(QtCore.QIODevice.WriteOnly)
263 256 image.save(buffer_, format.upper())
264 257 buffer_.close()
265 258 return '<img src="data:image/%s;base64,\n%s\n" />' % (
266 259 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
267 260
268 261 elif format == "svg":
269 262 try:
270 263 svg = str(self._name_to_svg_map[match.group("name")])
271 264 except KeyError:
272 265 if not self._svg_warning_displayed:
273 266 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
274 267 'Cannot convert PNG images to SVG, export with PNG figures instead. '
275 268 'If you want to export matplotlib figures as SVG, add '
276 269 'to your ipython config:\n\n'
277 270 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
278 271 'And regenerate the figures.',
279 272 QtGui.QMessageBox.Ok)
280 273 self._svg_warning_displayed = True
281 274 return ("<b>Cannot convert PNG images to SVG.</b> "
282 275 "You must export this session with PNG images. "
283 276 "If you want to export matplotlib figures as SVG, add to your config "
284 277 "<span>c.InlineBackend.figure_format = 'svg'</span> "
285 278 "and regenerate the figures.")
286 279
287 280 # Not currently checking path, because it's tricky to find a
288 281 # cross-browser way to embed external SVG images (e.g., via
289 282 # object or embed tags).
290 283
291 284 # Chop stand-alone header from matplotlib SVG
292 285 offset = svg.find("<svg")
293 286 assert(offset > -1)
294 287
295 288 return svg[offset:]
296 289
297 290 else:
298 291 return '<b>Unrecognized image format</b>'
299 292
300 293 def _insert_jpg(self, cursor, jpg, metadata=None):
301 294 """ Insert raw PNG data into the widget."""
302 295 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
303 296
304 297 def _insert_png(self, cursor, png, metadata=None):
305 298 """ Insert raw PNG data into the widget.
306 299 """
307 300 self._insert_img(cursor, png, 'png', metadata=metadata)
308 301
309 302 def _insert_img(self, cursor, img, fmt, metadata=None):
310 303 """ insert a raw image, jpg or png """
311 304 if metadata:
312 305 width = metadata.get('width', None)
313 306 height = metadata.get('height', None)
314 307 else:
315 308 width = height = None
316 309 try:
317 310 image = QtGui.QImage()
318 311 image.loadFromData(img, fmt.upper())
319 312 if width and height:
320 313 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
321 314 elif width and not height:
322 315 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
323 316 elif height and not width:
324 317 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
325 318 except ValueError:
326 319 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
327 320 else:
328 321 format = self._add_image(image)
329 322 cursor.insertBlock()
330 323 cursor.insertImage(format)
331 324 cursor.insertBlock()
332 325
333 326 def _insert_svg(self, cursor, svg):
334 327 """ Insert raw SVG data into the widet.
335 328 """
336 329 try:
337 330 image = svg_to_image(svg)
338 331 except ValueError:
339 332 self._insert_plain_text(cursor, 'Received invalid SVG data.')
340 333 else:
341 334 format = self._add_image(image)
342 335 self._name_to_svg_map[format.name()] = svg
343 336 cursor.insertBlock()
344 337 cursor.insertImage(format)
345 338 cursor.insertBlock()
346 339
347 340 def _save_image(self, name, format='PNG'):
348 341 """ Shows a save dialog for the ImageResource with 'name'.
349 342 """
350 343 dialog = QtGui.QFileDialog(self._control, 'Save Image')
351 344 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
352 345 dialog.setDefaultSuffix(format.lower())
353 346 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
354 347 if dialog.exec_():
355 348 filename = dialog.selectedFiles()[0]
356 349 image = self._get_image(name)
357 350 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now