Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding PDFFormatter and kernel side handling of PDF display data #4920

Merged
merged 9 commits into from
Feb 8, 2014
73 changes: 72 additions & 1 deletion IPython/core/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from IPython.utils.py3compat import (string_types, cast_bytes_py2, cast_unicode,
unicode_type)

from IPython.testing.skipdoctest import skip_doctest
from .displaypub import publish_display_data

#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -271,6 +271,24 @@ def display_javascript(*objs, **kwargs):
"""
_display_mimetype('application/javascript', objs, **kwargs)


def display_pdf(*objs, **kwargs):
"""Display the PDF representation of an object.

Parameters
----------
objs : tuple of objects
The Python objects to display, or if raw=True raw javascript data to
display.
raw : bool
Are the data objects raw data or Python objects that need to be
formatted before display? [default: False]
metadata : dict (optional)
Metadata to be associated with the specific mimetype output.
"""
_display_mimetype('application/pdf', objs, **kwargs)


#-----------------------------------------------------------------------------
# Smart classes
#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -699,3 +717,56 @@ def clear_output(wait=False):
io.stdout.flush()
print('\033[2K\r', file=io.stderr, end='')
io.stderr.flush()


@skip_doctest
def set_matplotlib_formats(*formats, **kwargs):
"""Select figure formats for the inline backend. Optionally pass quality for JPEG.

For example, this enables PNG and JPEG output with a JPEG quality of 90%::

In [1]: set_matplotlib_formats('png', 'jpeg', quality=90)

To set this in your config files use the following::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For equivalence with the example above, this should also indicate how to set the quality parameter in the config file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


c.InlineBackend.figure_formats = {'pdf', 'png', 'svg'}
c.InlineBackend.quality = 90

Parameters
----------
*formats : list, tuple
One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
quality : int
A percentage for the quality of JPEG figures. Defaults to 90.
"""
from IPython.core.interactiveshell import InteractiveShell
from IPython.core.pylabtools import select_figure_formats
shell = InteractiveShell.instance()
select_figure_formats(shell, formats, quality=90)

@skip_doctest
def set_matplotlib_close(close):
"""Set whether the inline backend closes all figures automatically or not.

By default, the inline backend used in the IPython Notebook will close all
matplotlib figures automatically after each cell is run. This means that
plots in different cells won't interfere. Sometimes, you may want to make
a plot in one cell and then refine it in later cells. This can be accomplished
by::

In [1]: set_matplotlib_close(False)

To set this in your config files use the following::

c.InlineBackend.close_figures = False

Parameters
----------
close : bool
Should all matplotlib figures be automatically closed after each cell is
run?
"""
from IPython.kernel.zmq.pylab.backend_inline import InlineBackend
ilbe = InlineBackend.instance()
ilbe.close_figures = close

21 changes: 21 additions & 0 deletions IPython/core/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def _formatters_default(self):
HTMLFormatter,
SVGFormatter,
PNGFormatter,
PDFFormatter,
JPEGFormatter,
LatexFormatter,
JSONFormatter,
Expand All @@ -116,6 +117,7 @@ def format(self, obj, include=None, exclude=None):
* text/latex
* application/json
* application/javascript
* application/pdf
* image/png
* image/jpeg
* image/svg+xml
Expand Down Expand Up @@ -766,11 +768,29 @@ class JavascriptFormatter(BaseFormatter):

print_method = ObjectName('_repr_javascript_')


class PDFFormatter(BaseFormatter):
"""A PDF formatter.

To defined the callables that compute to PDF representation of your
objects, define a :meth:`_repr_pdf_` method or use the :meth:`for_type`
or :meth:`for_type_by_name` methods to register functions that handle
this.

The return value of this formatter should be raw PDF data, *not*
base64 encoded.
"""
format_type = Unicode('application/pdf')

print_method = ObjectName('_repr_pdf_')


FormatterABC.register(BaseFormatter)
FormatterABC.register(PlainTextFormatter)
FormatterABC.register(HTMLFormatter)
FormatterABC.register(SVGFormatter)
FormatterABC.register(PNGFormatter)
FormatterABC.register(PDFFormatter)
FormatterABC.register(JPEGFormatter)
FormatterABC.register(LatexFormatter)
FormatterABC.register(JSONFormatter)
Expand All @@ -789,6 +809,7 @@ def format_display_data(obj, include=None, exclude=None):
* text/latex
* application/json
* application/javascript
* application/pdf
* image/png
* image/jpeg
* image/svg+xml
Expand Down
30 changes: 17 additions & 13 deletions IPython/core/magics/pylab.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,32 @@ def matplotlib(self, line=''):
"""Set up matplotlib to work interactively.

