Skip to content
Permalink
Browse files

Respect layer opacity and composition modes where possible when expor…

…ting GeoPDF

Fixes #33305
  • Loading branch information
nyalldawson committed Jun 8, 2020
1 parent 820c583 commit 8d38aa2c1515f4165182e915415afc0f8c9cd7cb
@@ -464,6 +464,10 @@ Moves to the next export part for a multi-layered export item, during a multi-la

QString mapLayerId;

QPainter::CompositionMode compositionMode;

double opacity;

QString mapTheme;
};

@@ -565,6 +565,8 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
QgsLayoutGeoPdfExporter::ComponentLayerDetail component;
component.name = layerDetail.name;
component.mapLayerId = layerDetail.mapLayerId;
component.opacity = layerDetail.opacity;
component.compositionMode = layerDetail.compositionMode;
component.group = layerDetail.mapTheme;
component.sourcePdfPath = settings.writeGeoPdf ? geoPdfExporter->generateTemporaryFilepath( QStringLiteral( "layer_%1.pdf" ).arg( layerId ) ) : baseDir.filePath( QStringLiteral( "%1_%2.pdf" ).arg( baseFileName ).arg( layerId, 4, 10, QChar( '0' ) ) );
pdfComponents << component;
@@ -492,6 +492,18 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt
//! Associated map layer ID, or an empty string if this export layer is not associated with a map layer
QString mapLayerId;

/**
* Associated composition mode if this layer is associated with a map layer
* \since QGIS 3.14
*/
QPainter::CompositionMode compositionMode = QPainter::CompositionMode_SourceOver;

/**
* Associated opacity, if this layer is associated with a map layer
* \since QGIS 3.14
*/
double opacity = 1.0;

//! Associated map theme, or an empty string if this export layer does not need to be associated with a map theme
QString mapTheme;
};
@@ -1165,7 +1165,9 @@ QgsLayoutItem::ExportLayerDetail QgsLayoutItemMap::exportLayerDetails() const
{
case QgsMapRendererStagedRenderJob::Symbology:
{
detail.mapLayerId = mStagedRendererJob->currentLayerId();
detail.mapLayerId = mStagedRendererJob->currentLayerId();
detail.compositionMode = mStagedRendererJob->currentLayerCompositionMode();
detail.opacity = mStagedRendererJob->currentLayerOpacity();
if ( const QgsMapLayer *layer = mLayout->project()->mapLayer( detail.mapLayerId ) )
{
if ( !detail.mapTheme.isEmpty() )
@@ -471,15 +471,73 @@ QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLa
page.appendChild( georeferencing );
}

auto createPdfDatasetElement = [&doc]( const ComponentLayerDetail & component ) -> QDomElement
{
QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
if ( component.opacity != 1.0 || component.compositionMode != QPainter::CompositionMode_SourceOver )
{
QDomElement blendingElement = doc.createElement( QStringLiteral( "Blending" ) );
blendingElement.setAttribute( QStringLiteral( "opacity" ), component.opacity );
QString function;
switch ( component.compositionMode )
{
case QPainter::CompositionMode_SourceOver:
function = QStringLiteral( "Normal" );
break;
case QPainter::CompositionMode_Multiply:
function = QStringLiteral( "Multiply" );
break;
case QPainter::CompositionMode_Screen:
function = QStringLiteral( "Screen" );
break;
case QPainter::CompositionMode_Overlay:
function = QStringLiteral( "Overlay" );
break;
case QPainter::CompositionMode_Darken:
function = QStringLiteral( "Darken" );
break;
case QPainter::CompositionMode_Lighten:
function = QStringLiteral( "Lighten" );
break;
case QPainter::CompositionMode_ColorDodge:
function = QStringLiteral( "ColorDodge" );
break;
case QPainter::CompositionMode_ColorBurn:
function = QStringLiteral( "ColorBurn" );
break;
case QPainter::CompositionMode_HardLight:
function = QStringLiteral( "HardLight" );
break;
case QPainter::CompositionMode_SoftLight:
function = QStringLiteral( "SoftLight" );
break;
case QPainter::CompositionMode_Difference:
function = QStringLiteral( "Difference" );
break;
case QPainter::CompositionMode_Exclusion:
function = QStringLiteral( "Exclusion" );
break;

default:
QgsDebugMsg( QStringLiteral( "Unsupported PDF blend mode %1" ).arg( component.compositionMode ) );
function = QStringLiteral( "Normal" );
break;
}
blendingElement.setAttribute( QStringLiteral( "function" ), function );

pdfDataset.appendChild( blendingElement );
}
return pdfDataset;
};

// content
QDomElement content = doc.createElement( QStringLiteral( "Content" ) );
for ( const ComponentLayerDetail &component : components )
{
if ( component.mapLayerId.isEmpty() )
{
QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
content.appendChild( pdfDataset );
content.appendChild( createPdfDatasetElement( component ) );
}
else if ( !component.group.isEmpty() )
{
@@ -493,9 +551,8 @@ QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLa
ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
else
ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
ifLayerOn.appendChild( pdfDataset );

ifLayerOn.appendChild( createPdfDatasetElement( component ) );
ifGroupOn.appendChild( ifLayerOn );
content.appendChild( ifGroupOn );
}
@@ -508,9 +565,7 @@ QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLa
ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
else
ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
ifLayerOn.appendChild( pdfDataset );
ifLayerOn.appendChild( createPdfDatasetElement( component ) );
content.appendChild( ifLayerOn );
}
}
@@ -21,6 +21,8 @@
#include <QTemporaryDir>
#include <QMutex>
#include <QDateTime>
#include <QPainter>

#include "qgsfeature.h"
#include "qgsabstractmetadatabase.h"
#include "qgspolygon.h"
@@ -126,6 +128,12 @@ class CORE_EXPORT QgsAbstractGeoPdfExporter
//! File path to the (already created) PDF to use as the source for this component layer
QString sourcePdfPath;

//! Component composition mode
QPainter::CompositionMode compositionMode = QPainter::CompositionMode_SourceOver;

//! Component opacity
double opacity = 1.0;

};

