Skip to content

Commit

Permalink
[FEATURE] GeoPDF export in map renderer task (e.g. save as canvas)
Browse files Browse the repository at this point in the history
Not exposed to UI yet
  • Loading branch information
nyalldawson committed Aug 17, 2019
1 parent b1e3d04 commit 93342a3
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 27 deletions.
8 changes: 7 additions & 1 deletion python/core/auto_generated/qgsmaprenderertask.sip.in
Expand Up @@ -35,9 +35,13 @@ task. This can be used to draw maps without blocking the QGIS interface.
QgsMapRendererTask( const QgsMapSettings &ms,
const QString &fileName,
const QString &fileFormat = QString( "PNG" ),
bool forceRaster = false );
bool forceRaster = false,
bool geoPdf = false );
%Docstring
Constructor for QgsMapRendererTask to render a map to an image file.

If the output ``fileFormat`` is set to PDF, the ``geoPdf`` argument controls whether a GeoPDF file is created.
See QgsAbstractGeoPdfExporter.geoPDFCreationAvailable() for conditions on GeoPDF creation availability.
%End

QgsMapRendererTask( const QgsMapSettings &ms,
Expand All @@ -46,6 +50,8 @@ Constructor for QgsMapRendererTask to render a map to an image file.
Constructor for QgsMapRendererTask to render a map to a QPainter object.
%End

~QgsMapRendererTask();

void addAnnotations( const QList<QgsAnnotation *> &annotations );
%Docstring
Adds ``annotations`` to be rendered on the map.
Expand Down
9 changes: 8 additions & 1 deletion src/core/layout/qgslayoutexporter.cpp
Expand Up @@ -572,7 +572,14 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
if ( result != Success )
return result;

geoPdfExporter->finalize( pdfComponents, filePath );
QgsAbstractGeoPdfExporter::ExportDetails details;
details.dpi = settings.dpi;
// TODO - multipages
QgsLayoutSize pageSize = mLayout->pageCollection()->page( 0 )->sizeWithUnits();
QgsLayoutSize pageSizeMM = mLayout->renderContext().measurementConverter().convert( pageSize, QgsUnitTypes::LayoutMillimeters );
details.pageSizeMm = pageSizeMM.toQSizeF();

geoPdfExporter->finalize( pdfComponents, filePath, details );
}
else
{
Expand Down
2 changes: 1 addition & 1 deletion src/core/layout/qgslayoutexporter.h
Expand Up @@ -314,7 +314,7 @@ class CORE_EXPORT QgsLayoutExporter
*
* \since QGIS 3.10
*/
bool writeGeoPdf = true;
bool writeGeoPdf = false;

};

