From 3d36519405b45477623b5b74a702624d9fa5a1e1 2014-02-21 19:15:47
From: Brian E. Granger <ellisonbg@gmail.com>
Date: 2014-02-21 19:15:47
Subject: [PATCH] Merge pull request #5110 from minrk/bbox_inches

add InlineBackend.print_figure_kwargs
---

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.