From 56e87ef9f870ea3e7a558ccdf798169c0cd57deb Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 28 Jun 2020 15:32:28 +0200 Subject: [PATCH 1/2] Tighten a bit the RendererAgg API. The get_content_extents and tostring_rgba_minimized methods of RendererAgg are used to implement 1) agg_filter and 2) rasterization in MixedModeRenderer (i.e. for vector backends). They can easily instead be implemented at the Python level (i.e., via _get_nonzero_slices), which makes it easier to plug in an alternative renderer (e.g., mplcairo) for rasterization in MixedModeRenderer. I'm not convinced _get_nonzero_slices needs to be exposed as public API (it's quite simple anyways), but it could be if we absolutely want to present an alternative for the deprecated methods. --- .../deprecations/17788-AL.rst | 3 +++ lib/matplotlib/backends/backend_agg.py | 24 ++++++++++--------- lib/matplotlib/backends/backend_mixed.py | 17 +++++++------ lib/matplotlib/cbook/__init__.py | 18 ++++++++++++++ 4 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/17788-AL.rst diff --git a/doc/api/next_api_changes/deprecations/17788-AL.rst b/doc/api/next_api_changes/deprecations/17788-AL.rst new file mode 100644 index 000000000000..7f4acc9894e0 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/17788-AL.rst @@ -0,0 +1,3 @@ +``RendererAgg.get_content_extents``, ``RendererAgg.tostring_rgba_minimized`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are deprecated. diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index b1ef3035290d..e81837822378 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -116,8 +116,12 @@ def _update_methods(self): # self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.copy_from_bbox = self._renderer.copy_from_bbox - self.get_content_extents = self._renderer.get_content_extents + @cbook.deprecated("3.4") # Also needs to be removed at C-level. + def get_content_extents(self): + return self._renderer.get_content_extents() + + @cbook.deprecated("3.4") def tostring_rgba_minimized(self): extents = self.get_content_extents() bbox = [[extents[0], self.height - (extents[1] + extents[3])], @@ -364,23 +368,21 @@ def post_processing(image, dpi): The saved renderer is restored and the returned image from post_processing is plotted (using draw_image) on it. """ - - width, height = int(self.width), int(self.height) - - buffer, (l, b, w, h) = self.tostring_rgba_minimized() + orig_img = np.asarray(self.buffer_rgba()) + slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3]) + cropped_img = orig_img[slice_y, slice_x] self._renderer = self._filter_renderers.pop() self._update_methods() - if w > 0 and h > 0: - img = np.frombuffer(buffer, np.uint8) - img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255., - self.dpi) + if cropped_img.size: + img, ox, oy = post_processing(cropped_img / 255, self.dpi) gc = self.new_gc() if img.dtype.kind == 'f': img = np.asarray(img * 255., np.uint8) - img = img[::-1] - self._renderer.draw_image(gc, l + ox, height - b - h + oy, img) + self._renderer.draw_image( + gc, slice_x.start + ox, int(self.height) - slice_y.stop + oy, + img[::-1]) class FigureCanvasAgg(FigureCanvasBase): diff --git a/lib/matplotlib/backends/backend_mixed.py b/lib/matplotlib/backends/backend_mixed.py index 9a588ece8f9d..f20e41fab1fe 100644 --- a/lib/matplotlib/backends/backend_mixed.py +++ b/lib/matplotlib/backends/backend_mixed.py @@ -1,5 +1,6 @@ import numpy as np +from matplotlib import cbook from matplotlib.backends.backend_agg import RendererAgg from matplotlib.tight_bbox import process_figure_for_rasterizing @@ -98,21 +99,19 @@ def stop_rasterizing(self): self._renderer = self._vector_renderer height = self._height * self.dpi - buffer, bounds = self._raster_renderer.tostring_rgba_minimized() - l, b, w, h = bounds - if w > 0 and h > 0: - image = np.frombuffer(buffer, dtype=np.uint8) - image = image.reshape((h, w, 4)) - image = image[::-1] + img = np.asarray(self._raster_renderer.buffer_rgba()) + slice_y, slice_x = cbook._get_nonzero_slices(img[..., 3]) + cropped_img = img[slice_y, slice_x] + if cropped_img.size: gc = self._renderer.new_gc() # TODO: If the mixedmode resolution differs from the figure's # dpi, the image must be scaled (dpi->_figdpi). Not all # backends support this. self._renderer.draw_image( gc, - l * self._figdpi / self.dpi, - (height-b-h) * self._figdpi / self.dpi, - image) + slice_x.start * self._figdpi / self.dpi, + (height - slice_y.stop) * self._figdpi / self.dpi, + cropped_img[::-1]) self._raster_renderer = None # restore the figure dpi. diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index dd8b6aed9b8a..f6813b95739e 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -2162,6 +2162,24 @@ def _unmultiplied_rgba8888_to_premultiplied_argb32(rgba8888): return argb32 +def _get_nonzero_slices(buf): + """ + Return the bounds of the nonzero region of a 2D array as a pair of slices. + + ``buf[_get_nonzero_slices(buf)]`` is the smallest sub-rectangle in *buf* + that encloses all non-zero entries in *buf*. If *buf* is fully zero, then + ``(slice(0, 0), slice(0, 0))`` is returned. + """ + x_nz, = buf.any(axis=0).nonzero() + y_nz, = buf.any(axis=1).nonzero() + if len(x_nz) and len(y_nz): + l, r = x_nz[[0, -1]] + b, t = y_nz[[0, -1]] + return slice(b, t + 1), slice(l, r + 1) + else: + return slice(0, 0), slice(0, 0) + + def _pformat_subprocess(command): """Pretty-format a subprocess command for printing/logging purposes.""" return (command if isinstance(command, str) From c3e1c1b7d54f63d97b8160abf95e362ea288d3a7 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 3 Jul 2020 11:45:25 +0200 Subject: [PATCH 2/2] Directly remove the C-level get_content_extents. --- lib/matplotlib/backends/backend_agg.py | 7 ++++-- src/_backend_agg.cpp | 35 -------------------------- src/_backend_agg_wrapper.cpp | 12 --------- 3 files changed, 5 insertions(+), 49 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index e81837822378..1dfff114d9bb 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -117,9 +117,12 @@ def _update_methods(self): self.draw_quad_mesh = self._renderer.draw_quad_mesh self.copy_from_bbox = self._renderer.copy_from_bbox - @cbook.deprecated("3.4") # Also needs to be removed at C-level. + @cbook.deprecated("3.4") def get_content_extents(self): - return self._renderer.get_content_extents() + orig_img = np.asarray(self.buffer_rgba()) + slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3]) + return (slice_x.start, slice_y.start, + slice_x.stop - slice_x.start, slice_y.stop - slice_y.start) @cbook.deprecated("3.4") def tostring_rgba_minimized(self): diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index 0ae17741d735..de06d0bfb2ab 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -167,41 +167,6 @@ bool RendererAgg::render_clippath(py::PathIterator &clippath, return has_clippath; } -agg::rect_i RendererAgg::get_content_extents() -{ - agg::rect_i r(width, height, 0, 0); - - // Looks at the alpha channel to find the minimum extents of the image - unsigned char *pixel = pixBuffer + 3; - for (int y = 0; y < (int)height; ++y) { - for (int x = 0; x < (int)width; ++x) { - if (*pixel) { - if (x < r.x1) - r.x1 = x; - if (y < r.y1) - r.y1 = y; - if (x > r.x2) - r.x2 = x; - if (y > r.y2) - r.y2 = y; - } - pixel += 4; - } - } - - if (r.x1 == (int)width && r.x2 == 0) { - // The buffer is completely empty. - r.x1 = r.y1 = r.x2 = r.y2 = 0; - } else { - r.x1 = std::max(0, r.x1); - r.y1 = std::max(0, r.y1); - r.x2 = std::min(r.x2 + 1, (int)width); - r.y2 = std::min(r.y2 + 1, (int)height); - } - - return r; -} - void RendererAgg::clear() { //"clear the rendered buffer"; diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 1377231b5201..e08642e248ef 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -526,17 +526,6 @@ PyRendererAgg_draw_gouraud_triangles(PyRendererAgg *self, PyObject *args, PyObje Py_RETURN_NONE; } -static PyObject * -PyRendererAgg_get_content_extents(PyRendererAgg *self, PyObject *args, PyObject *kwds) -{ - agg::rect_i extents; - - CALL_CPP("get_content_extents", (extents = self->x->get_content_extents())); - - return Py_BuildValue( - "iiii", extents.x1, extents.y1, extents.x2 - extents.x1, extents.y2 - extents.y1); -} - int PyRendererAgg_get_buffer(PyRendererAgg *self, Py_buffer *buf, int flags) { Py_INCREF(self); @@ -627,7 +616,6 @@ static PyTypeObject *PyRendererAgg_init_type(PyObject *m, PyTypeObject *type) {"draw_gouraud_triangle", (PyCFunction)PyRendererAgg_draw_gouraud_triangle, METH_VARARGS, NULL}, {"draw_gouraud_triangles", (PyCFunction)PyRendererAgg_draw_gouraud_triangles, METH_VARARGS, NULL}, - {"get_content_extents", (PyCFunction)PyRendererAgg_get_content_extents, METH_NOARGS, NULL}, {"clear", (PyCFunction)PyRendererAgg_clear, METH_NOARGS, NULL}, {"copy_from_bbox", (PyCFunction)PyRendererAgg_copy_from_bbox, METH_VARARGS, NULL},