From e68e13c3ce077bfe66afef0bf3e67792813b7c75 2014-02-08 18:11:34 From: Brian E. Granger Date: 2014-02-08 18:11:34 Subject: [PATCH] Merge pull request #4920 from ellisonbg/pdf-formatter Adding PDFFormatter and kernel side handling of PDF display data --- diff --git a/IPython/core/display.py b/IPython/core/display.py index b871906..6c48c6b 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -24,7 +24,7 @@ import struct 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 #----------------------------------------------------------------------------- @@ -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 #----------------------------------------------------------------------------- @@ -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:: + + 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 + diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py index 9a2644c..a54fa42 100644 --- a/IPython/core/formatters.py +++ b/IPython/core/formatters.py @@ -93,6 +93,7 @@ class DisplayFormatter(Configurable): HTMLFormatter, SVGFormatter, PNGFormatter, + PDFFormatter, JPEGFormatter, LatexFormatter, JSONFormatter, @@ -116,6 +117,7 @@ class DisplayFormatter(Configurable): * text/latex * application/json * application/javascript + * application/pdf * image/png * image/jpeg * image/svg+xml @@ -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) @@ -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 diff --git a/IPython/core/magics/pylab.py b/IPython/core/magics/pylab.py index bf616b0..05faf3c 100644 --- a/IPython/core/magics/pylab.py +++ b/IPython/core/magics/pylab.py @@ -47,28 +47,32 @@ class PylabMagics(Magics): """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 """ diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index a8779d2..106b856 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -25,6 +25,7 @@ from io import BytesIO 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 @@ -165,10 +166,17 @@ def mpl_runner(safe_execfile): 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 @@ -176,22 +184,26 @@ def select_figure_format(shell, fmt, quality=90): 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 @@ -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) diff --git a/IPython/core/tests/test_formatters.py b/IPython/core/tests/test_formatters.py index 184d5d5..710ce1b 100644 --- a/IPython/core/tests/test_formatters.py +++ b/IPython/core/tests/test_formatters.py @@ -8,7 +8,9 @@ except: 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): @@ -279,4 +281,11 @@ def test_warn_error_pretty_method(): 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') diff --git a/IPython/html/static/notebook/js/outputarea.js b/IPython/html/static/notebook/js/outputarea.js index 563565a..6e2e1f5 100644 --- a/IPython/html/static/notebook/js/outputarea.js +++ b/IPython/html/static/notebook/js/outputarea.js @@ -252,6 +252,7 @@ var IPython = (function (IPython) { 'image/svg+xml', 'image/png', 'image/jpeg', + 'application/pdf', 'text/plain' ]; @@ -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 = $('').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. @@ -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", @@ -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", @@ -830,6 +844,7 @@ var IPython = (function (IPython) { 'image/svg+xml', 'image/png', 'image/jpeg', + 'application/pdf', 'text/plain' ]; @@ -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; diff --git a/IPython/kernel/zmq/pylab/config.py b/IPython/kernel/zmq/pylab/config.py index 922ef5f..680155b 100644 --- a/IPython/kernel/zmq/pylab/config.py +++ b/IPython/kernel/zmq/pylab/config.py @@ -14,7 +14,9 @@ This module does not import anything from matplotlib. #----------------------------------------------------------------------------- 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 #----------------------------------------------------------------------------- @@ -63,21 +65,26 @@ class InlineBackend(InlineBackendConfig): 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.") diff --git a/IPython/utils/jsonutil.py b/IPython/utils/jsonutil.py index 6de6709..9fed8d2 100644 --- a/IPython/utils/jsonutil.py +++ b/IPython/utils/jsonutil.py @@ -119,6 +119,8 @@ PNG64 = b'iVBORw0KG' JPEG = b'\xff\xd8' # front of JPEG base64-encoded JPEG64 = b'/9' +# front of PDF base64-encoded +PDF64 = b'JVBER' def encode_images(format_dict): """b64-encodes images in a displaypub format dict @@ -136,7 +138,7 @@ def encode_images(format_dict): format_dict : dict A copy of the same dictionary, - but binary image data ('image/png' or 'image/jpeg') + but binary image data ('image/png', 'image/jpeg' or 'application/pdf') is base64-encoded. """ @@ -156,6 +158,13 @@ def encode_images(format_dict): jpegdata = encodebytes(jpegdata) encoded['image/jpeg'] = jpegdata.decode('ascii') + pdfdata = format_dict.get('application/pdf') + if isinstance(pdfdata, bytes): + # make sure we don't double-encode + if not pdfdata.startswith(PDF64): + pdfdata = encodebytes(pdfdata) + encoded['application/pdf'] = pdfdata.decode('ascii') + return encoded diff --git a/IPython/utils/tests/test_jsonutil.py b/IPython/utils/tests/test_jsonutil.py index cdb5a7c..012b168 100644 --- a/IPython/utils/tests/test_jsonutil.py +++ b/IPython/utils/tests/test_jsonutil.py @@ -69,10 +69,12 @@ def test_encode_images(): # invalid data, but the header and footer are from real files pngdata = b'\x89PNG\r\n\x1a\nblahblahnotactuallyvalidIEND\xaeB`\x82' jpegdata = b'\xff\xd8\xff\xe0\x00\x10JFIFblahblahjpeg(\xa0\x0f\xff\xd9' + pdfdata = b'%PDF-1.\ntrailer<>]>>>>>>' fmt = { 'image/png' : pngdata, 'image/jpeg' : jpegdata, + 'application/pdf' : pdfdata } encoded = encode_images(fmt) for key, value in iteritems(fmt):