| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- # Copyright (c) IPython Development Team.
- # Distributed under the terms of the Modified BSD License.
- from base64 import b64decode
- import os
- import re
- from warnings import warn
- from qtpy import QtCore, QtGui, QtWidgets
- from ipython_genutils.path import ensure_dir_exists
- from traitlets import Bool
- from qtconsole.svg import save_svg, svg_to_clipboard, svg_to_image
- from .jupyter_widget import JupyterWidget
- try:
- from IPython.lib.latextools import latex_to_png
- except ImportError:
- latex_to_png = None
- class LatexError(Exception):
- """Exception for Latex errors"""
- class RichIPythonWidget(JupyterWidget):
- """Dummy class for config inheritance. Destroyed below."""
- class RichJupyterWidget(RichIPythonWidget):
- """ An JupyterWidget that supports rich text, including lists, images, and
- tables. Note that raw performance will be reduced compared to the plain
- text version.
- """
- # RichJupyterWidget protected class variables.
- _payload_source_plot = 'ipykernel.pylab.backend_payload.add_plot_payload'
- _jpg_supported = Bool(False)
- # Used to determine whether a given html export attempt has already
- # displayed a warning about being unable to convert a png to svg.
- _svg_warning_displayed = False
- #---------------------------------------------------------------------------
- # 'object' interface
- #---------------------------------------------------------------------------
- def __init__(self, *args, **kw):
- """ Create a RichJupyterWidget.
- """
- kw['kind'] = 'rich'
- super(RichJupyterWidget, self).__init__(*args, **kw)
- # Configure the ConsoleWidget HTML exporter for our formats.
- self._html_exporter.image_tag = self._get_image_tag
- # Dictionary for resolving document resource names to SVG data.
- self._name_to_svg_map = {}
- # Do we support jpg ?
- # it seems that sometime jpg support is a plugin of QT, so try to assume
- # it is not always supported.
- self._jpg_supported = 'jpeg' in QtGui.QImageReader.supportedImageFormats()
- #---------------------------------------------------------------------------
- # 'ConsoleWidget' public interface overides
- #---------------------------------------------------------------------------
- def export_html(self):
- """ Shows a dialog to export HTML/XML in various formats.
- Overridden in order to reset the _svg_warning_displayed flag prior
- to the export running.
- """
- self._svg_warning_displayed = False
- super(RichJupyterWidget, self).export_html()
- #---------------------------------------------------------------------------
- # 'ConsoleWidget' protected interface
- #---------------------------------------------------------------------------
- def _context_menu_make(self, pos):
- """ Reimplemented to return a custom context menu for images.
- """
- format = self._control.cursorForPosition(pos).charFormat()
- name = format.stringProperty(QtGui.QTextFormat.ImageName)
- if name:
- menu = QtWidgets.QMenu(self)
- menu.addAction('Copy Image', lambda: self._copy_image(name))
- menu.addAction('Save Image As...', lambda: self._save_image(name))
- menu.addSeparator()
- svg = self._name_to_svg_map.get(name, None)
- if svg is not None:
- menu.addSeparator()
- menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
- menu.addAction('Save SVG As...',
- lambda: save_svg(svg, self._control))
- else:
- menu = super(RichJupyterWidget, self)._context_menu_make(pos)
- return menu
- #---------------------------------------------------------------------------
- # 'BaseFrontendMixin' abstract interface
- #---------------------------------------------------------------------------
- def _pre_image_append(self, msg, prompt_number):
- """Append the Out[] prompt and make the output nicer
- Shared code for some the following if statement
- """
- self._append_plain_text(self.output_sep, True)
- self._append_html(self._make_out_prompt(prompt_number), True)
- self._append_plain_text('\n', True)
- def _handle_execute_result(self, msg):
- """Overridden to handle rich data types, like SVG."""
- self.log.debug("execute_result: %s", msg.get('content', ''))
- if self.include_output(msg):
- self.flush_clearoutput()
- content = msg['content']
- prompt_number = content.get('execution_count', 0)
- data = content['data']
- metadata = msg['content']['metadata']
- if 'image/svg+xml' in data:
- self._pre_image_append(msg, prompt_number)
- self._append_svg(data['image/svg+xml'], True)
- self._append_html(self.output_sep2, True)
- elif 'image/png' in data:
- self._pre_image_append(msg, prompt_number)
- png = b64decode(data['image/png'].encode('ascii'))
- self._append_png(png, True, metadata=metadata.get('image/png',
- None))
- self._append_html(self.output_sep2, True)
- elif 'image/jpeg' in data and self._jpg_supported:
- self._pre_image_append(msg, prompt_number)
- jpg = b64decode(data['image/jpeg'].encode('ascii'))
- self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg',
- None))
- self._append_html(self.output_sep2, True)
- elif 'text/latex' in data:
- self._pre_image_append(msg, prompt_number)
- try:
- self._append_latex(data['text/latex'], True)
- except LatexError:
- return super(RichJupyterWidget, self)._handle_display_data(msg)
- self._append_html(self.output_sep2, True)
- else:
- # Default back to the plain text representation.
- return super(RichJupyterWidget, self)._handle_execute_result(msg)
- def _handle_display_data(self, msg):
- """Overridden to handle rich data types, like SVG."""
- self.log.debug("display_data: %s", msg.get('content', ''))
- if self.include_output(msg):
- self.flush_clearoutput()
- data = msg['content']['data']
- metadata = msg['content']['metadata']
- # Try to use the svg or html representations.
- # FIXME: Is this the right ordering of things to try?
- self.log.debug("display: %s", msg.get('content', ''))
- if 'image/svg+xml' in data:
- svg = data['image/svg+xml']
- self._append_svg(svg, True)
- elif 'image/png' in data:
- # PNG data is base64 encoded as it passes over the network
- # in a JSON structure so we decode it.
- png = b64decode(data['image/png'].encode('ascii'))
- self._append_png(png, True, metadata=metadata.get('image/png', None))
- elif 'image/jpeg' in data and self._jpg_supported:
- jpg = b64decode(data['image/jpeg'].encode('ascii'))
- self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
- elif 'text/latex' in data and latex_to_png:
- try:
- self._append_latex(data['text/latex'], True)
- except LatexError:
- return super(RichJupyterWidget, self)._handle_display_data(msg)
- else:
- # Default back to the plain text representation.
- return super(RichJupyterWidget, self)._handle_display_data(msg)
- #---------------------------------------------------------------------------
- # 'RichJupyterWidget' protected interface
- #---------------------------------------------------------------------------
- def _is_latex_math(self, latex):
- """
- Determine if a Latex string is in math mode
- This is the only mode supported by qtconsole
- """
- basic_envs = ['math', 'displaymath']
- starable_envs = ['equation', 'eqnarray' 'multline', 'gather', 'align',
- 'flalign', 'alignat']
- star_envs = [env + '*' for env in starable_envs]
- envs = basic_envs + starable_envs + star_envs
- env_syntax = [r'\begin{{{0}}} \end{{{0}}}'.format(env).split() for env in envs]
- math_syntax = [
- (r'\[', r'\]'), (r'\(', r'\)'),
- ('$$', '$$'), ('$', '$'),
- ]
- for start, end in math_syntax + env_syntax:
- inner = latex[len(start):-len(end)]
- if start in inner or end in inner:
- return False
- if latex.startswith(start) and latex.endswith(end):
- return True
- return False
- def _append_latex(self, latex, before_prompt=False, metadata=None):
- """ Append latex data to the widget."""
- png = None
- if self._is_latex_math(latex):
- png = latex_to_png(latex, wrap=False, backend='dvipng')
- # Matplotlib only supports strings enclosed in dollar signs
- if png is None and latex.startswith('$') and latex.endswith('$'):
- # To avoid long and ugly errors, like the one reported in
- # spyder-ide/spyder#7619
- try:
- png = latex_to_png(latex, wrap=False, backend='matplotlib')
- except Exception:
- pass
- if png:
- self._append_png(png, before_prompt, metadata)
- else:
- raise LatexError
- def _append_jpg(self, jpg, before_prompt=False, metadata=None):
- """ Append raw JPG data to the widget."""
- self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
- def _append_png(self, png, before_prompt=False, metadata=None):
- """ Append raw PNG data to the widget.
- """
- self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
- def _append_svg(self, svg, before_prompt=False):
- """ Append raw SVG data to the widget.
- """
- self._append_custom(self._insert_svg, svg, before_prompt)
- def _add_image(self, image):
- """ Adds the specified QImage to the document and returns a
- QTextImageFormat that references it.
- """
- document = self._control.document()
- name = str(image.cacheKey())
- document.addResource(QtGui.QTextDocument.ImageResource,
- QtCore.QUrl(name), image)
- format = QtGui.QTextImageFormat()
- format.setName(name)
- return format
- def _copy_image(self, name):
- """ Copies the ImageResource with 'name' to the clipboard.
- """
- image = self._get_image(name)
- QtWidgets.QApplication.clipboard().setImage(image)
- def _get_image(self, name):
- """ Returns the QImage stored as the ImageResource with 'name'.
- """
- document = self._control.document()
- image = document.resource(QtGui.QTextDocument.ImageResource,
- QtCore.QUrl(name))
- return image
- def _get_image_tag(self, match, path = None, format = "png"):
- """ Return (X)HTML mark-up for the image-tag given by match.
- Parameters
- ----------
- match : re.SRE_Match
- A match to an HTML image tag as exported by Qt, with
- match.group("Name") containing the matched image ID.
- path : string|None, optional [default None]
- If not None, specifies a path to which supporting files may be
- written (e.g., for linked images). If None, all images are to be
- included inline.
- format : "png"|"svg"|"jpg", optional [default "png"]
- Format for returned or referenced images.
- """
- if format in ("png","jpg"):
- try:
- image = self._get_image(match.group("name"))
- except KeyError:
- return "<b>Couldn't find image %s</b>" % match.group("name")
- if path is not None:
- ensure_dir_exists(path)
- relpath = os.path.basename(path)
- if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
- "PNG"):
- return '<img src="%s/qt_img%s.%s">' % (relpath,
- match.group("name"),format)
- else:
- return "<b>Couldn't save image!</b>"
- else:
- ba = QtCore.QByteArray()
- buffer_ = QtCore.QBuffer(ba)
- buffer_.open(QtCore.QIODevice.WriteOnly)
- image.save(buffer_, format.upper())
- buffer_.close()
- return '<img src="data:image/%s;base64,\n%s\n" />' % (
- format,re.sub(r'(.{60})',r'\1\n', str(ba.toBase64().data().decode())))
- elif format == "svg":
- try:
- svg = str(self._name_to_svg_map[match.group("name")])
- except KeyError:
- if not self._svg_warning_displayed:
- QtWidgets.QMessageBox.warning(self, 'Error converting PNG to SVG.',
- 'Cannot convert PNG images to SVG, export with PNG figures instead. '
- 'If you want to export matplotlib figures as SVG, add '
- 'to your ipython config:\n\n'
- '\tc.InlineBackend.figure_format = \'svg\'\n\n'
- 'And regenerate the figures.',
- QtWidgets.QMessageBox.Ok)
- self._svg_warning_displayed = True
- return ("<b>Cannot convert PNG images to SVG.</b> "
- "You must export this session with PNG images. "
- "If you want to export matplotlib figures as SVG, add to your config "
- "<span>c.InlineBackend.figure_format = 'svg'</span> "
- "and regenerate the figures.")
- # Not currently checking path, because it's tricky to find a
- # cross-browser way to embed external SVG images (e.g., via
- # object or embed tags).
- # Chop stand-alone header from matplotlib SVG
- offset = svg.find("<svg")
- assert(offset > -1)
- return svg[offset:]
- else:
- return '<b>Unrecognized image format</b>'
- def _insert_jpg(self, cursor, jpg, metadata=None):
- """ Insert raw PNG data into the widget."""
- self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
- def _insert_png(self, cursor, png, metadata=None):
- """ Insert raw PNG data into the widget.
- """
- self._insert_img(cursor, png, 'png', metadata=metadata)
- def _insert_img(self, cursor, img, fmt, metadata=None):
- """ insert a raw image, jpg or png """
- if metadata:
- width = metadata.get('width', None)
- height = metadata.get('height', None)
- else:
- width = height = None
- try:
- image = QtGui.QImage()
- image.loadFromData(img, fmt.upper())
- if width and height:
- image = image.scaled(width, height,
- QtCore.Qt.IgnoreAspectRatio,
- QtCore.Qt.SmoothTransformation)
- elif width and not height:
- image = image.scaledToWidth(width, QtCore.Qt.SmoothTransformation)
- elif height and not width:
- image = image.scaledToHeight(height, QtCore.Qt.SmoothTransformation)
- except ValueError:
- self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
- else:
- format = self._add_image(image)
- cursor.insertBlock()
- cursor.insertImage(format)
- cursor.insertBlock()
- def _insert_svg(self, cursor, svg):
- """ Insert raw SVG data into the widet.
- """
- try:
- image = svg_to_image(svg)
- except ValueError:
- self._insert_plain_text(cursor, 'Received invalid SVG data.')
- else:
- format = self._add_image(image)
- self._name_to_svg_map[format.name()] = svg
- cursor.insertBlock()
- cursor.insertImage(format)
- cursor.insertBlock()
- def _save_image(self, name, format='PNG'):
- """ Shows a save dialog for the ImageResource with 'name'.
- """
- dialog = QtWidgets.QFileDialog(self._control, 'Save Image')
- dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
- dialog.setDefaultSuffix(format.lower())
- dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
- if dialog.exec_():
- filename = dialog.selectedFiles()[0]
- image = self._get_image(name)
- image.save(filename, format)
- # Clobber RichIPythonWidget above:
- class RichIPythonWidget(RichJupyterWidget):
- """Deprecated class. Use RichJupyterWidget."""
- def __init__(self, *a, **kw):
- warn("RichIPythonWidget is deprecated, use RichJupyterWidget",
- DeprecationWarning)
- super(RichIPythonWidget, self).__init__(*a, **kw)
|