~ubuntu-branches/ubuntu/raring/ipython/raring

« back to all changes in this revision

Viewing changes to IPython/frontend/qt/console/rich_ipython_widget.py

  • Committer: Package Import Robot
  • Author(s): Julian Taylor
  • Date: 2011-11-22 23:40:57 UTC
  • mfrom: (6.1.5 sid)
  • Revision ID: package-import@ubuntu.com-20111122234057-ta86ocdahnhwmnd8
Tags: 0.11-2
* upload to unstable
* add patch fix-version-checks-for-pyzmq-2.1.10.patch
* fix debianize-error-messages.patch to reraise unknown exceptions
* suggest python-zmq for ipython package
* use dh_sphinxdoc
  - bump sphinx dependency to >= 1.0.7+dfsg-1~, replace libjs-jquery
    dependency with ${sphinxdoc:Depends} and drop ipython-doc.links
* remove empty directory from ipython
* link duplicate images in ipython-doc
* remove obsolete Conflicts and Replaces

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Standard libary imports.
 
2
from base64 import decodestring
 
3
import os
 
4
import re
 
5
 
 
6
# System libary imports.
 
7
from IPython.external.qt import QtCore, QtGui
 
8
 
 
9
# Local imports
 
10
from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
 
11
from ipython_widget import IPythonWidget
 
12
 
 
13
 
 
14
class RichIPythonWidget(IPythonWidget):
 
15
    """ An IPythonWidget that supports rich text, including lists, images, and
 
16
        tables. Note that raw performance will be reduced compared to the plain
 
17
        text version.
 
18
    """
 
19
 
 
20
    # RichIPythonWidget protected class variables.
 
21
    _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
 
22
 
 
23
    #---------------------------------------------------------------------------
 
24
    # 'object' interface
 
25
    #---------------------------------------------------------------------------
 
26
 
 
27
    def __init__(self, *args, **kw):
 
28
        """ Create a RichIPythonWidget.
 
29
        """
 
30
        kw['kind'] = 'rich'
 
31
        super(RichIPythonWidget, self).__init__(*args, **kw)
 
32
 
 
33
        # Configure the ConsoleWidget HTML exporter for our formats.
 
34
        self._html_exporter.image_tag = self._get_image_tag
 
35
 
 
36
        # Dictionary for resolving document resource names to SVG data.
 
37
        self._name_to_svg_map = {}
 
38
 
 
39
    #---------------------------------------------------------------------------
 
40
    # 'ConsoleWidget' protected interface
 
41
    #---------------------------------------------------------------------------
 
42
 
 
43
    def _context_menu_make(self, pos):
 
44
        """ Reimplemented to return a custom context menu for images.
 
45
        """
 
46
        format = self._control.cursorForPosition(pos).charFormat()
 
47
        name = format.stringProperty(QtGui.QTextFormat.ImageName)
 
48
        if name:
 
49
            menu = QtGui.QMenu()
 
50
 
 
51
            menu.addAction('Copy Image', lambda: self._copy_image(name))
 
52
            menu.addAction('Save Image As...', lambda: self._save_image(name))
 
53
            menu.addSeparator()
 
54
 
 
55
            svg = self._name_to_svg_map.get(name, None)
 
56
            if svg is not None:
 
57
                menu.addSeparator()
 
58
                menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
 
59
                menu.addAction('Save SVG As...', 
 
60
                               lambda: save_svg(svg, self._control))
 
61
        else:
 
62
            menu = super(RichIPythonWidget, self)._context_menu_make(pos)
 
63
        return menu
 
64
 
 
65
    #---------------------------------------------------------------------------
 
66
    # 'BaseFrontendMixin' abstract interface
 
67
    #---------------------------------------------------------------------------
 
68
 
 
69
    def _handle_pyout(self, msg):
 
70
        """ Overridden to handle rich data types, like SVG.
 
71
        """
 
72
        if not self._hidden and self._is_from_this_session(msg):
 