/**
@@ -217,12 +217,12 @@ bool QgsMapRendererStagedRenderJob::nextPart()
return false;
}

bool QgsMapRendererStagedRenderJob::isFinished()
bool QgsMapRendererStagedRenderJob::isFinished() const
{
return currentStage() == Finished;
}

QString QgsMapRendererStagedRenderJob::currentLayerId()
QString QgsMapRendererStagedRenderJob::currentLayerId() const
{
if ( mJobIt != mLayerJobs.end() )
{
@@ -237,6 +237,26 @@ QString QgsMapRendererStagedRenderJob::currentLayerId()
return QString();
}

double QgsMapRendererStagedRenderJob::currentLayerOpacity() const
{
if ( mJobIt != mLayerJobs.end() )
{
LayerRenderJob &job = *mJobIt;
return job.opacity;
}
return 1.0;
}

QPainter::CompositionMode QgsMapRendererStagedRenderJob::currentLayerCompositionMode() const
{
if ( mJobIt != mLayerJobs.end() )
{
LayerRenderJob &job = *mJobIt;
return job.blendMode;
}
return QPainter::CompositionMode_SourceOver;
}

QgsMapRendererStagedRenderJob::RenderStage QgsMapRendererStagedRenderJob::currentStage() const
{
if ( mJobIt != mLayerJobs.end() )
@@ -88,12 +88,26 @@ class CORE_EXPORT QgsMapRendererStagedRenderJob : public QgsMapRendererAbstractC
/**
* Returns TRUE if the job is finished, and nothing remains to render.
*/
bool isFinished();
bool isFinished() const;

/**
* Returns the ID of the current layer about to be rendered in the next render operation.
*/
QString currentLayerId();
QString currentLayerId() const;

/**
* Returns the opacity for the current layer about to be rendered in the next render operation.
*
* \since QGIS 3.14
*/
double currentLayerOpacity() const;

/**
* Returns the composition mode for the current layer about to be rendered in the next render operation.
*
* \since QGIS 3.14
*/
QPainter::CompositionMode currentLayerCompositionMode() const;

