Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[layouts][api] Expose to public API the option to create separate
files for each logical layer in a layout

The backend for this was previously used only when creating a GeoPDF
export, where we create an individual PDF per layout logical layer
which are then composited by GDAL.

By publicly exposing this functionality via the QgsLayoutExporter API
we allow for plugins which can export layouts to seperate logical
PDF files, allowing for other (non-GDAL) tools to be used to later
composite the layers (e.g. Adobe Illustrator (which stupidly cannot
handle multi-layer single PDF files!))

Sponsored by SMEC/SJ
  • Loading branch information
nyalldawson committed May 18, 2020
1 parent 0d54cac commit 6e71130
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 64 deletions.
2 changes: 2 additions & 0 deletions python/core/auto_generated/layout/qgslayoutexporter.sip.in
Expand Up @@ -198,6 +198,8 @@ Constructor for PdfExportSettings

bool writeGeoPdf;

bool exportLayersAsSeperateFiles;

bool useIso32000ExtensionFormatGeoreferencing;

bool useOgcBestPracticeFormatGeoreferencing;
Expand Down
139 changes: 75 additions & 64 deletions src/core/layout/qgslayoutexporter.cpp
Expand Up @@ -528,7 +528,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
}

std::unique_ptr< QgsLayoutGeoPdfExporter > geoPdfExporter;
if ( settings.writeGeoPdf )
if ( settings.writeGeoPdf || settings.exportLayersAsSeperateFiles )
geoPdfExporter = qgis::make_unique< QgsLayoutGeoPdfExporter >( mLayout );

mLayout->renderContext().setFlags( settings.flags );
Expand All @@ -542,27 +542,31 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
mLayout->renderContext().setExportThemes( settings.exportThemes );

ExportResult result = Success;
if ( settings.writeGeoPdf )
if ( settings.writeGeoPdf || settings.exportLayersAsSeperateFiles )
{
mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagRenderLabelsByMapLayer, true );

// here we need to export layers to individual PDFs
PdfExportSettings subSettings = settings;
subSettings.writeGeoPdf = false;
subSettings.exportLayersAsSeperateFiles = false;

const QList<QGraphicsItem *> items = mLayout->items( Qt::AscendingOrder );

QList< QgsLayoutGeoPdfExporter::ComponentLayerDetail > pdfComponents;

auto exportFunc = [this, &subSettings, &pdfComponents, &geoPdfExporter]( unsigned int layerId, const QgsLayoutItem::ExportLayerDetail & layerDetail )->QgsLayoutExporter::ExportResult
const QDir baseDir = settings.exportLayersAsSeperateFiles ? QFileInfo( filePath ).dir() : QDir();
const QString baseFileName = settings.exportLayersAsSeperateFiles ? QFileInfo( filePath ).completeBaseName() : QString();

