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