73
            content = msg['content']
 
74
            prompt_number = content['execution_count']
 
75
            data = content['data']
 
76
            if data.has_key('image/svg+xml'):
 
77
                self._append_plain_text(self.output_sep, True)
 
78
                self._append_html(self._make_out_prompt(prompt_number), True)
 
79
                self._append_svg(data['image/svg+xml'], True)
 
80
                self._append_html(self.output_sep2, True)
 
81
            elif data.has_key('image/png'):
 
82
                self._append_plain_text(self.output_sep, True)
 
83
                self._append_html(self._make_out_prompt(prompt_number), True)
 
84
                # This helps the output to look nice.
 
85
                self._append_plain_text('\n', True)
 
86
                self._append_png(decodestring(data['image/png']), True)
 
87
                self._append_html(self.output_sep2, True)
 
88
            else:
 
89
                # Default back to the plain text representation.
 
90
                return super(RichIPythonWidget, self)._handle_pyout(msg)
 
91
 
 
92
    def _handle_display_data(self, msg):
 
93
        """ Overridden to handle rich data types, like SVG.
 
94
        """
 
95
        if not self._hidden and self._is_from_this_session(msg):
 
96
            source = msg['content']['source']
 
97
            data = msg['content']['data']
 
98
            metadata = msg['content']['metadata']
 
99
            # Try to use the svg or html representations.
 
100
            # FIXME: Is this the right ordering of things to try?
 
101
            if data.has_key('image/svg+xml'):
 
102
                svg = data['image/svg+xml']
 
103
                self._append_svg(svg, True)
 
104
            elif data.has_key('image/png'):
 
105
                # PNG data is base64 encoded as it passes over the network
 
106
                # in a JSON structure so we decode it.
 
107
                png = decodestring(data['image/png'])
 
108
                self._append_png(png, True)
 
109
            else:
 
110
                # Default back to the plain text representation.
 
111
                return super(RichIPythonWidget, self)._handle_display_data(msg)
 
112
 
 
113
    #---------------------------------------------------------------------------
 
114
    # 'RichIPythonWidget' protected interface
 
115
    #---------------------------------------------------------------------------
 
116
 
 
117
    def _append_png(self, png, before_prompt=False):
 
118
        """ Append raw PNG data to the widget.
 
119
        """
 
120
        self._append_custom(self._insert_png, png, before_prompt)
 
121
 
 
122
    def _append_svg(self, svg, before_prompt=False):
 
123
        """ Append raw SVG data to the widget.
 
124
        """
 
125
        self._append_custom(self._insert_svg, svg, before_prompt)
 
126
 
 
127
    def _add_image(self, image):
 
128
        """ Adds the specified QImage to the document and returns a
 
129
            QTextImageFormat that references it.
 
130
        """
 
131
        document = self._control.document()
 
132
        name = str(image.cacheKey())
 
133
        document.addResource(QtGui.QTextDocument.ImageResource,
 
134
                             QtCore.QUrl(name), image)
 
135
        format = QtGui.QTextImageFormat()
 
136
        format.setName(name)
 
137
        return format
 
138
 
 
139
    def _copy_image(self, name):
 
140
        """ Copies the ImageResource with 'name' to the clipboard.
 
141
        """
 
142
        image = self._get_image(name)
 
143
        QtGui.QApplication.clipboard().setImage(image)
 
144
 
 
145
    def _get_image(self, name):
 
146
        """ Returns the QImage stored as the ImageResource with 'name'.
 
147
        """
 
148
        document = self._control.document()
 
149
        image = document.resource(QtGui.QTextDocument.ImageResource,
 
150
                                  QtCore.QUrl(name))
 
151
        return image
 
152
 
 
153
    def _get_image_tag(self, match, path = None, format = "png"):
 
154
        """ Return (X)HTML mark-up for the image-tag given by match.
 
155
 
 
156
        Parameters
 
157
        ----------
 
158
        match : re.SRE_Match
 
159
            A match to an HTML image tag as exported by Qt, with
 
160
            match.group("Name") containing the matched image ID.
 
161
 
 
162
        path : string|None, optional [default None]
 
163
            If not None, specifies a path to which supporting files may be
 
164
            written (e.g., for linked images).  If None, all images are to be
 
165
            included inline.
 
166
 
 
167
        format : "png"|"svg", optional [default "png"]
 
168
            Format for returned or referenced images.
 
169
        """
 
170
        if format == "png":
 
171
            try:
 
172
                image = self._get_image(match.group("name"))
 
173
            except KeyError:
 
174
                return "<b>Couldn't find image %s</b>" % match.group("name")
 
175
 
 
176
            if path is not None:
 
177
                if not os.path.exists(path):
 
178
                    os.mkdir(path)
 
179
                relpath = os.path.basename(path)
 
180
                if image.save("%s/qt_img%s.png" % (path,match.group("name")),
 
181
                              "PNG"):
 
182
                    return '<img src="%s/qt_img%s.png">' % (relpath,
 
183
                                                            match.group("name"))
 
184
                else:
 
185
                    return "<b>Couldn't save image!</b>"
 
186
            else:
 
187
                ba = QtCore.QByteArray()
 
188
                buffer_ = QtCore.QBuffer(ba)
 
189
                buffer_.open(QtCore.QIODevice.WriteOnly)
 
190
                image.save(buffer_, "PNG")
 
191
                buffer_.close()
 
192
                return '<img src="data:image/png;base64,\n%s\n" />' % (
 
193
                    re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
 
194
 
 
195
        elif format == "svg":
 
196
            try:
 
197
                svg = str(self._name_to_svg_map[match.group("name")])
 
198
            except KeyError:
 
199
                return "<b>Couldn't find image %s</b>" % match.group("name")
 
200
 
 
201
            # Not currently checking path, because it's tricky to find a
 
202
            # cross-browser way to embed external SVG images (e.g., via
 
203
            # object or embed tags).
 
204
 
 
205
            # Chop stand-alone header from matplotlib SVG
 
206
            offset = svg.find("<svg")
 
207
            assert(offset > -1)
 
208
            
 
209
            return svg[offset:]
 
210
 
 
211
        else:
 
212
            return '<b>Unrecognized image format</b>'
 
213
 
 
214
    def _insert_png(self, cursor, png):
 
215
        """ Insert raw PNG data into the widget.
 
216
        """
 
217
        try:
 
218
            image = QtGui.QImage()
 
219
            image.loadFromData(png, 'PNG')
 
220
        except ValueError:
 
221
            self._insert_plain_text(cursor, 'Received invalid PNG data.')
 
222
        else:
 
223
            format = self._add_image(image)
 
224
            cursor.insertBlock()
 
225
            cursor.insertImage(format)
 
226
            cursor.insertBlock()
 
227
 
 
228
    def _insert_svg(self, cursor, svg):
 
229
        """ Insert raw SVG data into the widet.
 
230
        """
 
231
        try:
 
232
            image = svg_to_image(svg)
 
233
        except ValueError:
 
234
            self._insert_plain_text(cursor, 'Received invalid SVG data.')
 
235
        else:
 
236
            format = self._add_image(image)
 
237
            self._name_to_svg_map[format.name()] = svg
 
238
            cursor.insertBlock()
 
239
            cursor.insertImage(format)
 
240
            cursor.insertBlock()
 
241
 
 
242
    def _save_image(self, name, format='PNG'):
 
243
        """ Shows a save dialog for the ImageResource with 'name'.
 
244
        """
 
245
        dialog = QtGui.QFileDialog(self._control, 'Save Image')
 
246
        dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
 
247
        dialog.setDefaultSuffix(format.lower())
 
248
        dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
 
249
        if dialog.exec_():
 
250
            filename = dialog.selectedFiles()[0]
 
251
            image = self._get_image(name)
 
252
            image.save(filename, format)