Skip to content

Commit

Permalink
Fix layout maps force rasterisation of whole layout when map item
Browse files Browse the repository at this point in the history
has transparency set

This isn't required -- we can safely just rasterise the map alone.

Expand unit tests of layout map opacity.
  • Loading branch information
nyalldawson committed Oct 10, 2023
1 parent b2ba30b commit d561a56
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/core/layout/qgslayoutitem.h
Expand Up @@ -1365,6 +1365,7 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt
friend class TestQgsLayoutView;
friend class QgsLayout;
friend class QgsLayoutItemGroup;
friend class QgsLayoutItemMap;
friend class QgsCompositionConverter;
};

Expand Down
58 changes: 55 additions & 3 deletions src/core/layout/qgslayoutitemmap.cpp
Expand Up @@ -498,16 +498,61 @@ bool QgsLayoutItemMap::containsWmsLayer() const

bool QgsLayoutItemMap::requiresRasterization() const
{
if ( QgsLayoutItem::requiresRasterization() )
if ( blendMode() != QPainter::CompositionMode_SourceOver )
return true;

// we MUST force the whole layout to render as a raster if any map item
// uses blend modes, and we are not drawing on a solid opaque background
// because in this case the map item needs to be rendered as a raster, but
// it also needs to interact with items below it
if ( !containsAdvancedEffects() )

// WARNING -- modifying this logic? Then ALSO update containsAdvancedEffects accordingly!

// BIG WARNING -- we CANNOT just check containsAdvancedEffects here, as that method MUST
// return true if the map item has transparency.
// BUT, we **DO NOT HAVE TO** force the WHOLE layout to be rasterized if a map item
// is semi-opaque, as we have logic in QgsLayoutItemMap::paint to automatically render the
// map to a temporary image surface. I.e, we can get away with just rasterising the map
// alone and leaving the rest of the content as vector.

// SO this logic is a COPY of containsAdvancedEffects, without the opacity check

auto containsAdvancedEffectsIgnoreItemOpacity = [ = ]()-> bool
{
if ( QgsLayoutItem::containsAdvancedEffects() )
return true;

//check easy things first

// WARNING -- modifying this logic? Then ALSO update containsAdvancedEffects accordingly!

//overviews
if ( mOverviewStack->containsAdvancedEffects() )
{
return true;
}

// WARNING -- modifying this logic? Then ALSO update containsAdvancedEffects accordingly!

//grids
if ( mGridStack->containsAdvancedEffects() )
{
return true;
}

// WARNING -- modifying this logic? Then ALSO update containsAdvancedEffects accordingly!

QgsMapSettings ms;
ms.setLayers( layersToRender() );
return ( !QgsMapSettingsUtils::containsAdvancedEffects( ms ).isEmpty() );
// WARNING -- modifying this logic? Then ALSO update requiresRasterization accordingly!
};

if ( !containsAdvancedEffectsIgnoreItemOpacity() )
return false;

// WARNING -- modifying this logic? Then ALSO update containsAdvancedEffects accordingly!

if ( hasBackground() && qgsDoubleNear( backgroundColor().alphaF(), 1.0 ) )
return false;

Expand All @@ -516,26 +561,33 @@ bool QgsLayoutItemMap::requiresRasterization() const

bool QgsLayoutItemMap::containsAdvancedEffects() const
{
if ( QgsLayoutItem::containsAdvancedEffects() )
if ( QgsLayoutItem::containsAdvancedEffects() || mEvaluatedOpacity < 1.0 )
return true;

//check easy things first

// WARNING -- modifying this logic? Then ALSO update requiresRasterization accordingly!

//overviews
if ( mOverviewStack->containsAdvancedEffects() )
{
return true;
}

// WARNING -- modifying this logic? Then ALSO update requiresRasterization accordingly!

//grids
if ( mGridStack->containsAdvancedEffects() )
{
return true;
}

// WARNING -- modifying this logic? Then ALSO update requiresRasterization accordingly!

QgsMapSettings ms;
ms.setLayers( layersToRender() );
return ( !QgsMapSettingsUtils::containsAdvancedEffects( ms ).isEmpty() );
// WARNING -- modifying this logic? Then ALSO update requiresRasterization accordingly!
}

void QgsLayoutItemMap::setMapRotation( double rotation )
Expand Down
88 changes: 87 additions & 1 deletion tests/src/python/test_qgslayoutmap.py
Expand Up @@ -13,11 +13,16 @@

import qgis # NOQA
from qgis.PyQt.QtCore import (
Qt,
QFileInfo,
QRectF,
QSizeF,
)
from qgis.PyQt.QtGui import QColor, QPainter
from qgis.PyQt.QtGui import (
QColor,
QPainter,
QImage
)
from qgis.PyQt.QtTest import QSignalSpy
from qgis.PyQt.QtXml import QDomDocument
from qgis.core import (
Expand Down Expand Up @@ -102,6 +107,87 @@ def __init__(self, methodName):
self.map.setLayers([self.raster_layer])
self.layout.addLayoutItem(self.map)

def test_opacity(self):
"""
Test rendering the map with opacity
"""
map_settings = QgsMapSettings()
map_settings.setLayers([self.vector_layer])
layout = QgsLayout(QgsProject.instance())
layout.initializeDefaults()

map = QgsLayoutItemMap(layout)
map.attemptSetSceneRect(QRectF(20, 20, 200, 100))
map.setFrameEnabled(True)
map.zoomToExtent(self.vector_layer.extent())
map.setLayers([self.vector_layer])
layout.addLayoutItem(map)

map.setItemOpacity(0.3)

self.assertFalse(
map.requiresRasterization()
)
self.assertTrue(
map.containsAdvancedEffects()
)

self.assertTrue(
self.render_layout_check('composermap_opacity', layout)
)

def test_opacity_rendering_designer_preview(self):
"""
Test rendering of map opacity while in designer dialogs
"""
p = QgsProject()
l = QgsLayout(p)
self.assertTrue(l.renderContext().isPreviewRender())

l.initializeDefaults()

map = QgsLayoutItemMap(l)
map.attemptSetSceneRect(QRectF(20, 20, 200, 100))
map.setFrameEnabled(True)
map.zoomToExtent(self.vector_layer.extent())
map.setLayers([self.vector_layer])
l.addLayoutItem(map)

map.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)

spy = QSignalSpy(map.previewRefreshed)

l.render(painter, QRectF(0, 0, painter.device().width(), painter.device().height()), paper_rect)
painter.end()

# we have to wait for the preview image to refresh, then redraw
# the map to get the actual content which was generated in the
# background thread
spy.wait()

im.fill(Qt.transparent)
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('composermap_opacity',
'composermap_opacity',
im, allowed_mismatch=0))

def testMapCrs(self):
# create layout with layout map
map_settings = QgsMapSettings()
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d561a56

Please sign in to comment.