Expand Down
10 changes: 6 additions & 4 deletions src/core/layout/qgslayoutitemmap.cpp
Expand Up @@ -1108,18 +1108,20 @@ QgsLayoutItem::ExportLayerDetail QgsLayoutItemMap::exportLayerDetails() const
switch ( mStagedRendererJob->currentStage() )
{
case QgsMapRendererStagedRenderJob::Symbology:
if ( const QgsMapLayer *layer = mStagedRendererJob->currentLayer() )
{
detail.mapLayerId = mStagedRendererJob->currentLayerId();
if ( const QgsMapLayer *layer = mLayout->project()->mapLayer( detail.mapLayerId ) )
{
detail.name = QStringLiteral( "%1: %2" ).arg( displayName(), layer->name() );
detail.mapLayerId = layer->id();
}
return detail;
}

case QgsMapRendererStagedRenderJob::Labels:
if ( const QgsMapLayer *layer = mStagedRendererJob->currentLayer() )
detail.mapLayerId = mStagedRendererJob->currentLayerId();
if ( const QgsMapLayer *layer = mLayout->project()->mapLayer( detail.mapLayerId ) )
{
detail.name = tr( "%1: %2 (Labels)" ).arg( displayName(), layer->name() );
detail.mapLayerId = layer->id();
}
else
{
Expand Down
23 changes: 13 additions & 10 deletions src/core/qgsabstractgeopdfexporter.cpp
Expand Up @@ -81,7 +81,7 @@ QString QgsAbstractGeoPdfExporter::geoPDFAvailabilityExplanation()
#endif
}

bool QgsAbstractGeoPdfExporter::finalize( const QList<ComponentLayerDetail> &components, const QString &destinationFile )
bool QgsAbstractGeoPdfExporter::finalize( const QList<ComponentLayerDetail> &components, const QString &destinationFile, const ExportDetails &details )
{
#if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0)
Q_UNUSED( components )
Expand All @@ -91,7 +91,8 @@ bool QgsAbstractGeoPdfExporter::finalize( const QList<ComponentLayerDetail> &com
if ( !saveTemporaryLayers() )
return false;

const QString composition = createCompositionXml( components );
const QString composition = createCompositionXml( components, details );
QgsDebugMsg( composition );
if ( composition.isEmpty() )
return false;

Expand Down Expand Up @@ -181,7 +182,7 @@ bool QgsAbstractGeoPdfExporter::saveTemporaryLayers()
return true;
}

QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLayerDetail> &components )
QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLayerDetail> &components, const ExportDetails &details )
{
QDomDocument doc;

Expand Down Expand Up @@ -243,26 +244,28 @@ QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLa
// pages
QDomElement page = doc.createElement( QStringLiteral( "Page" ) );
QDomElement dpi = doc.createElement( QStringLiteral( "DPI" ) );
dpi.appendChild( doc.createTextNode( QStringLiteral( "300" ) ) );
dpi.appendChild( doc.createTextNode( QString::number( details.dpi ) ) );
page.appendChild( dpi );
// assumes DPI of 72
QDomElement width = doc.createElement( QStringLiteral( "Width" ) );
width.appendChild( doc.createTextNode( QStringLiteral( "842" ) ) );
width.appendChild( doc.createTextNode( QString::number( std::ceil( details.pageSizeMm.width() / 25.4 * 72 ) ) ) );
page.appendChild( width );
QDomElement height = doc.createElement( QStringLiteral( "Height" ) );
height.appendChild( doc.createTextNode( QStringLiteral( "595" ) ) );
height.appendChild( doc.createTextNode( QString::number( std::ceil( details.pageSizeMm.height() / 25.4 * 72 ) ) ) );
page.appendChild( height );


// georeferencing - TODO
#if 0
QDomElement georeferencing = doc.createElement( QStringLiteral( "Georeferencing" ) );
georeferencing.setAttribute( QStringLiteral( "id" ), QStringLiteral( "georeferenced" ) );
georeferencing.setAttribute( QStringLiteral( "OGCBestPracticeFormat" ), QStringLiteral( "false" ) );
georeferencing.setAttribute( QStringLiteral( "ISO32000ExtensionFormat" ), QStringLiteral( "true" ) );
QDomElement srs = doc.createElement( QStringLiteral( "SRS" ) );
srs.setAttribute( QStringLiteral( "dataAxisToSRSAxisMapping" ), QStringLiteral( "2,1" ) );
// srs.setAttribute( QStringLiteral( "dataAxisToSRSAxisMapping" ), QStringLiteral( "2,1" ) );
srs.appendChild( doc.createTextNode( QStringLiteral( "EPSG:4326" ) ) );
georeferencing.appendChild( srs );
#if 0

/* Define the viewport where georeferenced coordinates are available.
If not specified, the extent of BoundingPolygon will be used instead.
If none of BoundingBox and BoundingPolygon are specified,
Expand All @@ -284,7 +287,7 @@ QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLa
QDomElement boundingPolygon = doc.createElement( QStringLiteral( "BoundingPolygon" ) );
boundingPolygon.appendChild( doc.createTextNode( QStringLiteral( "POLYGON((1 1,9 1,9 14,1 14,1 1))" ) ) );
georeferencing.appendChild( boundingPolygon );
#endif

QDomElement cp1 = doc.createElement( QStringLiteral( "ControlPoint" ) );
cp1.setAttribute( QStringLiteral( "x" ), QStringLiteral( "1" ) );
cp1.setAttribute( QStringLiteral( "y" ), QStringLiteral( "1" ) );
Expand All @@ -311,7 +314,7 @@ QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLa
georeferencing.appendChild( cp4 );

page.appendChild( georeferencing );

#endif

// content
QDomElement content = doc.createElement( QStringLiteral( "Content" ) );
Expand Down
14 changes: 12 additions & 2 deletions src/core/qgsabstractgeopdfexporter.h
Expand Up @@ -128,6 +128,16 @@ class CORE_EXPORT QgsAbstractGeoPdfExporter
*/
void pushRenderedFeature( const QString &layerId, const QgsAbstractGeoPdfExporter::RenderedFeature &feature );

struct ExportDetails
{
//! Page size, in millimeters
QSizeF pageSizeMm;

//! Output DPI
double dpi = 300;

};

/**
* To be called after the rendering operation is complete.
*
Expand All @@ -141,7 +151,7 @@ class CORE_EXPORT QgsAbstractGeoPdfExporter
* Returns TRUE if the operation was successful, or FALSE if an error occurred. If an error occurred, it
* can be retrieved by calling errorMessage().
*/
bool finalize( const QList< QgsAbstractGeoPdfExporter::ComponentLayerDetail > &components, const QString &destinationFile );
bool finalize( const QList< QgsAbstractGeoPdfExporter::ComponentLayerDetail > &components, const QString &destinationFile, const ExportDetails &details );

/**
* Returns the last error message encountered during the export.
Expand Down Expand Up @@ -194,7 +204,7 @@ class CORE_EXPORT QgsAbstractGeoPdfExporter

bool saveTemporaryLayers();

QString createCompositionXml( const QList< QgsAbstractGeoPdfExporter::ComponentLayerDetail > &components );
QString createCompositionXml( const QList< QgsAbstractGeoPdfExporter::ComponentLayerDetail > &components, const ExportDetails &details );

friend class TestQgsLayoutGeoPdfExport;
friend class TestQgsGeoPdfExport;
Expand Down
143 changes: 139 additions & 4 deletions src/core/qgsmaprenderertask.cpp
Expand Up @@ -21,19 +21,98 @@
#include "qgsmapsettingsutils.h"
#include "qgsogrutils.h"
#include "qgslogger.h"
#include "qgsabstractgeopdfexporter.h"
#include "qgsmaprendererstagedrenderjob.h"
#include "qgsrenderedfeaturehandlerinterface.h"
#include "qgsfeaturerequest.h"
#include "qgsvectorlayer.h"
#include <QFile>
#include <QTextStream>
#include <QPrinter>

#include "gdal.h"
#include "cpl_conv.h"

QgsMapRendererTask::QgsMapRendererTask( const QgsMapSettings &ms, const QString &fileName, const QString &fileFormat, const bool forceRaster )

class QgsMapRendererTaskGeoPdfExporter : public QgsAbstractGeoPdfExporter
{

public:

QgsMapRendererTaskGeoPdfExporter( const QgsMapSettings &ms )
{
// collect details upfront, while we are still in the main thread
const QList< QgsMapLayer * > layers = ms.layers();
for ( const QgsMapLayer *layer : layers )
{
VectorComponentDetail detail;
detail.name = layer->name();
detail.mapLayerId = layer->id();
if ( const QgsVectorLayer *vl = qobject_cast< const QgsVectorLayer * >( layer ) )
{
detail.displayAttribute = vl->displayField();
}
mLayerDetails[ layer->id() ] = detail;
}
}

private:

QgsAbstractGeoPdfExporter::VectorComponentDetail componentDetailForLayerId( const QString &layerId ) override
{
return mLayerDetails.value( layerId );
}

QMap< QString, VectorComponentDetail > mLayerDetails;
};


class QgsMapRendererTaskRenderedFeatureHandler : public QgsRenderedFeatureHandlerInterface
{
public:

QgsMapRendererTaskRenderedFeatureHandler( QgsMapRendererTaskGeoPdfExporter *exporter )
: mExporter( exporter )
{}

void handleRenderedFeature( const QgsFeature &feature, const QgsGeometry &renderedBounds, const QgsRenderedFeatureHandlerInterface::RenderedFeatureContext &context ) override
{
// is it a hack retrieving the layer ID from an expression context like this? possibly... BUT
// the alternative is adding a layer ID member to QgsRenderContext, and that's just asking for people to abuse it
// and use it to retrieve QgsMapLayers mid-way through a render operation. Lesser of two evils it is!
const QString layerId = context.renderContext.expressionContext().variable( QStringLiteral( "layer_id" ) ).toString();

// transform from pixels to PDF coordinates (TODO - check)
QTransform pixelToPdfTransform = QTransform::fromScale( 25.4 / context.renderContext.scaleFactor() / 72.0, 25.4 / context.renderContext.scaleFactor() / 72.0 );
QgsGeometry transformed = renderedBounds;
transformed.transform( pixelToPdfTransform );

// always convert to multitype, to make things consistent
transformed.convertToMultiType();

mExporter->pushRenderedFeature( layerId, QgsAbstractGeoPdfExporter::RenderedFeature( feature, transformed ) );
}

QSet<QString> usedAttributes( QgsVectorLayer *, const QgsRenderContext & ) const override
{
return QSet< QString >() << QgsFeatureRequest::ALL_ATTRIBUTES;
}

private:

QgsMapRendererTaskGeoPdfExporter *mExporter = nullptr;

};



QgsMapRendererTask::QgsMapRendererTask( const QgsMapSettings &ms, const QString &fileName, const QString &fileFormat, const bool forceRaster, const bool geoPDF )
: QgsTask( tr( "Saving as image" ) )
, mMapSettings( ms )
, mFileName( fileName )
, mFileFormat( fileFormat )
, mForceRaster( forceRaster )
, mGeoPDF( geoPDF && mFileFormat == QStringLiteral( "PDF" ) && QgsAbstractGeoPdfExporter::geoPDFCreationAvailable() )
{
prepare();
}
Expand All @@ -46,6 +125,8 @@ QgsMapRendererTask::QgsMapRendererTask( const QgsMapSettings &ms, QPainter *p )
prepare();
}

QgsMapRendererTask::~QgsMapRendererTask() = default;

void QgsMapRendererTask::addAnnotations( const QList< QgsAnnotation * > &annotations )
{
qDeleteAll( mAnnotations );
Expand Down Expand Up @@ -79,7 +160,50 @@ bool QgsMapRendererTask::run()
if ( mErrored )
return false;

mJob->renderPrepared();
if ( mGeoPDF )
{

QList< QgsAbstractGeoPdfExporter::ComponentLayerDetail > pdfComponents;

QgsMapRendererStagedRenderJob *job = static_cast< QgsMapRendererStagedRenderJob * >( mJob.get() );
int outputLayer = 1;
while ( !job->isFinished() )
{
QgsAbstractGeoPdfExporter::ComponentLayerDetail component;
component.name = QStringLiteral( "layer_%1" ).arg( outputLayer );
component.mapLayerId = job->currentLayerId();
component.sourcePdfPath = mGeoPdfExporter->generateTemporaryFilepath( QStringLiteral( "layer_%1.pdf" ).arg( outputLayer ) );
pdfComponents << component;

QPrinter printer;
printer.setOutputFileName( component.sourcePdfPath );
printer.setOutputFormat( QPrinter::PdfFormat );
printer.setOrientation( QPrinter::Portrait );
// paper size needs to be given in millimeters in order to be able to set a resolution to pass onto the map renderer
QSizeF outputSize = mMapSettings.outputSize();
printer.setPaperSize( outputSize * 25.4 / mMapSettings.outputDpi(), QPrinter::Millimeter );
printer.setPageMargins( 0, 0, 0, 0, QPrinter::Millimeter );
printer.setResolution( mMapSettings.outputDpi() );

QPainter p( &printer );
job->renderCurrentPart( &p );
p.end();

outputLayer++;
job->nextPart();
}
QgsAbstractGeoPdfExporter::ExportDetails exportDetails;
exportDetails.pageSizeMm = mMapSettings.outputSize() * 25.4 / mMapSettings.outputDpi();
exportDetails.dpi = mMapSettings.outputDpi();

mGeoPdfExporter->finalize( pdfComponents, mFileName, exportDetails );
mGeoPdfExporter.reset();
mTempPainter.reset();
mPrinter.reset();
return true;
}
else
static_cast< QgsMapRendererCustomPainterJob *>( mJob.get() )->renderPrepared();

mJobMutex.lock();
mJob.reset( nullptr );
Expand Down Expand Up @@ -288,6 +412,17 @@ void QgsMapRendererTask::prepare()
return;
}

mJob.reset( new QgsMapRendererCustomPainterJob( mMapSettings, mDestPainter ) );
mJob->prepare();
if ( mGeoPDF )
{
mGeoPdfExporter = qgis::make_unique< QgsMapRendererTaskGeoPdfExporter >( mMapSettings );
mRenderedFeatureHandler = qgis::make_unique< QgsMapRendererTaskRenderedFeatureHandler >( static_cast< QgsMapRendererTaskGeoPdfExporter * >( mGeoPdfExporter.get() ) );
mMapSettings.addRenderedFeatureHandler( mRenderedFeatureHandler.get() );
mJob.reset( new QgsMapRendererStagedRenderJob( mMapSettings, QgsMapRendererStagedRenderJob::RenderLabelsByMapLayer ) );
mJob->start();
}
else
{
mJob.reset( new QgsMapRendererCustomPainterJob( mMapSettings, mDestPainter ) );
static_cast< QgsMapRendererCustomPainterJob *>( mJob.get() )->prepare();
}
}

0 comments on commit 93342a3

Please sign in to comment.