Skip to content

Commit

Permalink
Fix vector rendering of fill symbol layer
Browse files Browse the repository at this point in the history
  • Loading branch information
troopa81 committed Feb 27, 2023
1 parent b3af80e commit 7ed5876
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 27 deletions.
30 changes: 30 additions & 0 deletions python/core/auto_generated/symbology/qgssymbollayer.sip.in
Expand Up @@ -645,6 +645,10 @@ This is a list of symbol layers of other layers that should be occluded.
Prepares all mask internal objects according to what is defined in ``context``
This should be called prior to calling :py:func:`~QgsSymbolLayer.startRender` method.

.. seealso:: :py:func:`addSymbolLayerClipPath`

.. seealso:: :py:func:`symbolLayerClipPaths`

.. versionadded:: 3.26
%End

Expand Down Expand Up @@ -701,6 +705,32 @@ Copies paint effect of this layer to another symbol layer
:param destLayer: destination layer

.. versionadded:: 2.9
%End

void installMasks( QgsRenderContext &context, bool recursive );
%Docstring
When renderering, install masks on ``context`` painter

:param recursive: if ``True`` masks are installed recursively for all children symbol layers

.. seealso:: :py:func:`prepareMasks`

.. seealso:: :py:func:`removeMasks`

.. versionadded:: 3.30
%End

void removeMasks( QgsRenderContext &context, bool recursive );
%Docstring
When renderering, remove previously installed masks from ``context`` painter

:param recursive: if ``True`` masks are removed recursively for all children symbol layers

.. seealso:: :py:func:`prepareMasks`

.. seealso:: :py:func:`installMasks`

.. versionadded:: 3.30
%End

private:
Expand Down
20 changes: 14 additions & 6 deletions src/core/symbology/qgsfillsymbollayer.cpp
Expand Up @@ -2626,14 +2626,16 @@ bool QgsLinePatternFillSymbolLayer::hasDataDefinedProperties() const
return false;
}

void QgsLinePatternFillSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext & )
void QgsLinePatternFillSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext &context )
{
// deliberately don't pass this on to subsymbol here
installMasks( context, true );
}

void QgsLinePatternFillSymbolLayer::stopFeatureRender( const QgsFeature &, QgsRenderContext & )
void QgsLinePatternFillSymbolLayer::stopFeatureRender( const QgsFeature &, QgsRenderContext &context )
{
// deliberately don't pass this on to subsymbol here
removeMasks( context, true );
}

QImage QgsLinePatternFillSymbolLayer::toTiledPatternImage() const
Expand Down Expand Up @@ -3091,6 +3093,7 @@ void QgsLinePatternFillSymbolLayer::applyPattern( const QgsSymbolRenderContext &
lineRenderContext.setForceVectorOutput( false );
lineRenderContext.setExpressionContext( context.renderContext().expressionContext() );
lineRenderContext.setFlag( Qgis::RenderContextFlag::RenderingSubSymbol );
lineRenderContext.setDisabledSymbolLayersV2( context.renderContext().disabledSymbolLayersV2() );

fillLineSymbol->startRender( lineRenderContext, context.fields() );

Expand Down Expand Up @@ -3934,18 +3937,20 @@ void QgsPointPatternFillSymbolLayer::stopRender( QgsSymbolRenderContext &context
}
}

void QgsPointPatternFillSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext & )
void QgsPointPatternFillSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext &context )
{
// The base class version passes this on to the subsymbol, but we deliberately don't do that here.
// Otherwise generators used in the subsymbol will only render a single point per feature (they
// have logic to only render once per paired call to startFeatureRender/stopFeatureRender).
installMasks( context, true );
}

void QgsPointPatternFillSymbolLayer::stopFeatureRender( const QgsFeature &, QgsRenderContext & )
void QgsPointPatternFillSymbolLayer::stopFeatureRender( const QgsFeature &, QgsRenderContext &context )
{
// The base class version passes this on to the subsymbol, but we deliberately don't do that here.
// Otherwise generators used in the subsymbol will only render a single point per feature (they
// have logic to only render once per paired call to startFeatureRender/stopFeatureRender).
removeMasks( context, true );
}

void QgsPointPatternFillSymbolLayer::renderPolygon( const QPolygonF &points, const QVector<QPolygonF> *rings, QgsSymbolRenderContext &context )
Expand Down Expand Up @@ -4784,8 +4789,10 @@ void QgsCentroidFillSymbolLayer::renderPolygon( const QPolygonF &points, const Q
}
}

void QgsCentroidFillSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext & )
void QgsCentroidFillSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext &context )
{
installMasks( context, true );

mRenderingFeature = true;
mCurrentParts.clear();
}
Expand All @@ -4800,6 +4807,8 @@ void QgsCentroidFillSymbolLayer::stopFeatureRender( const QgsFeature &feature, Q
render( context, mCurrentParts, feature, false );
mFeatureSymbolOpacity = 1;
mMarker->setOpacity( prevOpacity );

removeMasks( context, true );
}

void QgsCentroidFillSymbolLayer::render( QgsRenderContext &context, const QVector<QgsCentroidFillSymbolLayer::Part> &parts, const QgsFeature &feature, bool selected )
Expand Down Expand Up @@ -5759,4 +5768,3 @@ QgsMapUnitScale QgsRandomMarkerFillSymbolLayer::mapUnitScale() const
}
return QgsMapUnitScale();
}

45 changes: 35 additions & 10 deletions src/core/symbology/qgssymbollayer.cpp
Expand Up @@ -125,25 +125,18 @@ void QgsSymbolLayer::setDataDefinedProperty( QgsSymbolLayer::Property key, const

void QgsSymbolLayer::startFeatureRender( const QgsFeature &feature, QgsRenderContext &context )
{
installMasks( context, false );

if ( QgsSymbol *lSubSymbol = subSymbol() )
lSubSymbol->startFeatureRender( feature, context );

if ( !mClipPath.isEmpty() )
{
context.painter()->save();
context.painter()->setClipPath( mClipPath, Qt::IntersectClip );
}
}

void QgsSymbolLayer::stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context )
{
if ( QgsSymbol *lSubSymbol = subSymbol() )
lSubSymbol->stopFeatureRender( feature, context );

if ( !mClipPath.isEmpty() )
{
context.painter()->restore();
}
removeMasks( context, false );
}

QgsSymbol *QgsSymbolLayer::subSymbol()
Expand Down Expand Up @@ -932,6 +925,38 @@ void QgsSymbolLayer::prepareMasks( const QgsSymbolRenderContext &context )
}
}

void QgsSymbolLayer::installMasks( QgsRenderContext &context, bool recursive )
{
if ( !mClipPath.isEmpty() )
{
context.painter()->save();
context.painter()->setClipPath( mClipPath, Qt::IntersectClip );
}

QgsSymbol *lSubSymbol = recursive ? subSymbol() : nullptr;
if ( lSubSymbol )
{
for ( QgsSymbolLayer *sl : lSubSymbol->symbolLayers() )
sl->installMasks( context, true );
}
}

void QgsSymbolLayer::removeMasks( QgsRenderContext &context, bool recursive )
{
if ( !mClipPath.isEmpty() )
{
context.painter()->restore();
}

QgsSymbol *lSubSymbol = recursive ? subSymbol() : nullptr;
if ( lSubSymbol )
{
for ( QgsSymbolLayer *sl : lSubSymbol->symbolLayers() )
sl->removeMasks( context, true );
}
}


void QgsSymbolLayer::setId( const QString &id )
{
mId = id;
Expand Down
20 changes: 20 additions & 0 deletions src/core/symbology/qgssymbollayer.h
Expand Up @@ -626,6 +626,8 @@ class CORE_EXPORT QgsSymbolLayer
/**
* Prepares all mask internal objects according to what is defined in \a context
* This should be called prior to calling startRender() method.
* \see addSymbolLayerClipPath()
* \see symbolLayerClipPaths()
* \since QGIS 3.26
*/
virtual void prepareMasks( const QgsSymbolRenderContext &context );
Expand Down Expand Up @@ -697,6 +699,24 @@ class CORE_EXPORT QgsSymbolLayer
*/
void copyPaintEffect( QgsSymbolLayer *destLayer ) const;

/**
* When renderering, install masks on \a context painter
* \param recursive if TRUE masks are installed recursively for all children symbol layers
* \see prepareMasks()
* \see removeMasks()
* \since QGIS 3.30
*/
void installMasks( QgsRenderContext &context, bool recursive );

/**
* When renderering, remove previously installed masks from \a context painter
* \param recursive if TRUE masks are removed recursively for all children symbol layers
* \see prepareMasks()
* \see installMasks()
* \since QGIS 3.30
*/
void removeMasks( QgsRenderContext &context, bool recursive );

private:
static void initPropertyDefinitions();

Expand Down
122 changes: 111 additions & 11 deletions tests/src/python/test_selective_masking.py
Expand Up @@ -135,10 +135,9 @@ def tearDown(self):

def get_symbollayer_ref(self, layer, ruleId, symbollayer_ids):
"""
Returns the symbol layer according to given path to
Returns the symbol layer according to given layer, ruleId (None if no rule) and the path
to symbol layer id (for instance [0, 1])
"""

renderer = layer.renderer()
symbol = None
if renderer.type() == "categorizedSymbol":
Expand Down Expand Up @@ -171,8 +170,8 @@ def check_renderings(self, map_settings, control_name):
if use_cache:
cache = QgsMapRendererCache()
# render a first time to fill the cache
renderMapToImageWithTime(self.map_settings, parallel=do_parallel, cache=cache)
img, t = renderMapToImageWithTime(self.map_settings, parallel=do_parallel, cache=cache)
renderMapToImageWithTime(map_settings, parallel=do_parallel, cache=cache)
img, t = renderMapToImageWithTime(map_settings, parallel=do_parallel, cache=cache)
img.save(tmp)
print(f"Image rendered in {tmp}")

Expand All @@ -181,15 +180,17 @@ def check_renderings(self, map_settings, control_name):
suffix = ("_parallel" if do_parallel else "_sequential") + ("_cache" if use_cache else "_nocache")
res = self.checker.compareImages(control_name + suffix)
self.report += self.checker.report()
f = open("/tmp/merdier.html", "w")
f.write(self.report)
f.close()

# f = open("/tmp/report.html", "w")
# f.write(self.report)
# f.close()

self.assertTrue(res)

print(f"=== Rendering took {float(t) / 1000.0}s")

def check_layout_export(self, control_name, expected_nb_raster, layers=None, dpiTarget=None):
def check_layout_export(self, control_name, expected_nb_raster, layers=None, dpiTarget=None,
extent=None):
"""
Generate a PDF layout export and control the output matches expected_filename
"""
Expand All @@ -204,7 +205,7 @@ def check_layout_export(self, control_name, expected_nb_raster, layers=None, dpi
map.attemptSetSceneRect(QRectF(1, 1, 48, 32))
map.setFrameEnabled(True)
layout.addLayoutItem(map)
map.setExtent(self.lines_layer.extent())
map.setExtent(extent if extent is not None else self.lines_layer.extent())
map.setLayers(layers if layers is not None else [self.points_layer, self.lines_layer, self.polys_layer])

settings = QgsLayoutExporter.PdfExportSettings()
Expand All @@ -231,6 +232,8 @@ def check_layout_export(self, control_name, expected_nb_raster, layers=None, dpi
nb_raster = len([l for l in result_lines if "/Subtype /Image" in l])
self.assertEqual(nb_raster, expected_nb_raster)

print("pdf_file={}".format(result_filename))

# Generate an image from pdf to compare with expected control image
# keep PDF DPI resolution (300)
image_result_filename = getTempfilePath("png")
Expand All @@ -243,7 +246,7 @@ def check_layout_export(self, control_name, expected_nb_raster, layers=None, dpi
res = self.checker.compareImages(control_name)
self.report += self.checker.report()

f = open("/tmp/merdier.html", "w")
f = open("/tmp/report.html", "w")
f.write(self.report)
f.close()

Expand Down Expand Up @@ -1108,6 +1111,103 @@ def test_layout_export_2_sources_masking(self):

self.check_layout_export("layout_export_2_sources_masking", 0)

def test_raster_line_pattern_fill(self):
"""
Test raster rendering and masking when a line pattern fill symbol layer is involved
"""
self.assertTrue(QgsProject.instance().read(os.path.join(unitTestDataPath(), "selective_masking_linepattern.qgz")))

layer = QgsProject.instance().mapLayersByName('line_pattern_fill')[0]
self.assertTrue(layer)

map_settings = QgsMapSettings()
crs = QgsCoordinateReferenceSystem('epsg:4326')
extent = QgsRectangle(-0.972, -1.966, 1.58, 1.806)
map_settings.setBackgroundColor(QColor(152, 219, 249))
map_settings.setOutputSize(QSize(420, 280))
map_settings.setOutputDpi(72)
map_settings.setFlag(QgsMapSettings.Antialiasing, True)
map_settings.setFlag(QgsMapSettings.UseAdvancedEffects, False)
map_settings.setDestinationCrs(crs)
map_settings.setExtent(extent)

map_settings.setLayers([layer])

self.check_renderings(map_settings, "line_pattern_fill")

def test_vector_line_pattern_fill(self):
"""
Test vector rendering and masking when a line pattern fill symbol layer is involved
"""
self.assertTrue(QgsProject.instance().read(os.path.join(unitTestDataPath(), "selective_masking_linepattern.qgz")))

layer = QgsProject.instance().mapLayersByName('line_pattern_fill')[0]
self.assertTrue(layer)

map_settings = QgsMapSettings()
crs = QgsCoordinateReferenceSystem('epsg:4326')
extent = QgsRectangle(-0.972, -1.966, 1.58, 1.806)
map_settings.setBackgroundColor(QColor(152, 219, 249))
map_settings.setOutputSize(QSize(420, 280))
map_settings.setOutputDpi(72)
map_settings.setFlag(QgsMapSettings.Antialiasing, True)
map_settings.setFlag(QgsMapSettings.UseAdvancedEffects, False)
map_settings.setDestinationCrs(crs)
map_settings.setExtent(extent)

map_settings.setLayers([layer])

self.check_layout_export("layout_export_line_pattern_fill", 0, [layer], extent=QgsRectangle(-1.0073971192118132, -0.7875782447946843, 0.87882587741257345, 0.51640826470600099))

def test_vector_point_pattern_fill(self):
"""
Test vector rendering and masking when a point pattern fill symbol layer is involved
"""
self.assertTrue(QgsProject.instance().read(os.path.join(unitTestDataPath(), "selective_masking_linepattern.qgz")))

layer = QgsProject.instance().mapLayersByName('point_pattern_fill')[0]
self.assertTrue(layer)

map_settings = QgsMapSettings()
crs = QgsCoordinateReferenceSystem('epsg:4326')
extent = QgsRectangle(-0.972, -1.966, 1.58, 1.806)
map_settings.setBackgroundColor(QColor(152, 219, 249))
map_settings.setOutputSize(QSize(420, 280))
map_settings.setOutputDpi(72)
map_settings.setFlag(QgsMapSettings.Antialiasing, True)
map_settings.setFlag(QgsMapSettings.UseAdvancedEffects, False)
map_settings.setDestinationCrs(crs)
map_settings.setExtent(extent)

map_settings.setLayers([layer])

self.check_layout_export("layout_export_point_pattern_fill", 0, [layer], extent=QgsRectangle(-1.0073971192118132, -0.7875782447946843, 0.87882587741257345, 0.51640826470600099))

def test_vector_centroid_fill(self):
"""
Test masking when a centroid fill symbol layer is involved
"""
self.assertTrue(QgsProject.instance().read(os.path.join(unitTestDataPath(), "selective_masking_linepattern.qgz")))

layer = QgsProject.instance().mapLayersByName('centroid_fill')[0]
self.assertTrue(layer)

map_settings = QgsMapSettings()
crs = QgsCoordinateReferenceSystem('epsg:4326')
extent = QgsRectangle(-1.0073971192118132, -0.7875782447946843, 0.87882587741257345, 0.51640826470600099)
# extent = QgsRectangle(-0.972, -1.966, 1.58, 1.806)
map_settings.setBackgroundColor(QColor(152, 219, 249))
map_settings.setOutputSize(QSize(420, 280))
map_settings.setOutputDpi(72)
map_settings.setFlag(QgsMapSettings.Antialiasing, True)
map_settings.setFlag(QgsMapSettings.UseAdvancedEffects, False)
map_settings.setDestinationCrs(crs)
map_settings.setExtent(extent)

map_settings.setLayers([layer])

self.check_layout_export("layout_export_centroid_fill", 0, [layer], extent=QgsRectangle(-1.0073971192118132, -0.7875782447946843, 0.87882587741257345, 0.51640826470600099))


if __name__ == '__main__':
start_app()
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.
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.
Binary file not shown.
Binary file added tests/testdata/selective_masking_linepattern.qgz
Binary file not shown.

0 comments on commit 7ed5876

Please sign in to comment.