diff --git a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in index 2220441a9b6..24076990311 100644 --- a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in @@ -94,6 +94,10 @@ The caller takes responsibility for deleting the returned object. virtual QString displayName() const; + virtual bool requiresRasterization() const; + + virtual bool containsAdvancedEffects() const; + void adjustBoxSize(); %Docstring diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index 9f692ddaa37..99be03fb50e 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -1366,6 +1366,7 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt friend class QgsLayout; friend class QgsLayoutItemGroup; friend class QgsLayoutItemMap; + friend class QgsLayoutItemLegend; friend class QgsCompositionConverter; }; diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index f972dc6397a..3ed55d97dcb 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -856,6 +856,15 @@ QString QgsLayoutItemLegend::displayName() const } } +bool QgsLayoutItemLegend::requiresRasterization() const +{ + return blendMode() != QPainter::CompositionMode_SourceOver; +} + +bool QgsLayoutItemLegend::containsAdvancedEffects() const +{ + return mEvaluatedOpacity < 1.0; +} void QgsLayoutItemLegend::setupMapConnections( QgsLayoutItemMap *map, bool connectSlots ) { diff --git a/src/core/layout/qgslayoutitemlegend.h b/src/core/layout/qgslayoutitemlegend.h index 279fa65c5df..39d7410f595 100644 --- a/src/core/layout/qgslayoutitemlegend.h +++ b/src/core/layout/qgslayoutitemlegend.h @@ -133,6 +133,8 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem QgsLayoutItem::Flags itemFlags() const override; //Overridden to show legend title QString displayName() const override; + bool requiresRasterization() const override; + bool containsAdvancedEffects() const override; /** * Sets the legend's item bounds to fit the whole legend content. diff --git a/tests/src/python/test_qgslayoutlegend.py b/tests/src/python/test_qgslayoutlegend.py index 5e802c177c3..cd475bc1c93 100644 --- a/tests/src/python/test_qgslayoutlegend.py +++ b/tests/src/python/test_qgslayoutlegend.py @@ -12,8 +12,15 @@ __copyright__ = 'Copyright 2017, The QGIS Project' import os from time import sleep -from qgis.PyQt.QtCore import QDir, QRectF -from qgis.PyQt.QtGui import QColor +from qgis.PyQt.QtCore import ( + Qt, + QRectF +) +from qgis.PyQt.QtGui import ( + QColor, + QImage, + QPainter +) from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( @@ -51,6 +58,8 @@ from qgis.core import ( QgsReadWriteContext, QgsTextFormat, QgsFeatureRequest, + QgsLayoutItemShape, + QgsSimpleFillSymbolLayer ) import unittest from qgis.testing import start_app, QgisTestCase @@ -73,6 +82,213 @@ class TestQgsLayoutItemLegend(QgisTestCase, LayoutItemTestCase): def control_path_prefix(cls): return "composer_legend" + def test_opacity(self): + """ + Test rendering the legend with opacity + """ + layout = QgsLayout(QgsProject.instance()) + layout.initializeDefaults() + + legend = QgsLayoutItemLegend(layout) + legend.setTitle("Legend") + legend.attemptSetSceneRect(QRectF(120, 20, 80, 80)) + legend.setFrameEnabled(True) + legend.setFrameStrokeWidth(QgsLayoutMeasurement(2)) + legend.setBackgroundColor(QColor(200, 200, 200)) + layout.addLayoutItem(legend) + + text_format = QgsTextFormat() + text_format.setFont(QgsFontUtils.getStandardTestFont("Bold")) + text_format.setSize(16) + + for legend_item in [QgsLegendStyle.Title, QgsLegendStyle.Group, QgsLegendStyle.Subgroup, + QgsLegendStyle.Symbol, QgsLegendStyle.SymbolLabel]: + style = legend.style(legend_item) + style.setTextFormat(text_format) + legend.setStyle(legend_item, style) + + legend.setItemOpacity(0.3) + + self.assertFalse( + legend.requiresRasterization() + ) + self.assertTrue( + legend.containsAdvancedEffects() + ) + + self.assertTrue( + self.render_layout_check('composerlegend_opacity', layout) + ) + + def test_opacity_rendering_designer_preview(self): + """ + Test rendering of legend opacity while in designer dialogs + """ + p = QgsProject() + l = QgsLayout(p) + self.assertTrue(l.renderContext().isPreviewRender()) + + l.initializeDefaults() + legend = QgsLayoutItemLegend(l) + legend.setTitle("Legend") + legend.attemptSetSceneRect(QRectF(120, 20, 80, 80)) + legend.setFrameEnabled(True) + legend.setFrameStrokeWidth(QgsLayoutMeasurement(2)) + legend.setBackgroundColor(QColor(200, 200, 200)) + l.addLayoutItem(legend) + + text_format = QgsTextFormat() + text_format.setFont(QgsFontUtils.getStandardTestFont("Bold")) + text_format.setSize(16) + + for legend_item in [QgsLegendStyle.Title, QgsLegendStyle.Group, QgsLegendStyle.Subgroup, + QgsLegendStyle.Symbol, QgsLegendStyle.SymbolLabel]: + style = legend.style(legend_item) + style.setTextFormat(text_format) + legend.setStyle(legend_item, style) + + legend.setItemOpacity(0.3) + + page_item = l.pageCollection().page(0) + paper_rect = QRectF(page_item.pos().x(), + page_item.pos().y(), + page_item.rect().width(), + page_item.rect().height()) + + im = QImage(1122, 794, QImage.Format_ARGB32) + im.fill(Qt.transparent) + im.setDotsPerMeterX(int(300 / 25.4 * 1000)) + im.setDotsPerMeterY(int(300 / 25.4 * 1000)) + painter = QPainter(im) + painter.setRenderHint(QPainter.Antialiasing, True) + + l.render(painter, QRectF(0, 0, painter.device().width(), painter.device().height()), paper_rect) + painter.end() + + self.assertTrue(self.image_check('composerlegend_opacity', + 'composerlegend_opacity', + im, allowed_mismatch=0)) + + def test_blend_mode(self): + """ + Test rendering the legend with a blend mode + """ + layout = QgsLayout(QgsProject.instance()) + layout.initializeDefaults() + + item1 = QgsLayoutItemShape(layout) + item1.attemptSetSceneRect(QRectF(20, 20, 150, 100)) + item1.setShapeType(QgsLayoutItemShape.Rectangle) + simple_fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, simple_fill) + simple_fill.setColor(QColor(0, 100, 50)) + simple_fill.setStrokeColor(Qt.black) + item1.setSymbol(fill_symbol) + layout.addLayoutItem(item1) + + legend = QgsLayoutItemLegend(layout) + legend.setTitle("Legend") + legend.attemptSetSceneRect(QRectF(120, 20, 80, 80)) + legend.setFrameEnabled(True) + legend.setFrameStrokeWidth(QgsLayoutMeasurement(2)) + legend.setBackgroundColor(QColor(200, 200, 200)) + layout.addLayoutItem(legend) + + text_format = QgsTextFormat() + text_format.setFont(QgsFontUtils.getStandardTestFont("Bold")) + text_format.setSize(16) + + for legend_item in [ + QgsLegendStyle.Title, + QgsLegendStyle.Group, + QgsLegendStyle.Subgroup, + QgsLegendStyle.Symbol, + QgsLegendStyle.SymbolLabel, + ]: + style = legend.style(legend_item) + style.setTextFormat(text_format) + legend.setStyle(legend_item, style) + + legend.setBlendMode(QPainter.CompositionMode_Darken) + + self.assertTrue(legend.requiresRasterization()) + + self.assertTrue(self.render_layout_check("composerlegend_blendmode", layout)) + + def test_blend_mode_designer_preview(self): + """ + Test rendering the legend with a blend mode + """ + layout = QgsLayout(QgsProject.instance()) + layout.initializeDefaults() + self.assertTrue(layout.renderContext().isPreviewRender()) + + item1 = QgsLayoutItemShape(layout) + item1.attemptSetSceneRect(QRectF(20, 20, 150, 100)) + item1.setShapeType(QgsLayoutItemShape.Rectangle) + simple_fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, simple_fill) + simple_fill.setColor(QColor(0, 100, 50)) + simple_fill.setStrokeColor(Qt.black) + item1.setSymbol(fill_symbol) + layout.addLayoutItem(item1) + + legend = QgsLayoutItemLegend(layout) + legend.setTitle("Legend") + legend.attemptSetSceneRect(QRectF(120, 20, 80, 80)) + legend.setFrameEnabled(True) + legend.setFrameStrokeWidth(QgsLayoutMeasurement(2)) + legend.setBackgroundColor(QColor(200, 200, 200)) + layout.addLayoutItem(legend) + + text_format = QgsTextFormat() + text_format.setFont(QgsFontUtils.getStandardTestFont("Bold")) + text_format.setSize(16) + + for legend_item in [ + QgsLegendStyle.Title, + QgsLegendStyle.Group, + QgsLegendStyle.Subgroup, + QgsLegendStyle.Symbol, + QgsLegendStyle.SymbolLabel, + ]: + style = legend.style(legend_item) + style.setTextFormat(text_format) + legend.setStyle(legend_item, style) + + legend.setBlendMode(QPainter.CompositionMode_Darken) + + page_item = layout.pageCollection().page(0) + paper_rect = QRectF( + page_item.pos().x(), + page_item.pos().y(), + page_item.rect().width(), + page_item.rect().height(), + ) + + im = QImage(1122, 794, QImage.Format_ARGB32) + im.fill(Qt.transparent) + im.setDotsPerMeterX(int(300 / 25.4 * 1000)) + im.setDotsPerMeterY(int(300 / 25.4 * 1000)) + painter = QPainter(im) + painter.setRenderHint(QPainter.Antialiasing, True) + + layout.render( + painter, + QRectF(0, 0, painter.device().width(), painter.device().height()), + paper_rect, + ) + painter.end() + + self.assertTrue( + self.image_check( + "composerlegend_blendmode", "composerlegend_blendmode", im, allowed_mismatch=0 + ) + ) + + def testInitialSizeSymbolMapUnits(self): """ Test initial size of legend with a symbol size in map units diff --git a/tests/testdata/control_images/composer_legend/expected_composerlegend_blendmode/expected_composerlegend_blendmode.png b/tests/testdata/control_images/composer_legend/expected_composerlegend_blendmode/expected_composerlegend_blendmode.png new file mode 100644 index 00000000000..5f92f60b695 Binary files /dev/null and b/tests/testdata/control_images/composer_legend/expected_composerlegend_blendmode/expected_composerlegend_blendmode.png differ diff --git a/tests/testdata/control_images/composer_legend/expected_composerlegend_blendmode/expected_composerlegend_blendmode_mask.png b/tests/testdata/control_images/composer_legend/expected_composerlegend_blendmode/expected_composerlegend_blendmode_mask.png new file mode 100644 index 00000000000..b6c9b883abd Binary files /dev/null and b/tests/testdata/control_images/composer_legend/expected_composerlegend_blendmode/expected_composerlegend_blendmode_mask.png differ diff --git a/tests/testdata/control_images/composer_legend/expected_composerlegend_opacity/expected_composerlegend_opacity.png b/tests/testdata/control_images/composer_legend/expected_composerlegend_opacity/expected_composerlegend_opacity.png new file mode 100644 index 00000000000..a3dc6c5cbe0 Binary files /dev/null and b/tests/testdata/control_images/composer_legend/expected_composerlegend_opacity/expected_composerlegend_opacity.png differ diff --git a/tests/testdata/control_images/composer_legend/expected_composerlegend_opacity/expected_composerlegend_opacity_mask.png b/tests/testdata/control_images/composer_legend/expected_composerlegend_opacity/expected_composerlegend_opacity_mask.png new file mode 100644 index 00000000000..5cd6a27e34f Binary files /dev/null and b/tests/testdata/control_images/composer_legend/expected_composerlegend_opacity/expected_composerlegend_opacity_mask.png differ