Skip to content

Commit 3902a40

Browse files
committed
fix(graphic): render toPixmap/toImage into a QRectF
QwtGraphic.toPixmap(size), QwtGraphic.toImage() and QwtGraphic.toImage(size) raised a TypeError instead of rendering the graphic, and could abort the process because the painter was still active when the pixmap or image was destroyed. The Qwt C++ code passes a QRect to render(), which is implicitly converted to a QRectF. The Python port passed the QRect as is, which render() rejects, and QRect(0, 0, width, height) also fails when the size is a QSizeF. toImage(size) did not end its painter either. - Build a QRectF, like toPixmap() already does, and end the painter in toImage(size). - Regression tests: default size, given size and kept aspect ratio, for both toPixmap and toImage. - Changelog entry. Refs #115 Assisted-by: Claude Opus 5.5
1 parent 36fffb6 commit 3902a40

3 files changed

Lines changed: 111 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Bug fixes
1010

1111
- Fixed [Issue #113](https://github.com/PlotPyStack/PythonQwt/issues/113): after replacing the scale draw of an axis (`QwtPlot.setAxisScaleDraw`), the axis ticks could be misaligned with the grid lines and plot items, until the next scale division change (e.g. after panning or zooming): a replot did not fix it. The new scale draw was laid out before getting the position and length of the previous one, so that the axis reserved space for its end labels that the canvas did not take into account. The new scale draw now takes the geometry of the previous one, and the axis stays aligned with the canvas
12+
- Fixed `QwtGraphic.toPixmap(size)`, `QwtGraphic.toImage()` and `QwtGraphic.toImage(size)`, which raised a `TypeError` (possibly aborting the process, as the painter was still active on the destroyed pixmap or image) instead of rendering the graphic: they now render it like `QwtGraphic.toPixmap()` ([Issue #115](https://github.com/PlotPyStack/PythonQwt/issues/115))
1213

1314
## Version 0.16.3
1415

‎qwt/graphic.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import math
1717

18-
from qtpy.QtCore import QObject, QPointF, QRect, QRectF, QSize, QSizeF, Qt
18+
from qtpy.QtCore import QObject, QPointF, QRectF, QSize, QSizeF, Qt
1919
from qtpy.QtGui import (
2020
QImage,
2121
QPaintEngine,
@@ -603,7 +603,7 @@ def toPixmap(self, *args):
603603
aspectRatioMode = args[-1]
604604
pixmap = QPixmap(size)
605605
pixmap.fill(Qt.transparent)
606-
r = QRect(0, 0, size.width(), size.height())
606+
r = QRectF(0.0, 0.0, size.width(), size.height())
607607
painter = QPainter(pixmap)
608608
self.render(painter, r, aspectRatioMode)
609609
painter.end()
@@ -652,7 +652,7 @@ def toImage(self, *args):
652652
h = math.ceil(sz.height())
653653
image = QImage(w, h, QImage.Format_ARGB32)
654654
image.fill(0)
655-
r = QRect(0, 0, sz.width(), sz.height())
655+
r = QRectF(0.0, 0.0, sz.width(), sz.height())
656656
painter = QPainter(image)
657657
self.render(painter, r, Qt.KeepAspectRatio)
658658
painter.end()
@@ -664,9 +664,10 @@ def toImage(self, *args):
664664
aspectRatioMode = args[-1]
665665
image = QImage(size, QImage.Format_ARGB32_Premultiplied)
666666
image.fill(0)
667-
r = QRect(0, 0, size.width(), size.height())
667+
r = QRectF(0.0, 0.0, size.width(), size.height())
668668
painter = QPainter(image)
669669
self.render(painter, r, aspectRatioMode)
670+
painter.end()
670671
return image
671672

672673
def drawPath(self, path):
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Licensed under the terms of the Qwt License
4+
# (see LICENSE file for more details)
5+
6+
"""
7+
Tests for issue 115: porting bugs making several rarely used APIs fail.
8+
9+
Each test covers a difference between the Python port and the Qwt C++ code,
10+
which made the API raise an exception or ignore some of its arguments.
11+
"""
12+
13+
import pytest
14+
from qtpy import QtCore as QC
15+
from qtpy import QtGui as QG
16+
from qtpy import QtWidgets as QW
17+
18+
from qwt.graphic import QwtGraphic
19+
20+
RED = QG.QColor(QC.Qt.red)
21+
22+
23+
def _ensure_app():
24+
# A live QApplication must exist before constructing any QWidget or
25+
# QPixmap, otherwise Qt aborts the process. Tests run in a shared
26+
# interpreter, but no test keeps a persistent Python reference to the
27+
# application, so the singleton may be garbage-collected between tests.
28+
return QW.QApplication.instance() or QW.QApplication([])
29+
30+
31+
# ---------------------------------------------------------------------------
32+
# QwtGraphic.toPixmap / QwtGraphic.toImage
33+
# ---------------------------------------------------------------------------
34+
35+
36+
def _make_graphic():
37+
"""Return a graphic made of a red rectangle of 20 x 10 points"""
38+
graphic = QwtGraphic()
39+
path = QG.QPainterPath()
40+
path.addRect(QC.QRectF(0.0, 0.0, 20.0, 10.0))
41+
painter = QG.QPainter(graphic)
42+
painter.setPen(QC.Qt.NoPen)
43+
painter.setBrush(RED)
44+
painter.drawPath(path)
45+
painter.end()
46+
return graphic
47+
48+
49+
def _to_qimage(result):
50+
"""Return result as a QImage (result: QPixmap or QImage)"""
51+
if isinstance(result, QG.QPixmap):
52+
return result.toImage()
53+
return result
54+
55+
56+
def _is_red(image, x, y):
57+
color = image.pixelColor(x, y)
58+
return color.alpha() == 255 and color.rgb() == RED.rgb()
59+
60+
61+
@pytest.mark.parametrize("method", ("toPixmap", "toImage"))
62+
def test_graphic_conversion_default_size(method):
63+
"""toPixmap() and toImage() render the graphic in its default size"""
64+
app = _ensure_app() # keep a reference alive for the duration of the test
65+
result = getattr(_make_graphic(), method)()
66+
assert not result.isNull()
67+
assert result.size() == QC.QSize(20, 10)
68+
assert _is_red(_to_qimage(result), 10, 5)
69+
del app
70+
71+
72+
@pytest.mark.parametrize("method", ("toPixmap", "toImage"))
73+
def test_graphic_conversion_given_size(method):
74+
"""toPixmap(size) and toImage(size) scale the graphic to the given size"""
75+
app = _ensure_app() # keep a reference alive for the duration of the test
76+
result = getattr(_make_graphic(), method)(QC.QSize(40, 20))
77+
assert not result.isNull()
78+
assert result.size() == QC.QSize(40, 20)
79+
image = _to_qimage(result)
80+
assert _is_red(image, 20, 10)
81+
assert _is_red(image, 2, 2)
82+
assert _is_red(image, 37, 17)
83+
del app
84+
85+
86+
@pytest.mark.parametrize("method", ("toPixmap", "toImage"))
87+
def test_graphic_conversion_keep_aspect_ratio(method):
88+
"""toPixmap(size, mode) and toImage(size, mode) honor the aspect ratio"""
89+
app = _ensure_app() # keep a reference alive for the duration of the test
90+
result = getattr(_make_graphic(), method)(QC.QSize(40, 40), QC.Qt.KeepAspectRatio)
91+
assert result.size() == QC.QSize(40, 40)
92+
image = _to_qimage(result)
93+
# The 2:1 graphic is scaled to 40 x 20 and centered vertically
94+
assert _is_red(image, 20, 20)
95+
assert image.pixelColor(20, 2).alpha() == 0
96+
assert image.pixelColor(20, 37).alpha() == 0
97+
del app
98+
99+
100+
if __name__ == "__main__":
101+
for name in ("toPixmap", "toImage"):
102+
test_graphic_conversion_default_size(name)
103+
test_graphic_conversion_given_size(name)
104+
test_graphic_conversion_keep_aspect_ratio(name)
105+
print("OK")

0 commit comments

Comments
 (0)