Skip to content

Commit f820c27

Browse files
authored
Merge pull request #17560 from tacaswell/fix_noop_tight_bbox
FIX: do not let no-op monkey patches to renderer leak out
2 parents 5157797 + f777177 commit f820c27

File tree

4 files changed

+63
-21
lines changed

4 files changed

+63
-21
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
The base class for the Toolbar class of each interactive backend.
2626
"""
2727

28-
from contextlib import contextmanager
28+
from contextlib import contextmanager, suppress
2929
from enum import Enum, IntEnum
3030
import functools
3131
import importlib
@@ -46,6 +46,7 @@
4646
from matplotlib.backend_managers import ToolManager
4747
from matplotlib.transforms import Affine2D
4848
from matplotlib.path import Path
49+
from matplotlib.cbook import _setattr_cm
4950

5051

5152
_log = logging.getLogger(__name__)
@@ -708,6 +709,23 @@ def stop_filter(self, filter_func):
708709
Currently only supported by the agg renderer.
709710
"""
710711

712+
def _draw_disabled(self):
713+
"""
714+
Context manager to temporary disable drawing.
715+
716+
This is used for getting the drawn size of Artists. This lets us
717+
run the draw process to update any Python state but does not pay the
718+
cost of the draw_XYZ calls on the canvas.
719+
"""
720+
no_ops = {
721+
meth_name: lambda *args, **kwargs: None
722+
for meth_name in dir(RendererBase)
723+
if (meth_name.startswith("draw_")
724+
or meth_name in ["open_group", "close_group"])
725+
}
726+
727+
return _setattr_cm(self, **no_ops)
728+
711729

712730
class GraphicsContextBase:
713731
"""An abstract base class that provides color, line styles, etc."""
@@ -1506,15 +1524,14 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None):
15061524
LocationEvent.__init__(self, name, canvas, x, y, guiEvent=guiEvent)
15071525

15081526

1509-
def _get_renderer(figure, print_method=None, *, draw_disabled=False):
1527+
def _get_renderer(figure, print_method=None):
15101528
"""
15111529
Get the renderer that would be used to save a `~.Figure`, and cache it on
15121530
the figure.
15131531
1514-
If *draw_disabled* is True, additionally replace drawing methods on
1515-
*renderer* by no-ops. This is used by the tight-bbox-saving renderer,
1516-
which needs to walk through the artist tree to compute the tight-bbox, but
1517-
for which the output file may be closed early.
1532+
If you need a renderer without any active draw methods use
1533+
renderer._draw_disabled to temporary patch them out at your call site.
1534+
15181535
"""
15191536
# This is implemented by triggering a draw, then immediately jumping out of
15201537
# Figure.draw() by raising an exception.
@@ -1533,12 +1550,6 @@ def _draw(renderer): raise Done(renderer)
15331550
except Done as exc:
15341551
renderer, = figure._cachedRenderer, = exc.args
15351552

1536-
if draw_disabled:
1537-
for meth_name in dir(RendererBase):
1538-
if (meth_name.startswith("draw_")
1539-
or meth_name in ["open_group", "close_group"]):
1540-
setattr(renderer, meth_name, lambda *args, **kwargs: None)
1541-
15421553
return renderer
15431554

15441555

@@ -2097,9 +2108,14 @@ def print_figure(
20972108
renderer = _get_renderer(
20982109
self.figure,
20992110
functools.partial(
2100-
print_method, orientation=orientation),
2101-
draw_disabled=True)
2102-
self.figure.draw(renderer)
2111+
print_method, orientation=orientation)
2112+
)
2113+
ctx = (renderer._draw_disabled()
2114+
if hasattr(renderer, '_draw_disabled')
2115+
else suppress())
2116+
with ctx:
2117+
self.figure.draw(renderer)
2118+
21032119
bbox_inches = self.figure.get_tightbbox(
21042120
renderer, bbox_extra_artists=bbox_extra_artists)
21052121
if pad_inches is None:

lib/matplotlib/figure.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2392,7 +2392,7 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,
23922392

23932393
from .tight_layout import (
23942394
get_renderer, get_subplotspec_list, get_tight_layout_figure)
2395-
2395+
from contextlib import suppress
23962396
subplotspec_list = get_subplotspec_list(self.axes)
23972397
if None in subplotspec_list:
23982398
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,
24012401

24022402
if renderer is None:
24032403
renderer = get_renderer(self)
2404-
2405-
kwargs = get_tight_layout_figure(
2406-
self, self.axes, subplotspec_list, renderer,
2407-
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
2404+
ctx = (renderer._draw_disabled()
2405+
if hasattr(renderer, '_draw_disabled')
2406+
else suppress())
2407+
with ctx:
2408+
kwargs = get_tight_layout_figure(
2409+
self, self.axes, subplotspec_list, renderer,
2410+
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
24082411
if kwargs:
24092412
self.subplots_adjust(**kwargs)
24102413

lib/matplotlib/tests/test_bbox_tight.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,26 @@ def test_tight_pcolorfast():
110110
# Previously, the bbox would include the area of the image clipped out by
111111
# the axes, resulting in a very tall image given the y limits of (0, 0.1).
112112
assert width > height
113+
114+
115+
def test_noop_tight_bbox():
116+
from PIL import Image
117+
x_size, y_size = (10, 7)
118+
dpi = 100
119+
# make the figure just the right size up front
120+
fig = plt.figure(frameon=False, dpi=dpi, figsize=(x_size/dpi, y_size/dpi))
121+
ax = plt.Axes(fig, [0., 0., 1., 1.])
122+
fig.add_axes(ax)
123+
ax.set_axis_off()
124+
ax.get_xaxis().set_visible(False)
125+
ax.get_yaxis().set_visible(False)
126+
127+
data = np.arange(x_size * y_size).reshape(y_size, x_size)
128+
ax.imshow(data)
129+
out = BytesIO()
130+
fig.savefig(out, bbox_inches='tight', pad_inches=0)
131+
out.seek(0)
132+
im = np.asarray(Image.open(out))
133+
assert (im[:, :, 3] == 255).all()
134+
assert not (im[:, :, :3] == 255).all()
135+
assert im.shape == (7, 10, 4)

lib/matplotlib/tight_layout.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def get_renderer(fig):
173173
return canvas.get_renderer()
174174
else:
175175
from . import backend_bases
176-
return backend_bases._get_renderer(fig, draw_disabled=True)
176+
return backend_bases._get_renderer(fig)
177177

178178

179179
def get_subplotspec_list(axes_list, grid_spec=None):

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy