Skip to content

Commit

Permalink
Add hidden QSettings switch to use a QgsGeometry/GEOS backend for lab…
Browse files Browse the repository at this point in the history
…el masking

If the 'map/maskBackend' QSettings key is set to 'geometry', then
QgsGeometryPaintEngine will be used for calculating clip paths
for label masking instead of QPainterPath

The intention here is that we gain improved flexibility to optimise
the creation and logic behind clipping path generation, vs the
limited API we get from QPainterPath.
  • Loading branch information
nyalldawson committed Jun 3, 2024
1 parent 3bf9aaa commit 65fc7b0
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 23 deletions.
22 changes: 22 additions & 0 deletions python/PyQt6/core/auto_generated/qgsrendercontext.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -955,14 +955,36 @@ rendering using QBrush objects.
%Docstring
Add a clip ``path`` to be applied to the ``symbolLayer`` before rendering

.. seealso:: :py:func:`addSymbolLayerClipGeometry`

.. versionadded:: 3.26
%End

QList<QPainterPath> symbolLayerClipPaths( const QString &symbolLayerId ) const;
%Docstring
Returns clip paths to be applied to the ``symbolLayer`` before rendering

.. seealso:: :py:func:`symbolLayerClipGeometries`

.. versionadded:: 3.26
%End

void addSymbolLayerClipGeometry( const QString &symbolLayerId, const QgsGeometry &geometry );
%Docstring
Add a clip ``geometry`` to be applied to the ``symbolLayer`` before rendering.

.. seealso:: :py:func:`symbolLayerClipGeometries`

.. versionadded:: 3.38
%End

QVector<QgsGeometry> symbolLayerClipGeometries( const QString &symbolLayerId ) const;
%Docstring
Returns clipping geometries to be applied to the ``symbolLayer`` before rendering

.. seealso:: :py:func:`addSymbolLayerClipGeometry`

.. versionadded:: 3.38
%End

QgsDoubleRange zRange() const;
Expand Down
22 changes: 22 additions & 0 deletions python/core/auto_generated/qgsrendercontext.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -955,14 +955,36 @@ rendering using QBrush objects.
%Docstring
Add a clip ``path`` to be applied to the ``symbolLayer`` before rendering

.. seealso:: :py:func:`addSymbolLayerClipGeometry`

.. versionadded:: 3.26
%End

QList<QPainterPath> symbolLayerClipPaths( const QString &symbolLayerId ) const;
%Docstring
Returns clip paths to be applied to the ``symbolLayer`` before rendering

.. seealso:: :py:func:`symbolLayerClipGeometries`

.. versionadded:: 3.26
%End

void addSymbolLayerClipGeometry( const QString &symbolLayerId, const QgsGeometry &geometry );
%Docstring
Add a clip ``geometry`` to be applied to the ``symbolLayer`` before rendering.

.. seealso:: :py:func:`symbolLayerClipGeometries`

.. versionadded:: 3.38
%End

QVector<QgsGeometry> symbolLayerClipGeometries( const QString &symbolLayerId ) const;
%Docstring
Returns clipping geometries to be applied to the ``symbolLayer`` before rendering

.. seealso:: :py:func:`addSymbolLayerClipGeometry`

.. versionadded:: 3.38
%End

QgsDoubleRange zRange() const;
Expand Down
27 changes: 22 additions & 5 deletions src/core/maprenderer/qgsmaprendererjob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "qgsvectorlayerrenderer.h"
#include "qgsrendereditemresults.h"
#include "qgsmaskpaintdevice.h"
#include "qgsgeometrypaintdevice.h"
#include "qgsrasterrenderer.h"
#include "qgselevationmap.h"
#include "qgssettingsentryimpl.h"
Expand All @@ -51,6 +52,7 @@
#include "qgsmeshlayerlabeling.h"

const QgsSettingsEntryBool *QgsMapRendererJob::settingsLogCanvasRefreshEvent = new QgsSettingsEntryBool( QStringLiteral( "logCanvasRefreshEvent" ), QgsSettingsTree::sTreeMap, false );
const QgsSettingsEntryString *QgsMapRendererJob::settingsMaskBackend = new QgsSettingsEntryString( QStringLiteral( "maskBackend" ), QgsSettingsTree::sTreeMap, QString(), QStringLiteral( "Backend engine to use for selective masking" ) );

///@cond PRIVATE

Expand Down Expand Up @@ -683,6 +685,8 @@ std::vector<LayerRenderJob> QgsMapRendererJob::prepareJobs( QPainter *painter, Q

std::vector< LayerRenderJob > QgsMapRendererJob::prepareSecondPassJobs( std::vector< LayerRenderJob > &firstPassJobs, LabelRenderJob &labelJob )
{
const bool useGeometryBackend = settingsMaskBackend->value().compare( QLatin1String( "geometry" ), Qt::CaseInsensitive ) == 0;

std::vector< LayerRenderJob > secondPassJobs;

// We will need to quickly access the associated rendering job of a layer
Expand Down Expand Up @@ -812,7 +816,7 @@ std::vector< LayerRenderJob > QgsMapRendererJob::prepareSecondPassJobs( std::vec
if ( forceVector && !labelHasEffects[ maskId ] )
{
// set a painter to get all masking instruction in order to later clip masked symbol layer
maskPaintDevice = new QgsMaskPaintDevice( true );
maskPaintDevice = useGeometryBackend ? dynamic_cast< QPaintDevice *>( new QgsGeometryPaintDevice( true ) ) : dynamic_cast< QPaintDevice * >( new QgsMaskPaintDevice( true ) );
maskPainter = new QPainter( maskPaintDevice );
}
else
Expand Down Expand Up @@ -887,7 +891,7 @@ std::vector< LayerRenderJob > QgsMapRendererJob::prepareSecondPassJobs( std::vec
if ( forceVector && !maskLayerHasEffects[ job.layerId ] )
{
// set a painter to get all masking instruction in order to later clip masked symbol layer
maskPaintDevice = new QgsMaskPaintDevice();
maskPaintDevice = useGeometryBackend ? dynamic_cast< QPaintDevice *>( new QgsGeometryPaintDevice( true ) ) : dynamic_cast< QPaintDevice * >( new QgsMaskPaintDevice( true ) );
maskPainter = new QPainter( maskPaintDevice );
}
else
Expand Down Expand Up @@ -990,10 +994,23 @@ void QgsMapRendererJob::initSecondPassJobs( std::vector< LayerRenderJob > &secon
for ( const QPair<LayerRenderJob *, int> &p : std::as_const( job.maskJobs ) )
{
QPainter *maskPainter = p.first ? p.first->maskPainter.get() : labelJob.maskPainters[p.second].get();
QPainterPath path = static_cast<QgsMaskPaintDevice *>( maskPainter->device() )->maskPainterPath();
for ( const QString &symbolLayerId : job.context()->disabledSymbolLayersV2() )

const QSet<QString> layers = job.context()->disabledSymbolLayersV2();
if ( QgsMaskPaintDevice *maskDevice = dynamic_cast<QgsMaskPaintDevice *>( maskPainter->device() ) )
{
QPainterPath path = maskDevice->maskPainterPath();
for ( const QString &symbolLayerId : layers )
{
job.context()->addSymbolLayerClipPath( symbolLayerId, path );
}
}
else if ( QgsGeometryPaintDevice *geometryDevice = dynamic_cast<QgsGeometryPaintDevice *>( maskPainter->device() ) )
{
job.context()->addSymbolLayerClipPath( symbolLayerId, path );
const QgsGeometry geometry( geometryDevice->geometry().clone() );
for ( const QString &symbolLayerId : layers )
{
job.context()->addSymbolLayerClipGeometry( symbolLayerId, geometry );
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/core/maprenderer/qgsmaprendererjob.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class QgsFeatureFilterProvider;
class QgsRenderedItemResults;
class QgsElevationMap;
class QgsSettingsEntryBool;
class QgsSettingsEntryString;

#ifndef SIP_RUN
/// @cond PRIVATE
Expand Down Expand Up @@ -481,6 +482,13 @@ class CORE_EXPORT QgsMapRendererJob : public QObject SIP_ABSTRACT
#ifndef SIP_RUN
//! Settings entry log canvas refresh event
static const QgsSettingsEntryBool *settingsLogCanvasRefreshEvent;

/**
* Settings entry for mask painting backend engine.
*
* \since QGIS 3.38
*/
static const QgsSettingsEntryString *settingsMaskBackend;
#endif

signals:
Expand Down
12 changes: 12 additions & 0 deletions src/core/qgsrendercontext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ QgsRenderContext::QgsRenderContext( const QgsRenderContext &rh )
, mFrameRate( rh.mFrameRate )
, mCurrentFrame( rh.mCurrentFrame )
, mSymbolLayerClipPaths( rh.mSymbolLayerClipPaths )
, mSymbolLayerClippingGeometries( rh.mSymbolLayerClippingGeometries )
#ifdef QGISDEBUG
, mHasTransformContext( rh.mHasTransformContext )
#endif
Expand Down Expand Up @@ -134,6 +135,7 @@ QgsRenderContext &QgsRenderContext::operator=( const QgsRenderContext &rh )
mFrameRate = rh.mFrameRate;
mCurrentFrame = rh.mCurrentFrame;
mSymbolLayerClipPaths = rh.mSymbolLayerClipPaths;
mSymbolLayerClippingGeometries = rh.mSymbolLayerClippingGeometries;
if ( isTemporal() )
setTemporalRange( rh.temporalRange() );
#ifdef QGISDEBUG
Expand Down Expand Up @@ -732,6 +734,16 @@ QList<QPainterPath> QgsRenderContext::symbolLayerClipPaths( const QString &symbo
return mSymbolLayerClipPaths[ symbolLayerId ];
}

void QgsRenderContext::addSymbolLayerClipGeometry( const QString &symbolLayerId, const QgsGeometry &geometry )
{
mSymbolLayerClippingGeometries[ symbolLayerId ].append( geometry );
}

QVector<QgsGeometry> QgsRenderContext::symbolLayerClipGeometries( const QString &symbolLayerId ) const
{
return mSymbolLayerClippingGeometries[ symbolLayerId ];
}

void QgsRenderContext::setDisabledSymbolLayers( const QSet<const QgsSymbolLayer *> &symbolLayers )
{
mDisabledSymbolLayers.clear();
Expand Down
23 changes: 23 additions & 0 deletions src/core/qgsrendercontext.h
Original file line number Diff line number Diff line change
Expand Up @@ -931,16 +931,36 @@ class CORE_EXPORT QgsRenderContext : public QgsTemporalRangeObject

/**
* Add a clip \a path to be applied to the \a symbolLayer before rendering
* \see addSymbolLayerClipGeometry()
*
* \since QGIS 3.26, arguments changed and public API since 3.30
*/
void addSymbolLayerClipPath( const QString &symbolLayerId, QPainterPath path );

/**
* Returns clip paths to be applied to the \a symbolLayer before rendering
*
*\see symbolLayerClipGeometries()
* \since QGIS 3.26, arguments changed and public API since 3.30
*/
QList<QPainterPath> symbolLayerClipPaths( const QString &symbolLayerId ) const;

/**
* Add a clip \a geometry to be applied to the \a symbolLayer before rendering.
*
* \see symbolLayerClipGeometries()
* \since QGIS 3.38
*/
void addSymbolLayerClipGeometry( const QString &symbolLayerId, const QgsGeometry &geometry );

/**
* Returns clipping geometries to be applied to the \a symbolLayer before rendering
*
* \see addSymbolLayerClipGeometry()
* \since QGIS 3.38
*/
QVector<QgsGeometry> symbolLayerClipGeometries( const QString &symbolLayerId ) const;

/**
* Returns the range of z-values which should be rendered.
*
Expand Down Expand Up @@ -1227,6 +1247,9 @@ class CORE_EXPORT QgsRenderContext : public QgsTemporalRangeObject
//! clip paths to be applied to the symbol layer before rendering
QMap< QString, QList<QPainterPath> > mSymbolLayerClipPaths;

//! Clip geometries to be applied to the symbol layer before rendering
QMap< QString, QVector<QgsGeometry> > mSymbolLayerClippingGeometries;

#ifdef QGISDEBUG
bool mHasTransformContext = false;
#endif
Expand Down
44 changes: 34 additions & 10 deletions src/core/symbology/qgssymbollayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -957,21 +957,45 @@ void QgsSymbolLayer::prepareMasks( const QgsSymbolRenderContext &context )
mClipPath.clear();

const QgsRenderContext &renderContext = context.renderContext();
const QList<QPainterPath> clipPaths = renderContext.symbolLayerClipPaths( id() );
if ( !clipPaths.isEmpty() )

const QVector<QgsGeometry> clipGeometries = renderContext.symbolLayerClipGeometries( id() );
if ( !clipGeometries.empty() )
{
QPainterPath mergedPaths;
mergedPaths.setFillRule( Qt::WindingFill );
for ( const QPainterPath &path : clipPaths )
QVector< QgsGeometry > fixed;
for ( const QgsGeometry &geometry : clipGeometries )
{
mergedPaths.addPath( path );
fixed << geometry.makeValid( Qgis::MakeValidMethod::Structure );
}

if ( !mergedPaths.isEmpty() )
const QgsGeometry mergedGeom = QgsGeometry::unaryUnion( fixed );
if ( !mergedGeom.isEmpty() )
{
const QgsGeometry exterior = QgsGeometry::fromRect(
QgsRectangle( 0, 0,
renderContext.outputSize().width(),
renderContext.outputSize().height() ) );
const QgsGeometry maskGeom = exterior.difference( mergedGeom );
mClipPath = maskGeom.constGet()->asQPainterPath();
}
}
else
{
const QList<QPainterPath> clipPaths = renderContext.symbolLayerClipPaths( id() );
if ( !clipPaths.isEmpty() )
{
mClipPath.addRect( 0, 0, renderContext.outputSize().width(),
renderContext.outputSize().height() );
mClipPath = mClipPath.subtracted( mergedPaths );
QPainterPath mergedPaths;
mergedPaths.setFillRule( Qt::WindingFill );
for ( const QPainterPath &path : clipPaths )
{
mergedPaths.addPath( path );
}

if ( !mergedPaths.isEmpty() )
{
mClipPath.addRect( 0, 0, renderContext.outputSize().width(),
renderContext.outputSize().height() );
mClipPath = mClipPath.subtracted( mergedPaths );
}
}
}
}
Expand Down
51 changes: 43 additions & 8 deletions tests/src/python/test_selective_masking.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import subprocess
import tempfile

from qgis.PyQt.QtCore import QRectF, QSize, Qt, QUuid
from qgis.PyQt.QtCore import QRectF, QSize, Qt, QUuid, QCoreApplication
from qgis.PyQt.QtGui import QColor, QImage, QPainter
from qgis.core import (
Qgis,
Expand Down Expand Up @@ -48,7 +48,8 @@
QgsSymbolLayerUtils,
QgsUnitTypes,
QgsWkbTypes,
QgsFontUtils
QgsFontUtils,
QgsSettings
)
import unittest
from qgis.testing import start_app, QgisTestCase
Expand Down Expand Up @@ -77,14 +78,9 @@ def renderMapToImageWithTime(mapsettings, parallel=False, cache=None):
return (job.renderedImage(), job.renderingTime())


class TestSelectiveMasking(QgisTestCase):

@classmethod
def control_path_prefix(cls):
return "selective_masking"
class SelectiveMaskingTestBase():

def setUp(self):

self.map_settings = QgsMapSettings()
crs = QgsCoordinateReferenceSystem('epsg:4326')
extent = QgsRectangle(-123.0, 22.7, -76.4, 46.9)
Expand Down Expand Up @@ -1350,6 +1346,45 @@ def test_layout_export_svg_marker_masking(self):
self.check_layout_export("layout_export_svg_marker_masking", 0, [self.points_layer, self.lines_layer])


class TestSelectiveMaskingQPainterPathBackend(QgisTestCase, SelectiveMaskingTestBase):
"""
Test selective masking with the QPainterPath backend
"""
@classmethod
def control_path_prefix(cls):
return "selective_masking"

@classmethod
def setUpClass(cls):
QgsSettings().setValue('map/maskBackend', 'qpainterpath')
QgisTestCase.setUpClass()

def setUp(self):
SelectiveMaskingTestBase.setUp(self)


class TestSelectiveMaskingGeometryBackend(QgisTestCase, SelectiveMaskingTestBase):
"""
Test selective masking with the QgsGeometry backend
"""
@classmethod
def control_path_prefix(cls):
return "selective_masking"

@classmethod
def setUpClass(cls):
QgsSettings().setValue('map/maskBackend', 'geometry')
QgisTestCase.setUpClass()

def setUp(self):
SelectiveMaskingTestBase.setUp(self)


if __name__ == '__main__':
QCoreApplication.setOrganizationName("QGIS_Test")
QCoreApplication.setOrganizationDomain("SelectiveMaskingTestBase.com")
QCoreApplication.setApplicationName("SelectiveMaskingTestBase")
QgsSettings().clear()

start_app()
unittest.main()

0 comments on commit 65fc7b0

Please sign in to comment.