diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 872550018f3f..7d9b55ff7b1f 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -45,7 +45,7 @@ import matplotlib as mpl from matplotlib import ( _api, backend_tools as tools, cbook, colors, docstring, textpath, - tight_bbox, transforms, widgets, get_backend, is_interactive, rcParams) + transforms, widgets, get_backend, is_interactive, rcParams) from matplotlib._pylab_helpers import Gcf from matplotlib.backend_managers import ToolManager from matplotlib.cbook import _setattr_cm @@ -2276,8 +2276,9 @@ def print_figure( if bbox_inches is None: bbox_inches = rcParams['savefig.bbox'] - if (self.figure.get_constrained_layout() or - bbox_inches == "tight"): + orig_dpi = self.figure.dpi + + if self.figure.get_constrained_layout() or bbox_inches == "tight": # we need to trigger a draw before printing to make sure # CL works. "tight" also needs a draw to get the right # locations: @@ -2289,6 +2290,8 @@ def print_figure( with getattr(renderer, "_draw_disabled", nullcontext)(): self.figure.draw(renderer) + save_dpi = self.figure.dpi + if bbox_inches: if bbox_inches == "tight": bbox_inches = self.figure.get_tightbbox( @@ -2296,14 +2299,12 @@ def print_figure( if pad_inches is None: pad_inches = rcParams['savefig.pad_inches'] bbox_inches = bbox_inches.padded(pad_inches) - - # call adjust_bbox to save only the given area - restore_bbox = tight_bbox.adjust_bbox(self.figure, bbox_inches, - canvas.fixed_dpi) - - _bbox_inches_restore = (bbox_inches, restore_bbox) - else: - _bbox_inches_restore = None + stack.enter_context( + self.figure._install_bbox_renderer_hook( + bbox_inches + # Affine2D().scale(save_dpi / orig_dpi) + # .transform_bbox(bbox_inches) + )) # we have already done CL above, so turn it off: stack.enter_context(self.figure._cm_set(constrained_layout=False)) @@ -2316,12 +2317,8 @@ def print_figure( facecolor=facecolor, edgecolor=edgecolor, orientation=orientation, - bbox_inches_restore=_bbox_inches_restore, **kwargs) finally: - if bbox_inches and restore_bbox: - restore_bbox() - self.figure.set_canvas(self) return result diff --git a/lib/matplotlib/backends/backend_mixed.py b/lib/matplotlib/backends/backend_mixed.py index 54ca8f81ba02..9ac9fcb8cdaa 100644 --- a/lib/matplotlib/backends/backend_mixed.py +++ b/lib/matplotlib/backends/backend_mixed.py @@ -68,6 +68,36 @@ def __getattr__(self, attr): # to the underlying C implementation). return getattr(self._renderer, attr) + # These methods are explicitly defined as forwarders here so that they + # can be setattr_cm'd by _install_bbox_renderer_hook. + + def draw_path(self, *args, **kwargs): + return self._renderer.draw_path(*args, **kwargs) + + def draw_markers(self, *args, **kwargs): + return self._renderer.draw_markers(*args, **kwargs) + + def draw_path_collection(self, *args, **kwargs): + return self._renderer.draw_path_collection(*args, **kwargs) + + def draw_quad_mesh(self, *args, **kwargs): + return self._renderer.draw_quad_mesh(*args, **kwargs) + + def draw_gouraud_triangle(self, *args, **kwargs): + return self._renderer.draw_gouraud_triangle(*args, **kwargs) + + def draw_gouraud_triangles(self, *args, **kwargs): + return self._renderer.draw_gouraud_triangles(*args, **kwargs) + + def draw_image(self, *args, **kwargs): + return self._renderer.draw_image(*args, **kwargs) + + def draw_tex(self, *args, **kwargs): + return self._renderer.draw_tex(*args, **kwargs) + + def draw_text(self, *args, **kwargs): + return self._renderer.draw_text(*args, **kwargs) + def start_rasterizing(self): """ Enter "raster" mode. All subsequent drawing commands (until diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 3e03f7a7f198..2d1bec270344 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -14,6 +14,7 @@ Control the default spacing between subplots. """ +import contextlib from contextlib import ExitStack import inspect import logging @@ -39,8 +40,9 @@ import matplotlib.legend as mlegend from matplotlib.patches import Rectangle from matplotlib.text import Text -from matplotlib.transforms import (Affine2D, Bbox, BboxTransformTo, - TransformedBbox) +from matplotlib.transforms import ( + Affine2D, Bbox, BboxTransformTo, IdentityTransform, ScaledTranslation, + TransformedBbox, TransformedPath) import matplotlib._layoutgrid as layoutgrid _log = logging.getLogger(__name__) @@ -2723,6 +2725,9 @@ def clear(self, keep_observers=False): @allow_rasterization def draw(self, renderer): # docstring inherited + if hasattr(self, "_bbox_renderer_hook"): + self._bbox_renderer_hook(renderer) + self._cachedRenderer = renderer # draw the figure bounding box, perhaps none for white figure @@ -2755,6 +2760,157 @@ def draw(self, renderer): self.canvas.draw_event(renderer) + @contextlib.contextmanager + def _install_bbox_renderer_hook(self, bbox_inches): + """ + Helper for rendering only part of the figure. + + Temporarily that the figure's inches bbox is *bbox_inches*. When + `draw` is called, patch the renderer so that all its draws are shifted + according to the origin of *bbox_inches*, and undo the bbox patching. + """ + orig_bbox_inches_bounds = self.bbox_inches.bounds + self.bbox_inches.x1 = bbox_inches.width + self.bbox_inches.y1 = bbox_inches.height + x0 = bbox_inches.x0 + y0 = bbox_inches.y0 + shift_trf = ScaledTranslation(-x0, -y0, self.dpi_scale_trans) + + def bbox_renderer_hook(renderer): + try: # o_: original, s_: shifted. + origs = { + "draw_path": renderer.draw_path, + "draw_markers": renderer.draw_markers, + "draw_path_collection": renderer.draw_path_collection, + "draw_quad_mesh": renderer.draw_quad_mesh, + "draw_gouraud_triangle": renderer.draw_gouraud_triangle, + "draw_gouraud_triangles": renderer.draw_gouraud_triangles, + "draw_image": renderer.draw_image, + "draw_tex": renderer.draw_tex, + "draw_text": renderer.draw_text, + } + + @contextlib.contextmanager + def _shifted_clips_and_unpatched(gc): + cr = gc.get_clip_rectangle() + if cr is not None: + gc.set_clip_rectangle(shift_trf.transform_bbox(cr)) + tp, tr = gc.get_clip_path() + if tp is not None: + gc.set_clip_path(TransformedPath(tp, tr + shift_trf)) + try: + with cbook._setattr_cm(renderer, **origs): + yield + finally: + if tp: + gc.set_clip_path(TransformedPath(tp, tr)) + if cr: + gc.set_clip_rectangle(cr) + + def s_draw_path(gc, path, transform, rgbFace=None): + with _shifted_clips_and_unpatched(gc): + return origs["draw_path"]( + gc, path, transform + shift_trf, rgbFace) + + def s_draw_markers( + gc, marker_path, marker_trans, path, trans, + rgbFace=None): + with _shifted_clips_and_unpatched(gc): + return origs["draw_markers"]( + gc, marker_path, marker_trans, path, + trans + shift_trf, rgbFace) + + def s_draw_path_collection( + gc, master_transform, paths, all_transforms, + offsets, offsetTrans, facecolors, edgecolors, + linewidths, linestyles, antialiaseds, urls, + offset_position): + with _shifted_clips_and_unpatched(gc): + return orig["draw_path_collection"]( + gc, master_transform + shift_trf, paths, + all_transforms, offsets, offsetTrans, facecolors, + edgecolors, linewidths, linestyles, antialiaseds, + urls, offset_position) + + def s_draw_quad_mesh( + gc, master_transform, meshWidth, meshHeight, + coordinates, offsets, offsetTrans, facecolors, + antialiased, edgecolors): + with _shifted_clips_and_unpatched(gc): + return orig["draw_quad_mesh"]( + gc, master_transform + shift_trf, meshWidth, + meshHeight, coordinates, offsets, offsetTrans, + facecolors, antialiased, edgecolors) + + def s_draw_gouraud_triangle(gc, points, colors, transform): + with _shifted_clips_and_unpatched(gc): + return orig["draw_gouraud_triangle"]( + gc, points, colors, transform + shift_trf) + + def s_draw_gouraud_triangles( + gc, triangles_array, colors_array, transform): + with _shifted_clips_and_unpatched(gc): + return orig["draw_gouraud_triangles"]( + gc, triangles_array, colors_array, transform) + + # draw_image may take a transform kwarg, or not. + def s_draw_image(gc, x, y, *args, **kwargs): + with _shifted_clips_and_unpatched(gc): + dx = -x0 * self.dpi + dy = -y0 * self.dpi + return origs["draw_image"]( + gc, x + dx, y + dy, *args, **kwargs) + + def s_draw_tex(gc, x, y, s, prop, angle, *, mtext=None): + with _shifted_clips_and_unpatched(gc): + dx = -x0 * self.dpi + dy = -y0 * self.dpi + if renderer.flipy(): + dy = -dy + return origs["draw_tex"]( + gc, x + dx, y + dy, s, prop, angle, mtext) + + def s_draw_text( + gc, x, y, s, prop, angle, ismath=False, mtext=None): + with _shifted_clips_and_unpatched(gc): + dx = -x0 * self.dpi + dy = -y0 * self.dpi + if renderer.flipy(): + dy = -dy + # Some backends ignore x and y and directly read + # mtext.get_position()/get_unitless_position(). + with cbook._setattr_cm( + mtext, + get_position=lambda: (x + dx, y + dy), + get_unitless_position=lambda: (x + dx, y + dy), + get_transform=lambda: IdentityTransform(), + ): + return origs["draw_text"]( + gc, x + dx, y + dy, s, prop, angle, ismath, + mtext) + + stack.enter_context(cbook._setattr_cm( + renderer, + draw_path=s_draw_path, + draw_markers=s_draw_markers, + draw_path_collection=s_draw_path_collection, + draw_quad_mesh=s_draw_quad_mesh, + draw_gouraud_triangle=s_draw_gouraud_triangle, + draw_gouraud_triangles=s_draw_gouraud_triangles, + draw_image=s_draw_image, + draw_tex=s_draw_tex, + draw_text=s_draw_text, + )) + + finally: + self.bbox_inches.bounds = orig_bbox_inches_bounds + del self._bbox_renderer_hook + + self._bbox_renderer_hook = bbox_renderer_hook + + with ExitStack() as stack: + yield + def draw_no_output(self): """ Draw the figure with no output. Useful to get the final size of