This function lets you activate matplotlib interactive support
at any point during an IPython session.
It does not import anything into the interactive namespace.
at any point during an IPython session. It does not import anything
into the interactive namespace.

If you are using the inline matplotlib backend for embedded figures,
you can adjust its behavior via the %config magic::

# enable SVG figures, necessary for SVG+XHTML export in the qtconsole
In [1]: %config InlineBackend.figure_format = 'svg'
If you are using the inline matplotlib backend in the IPython Notebook
you can set which figure formats are enabled using the following::

In [1]: from IPython.display import set_matplotlib_formats

In [2]: set_matplotlib_formats('pdf', 'svg')

# change the behavior of closing all figures at the end of each
# execution (cell), or allowing reuse of active figures across
# cells:
In [2]: %config InlineBackend.close_figures = False
See the docstring of `IPython.display.set_matplotlib_formats` and
`IPython.display.set_matplotlib_close` for more information on
changing the behavior of the inline backend.

Examples
--------
In this case, where the MPL default is TkAgg::
To enable the inline backend for usage with the IPython Notebook::

In [1]: %matplotlib inline

In this case, where the matplotlib default is TkAgg::

In [2]: %matplotlib
Using matplotlib backend: TkAgg

But you can explicitly request a different backend::
But you can explicitly request a different GUI backend::

In [3]: %matplotlib qt
"""
Expand Down
46 changes: 29 additions & 17 deletions IPython/core/pylabtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from IPython.core.display import _pngxy
from IPython.utils.decorators import flag_calls
from IPython.utils import py3compat

# If user specifies a GUI, that dictates the backend, otherwise we read the
# user's mpl default from the mpl rc structure
Expand Down Expand Up @@ -165,33 +166,44 @@ def mpl_execfile(fname,*where,**kw):
return mpl_execfile


def select_figure_format(shell, fmt, quality=90):
"""Select figure format for inline backend, can be 'png', 'retina', 'jpg', or 'svg'.
def select_figure_formats(shell, formats, quality=90):
"""Select figure formats for the inline backend.

Using this method ensures only one figure format is active at a time.
Parameters
==========
shell : InteractiveShell
The main IPython instance.
formats : list
One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
quality : int
A percentage for the quality of JPEG figures.
"""
from matplotlib.figure import Figure
from IPython.kernel.zmq.pylab import backend_inline

svg_formatter = shell.display_formatter.formatters['image/svg+xml']
png_formatter = shell.display_formatter.formatters['image/png']
jpg_formatter = shell.display_formatter.formatters['image/jpeg']
pdf_formatter = shell.display_formatter.formatters['application/pdf']

[ f.type_printers.pop(Figure, None) for f in {svg_formatter, png_formatter, jpg_formatter} ]
if isinstance(formats, py3compat.string_types):
formats = {formats}

if fmt == 'png':
png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png'))
elif fmt in ('png2x', 'retina'):
png_formatter.for_type(Figure, retina_figure)
elif fmt in ('jpg', 'jpeg'):
jpg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'jpg', quality))
elif fmt == 'svg':
svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg'))
else:
raise ValueError("supported formats are: 'png', 'retina', 'svg', 'jpg', not %r" % fmt)
[ f.type_printers.pop(Figure, None) for f in {svg_formatter, png_formatter, jpg_formatter} ]

# set the format to be used in the backend()
backend_inline._figure_format = fmt
for fmt in formats:
if fmt == 'png':
png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png'))
elif fmt in ('png2x', 'retina'):
png_formatter.for_type(Figure, retina_figure)
elif fmt in ('jpg', 'jpeg'):
jpg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'jpg', quality))
elif fmt == 'svg':
svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg'))
elif fmt == 'pdf':
pdf_formatter.for_type(Figure, lambda fig: print_figure(fig, 'pdf'))
else:
raise ValueError("supported formats are: 'png', 'retina', 'svg', 'jpg', 'pdf' not %r" % fmt)

#-----------------------------------------------------------------------------
# Code for initializing matplotlib and importing pylab
Expand Down Expand Up @@ -342,5 +354,5 @@ def configure_inline_support(shell, backend):
del shell._saved_rcParams

# Setup the default figure format
select_figure_format(shell, cfg.figure_format, cfg.quality)
select_figure_formats(shell, cfg.figure_formats, cfg.quality)

11 changes: 10 additions & 1 deletion IPython/core/tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
numpy = None
import nose.tools as nt

from IPython.core.formatters import PlainTextFormatter, HTMLFormatter, _mod_name_key
from IPython.core.formatters import (
PlainTextFormatter, HTMLFormatter, PDFFormatter, _mod_name_key
)
from IPython.utils.io import capture_output

class A(object):
Expand Down Expand Up @@ -279,4 +281,11 @@ def _repr_pretty_(self):
nt.assert_in("text/plain", captured.stderr)
nt.assert_in("argument", captured.stderr)

class MakePDF(object):
def _repr_pdf_(self):
return 'PDF'

def test_pdf_formatter():
pdf = MakePDF()
f = PDFFormatter()
nt.assert_equal(f(pdf), 'PDF')
16 changes: 16 additions & 0 deletions IPython/html/static/notebook/js/outputarea.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ var IPython = (function (IPython) {
'image/svg+xml',
'image/png',
'image/jpeg',
'application/pdf',
'text/plain'
];

Expand Down Expand Up @@ -620,6 +621,17 @@ var IPython = (function (IPython) {
};


OutputArea.prototype.append_pdf = function (pdf, md, element) {
var type = 'application/pdf';
var toinsert = this.create_output_subarea(md, "output_pdf", type);
var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
a.attr('target', '_blank');
a.text('View PDF')
toinsert.append(a);
element.append(toinsert);
return toinsert;
}

OutputArea.prototype.append_latex = function (latex, md, element) {
// This method cannot do the typesetting because the latex first has to
// be on the page.
Expand Down Expand Up @@ -807,6 +819,7 @@ var IPython = (function (IPython) {
"image/svg+xml" : "svg",
"image/png" : "png",
"image/jpeg" : "jpeg",
"application/pdf" : "pdf",
"text/latex" : "latex",
"application/json" : "json",
"application/javascript" : "javascript",
Expand All @@ -818,6 +831,7 @@ var IPython = (function (IPython) {
"svg" : "image/svg+xml",
"png" : "image/png",
"jpeg" : "image/jpeg",
"pdf" : "application/pdf",
"latex" : "text/latex",
"json" : "application/json",
"javascript" : "application/javascript",
Expand All @@ -830,6 +844,7 @@ var IPython = (function (IPython) {
'image/svg+xml',
'image/png',
'image/jpeg',
'application/pdf',
'text/plain'
];

Expand All @@ -842,6 +857,7 @@ var IPython = (function (IPython) {
"text/latex" : OutputArea.prototype.append_latex,
"application/json" : OutputArea.prototype.append_json,
"application/javascript" : OutputArea.prototype.append_javascript,
"application/pdf" : OutputArea.prototype.append_pdf
};

IPython.OutputArea = OutputArea;
Expand Down
27 changes: 17 additions & 10 deletions IPython/kernel/zmq/pylab/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
#-----------------------------------------------------------------------------

from IPython.config.configurable import SingletonConfigurable
from IPython.utils.traitlets import Dict, Instance, CaselessStrEnum, Bool, Int, TraitError
from IPython.utils.traitlets import (
Dict, Instance, CaselessStrEnum, Set, Bool, Int, TraitError, Unicode
)
from IPython.utils.warn import warn

#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -63,21 +65,26 @@ def _config_changed(self, name, old, new):
inline backend."""
)

figure_formats = Set({'png'}, config=True,
help="""A set of figure formats to enable: 'png',
'retina', 'jpeg', 'svg', 'pdf'.""")

figure_format = CaselessStrEnum(['svg', 'png', 'retina', 'jpg'],
default_value='png', config=True,
help="""The image format for figures with the inline
backend. JPEG requires the PIL/Pillow library.""")

def _figure_format_changed(self, name, old, new):
from IPython.core.pylabtools import select_figure_format
if new in {"jpg", "jpeg"}:
def _figure_formats_changed(self, name, old, new):
from IPython.core.pylabtools import select_figure_formats
if 'jpg' in new or 'jpeg' in new:
if not pil_available():
raise TraitError("Requires PIL/Pillow for JPG figures")
if self.shell is None:
return
else:
select_figure_format(self.shell, new)
select_figure_formats(self.shell, new)

figure_format = Unicode(config=True, help="""The figure format to enable (deprecated
use `figure_formats` instead)""")

def _figure_format_changed(self, name, old, new):
if new:
self.figure_formats = {new}

quality = Int(default_value=90, config=True,
help="Quality of compression [10-100], currently for lossy JPEG only.")
Expand Down