diff --git a/IPython/core/display.py b/IPython/core/display.py index 6c48c6b..301e8aa 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -729,23 +729,29 @@ def set_matplotlib_formats(*formats, **kwargs): To set this in your config files use the following:: - c.InlineBackend.figure_formats = {'pdf', 'png', 'svg'} - c.InlineBackend.quality = 90 + c.InlineBackend.figure_formats = {'png', 'jpeg'} + c.InlineBackend.print_figure_kwargs.update({'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. + *formats : strs + One or more figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'. + **kwargs : + Keyword args will be relayed to ``figure.canvas.print_figure``. """ from IPython.core.interactiveshell import InteractiveShell from IPython.core.pylabtools import select_figure_formats + from IPython.kernel.zmq.pylab.config import InlineBackend + # build kwargs, starting with InlineBackend config + kw = {} + cfg = InlineBackend.instance() + kw.update(cfg.print_figure_kwargs) + kw.update(**kwargs) shell = InteractiveShell.instance() - select_figure_formats(shell, formats, quality=90) + select_figure_formats(shell, formats, **kw) @skip_doctest -def set_matplotlib_close(close): +def set_matplotlib_close(close=True): """Set whether the inline backend closes all figures automatically or not. By default, the inline backend used in the IPython Notebook will close all @@ -766,7 +772,7 @@ def set_matplotlib_close(close): 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 + from IPython.kernel.zmq.pylab.config import InlineBackend + cfg = InlineBackend.instance() + cfg.close_figures = close diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index 106b856..01a0fb0 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -95,9 +95,11 @@ def figsize(sizex, sizey): matplotlib.rcParams['figure.figsize'] = [sizex, sizey] -def print_figure(fig, fmt='png', quality=90): - """Convert a figure to svg, png or jpg for inline display. - Quality is only relevant for jpg. +def print_figure(fig, fmt='png', bbox_inches='tight', **kwargs): + """Print a figure to an image, and return the resulting bytes + + Any keyword args are passed to fig.canvas.print_figure, + such as ``quality`` or ``bbox_inches``. """ from matplotlib import rcParams # When there's an empty figure, we shouldn't return anything, otherwise we @@ -105,21 +107,29 @@ def print_figure(fig, fmt='png', quality=90): if not fig.axes and not fig.lines: return - fc = fig.get_facecolor() - ec = fig.get_edgecolor() - bytes_io = BytesIO() dpi = rcParams['savefig.dpi'] if fmt == 'retina': dpi = dpi * 2 fmt = 'png' - fig.canvas.print_figure(bytes_io, format=fmt, bbox_inches='tight', - facecolor=fc, edgecolor=ec, dpi=dpi, quality=quality) - data = bytes_io.getvalue() - return data -def retina_figure(fig): + # build keyword args + kw = dict( + format=fmt, + fc=fig.get_facecolor(), + ec=fig.get_edgecolor(), + dpi=dpi, + bbox_inches=bbox_inches, + ) + # **kwargs get higher priority + kw.update(kwargs) + + bytes_io = BytesIO() + fig.canvas.print_figure(bytes_io, **kw) + return bytes_io.getvalue() + +def retina_figure(fig, **kwargs): """format a figure as a pixel-doubled (retina) PNG""" - pngdata = print_figure(fig, fmt='retina') + pngdata = print_figure(fig, fmt='retina', **kwargs) w, h = _pngxy(pngdata) metadata = dict(width=w//2, height=h//2) return pngdata, metadata @@ -166,17 +176,17 @@ def mpl_runner(safe_execfile): return mpl_execfile -def select_figure_formats(shell, formats, quality=90): +def select_figure_formats(shell, formats, **kwargs): """Select figure formats for the inline backend. Parameters ========== shell : InteractiveShell The main IPython instance. - formats : list + formats : str or set One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'. - quality : int - A percentage for the quality of JPEG figures. + **kwargs : any + Extra keyword arguments to be passed to fig.canvas.print_figure. """ from matplotlib.figure import Figure from IPython.kernel.zmq.pylab import backend_inline @@ -188,22 +198,28 @@ def select_figure_formats(shell, formats, quality=90): if isinstance(formats, py3compat.string_types): formats = {formats} + # cast in case of list / tuple + formats = set(formats) - [ f.type_printers.pop(Figure, None) for f in {svg_formatter, png_formatter, jpg_formatter} ] - - 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) + [ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ] + + supported = {'png', 'png2x', 'retina', 'jpg', 'jpeg', 'svg', 'pdf'} + bad = formats.difference(supported) + if bad: + bs = "%s" % ','.join([repr(f) for f in bad]) + gs = "%s" % ','.join([repr(f) for f in supported]) + raise ValueError("supported formats are: %s not %s" % (gs, bs)) + + if 'png' in formats: + png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs)) + if 'retina' in formats or 'png2x' in formats: + png_formatter.for_type(Figure, lambda fig: retina_figure(fig, **kwargs)) + if 'jpg' in formats or 'jpeg' in formats: + jpg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'jpg', **kwargs)) + if 'svg' in formats: + svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg', **kwargs)) + if 'pdf' in formats: + pdf_formatter.for_type(Figure, lambda fig: print_figure(fig, 'pdf', **kwargs)) #----------------------------------------------------------------------------- # Code for initializing matplotlib and importing pylab @@ -354,5 +370,5 @@ def configure_inline_support(shell, backend): del shell._saved_rcParams # Setup the default figure format - select_figure_formats(shell, cfg.figure_formats, cfg.quality) + select_figure_formats(shell, cfg.figure_formats, **cfg.print_figure_kwargs) diff --git a/IPython/core/tests/test_display.py b/IPython/core/tests/test_display.py index 67fb620..8e55cc0 100644 --- a/IPython/core/tests/test_display.py +++ b/IPython/core/tests/test_display.py @@ -10,8 +10,11 @@ import os import nose.tools as nt from IPython.core import display +from IPython.core.getipython import get_ipython from IPython.utils import path as ipath +import IPython.testing.decorators as dec + def test_image_size(): """Simple test for display.Image(args, width=x,height=y)""" thisurl = 'http://www.google.fr/images/srpr/logo3w.png' @@ -58,3 +61,58 @@ def test_image_filename_defaults(): img = display.Image(filename=os.path.join(tpath, 'testing/tests/logo.jpg'), embed=False) nt.assert_equal('jpeg', img.format) nt.assert_is_none(img._repr_jpeg_()) + +def _get_inline_config(): + from IPython.kernel.zmq.pylab.config import InlineBackend + return InlineBackend.instance() + +@dec.skip_without('matplotlib') +def test_set_matplotlib_close(): + cfg = _get_inline_config() + cfg.close_figures = False + display.set_matplotlib_close() + assert cfg.close_figures + display.set_matplotlib_close(False) + assert not cfg.close_figures + +_fmt_mime_map = { + 'png': 'image/png', + 'jpeg': 'image/jpeg', + 'pdf': 'application/pdf', + 'retina': 'image/png', + 'svg': 'image/svg+xml', +} + +@dec.skip_without('matplotlib') +def test_set_matplotlib_formats(): + from matplotlib.figure import Figure + formatters = get_ipython().display_formatter.formatters + for formats in [ + ('png',), + ('pdf', 'svg'), + ('jpeg', 'retina', 'png'), + (), + ]: + active_mimes = {_fmt_mime_map[fmt] for fmt in formats} + display.set_matplotlib_formats(*formats) + for mime, f in formatters.items(): + if mime in active_mimes: + nt.assert_in(Figure, f) + else: + nt.assert_not_in(Figure, f) + +@dec.skip_without('matplotlib') +def test_set_matplotlib_formats_kwargs(): + from matplotlib.figure import Figure + ip = get_ipython() + cfg = _get_inline_config() + cfg.print_figure_kwargs.update(dict(foo='bar')) + kwargs = dict(quality=10) + display.set_matplotlib_formats('png', **kwargs) + formatter = ip.display_formatter.formatters['image/png'] + f = formatter.lookup_by_type(Figure) + cell = f.__closure__[0].cell_contents + expected = kwargs + expected.update(cfg.print_figure_kwargs) + nt.assert_equal(cell, expected) + diff --git a/IPython/core/tests/test_pylabtools.py b/IPython/core/tests/test_pylabtools.py index df648c7..f562a4c 100644 --- a/IPython/core/tests/test_pylabtools.py +++ b/IPython/core/tests/test_pylabtools.py @@ -13,17 +13,19 @@ #----------------------------------------------------------------------------- from __future__ import print_function -# Stdlib imports +import matplotlib +matplotlib.use('Agg') +from matplotlib.figure import Figure -# Third-party imports -import matplotlib; matplotlib.use('Agg') import nose.tools as nt from matplotlib import pyplot as plt import numpy as np # Our own imports +from IPython.core.getipython import get_ipython from IPython.core.interactiveshell import InteractiveShell +from IPython.core.display import _PNG, _JPEG from .. import pylabtools as pt from IPython.testing import decorators as dec @@ -62,12 +64,81 @@ def test_figure_to_jpg(): ax = fig.add_subplot(1,1,1) ax.plot([1,2,3]) plt.draw() - jpg = pt.print_figure(fig, 'jpg')[:100].lower() - assert jpg.startswith(b'\xff\xd8') + jpg = pt.print_figure(fig, 'jpg', quality=50)[:100].lower() + assert jpg.startswith(_JPEG) +def test_retina_figure(): + fig = plt.figure() + ax = fig.add_subplot(1,1,1) + ax.plot([1,2,3]) + plt.draw() + png, md = pt.retina_figure(fig) + assert png.startswith(_PNG) + nt.assert_in('width', md) + nt.assert_in('height', md) + +_fmt_mime_map = { + 'png': 'image/png', + 'jpeg': 'image/jpeg', + 'pdf': 'application/pdf', + 'retina': 'image/png', + 'svg': 'image/svg+xml', +} + +def test_select_figure_formats_str(): + ip = get_ipython() + for fmt, active_mime in _fmt_mime_map.items(): + pt.select_figure_formats(ip, fmt) + for mime, f in ip.display_formatter.formatters.items(): + if mime == active_mime: + nt.assert_in(Figure, f) + else: + nt.assert_not_in(Figure, f) + +def test_select_figure_formats_kwargs(): + ip = get_ipython() + kwargs = dict(quality=10, bbox_inches='tight') + pt.select_figure_formats(ip, 'png', **kwargs) + formatter = ip.display_formatter.formatters['image/png'] + f = formatter.lookup_by_type(Figure) + cell = f.__closure__[0].cell_contents + nt.assert_equal(cell, kwargs) + + # check that the formatter doesn't raise + fig = plt.figure() + ax = fig.add_subplot(1,1,1) + ax.plot([1,2,3]) + plt.draw() + formatter.enabled = True + png = formatter(fig) + assert png.startswith(_PNG) -def test_import_pylab(): +def test_select_figure_formats_set(): ip = get_ipython() + for fmts in [ + {'png', 'svg'}, + ['png'], + ('jpeg', 'pdf', 'retina'), + {'svg'}, + ]: + active_mimes = {_fmt_mime_map[fmt] for fmt in fmts} + pt.select_figure_formats(ip, fmts) + for mime, f in ip.display_formatter.formatters.items(): + if mime in active_mimes: + nt.assert_in(Figure, f) + else: + nt.assert_not_in(Figure, f) + +def test_select_figure_formats_bad(): + ip = get_ipython() + with nt.assert_raises(ValueError): + pt.select_figure_formats(ip, 'foo') + with nt.assert_raises(ValueError): + pt.select_figure_formats(ip, {'png', 'foo'}) + with nt.assert_raises(ValueError): + pt.select_figure_formats(ip, ['retina', 'pdf', 'bar', 'bad']) + +def test_import_pylab(): ns = {} pt.import_pylab(ns, import_all=False) nt.assert_true('plt' in ns) diff --git a/IPython/kernel/zmq/pylab/config.py b/IPython/kernel/zmq/pylab/config.py index 680155b..d7eaa3a 100644 --- a/IPython/kernel/zmq/pylab/config.py +++ b/IPython/kernel/zmq/pylab/config.py @@ -69,15 +69,16 @@ class InlineBackend(InlineBackendConfig): help="""A set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.""") + def _update_figure_formatters(self): + if self.shell is not None: + select_figure_formats(self.shell, self.figure_formats, **self.print_figure_kwargs) + 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_formats(self.shell, new) + self._update_figure_formatters() figure_format = Unicode(config=True, help="""The figure format to enable (deprecated use `figure_formats` instead)""") @@ -86,12 +87,13 @@ class InlineBackend(InlineBackendConfig): if new: self.figure_formats = {new} - quality = Int(default_value=90, config=True, - help="Quality of compression [10-100], currently for lossy JPEG only.") - - def _quality_changed(self, name, old, new): - if new < 10 or new > 100: - raise TraitError("figure JPEG quality must be in [10-100] range.") + print_figure_kwargs = Dict({'bbox_inches' : 'tight'}, config=True, + help="""Extra kwargs to be passed to fig.canvas.print_figure. + + Logical examples include: bbox_inches, quality (for jpeg figures), etc. + """ + ) + _print_figure_kwargs_changed = _update_figure_formatters close_figures = Bool(True, config=True, help="""Close all figures at the end of each cell. @@ -109,7 +111,7 @@ class InlineBackend(InlineBackendConfig): other matplotlib backends, but figure barriers between cells must be explicit. """) - + shell = Instance('IPython.core.interactiveshell.InteractiveShellABC') diff --git a/docs/source/whatsnew/development.rst b/docs/source/whatsnew/development.rst index aaf67d5..a2f4e3e 100644 --- a/docs/source/whatsnew/development.rst +++ b/docs/source/whatsnew/development.rst @@ -231,8 +231,6 @@ Other changes 1.12.1. * The InlineBackend.figure_format flag now supports JPEG output if PIL/Pillow is available. -* The new ``InlineBackend.quality`` flag is a Integer in the range [10, 100] which controls - the quality of figures where higher values give nicer images (currently JPEG only). * Input transformers (see :doc:`/config/inputtransforms`) may now raise :exc:`SyntaxError` if they determine that input is invalid. The input diff --git a/docs/source/whatsnew/pr/print-figure-kwargs.rst b/docs/source/whatsnew/pr/print-figure-kwargs.rst new file mode 100644 index 0000000..53557a3 --- /dev/null +++ b/docs/source/whatsnew/pr/print-figure-kwargs.rst @@ -0,0 +1,3 @@ +* added ``InlineBackend.print_figure_kwargs`` to allow passing keyword arguments + to matplotlib's ``Canvas.print_figure``. This can be used to change the value of + ``bbox_inches``, which is 'tight' by default, or set the quality of JPEG figures.