auto exportFunc = [this, &subSettings, &pdfComponents, &geoPdfExporter, &settings, &baseDir, &baseFileName]( unsigned int layerId, const QgsLayoutItem::ExportLayerDetail & layerDetail )->QgsLayoutExporter::ExportResult
{
ExportResult layerExportResult = Success;
QPrinter printer;
QgsLayoutGeoPdfExporter::ComponentLayerDetail component;
component.name = layerDetail.name;
component.mapLayerId = layerDetail.mapLayerId;
component.group = layerDetail.mapTheme;
component.sourcePdfPath = geoPdfExporter->generateTemporaryFilepath( QStringLiteral( "layer_%1.pdf" ).arg( layerId ) );
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;
preparePrintAsPdf( mLayout, printer, component.sourcePdfPath );
preparePrint( mLayout, printer, false );
Expand All @@ -581,72 +585,79 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
if ( result != Success )
return result;

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();

if ( settings.exportMetadata )
if ( settings.writeGeoPdf )
{
// copy layout metadata to GeoPDF export settings
details.author = mLayout->project()->metadata().author();
details.producer = QStringLiteral( "QGIS %1" ).arg( Qgis::version() );
details.creator = QStringLiteral( "QGIS %1" ).arg( Qgis::version() );
details.creationDateTime = mLayout->project()->metadata().creationDateTime();
details.subject = mLayout->project()->metadata().abstract();
details.title = mLayout->project()->metadata().title();
details.keywords = mLayout->project()->metadata().keywords();
}
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();

if ( settings.appendGeoreference )
{
// setup georeferencing
QList< QgsLayoutItemMap * > maps;
mLayout->layoutItems( maps );
for ( QgsLayoutItemMap *map : qgis::as_const( maps ) )
if ( settings.exportMetadata )
{
QgsAbstractGeoPdfExporter::GeoReferencedSection georef;
georef.crs = map->crs();

const QPointF topLeft = map->mapToScene( QPointF( 0, 0 ) );
const QPointF topRight = map->mapToScene( QPointF( map->rect().width(), 0 ) );
const QPointF bottomLeft = map->mapToScene( QPointF( 0, map->rect().height() ) );
const QPointF bottomRight = map->mapToScene( QPointF( map->rect().width(), map->rect().height() ) );
const QgsLayoutPoint topLeftMm = mLayout->convertFromLayoutUnits( topLeft, QgsUnitTypes::LayoutMillimeters );
const QgsLayoutPoint topRightMm = mLayout->convertFromLayoutUnits( topRight, QgsUnitTypes::LayoutMillimeters );
const QgsLayoutPoint bottomLeftMm = mLayout->convertFromLayoutUnits( bottomLeft, QgsUnitTypes::LayoutMillimeters );
const QgsLayoutPoint bottomRightMm = mLayout->convertFromLayoutUnits( bottomRight, QgsUnitTypes::LayoutMillimeters );

georef.pageBoundsPolygon.setExteriorRing( new QgsLineString( QVector< QgsPointXY >() << QgsPointXY( topLeftMm.x(), topLeftMm.y() )
<< QgsPointXY( topRightMm.x(), topRightMm.y() )
<< QgsPointXY( bottomRightMm.x(), bottomRightMm.y() )
<< QgsPointXY( bottomLeftMm.x(), bottomLeftMm.y() )
<< QgsPointXY( topLeftMm.x(), topLeftMm.y() ) ) );

georef.controlPoints.reserve( 4 );
const QTransform t = map->layoutToMapCoordsTransform();
const QgsPointXY topLeftMap = t.map( topLeft );
const QgsPointXY topRightMap = t.map( topRight );
const QgsPointXY bottomLeftMap = t.map( bottomLeft );
const QgsPointXY bottomRightMap = t.map( bottomRight );

georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( topLeftMm.x(), topLeftMm.y() ), topLeftMap );
georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( topRightMm.x(), topRightMm.y() ), topRightMap );
georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( bottomLeftMm.x(), bottomLeftMm.y() ), bottomLeftMap );
georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( bottomRightMm.x(), bottomRightMm.y() ), bottomRightMap );
details.georeferencedSections << georef;
// copy layout metadata to GeoPDF export settings
details.author = mLayout->project()->metadata().author();
details.producer = QStringLiteral( "QGIS %1" ).arg( Qgis::version() );
details.creator = QStringLiteral( "QGIS %1" ).arg( Qgis::version() );
details.creationDateTime = mLayout->project()->metadata().creationDateTime();
details.subject = mLayout->project()->metadata().abstract();
details.title = mLayout->project()->metadata().title();
details.keywords = mLayout->project()->metadata().keywords();
}
}

