From aa7a8d7f34a60d25b74ce1b211f8702f85a000e7 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 1 Oct 2024 13:28:46 +1000 Subject: [PATCH] Fix clipped render when using geometry generator symbol in layout items Fixes #58909 --- src/core/layout/qgslayoutitempolygon.cpp | 13 +++-- src/core/layout/qgslayoutitempolyline.cpp | 21 +++++--- src/core/layout/qgslayoutitemshape.cpp | 15 ++++-- tests/src/python/test_qgslayoutpolygon.py | 51 +++++++++++++++++- tests/src/python/test_qgslayoutpolyline.py | 49 ++++++++++++++++- tests/src/python/test_qgslayoutshape.py | 51 ++++++++++++++++++ .../expected_polygon_generator.png | Bin 0 -> 4333 bytes .../expected_polyline_generator.png | Bin 0 -> 4640 bytes .../expected_layoutshape_generator.png | Bin 0 -> 4333 bytes 9 files changed, 180 insertions(+), 20 deletions(-) create mode 100644 tests/testdata/control_images/composer_polygon/expected_polygon_generator/expected_polygon_generator.png create mode 100644 tests/testdata/control_images/composer_polyline/expected_polyline_generator/expected_polyline_generator.png create mode 100644 tests/testdata/control_images/layout_shape/expected_layoutshape_generator/expected_layoutshape_generator.png diff --git a/src/core/layout/qgslayoutitempolygon.cpp b/src/core/layout/qgslayoutitempolygon.cpp index c101c7ceb639..e75cec3a1a6b 100644 --- a/src/core/layout/qgslayoutitempolygon.cpp +++ b/src/core/layout/qgslayoutitempolygon.cpp @@ -136,18 +136,23 @@ QgsFillSymbol *QgsLayoutItemPolygon::symbol() void QgsLayoutItemPolygon::_draw( QgsLayoutItemRenderContext &context, const QStyleOptionGraphicsItem * ) { + QgsRenderContext renderContext = context.renderContext(); + // symbol clipping messes with geometry generators used in the symbol for this item, and has no + // valid use here. See https://github.com/qgis/QGIS/issues/58909 + renderContext.setFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent ); + //setup painter scaling to dots so that raster symbology is drawn to scale - const double scale = context.renderContext().convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); + const double scale = renderContext.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); const QTransform t = QTransform::fromScale( scale, scale ); const QVector rings; //empty QPainterPath polygonPath; polygonPath.addPolygon( mPolygon ); - mPolygonStyleSymbol->startRender( context.renderContext() ); + mPolygonStyleSymbol->startRender( renderContext ); mPolygonStyleSymbol->renderPolygon( polygonPath.toFillPolygon( t ), &rings, - nullptr, context.renderContext() ); - mPolygonStyleSymbol->stopRender( context.renderContext() ); + nullptr, renderContext ); + mPolygonStyleSymbol->stopRender( renderContext ); } void QgsLayoutItemPolygon::_readXmlStyle( const QDomElement &elmt, const QgsReadWriteContext &context ) diff --git a/src/core/layout/qgslayoutitempolyline.cpp b/src/core/layout/qgslayoutitempolyline.cpp index 73f0350b873c..8702eaa665ae 100644 --- a/src/core/layout/qgslayoutitempolyline.cpp +++ b/src/core/layout/qgslayoutitempolyline.cpp @@ -280,20 +280,25 @@ QString QgsLayoutItemPolyline::displayName() const void QgsLayoutItemPolyline::_draw( QgsLayoutItemRenderContext &context, const QStyleOptionGraphicsItem * ) { - const QgsScopedQPainterState painterState( context.renderContext().painter() ); + QgsRenderContext renderContext = context.renderContext(); + // symbol clipping messes with geometry generators used in the symbol for this item, and has no + // valid use here. See https://github.com/qgis/QGIS/issues/58909 + renderContext.setFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent ); + + const QgsScopedQPainterState painterState( renderContext.painter() ); //setup painter scaling to dots so that raster symbology is drawn to scale - const double scale = context.renderContext().convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); + const double scale = renderContext.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); const QTransform t = QTransform::fromScale( scale, scale ); - mPolylineStyleSymbol->startRender( context.renderContext() ); - mPolylineStyleSymbol->renderPolyline( t.map( mPolygon ), nullptr, context.renderContext() ); - mPolylineStyleSymbol->stopRender( context.renderContext() ); + mPolylineStyleSymbol->startRender( renderContext ); + mPolylineStyleSymbol->renderPolyline( t.map( mPolygon ), nullptr, renderContext ); + mPolylineStyleSymbol->stopRender( renderContext ); // painter is scaled to dots, so scale back to layout units - context.renderContext().painter()->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() ); + renderContext.painter()->scale( renderContext.scaleFactor(), renderContext.scaleFactor() ); - drawStartMarker( context.renderContext().painter() ); - drawEndMarker( context.renderContext().painter() ); + drawStartMarker( renderContext.painter() ); + drawEndMarker( renderContext.painter() ); } void QgsLayoutItemPolyline::_readXmlStyle( const QDomElement &elmt, const QgsReadWriteContext &context ) diff --git a/src/core/layout/qgslayoutitemshape.cpp b/src/core/layout/qgslayoutitemshape.cpp index 04ac544b156f..90cf87c12547 100644 --- a/src/core/layout/qgslayoutitemshape.cpp +++ b/src/core/layout/qgslayoutitemshape.cpp @@ -196,17 +196,22 @@ bool QgsLayoutItemShape::accept( QgsStyleEntityVisitorInterface *visitor ) const void QgsLayoutItemShape::draw( QgsLayoutItemRenderContext &context ) { - QPainter *painter = context.renderContext().painter(); + QgsRenderContext renderContext = context.renderContext(); + // symbol clipping messes with geometry generators used in the symbol for this item, and has no + // valid use here. See https://github.com/qgis/QGIS/issues/58909 + renderContext.setFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent ); + + QPainter *painter = renderContext.painter(); painter->setPen( Qt::NoPen ); painter->setBrush( Qt::NoBrush ); - const double scale = context.renderContext().convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); + const double scale = renderContext.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); const QVector rings; //empty list - symbol()->startRender( context.renderContext() ); - symbol()->renderPolygon( calculatePolygon( scale ), &rings, nullptr, context.renderContext() ); - symbol()->stopRender( context.renderContext() ); + symbol()->startRender( renderContext ); + symbol()->renderPolygon( calculatePolygon( scale ), &rings, nullptr, renderContext ); + symbol()->stopRender( renderContext ); } QPolygonF QgsLayoutItemShape::calculatePolygon( double scale ) const diff --git a/tests/src/python/test_qgslayoutpolygon.py b/tests/src/python/test_qgslayoutpolygon.py index bff8a02c5a94..115e41868a21 100644 --- a/tests/src/python/test_qgslayoutpolygon.py +++ b/tests/src/python/test_qgslayoutpolygon.py @@ -22,7 +22,11 @@ QgsLayoutItemRenderContext, QgsLayoutUtils, QgsProject, - QgsReadWriteContext + QgsReadWriteContext, + QgsLayoutItemMap, + QgsRectangle, + Qgis, + QgsGeometryGeneratorSymbolLayer ) import unittest from qgis.testing import start_app, QgisTestCase @@ -373,6 +377,51 @@ def testClipPath(self): p.end() self.assertEqual(len(spy), 5) + def test_generator(self): + project = QgsProject() + layout = QgsLayout(project) + layout.initializeDefaults() + + p = QPolygonF() + p.append(QPointF(0.0, 0.0)) + p.append(QPointF(100.0, 10.0)) + p.append(QPointF(200.0, 100.0)) + shape = QgsLayoutItemPolygon(p, layout) + layout.addLayoutItem(shape) + + map = QgsLayoutItemMap(layout) + map.attemptSetSceneRect(QRectF(0, 0, 10, 10)) + map.zoomToExtent(QgsRectangle(1, 1, 2, 2)) + layout.addLayoutItem(map) + + props = {} + props["color"] = "green" + props["style"] = "solid" + props["style_border"] = "solid" + props["color_border"] = "red" + props["width_border"] = "6.0" + props["joinstyle"] = "miter" + + sub_symbol = QgsFillSymbol.createSimple(props) + + line_symbol = QgsFillSymbol() + generator = QgsGeometryGeneratorSymbolLayer.create({ + 'geometryModifier': "geom_from_wkt('POLYGON((10 10,287 10,287 200,10 200,10 10))')", + 'SymbolType': 'Fill', + }) + generator.setUnits(Qgis.RenderUnit.Millimeters) + generator.setSubSymbol(sub_symbol) + + line_symbol.changeSymbolLayer(0, generator) + shape.setSymbol(line_symbol) + + self.assertTrue( + self.render_layout_check( + 'polygon_generator', + layout + ) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgslayoutpolyline.py b/tests/src/python/test_qgslayoutpolyline.py index 8c165306a0cd..85350384e7e4 100644 --- a/tests/src/python/test_qgslayoutpolyline.py +++ b/tests/src/python/test_qgslayoutpolyline.py @@ -9,16 +9,20 @@ __date__ = '14/03/2016' __copyright__ = 'Copyright 2016, The QGIS Project' -from qgis.PyQt.QtCore import QPointF +from qgis.PyQt.QtCore import QPointF, QRectF from qgis.PyQt.QtGui import QPolygonF from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( + Qgis, QgsLayout, QgsLayoutItemPolyline, + QgsLayoutItemMap, QgsLayoutItemRegistry, QgsLineSymbol, QgsProject, - QgsReadWriteContext + QgsReadWriteContext, + QgsGeometryGeneratorSymbolLayer, + QgsRectangle ) import unittest from qgis.testing import start_app, QgisTestCase @@ -384,6 +388,47 @@ def testVerticalLine(self): ) ) + def test_generator(self): + project = QgsProject() + layout = QgsLayout(project) + layout.initializeDefaults() + + p = QPolygonF() + p.append(QPointF(0.0, 0.0)) + p.append(QPointF(100.0, 100.0)) + shape = QgsLayoutItemPolyline(p, layout) + layout.addLayoutItem(shape) + + map = QgsLayoutItemMap(layout) + map.attemptSetSceneRect(QRectF(0, 0, 10, 10)) + map.zoomToExtent(QgsRectangle(1, 1, 2, 2)) + layout.addLayoutItem(map) + + props = {} + props["color"] = "0,0,0,255" + props["width"] = "10.0" + props["capstyle"] = "square" + + sub_symbol = QgsLineSymbol.createSimple(props) + + line_symbol = QgsLineSymbol() + generator = QgsGeometryGeneratorSymbolLayer.create({ + 'geometryModifier': "geom_from_wkt('POLYGON((10 10,287 10,287 200,10 200,10 10))')", + 'SymbolType': 'Line', + }) + generator.setUnits(Qgis.RenderUnit.Millimeters) + generator.setSubSymbol(sub_symbol) + + line_symbol.changeSymbolLayer(0, generator) + shape.setSymbol(line_symbol) + + self.assertTrue( + self.render_layout_check( + 'polyline_generator', + layout + ) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgslayoutshape.py b/tests/src/python/test_qgslayoutshape.py index 7b76e7ef72a8..25a951149dc6 100644 --- a/tests/src/python/test_qgslayoutshape.py +++ b/tests/src/python/test_qgslayoutshape.py @@ -21,6 +21,10 @@ QgsProject, QgsReadWriteContext, QgsUnitTypes, + QgsLayoutItemMap, + Qgis, + QgsGeometryGeneratorSymbolLayer, + QgsRectangle ) import unittest from qgis.testing import start_app, QgisTestCase @@ -37,6 +41,10 @@ def setUpClass(cls): super(TestQgsLayoutShape, cls).setUpClass() cls.item_class = QgsLayoutItemShape + @classmethod + def control_path_prefix(cls): + return "layout_shape" + def testClipPath(self): pr = QgsProject() l = QgsLayout(pr) @@ -101,6 +109,49 @@ def testBoundingRectForStrokeSizeOnRestore(self): # bounding rect for item should include stroke self.assertEqual(shape2.boundingRect(), QRectF(-20.0, -20.0, 140.0, 240.0)) + def test_generator(self): + project = QgsProject() + layout = QgsLayout(project) + layout.initializeDefaults() + + shape = QgsLayoutItemShape(layout) + shape.setShapeType(QgsLayoutItemShape.Shape.Rectangle) + shape.attemptSetSceneRect(QRectF(0, 0, 100, 200)) + layout.addLayoutItem(shape) + + map = QgsLayoutItemMap(layout) + map.attemptSetSceneRect(QRectF(0, 0, 10, 10)) + map.zoomToExtent(QgsRectangle(1, 1, 2, 2)) + layout.addLayoutItem(map) + + props = {} + props["color"] = "green" + props["style"] = "solid" + props["style_border"] = "solid" + props["color_border"] = "red" + props["width_border"] = "6.0" + props["joinstyle"] = "miter" + + sub_symbol = QgsFillSymbol.createSimple(props) + + line_symbol = QgsFillSymbol() + generator = QgsGeometryGeneratorSymbolLayer.create({ + 'geometryModifier': "geom_from_wkt('POLYGON((10 10,287 10,287 200,10 200,10 10))')", + 'SymbolType': 'Fill', + }) + generator.setUnits(Qgis.RenderUnit.Millimeters) + generator.setSubSymbol(sub_symbol) + + line_symbol.changeSymbolLayer(0, generator) + shape.setSymbol(line_symbol) + + self.assertTrue( + self.render_layout_check( + 'layoutshape_generator', + layout + ) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/control_images/composer_polygon/expected_polygon_generator/expected_polygon_generator.png b/tests/testdata/control_images/composer_polygon/expected_polygon_generator/expected_polygon_generator.png new file mode 100644 index 0000000000000000000000000000000000000000..5da6733807d2652914df72a94a39ea0cd264fe0e GIT binary patch literal 4333 zcmeAS@N?(olHy`uVBq!ia0y~yU`b+NV3y)w0*W}bm%jp1oCO|{#S9F5he4R}c>anM z1_puoo-U3d6?5L++L$ZtDBy5$>cJU04OvGfW(DxSYfo5L_l7Nmfsw7}<^jv14~b3} zyWCH|KiYotV5`)%UE5 zdpRk#Z=7lKzC>#JX?OWO`~B+#85j~)-JDrs(et?QlMm1<)-TfgA&clpPA zVyl4x^Wd{>{PFYKw@&|YS~=}fr8Y2J8m?OGVy184Q9n9HO{8=B9}V!)0H;GSJDSW! zli6r8qfr}XG;@t+uF=djnz;s||1?_m(68*dv1ac9-u?HERqp-<>=qw*UcGPK-h;aR zGdzF4zG465JkTNk*!$kmr7J!AanM z1_r@Po-U3d6?5L+Hp~_0CL6d-PJ7CA74G}mt10pbwM*|{o=Gsyfpu-#f-rO<;g@^g-tO&rnkRwYeqb4~5v=Ef&zexMk_x*pO|Y>x+Wq=VFQ*=V_x^qT zpC5(g<=?ln@2~&AF9+!01I&Llm-^4Qn>%-|uBE+xQFZn1f1$vjyH*Tjttq$J#Y}SG z5fREAmy`8Bi0kR;_3Z_EZo`F#1s319__xmX*|lTGjy-#9zKH={<|J=dbHiJ+vV@(K zs3XF55);FyiPXtdqv1Fjj^r1~qp4>!^^B$-(wZ@&d1f@vjOLlqJVV8v(P)t}TBI;M z`f`KY=E}nYL|3i-@WUJ0HdpeDUOsv9QX~jQUbst-2meke#`}Fkm*6Bcd zB95E*K7an4oBzCbY;3HiDzG!mIcwR=SFf@P-{_oISqkiX+c@^;EIFPwDG1ozoHymT z`6aHIOQry0Qax#^Ta^0be^V}*_#XeK2JChIzi#4t{rdIQEa$8sN|d8EPP}#oY>Ld$ zUAM*p1Faf`%RWjWJ?7bD3=+b z+_-wO(#`DT2y!-3^{`&d(`9xtxhKAVc@9*x;HqUqSlbH(| s3h}>Ve>@IIKggU&`N#;i{QpsZ)rosfAB9Ta0c~OMboFyt=akR{06^4)NB{r; literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_shape/expected_layoutshape_generator/expected_layoutshape_generator.png b/tests/testdata/control_images/layout_shape/expected_layoutshape_generator/expected_layoutshape_generator.png new file mode 100644 index 0000000000000000000000000000000000000000..5da6733807d2652914df72a94a39ea0cd264fe0e GIT binary patch literal 4333 zcmeAS@N?(olHy`uVBq!ia0y~yU`b+NV3y)w0*W}bm%jp1oCO|{#S9F5he4R}c>anM z1_puoo-U3d6?5L++L$ZtDBy5$>cJU04OvGfW(DxSYfo5L_l7Nmfsw7}<^jv14~b3} zyWCH|KiYotV5`)%UE5 zdpRk#Z=7lKzC>#JX?OWO`~B+#85j~)-JDrs(et?QlMm1<)-TfgA&clpPA zVyl4x^Wd{>{PFYKw@&|YS~=}fr8Y2J8m?OGVy184Q9n9HO{8=B9}V!)0H;GSJDSW! zli6r8qfr}XG;@t+uF=djnz;s||1?_m(68*dv1ac9-u?HERqp-<>=qw*UcGPK-h;aR zGdzF4zG465JkTNk*!$kmr7J!A