From 71212343ed7ef6e468063ecb82fb58b4ba2d67f3 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 13 Mar 2024 04:35:46 +0100 Subject: [PATCH 1/4] Simplify the qt backend by using buffers to construct the image to be restored --- lib/matplotlib/backends/backend_qtagg.py | 28 ++++++++++-------------- src/_backend_agg_wrapper.cpp | 9 ++++++++ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py index 256e50a3d1c3..830f0b29329e 100644 --- a/lib/matplotlib/backends/backend_qtagg.py +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -4,13 +4,14 @@ import ctypes -from matplotlib.transforms import Bbox +import numpy as np from .qt_compat import QT_API, QtCore, QtGui from .backend_agg import FigureCanvasAgg from .backend_qt import _BackendQT, FigureCanvasQT from .backend_qt import ( # noqa: F401 # pylint: disable=W0611 FigureManagerQT, NavigationToolbar2QT) +from ..transforms import Bbox class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT): @@ -47,25 +48,20 @@ def paintEvent(self, event): right = left + width # create a buffer using the image bounding box bbox = Bbox([[left, bottom], [right, top]]) - buf = memoryview(self.copy_from_bbox(bbox)) + region = self.copy_from_bbox(bbox) - if QT_API == "PyQt6": - from PyQt6 import sip - ptr = int(sip.voidptr(buf)) - else: - ptr = buf + # Now draw the region we copied with the painter. + r_width, r_height = region.get_bounds()[2:] - painter.eraseRect(rect) # clear the widget canvas - qimage = QtGui.QImage(ptr, buf.shape[1], buf.shape[0], - QtGui.QImage.Format.Format_RGBA8888) + # Draw the region copied with the QPainter + qimage = QtGui.QImage( + np.frombuffer(region, dtype=np.uint8), + r_width, r_height, + QtGui.QImage.Format.Format_RGBA8888, + ) qimage.setDevicePixelRatio(self.device_pixel_ratio) # set origin using original QT coordinates - origin = QtCore.QPoint(rect.left(), rect.top()) - painter.drawImage(origin, qimage) - # Adjust the buf reference count to work around a memory - # leak bug in QImage under PySide. - if QT_API == "PySide2" and QtCore.__version_info__ < (5, 12): - ctypes.c_long.from_address(id(buf)).value = 1 + painter.drawImage(rect.topLeft(), qimage) self._draw_rect_callback(painter) finally: diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index eaf4bf6f5f9d..77e68785476e 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -75,6 +75,14 @@ static PyObject *PyBufferRegion_get_extents(PyBufferRegion *self, PyObject *args return Py_BuildValue("IIII", rect.x1, rect.y1, rect.x2, rect.y2); } +static PyObject *PyBufferRegion_get_bounds(PyBufferRegion *self, PyObject *args) +{ + agg::rect_i rect = self->x->get_rect(); + + return Py_BuildValue("IIII", rect.x1, rect.y1, rect.x2 - rect.x1, rect.y2 - rect.y1); +} + + int PyBufferRegion_get_buffer(PyBufferRegion *self, Py_buffer *buf, int flags) { Py_INCREF(self); @@ -105,6 +113,7 @@ static PyTypeObject *PyBufferRegion_init_type() { "set_x", (PyCFunction)PyBufferRegion_set_x, METH_VARARGS, NULL }, { "set_y", (PyCFunction)PyBufferRegion_set_y, METH_VARARGS, NULL }, { "get_extents", (PyCFunction)PyBufferRegion_get_extents, METH_NOARGS, NULL }, + { "get_bounds", (PyCFunction)PyBufferRegion_get_bounds, METH_NOARGS, NULL }, { NULL } }; From 3d1ce960afa030c264381f5694f5673550c6f6c4 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 13 Mar 2024 05:02:48 +0100 Subject: [PATCH 2/4] Fix flake --- lib/matplotlib/backends/backend_qtagg.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py index 830f0b29329e..70a99c20ce21 100644 --- a/lib/matplotlib/backends/backend_qtagg.py +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -2,11 +2,9 @@ Render to qt from agg. """ -import ctypes - import numpy as np -from .qt_compat import QT_API, QtCore, QtGui +from .qt_compat import QtGui from .backend_agg import FigureCanvasAgg from .backend_qt import _BackendQT, FigureCanvasQT from .backend_qt import ( # noqa: F401 # pylint: disable=W0611 From 391ac40ae085d94ef2dacd2b9a5cdc4d46327c81 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 13 Mar 2024 09:02:55 +0100 Subject: [PATCH 3/4] Ensure that we maintain the eraseRect behaviour to avoid reverting the fix from https://github.com/matplotlib/matplotlib/pull/13050 --- lib/matplotlib/backends/backend_qtagg.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py index 70a99c20ce21..624bee2d0b66 100644 --- a/lib/matplotlib/backends/backend_qtagg.py +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -33,6 +33,7 @@ def paintEvent(self, event): # See documentation of QRect: bottom() and right() are off # by 1, so use left() + width() and top() + height(). rect = event.rect() + # scale rect dimensions using the screen dpi ratio to get # correct values for the Figure coordinates (rather than # QT5's coords) @@ -58,6 +59,12 @@ def paintEvent(self, event): QtGui.QImage.Format.Format_RGBA8888, ) qimage.setDevicePixelRatio(self.device_pixel_ratio) + + # Clear the rect, ensuring that issues encountered with images + # with transparency compose correctly, as seen in + # https://github.com/matplotlib/matplotlib/pull/13050. + painter.eraseRect(rect) + # set origin using original QT coordinates painter.drawImage(rect.topLeft(), qimage) From 0258fb73bb043c09f49b9da91bb82a25d3693a55 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Thu, 14 Mar 2024 10:14:36 +0100 Subject: [PATCH 4/4] Apply the proposed improvement to backend_qtagg to avoid needing to extend the API of the inaccessible PyBufferRegion object (Agg only) --- lib/matplotlib/backends/backend_qtagg.py | 18 +++++------------- src/_backend_agg_wrapper.cpp | 15 ++++++--------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py index 624bee2d0b66..0fdea169a179 100644 --- a/lib/matplotlib/backends/backend_qtagg.py +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -33,7 +33,6 @@ def paintEvent(self, event): # See documentation of QRect: bottom() and right() are off # by 1, so use left() + width() and top() + height(). rect = event.rect() - # scale rect dimensions using the screen dpi ratio to get # correct values for the Figure coordinates (rather than # QT5's coords) @@ -47,24 +46,17 @@ def paintEvent(self, event): right = left + width # create a buffer using the image bounding box bbox = Bbox([[left, bottom], [right, top]]) - region = self.copy_from_bbox(bbox) + img = np.asarray(self.copy_from_bbox(bbox), dtype=np.uint8) - # Now draw the region we copied with the painter. - r_width, r_height = region.get_bounds()[2:] + # Clear the widget canvas, to avoid issues as seen in + # https://github.com/matplotlib/matplotlib/issues/13012 + painter.eraseRect(rect) - # Draw the region copied with the QPainter qimage = QtGui.QImage( - np.frombuffer(region, dtype=np.uint8), - r_width, r_height, + img, img.shape[1], img.shape[0], QtGui.QImage.Format.Format_RGBA8888, ) qimage.setDevicePixelRatio(self.device_pixel_ratio) - - # Clear the rect, ensuring that issues encountered with images - # with transparency compose correctly, as seen in - # https://github.com/matplotlib/matplotlib/pull/13050. - painter.eraseRect(rect) - # set origin using original QT coordinates painter.drawImage(rect.topLeft(), qimage) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 77e68785476e..1f271a1aeaeb 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -75,14 +75,6 @@ static PyObject *PyBufferRegion_get_extents(PyBufferRegion *self, PyObject *args return Py_BuildValue("IIII", rect.x1, rect.y1, rect.x2, rect.y2); } -static PyObject *PyBufferRegion_get_bounds(PyBufferRegion *self, PyObject *args) -{ - agg::rect_i rect = self->x->get_rect(); - - return Py_BuildValue("IIII", rect.x1, rect.y1, rect.x2 - rect.x1, rect.y2 - rect.y1); -} - - int PyBufferRegion_get_buffer(PyBufferRegion *self, Py_buffer *buf, int flags) { Py_INCREF(self); @@ -113,7 +105,6 @@ static PyTypeObject *PyBufferRegion_init_type() { "set_x", (PyCFunction)PyBufferRegion_set_x, METH_VARARGS, NULL }, { "set_y", (PyCFunction)PyBufferRegion_set_y, METH_VARARGS, NULL }, { "get_extents", (PyCFunction)PyBufferRegion_get_extents, METH_NOARGS, NULL }, - { "get_bounds", (PyCFunction)PyBufferRegion_get_bounds, METH_NOARGS, NULL }, { NULL } }; @@ -481,6 +472,12 @@ static PyObject *PyRendererAgg_clear(PyRendererAgg *self, PyObject *args) static PyObject *PyRendererAgg_copy_from_bbox(PyRendererAgg *self, PyObject *args) { + // Note that whilst the copy_from_bbox call can technically return an image that + // is of a different rect than was requested, this is not used in the underlying + // backend. In the future, this copy_from_bbox will not return a PyBufferRegion, + // and instead simply return an image (the renderer interface may still expose a + // bbox in the response for convenience, but this doesn't need to be a special + // type at the C++ level). agg::rect_d bbox; BufferRegion *reg; PyObject *regobj;