diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 71eb153f24ab..447fa0aa0e31 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -25,7 +25,7 @@ The base class for the Toolbar class of each interactive backend. """ -from contextlib import contextmanager +from contextlib import contextmanager, suppress from enum import Enum, IntEnum import functools import importlib @@ -46,6 +46,7 @@ from matplotlib.backend_managers import ToolManager from matplotlib.transforms import Affine2D from matplotlib.path import Path +from matplotlib.cbook import _setattr_cm _log = logging.getLogger(__name__) @@ -708,6 +709,23 @@ def stop_filter(self, filter_func): Currently only supported by the agg renderer. """ + def _draw_disabled(self): + """ + Context manager to temporary disable drawing. + + This is used for getting the drawn size of Artists. This lets us + run the draw process to update any Python state but does not pay the + cost of the draw_XYZ calls on the canvas. + """ + no_ops = { + meth_name: lambda *args, **kwargs: None + for meth_name in dir(RendererBase) + if (meth_name.startswith("draw_") + or meth_name in ["open_group", "close_group"]) + } + + return _setattr_cm(self, **no_ops) + class GraphicsContextBase: """An abstract base class that provides color, line styles, etc.""" @@ -1502,15 +1520,14 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): self.key = key -def _get_renderer(figure, print_method=None, *, draw_disabled=False): +def _get_renderer(figure, print_method=None): """ Get the renderer that would be used to save a `~.Figure`, and cache it on the figure. - If *draw_disabled* is True, additionally replace drawing methods on - *renderer* by no-ops. This is used by the tight-bbox-saving renderer, - which needs to walk through the artist tree to compute the tight-bbox, but - for which the output file may be closed early. + If you need a renderer without any active draw methods use + renderer._draw_disabled to temporary patch them out at your call site. + """ # This is implemented by triggering a draw, then immediately jumping out of # Figure.draw() by raising an exception. @@ -1529,12 +1546,6 @@ def _draw(renderer): raise Done(renderer) except Done as exc: renderer, = figure._cachedRenderer, = exc.args - if draw_disabled: - for meth_name in dir(RendererBase): - if (meth_name.startswith("draw_") - or meth_name in ["open_group", "close_group"]): - setattr(renderer, meth_name, lambda *args, **kwargs: None) - return renderer @@ -2093,9 +2104,14 @@ def print_figure( renderer = _get_renderer( self.figure, functools.partial( - print_method, orientation=orientation), - draw_disabled=True) - self.figure.draw(renderer) + print_method, orientation=orientation) + ) + ctx = (renderer._draw_disabled() + if hasattr(renderer, '_draw_disabled') + else suppress()) + with ctx: + self.figure.draw(renderer) + bbox_inches = self.figure.get_tightbbox( renderer, bbox_extra_artists=bbox_extra_artists) if pad_inches is None: diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 9e7fc4c2d16e..6af6870832af 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2392,7 +2392,7 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None, from .tight_layout import ( get_renderer, get_subplotspec_list, get_tight_layout_figure) - + from contextlib import suppress subplotspec_list = get_subplotspec_list(self.axes) if None in subplotspec_list: cbook._warn_external("This figure includes Axes that are not " @@ -2401,10 +2401,13 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None, if renderer is None: renderer = get_renderer(self) - - kwargs = get_tight_layout_figure( - self, self.axes, subplotspec_list, renderer, - pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) + ctx = (renderer._draw_disabled() + if hasattr(renderer, '_draw_disabled') + else suppress()) + with ctx: + kwargs = get_tight_layout_figure( + self, self.axes, subplotspec_list, renderer, + pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) if kwargs: self.subplots_adjust(**kwargs) diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index 4d52580e8b5d..235e02461f37 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -110,3 +110,26 @@ def test_tight_pcolorfast(): # Previously, the bbox would include the area of the image clipped out by # the axes, resulting in a very tall image given the y limits of (0, 0.1). assert width > height + + +def test_noop_tight_bbox(): + from PIL import Image + x_size, y_size = (10, 7) + dpi = 100 + # make the figure just the right size up front + fig = plt.figure(frameon=False, dpi=dpi, figsize=(x_size/dpi, y_size/dpi)) + ax = plt.Axes(fig, [0., 0., 1., 1.]) + fig.add_axes(ax) + ax.set_axis_off() + ax.get_xaxis().set_visible(False) + ax.get_yaxis().set_visible(False) + + data = np.arange(x_size * y_size).reshape(y_size, x_size) + ax.imshow(data) + out = BytesIO() + fig.savefig(out, bbox_inches='tight', pad_inches=0) + out.seek(0) + im = np.asarray(Image.open(out)) + assert (im[:, :, 3] == 255).all() + assert not (im[:, :, :3] == 255).all() + assert im.shape == (7, 10, 4) diff --git a/lib/matplotlib/tight_layout.py b/lib/matplotlib/tight_layout.py index 43b578fef625..df55005047f9 100644 --- a/lib/matplotlib/tight_layout.py +++ b/lib/matplotlib/tight_layout.py @@ -173,7 +173,7 @@ def get_renderer(fig): return canvas.get_renderer() else: from . import backend_bases - return backend_bases._get_renderer(fig, draw_disabled=True) + return backend_bases._get_renderer(fig) def get_subplotspec_list(axes_list, grid_spec=None):