/**
* Returns the current stage which will be rendered in the next render operation.
@@ -187,6 +187,8 @@ bool QgsMapRendererTask::run()

component.name = QStringLiteral( "layer_%1" ).arg( outputLayer );
component.mapLayerId = job->currentLayerId();
component.opacity = job->currentLayerOpacity();
component.compositionMode = job->currentLayerCompositionMode();
component.sourcePdfPath = mGeoPdfExporter->generateTemporaryFilepath( QStringLiteral( "layer_%1.pdf" ).arg( outputLayer ) );
pdfComponents << component;

@@ -223,8 +223,16 @@ void TestQgsGeoPdfExport::testComposition()
}

// test creation of the composition xml
QList< QgsAbstractGeoPdfExporter::ComponentLayerDetail > renderedLayers; // no extra layers for now
QList< QgsAbstractGeoPdfExporter::ComponentLayerDetail > renderedLayers;
QgsAbstractGeoPdfExporter::ComponentLayerDetail detail;
detail.mapLayerId = QStringLiteral( "layer3" );
detail.opacity = 0.7;
detail.compositionMode = QPainter::CompositionMode_Screen;
detail.sourcePdfPath = QStringLiteral( "a pdf.pdf" );
renderedLayers << detail;

QgsAbstractGeoPdfExporter::ExportDetails details;

details.layerIdToPdfLayerTreeNameMap.insert( QStringLiteral( "layer1" ), QStringLiteral( "my first layer" ) );
details.initialLayerVisibility.insert( QStringLiteral( "layer2" ), false );
details.layerOrder = QStringList() << QStringLiteral( "layer2" );
@@ -233,10 +241,14 @@ void TestQgsGeoPdfExport::testComposition()
QDomDocument doc;
doc.setContent( composition );
QDomNodeList ifLayerOnList = doc.elementsByTagName( QStringLiteral( "IfLayerOn" ) );
QCOMPARE( ifLayerOnList.count(), 2 );

int layer1Idx = ifLayerOnList.at( 0 ).toElement().attribute( QStringLiteral( "layerId" ) ) == QStringLiteral( "layer1" ) ? 0 : 1;
int layer2Idx = layer1Idx == 0 ? 1 : 0;
QCOMPARE( ifLayerOnList.count(), 3 );

int layer1Idx = ifLayerOnList.at( 0 ).toElement().attribute( QStringLiteral( "layerId" ) ) == QStringLiteral( "layer1" ) ? 0 :
ifLayerOnList.at( 1 ).toElement().attribute( QStringLiteral( "layerId" ) ) == QStringLiteral( "layer1" ) ? 1 : 2;
int layer2Idx = ifLayerOnList.at( 0 ).toElement().attribute( QStringLiteral( "layerId" ) ) == QStringLiteral( "layer2" ) ? 0 :
ifLayerOnList.at( 1 ).toElement().attribute( QStringLiteral( "layerId" ) ) == QStringLiteral( "layer2" ) ? 1 : 2;
int layer3Idx = ifLayerOnList.at( 0 ).toElement().attribute( QStringLiteral( "layerId" ) ) == QStringLiteral( "layer3" ) ? 0 :
ifLayerOnList.at( 1 ).toElement().attribute( QStringLiteral( "layerId" ) ) == QStringLiteral( "layer3" ) ? 1 : 2;
QCOMPARE( ifLayerOnList.at( layer1Idx ).toElement().attribute( QStringLiteral( "layerId" ) ), QStringLiteral( "layer1" ) );
QCOMPARE( ifLayerOnList.at( layer1Idx ).toElement().elementsByTagName( QStringLiteral( "Vector" ) ).at( 0 ).toElement().attribute( QStringLiteral( "dataset" ) ), layer1Path );
QCOMPARE( ifLayerOnList.at( layer1Idx ).toElement().elementsByTagName( QStringLiteral( "Vector" ) ).at( 0 ).toElement().attribute( QStringLiteral( "layer" ) ), layer1Layer );
@@ -251,9 +263,13 @@ void TestQgsGeoPdfExport::testComposition()
QCOMPARE( ifLayerOnList.at( layer2Idx ).toElement().elementsByTagName( QStringLiteral( "LogicalStructure" ) ).at( 0 ).toElement().attribute( QStringLiteral( "fieldToDisplay" ) ), QStringLiteral( "attr layer2" ) );
QCOMPARE( ifLayerOnList.at( layer2Idx ).toElement().elementsByTagName( QStringLiteral( "LogicalStructure" ) ).at( 0 ).toElement().attribute( QStringLiteral( "displayLayerName" ) ), QStringLiteral( "name layer2" ) );

QCOMPARE( ifLayerOnList.at( layer3Idx ).toElement().attribute( QStringLiteral( "layerId" ) ), QStringLiteral( "layer3" ) );
QCOMPARE( ifLayerOnList.at( layer3Idx ).toElement().elementsByTagName( QStringLiteral( "PDF" ) ).at( 0 ).toElement().attribute( QStringLiteral( "dataset" ) ), QStringLiteral( "a pdf.pdf" ) );
QCOMPARE( ifLayerOnList.at( layer3Idx ).toElement().elementsByTagName( QStringLiteral( "PDF" ) ).at( 0 ).toElement().elementsByTagName( QStringLiteral( "Blending" ) ).at( 0 ).toElement().attribute( QStringLiteral( "opacity" ) ).toDouble(), 0.7 );
QCOMPARE( ifLayerOnList.at( layer3Idx ).toElement().elementsByTagName( QStringLiteral( "PDF" ) ).at( 0 ).toElement().elementsByTagName( QStringLiteral( "Blending" ) ).at( 0 ).toElement().attribute( QStringLiteral( "function" ) ), QStringLiteral( "Screen" ) );

QDomNodeList layerTreeList = doc.elementsByTagName( QStringLiteral( "LayerTree" ) ).at( 0 ).toElement().childNodes();
QCOMPARE( layerTreeList.count(), 2 );
QCOMPARE( layerTreeList.count(), 3 );

QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( QStringLiteral( "id" ) ), QStringLiteral( "layer1" ) );
QCOMPARE( layerTreeList.at( 1 ).toElement().attribute( QStringLiteral( "name" ) ), QStringLiteral( "my first layer" ) );
@@ -262,6 +278,8 @@ void TestQgsGeoPdfExport::testComposition()
QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( QStringLiteral( "id" ) ), QStringLiteral( "layer2" ) );
QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( QStringLiteral( "name" ) ), QStringLiteral( "name layer2" ) );
QCOMPARE( layerTreeList.at( 0 ).toElement().attribute( QStringLiteral( "initiallyVisible" ) ), QStringLiteral( "false" ) );

QCOMPARE( layerTreeList.at( 2 ).toElement().attribute( QStringLiteral( "id" ) ), QStringLiteral( "layer3" ) );
}

void TestQgsGeoPdfExport::testMetadata()

0 comments on commit 8d38aa2

Please sign in to comment.
You can’t perform that action at this time.