Skip to content

Commit 13e0a0f

Browse files
committed
fix(graphic): record clipped paint operations
Painting into a QwtGraphic with clipping enabled raised "TypeError: unsupported operand type(s) for &=: 'QRectF' and 'QRect'" on each paint operation. As the exception is raised in a Qt virtual method (paint engine drawPath, drawPixmap and drawImage), PyQt aborts the process with its default exception hook. QwtGraphic.updateBoundingRect intersects the bounding rectangle with the bounding rectangle of the clip region, which is a QRect. The Qwt C++ code converts it implicitly to a QRectF; the Python port did not. - Convert the clip region bounding rectangle to a QRectF before mapping and intersecting it. - Regression test: clipped path, pixmap and image recorded with a translated painter; control point rectangle not clipped, bounding rectangle clipped. Exceptions raised in Qt virtual methods are collected so that the test fails instead of aborting the process. - Changelog entry. Found while writing the tests for the painter command copy. Refs #115 Assisted-by: Claude Opus 5.5
1 parent e8a2570 commit 13e0a0f

3 files changed

Lines changed: 55 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Fixed `QwtPlotGrid.make`, which raised a `TypeError` when only some of the minor pen arguments (`mincolor`, `minwidth`, `minstyle`) were given: each of them is now honored, the missing ones taking their documented default value ([Issue #115](https://github.com/PlotPyStack/PythonQwt/issues/115))
1515
- Fixed `QwtPowerTransform`, which raised a `ValueError` when drawing a curve on an axis with a power scale (curve data are mapped as NumPy arrays), and an `AttributeError` with NumPy < 2.0 (`numpy.pow` does not exist there): it now transforms scalars (with unchanged results) as well as NumPy arrays, with any supported NumPy version ([Issue #115](https://github.com/PlotPyStack/PythonQwt/issues/115))
1616
- Fixed `QwtPlotDirectPainter.setAttribute`, which did not set or clear attributes as requested: enabling `AtomicPainter` twice disabled it, and `FullRepaint` or `CopyBackingStore` could not be disabled anymore once enabled ([Issue #115](https://github.com/PlotPyStack/PythonQwt/issues/115))
17+
- Fixed painting into a `QwtGraphic` with clipping enabled, which raised a `TypeError` on each paint operation (aborting the process with PyQt's default exception hook): clipped paint operations are now recorded, and the bounding rectangle of the graphic is clipped as in Qwt ([Issue #115](https://github.com/PlotPyStack/PythonQwt/issues/115))
1718

1819
## Version 0.16.3
1920

‎qwt/graphic.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ def updateBoundingRect(self, rect):
757757
br = QRectF(rect)
758758
painter = self.paintEngine().painter()
759759
if painter and painter.hasClipping():
760-
cr = painter.clipRegion().boundingRect()
760+
cr = QRectF(painter.clipRegion().boundingRect())
761761
cr = painter.transform().mapRect(cr)
762762
br &= cr
763763
if self.__data.boundingRect.width() < 0:

‎qwt/tests/test_issue115_porting_bugs.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
which made the API raise an exception or ignore some of its arguments.
1111
"""
1212

13+
import contextlib
1314
import math
15+
import sys
16+
import traceback
1417

1518
import numpy as np
1619
import pytest
@@ -46,6 +49,26 @@ def _ensure_app():
4649
return QW.QApplication.instance() or QW.QApplication([])
4750

4851

52+
@contextlib.contextmanager
53+
def _qt_exceptions():
54+
"""Collect the exceptions raised in Qt virtual method overrides.
55+
56+
PyQt aborts the process on such exceptions when the default exception
57+
hook is installed: collecting them makes the test fail instead.
58+
"""
59+
errors = []
60+
old_hook = sys.excepthook
61+
sys.excepthook = lambda *exc_info: errors.append(exc_info)
62+
try:
63+
yield errors
64+
finally:
65+
sys.excepthook = old_hook
66+
67+
68+
def _format_exceptions(errors):
69+
return "".join("".join(traceback.format_exception(*exc)) for exc in errors)
70+
71+
4972
# ---------------------------------------------------------------------------
5073
# QwtGraphic.toPixmap / QwtGraphic.toImage
5174
# ---------------------------------------------------------------------------
@@ -115,6 +138,35 @@ def test_graphic_conversion_keep_aspect_ratio(method):
115138
del app
116139

117140

141+
def test_graphic_clipped_recording():
142+
"""Clipped paint operations are recorded, with a clipped bounding rect"""
143+
app = _ensure_app() # keep a reference alive for the duration of the test
144+
pixmap = QG.QPixmap(4, 4)
145+
pixmap.fill(QC.Qt.blue)
146+
image = QG.QImage(4, 4, QG.QImage.Format_ARGB32)
147+
image.fill(QC.Qt.green)
148+
path = QG.QPainterPath()
149+
path.addRect(QC.QRectF(0.0, 0.0, 20.0, 20.0))
150+
subrect = QC.QRectF(0.0, 0.0, 4.0, 4.0)
151+
graphic = QwtGraphic()
152+
with _qt_exceptions() as errors:
153+
painter = QG.QPainter(graphic)
154+
painter.translate(5.0, 5.0)
155+
painter.setClipRect(QC.QRectF(0.0, 0.0, 10.0, 10.0))
156+
painter.setPen(QC.Qt.NoPen)
157+
painter.setBrush(RED)
158+
painter.drawPath(path)
159+
painter.drawPixmap(QC.QRectF(0.0, 0.0, 4.0, 4.0), pixmap, subrect)
160+
painter.drawImage(QC.QRectF(8.0, 8.0, 4.0, 4.0), image, subrect)
161+
painter.end()
162+
assert not errors, _format_exceptions(errors)
163+
assert len(graphic.commands()) > 3
164+
# Control points are not clipped, the bounding rectangle is
165+
assert graphic.controlPointRect() == QC.QRectF(5.0, 5.0, 20.0, 20.0)
166+
assert graphic.boundingRect() == QC.QRectF(5.0, 5.0, 10.0, 10.0)
167+
del app
168+
169+
118170
# ---------------------------------------------------------------------------
119171
# QwtPlotMarker.legendIcon
120172
# ---------------------------------------------------------------------------
@@ -373,6 +425,7 @@ def test_direct_painter_atomic_painter_reset():
373425
test_graphic_conversion_default_size(name)
374426
test_graphic_conversion_given_size(name)
375427
test_graphic_conversion_keep_aspect_ratio(name)
428+
test_graphic_clipped_recording()
376429
for size in (QC.QSize(8, 8), QC.QSizeF(8.0, 8.0)):
377430
test_marker_legend_icon_with_symbol(size)
378431
test_marker_with_symbol_in_legend()

0 commit comments

Comments
 (0)