details.customLayerTreeGroups = geoPdfExporter->customLayerTreeGroups();
details.includeFeatures = settings.includeGeoPdfFeatures;
details.useOgcBestPracticeFormatGeoreferencing = settings.useOgcBestPracticeFormatGeoreferencing;
details.useIso32000ExtensionFormatGeoreferencing = settings.useIso32000ExtensionFormatGeoreferencing;
if ( settings.appendGeoreference )
{
// setup georeferencing
QList< QgsLayoutItemMap * > maps;
mLayout->layoutItems( maps );
for ( QgsLayoutItemMap *map : qgis::as_const( maps ) )
{
QgsAbstractGeoPdfExporter::GeoReferencedSection georef;
georef.crs = map->crs();

const QPointF topLeft = map->mapToScene( QPointF( 0, 0 ) );
const QPointF topRight = map->mapToScene( QPointF( map->rect().width(), 0 ) );
const QPointF bottomLeft = map->mapToScene( QPointF( 0, map->rect().height() ) );
const QPointF bottomRight = map->mapToScene( QPointF( map->rect().width(), map->rect().height() ) );
const QgsLayoutPoint topLeftMm = mLayout->convertFromLayoutUnits( topLeft, QgsUnitTypes::LayoutMillimeters );
const QgsLayoutPoint topRightMm = mLayout->convertFromLayoutUnits( topRight, QgsUnitTypes::LayoutMillimeters );
const QgsLayoutPoint bottomLeftMm = mLayout->convertFromLayoutUnits( bottomLeft, QgsUnitTypes::LayoutMillimeters );
const QgsLayoutPoint bottomRightMm = mLayout->convertFromLayoutUnits( bottomRight, QgsUnitTypes::LayoutMillimeters );

georef.pageBoundsPolygon.setExteriorRing( new QgsLineString( QVector< QgsPointXY >() << QgsPointXY( topLeftMm.x(), topLeftMm.y() )
<< QgsPointXY( topRightMm.x(), topRightMm.y() )
<< QgsPointXY( bottomRightMm.x(), bottomRightMm.y() )
<< QgsPointXY( bottomLeftMm.x(), bottomLeftMm.y() )
<< QgsPointXY( topLeftMm.x(), topLeftMm.y() ) ) );

georef.controlPoints.reserve( 4 );
const QTransform t = map->layoutToMapCoordsTransform();
const QgsPointXY topLeftMap = t.map( topLeft );
const QgsPointXY topRightMap = t.map( topRight );
const QgsPointXY bottomLeftMap = t.map( bottomLeft );
const QgsPointXY bottomRightMap = t.map( bottomRight );

georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( topLeftMm.x(), topLeftMm.y() ), topLeftMap );
georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( topRightMm.x(), topRightMm.y() ), topRightMap );
georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( bottomLeftMm.x(), bottomLeftMm.y() ), bottomLeftMap );
georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( bottomRightMm.x(), bottomRightMm.y() ), bottomRightMap );
details.georeferencedSections << georef;
}
}

if ( !geoPdfExporter->finalize( pdfComponents, filePath, details ) )
result = PrintError;
details.customLayerTreeGroups = geoPdfExporter->customLayerTreeGroups();
details.includeFeatures = settings.includeGeoPdfFeatures;
details.useOgcBestPracticeFormatGeoreferencing = settings.useOgcBestPracticeFormatGeoreferencing;
details.useIso32000ExtensionFormatGeoreferencing = settings.useIso32000ExtensionFormatGeoreferencing;

if ( !geoPdfExporter->finalize( pdfComponents, filePath, details ) )
result = PrintError;
}
else
{
result = Success;
}
}
else
{
Expand Down
15 changes: 15 additions & 0 deletions src/core/layout/qgslayoutexporter.h
Expand Up @@ -323,6 +323,21 @@ class CORE_EXPORT QgsLayoutExporter
*/
bool writeGeoPdf = false;

/**
* TRUE if individual layers from the layout should be rendered to separate PDF files.
*
* This option allows for separation of logic layout layers to individual PDF files. For instance,
* if this option is TRUE, then a separate PDF file will be created per layer per map item in the
* layout. Additionally, separate PDF files may be created for other complex layout items, resulting
* in a set of PDF files which contain logical atomic components of the layout.
*
* This option is designed to allow the PDF files to be composited back together in an external
* application (e.g. Adobe Illustrator) as a non-QGIS, post-production step.
*
* \since QGIS 3.14
*/
bool exportLayersAsSeperateFiles = false;

/**
* TRUE if ISO3200 extension format georeferencing should be used.
*
Expand Down

0 comments on commit 6e71130

Please sign in to comment.