QGIS API Documentation 4.1.0-Master (3b8ef1f72a3)
Loading...
Searching...
No Matches
qgsabstractgeopdfexporter.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsabtractgeopdfexporter.cpp
3 --------------------------
4 begin : August 2019
5 copyright : (C) 2019 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8/***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16
18
19#include <cpl_string.h>
20#include <gdal.h>
21
23#include "qgsfileutils.h"
24#include "qgsgeometry.h"
25#include "qgslogger.h"
26#include "qgsvectorfilewriter.h"
27#include "qgsvectorlayer.h"
28
29#include <QDomDocument>
30#include <QDomElement>
31#include <QMutex>
32#include <QMutexLocker>
33#include <QString>
34#include <QTextStream>
35#include <QTimeZone>
36#include <QUuid>
37
38using namespace Qt::StringLiterals;
39
41{
42 // test if GDAL has read support in PDF driver
43 GDALDriverH hDriverMem = GDALGetDriverByName( "PDF" );
44 if ( !hDriverMem )
45 {
46 return false;
47 }
48
49 const char *pHavePoppler = GDALGetMetadataItem( hDriverMem, "HAVE_POPPLER", nullptr );
50 if ( pHavePoppler && strstr( pHavePoppler, "YES" ) )
51 return true;
52
53 const char *pHavePdfium = GDALGetMetadataItem( hDriverMem, "HAVE_PDFIUM", nullptr );
54 if ( pHavePdfium && strstr( pHavePdfium, "YES" ) )
55 return true;
56
57 return false;
58}
59
61{
62 // test if GDAL has read support in PDF driver
63 GDALDriverH hDriverMem = GDALGetDriverByName( "PDF" );
64 if ( !hDriverMem )
65 {
66 return QObject::tr( "No GDAL PDF driver available." );
67 }
68
69 const char *pHavePoppler = GDALGetMetadataItem( hDriverMem, "HAVE_POPPLER", nullptr );
70 if ( pHavePoppler && strstr( pHavePoppler, "YES" ) )
71 return QString();
72
73 const char *pHavePdfium = GDALGetMetadataItem( hDriverMem, "HAVE_PDFIUM", nullptr );
74 if ( pHavePdfium && strstr( pHavePdfium, "YES" ) )
75 return QString();
76
77 return QObject::tr( "GDAL PDF driver was not built with PDF read support. A build with PDF read support is required for geospatial PDF creation." );
78}
79
80void CPL_STDCALL collectErrors( CPLErr, int, const char *msg )
81{
82 QgsDebugError( u"GDAL PDF creation error: %1 "_s.arg( msg ) );
83 if ( QStringList *errorList = static_cast< QStringList * >( CPLGetErrorHandlerUserData() ) )
84 {
85 errorList->append( QString( msg ) );
86 }
87}
88
89bool QgsAbstractGeospatialPdfExporter::finalize( const QList<ComponentLayerDetail> &components, const QString &destinationFile, const ExportDetails &details )
90{
91 if ( details.includeFeatures && !saveTemporaryLayers() )
92 return false;
93
94 const QString composition = createCompositionXml( components, details );
95 QgsDebugMsgLevel( composition, 2 );
96 if ( composition.isEmpty() )
97 return false;
98
99 // do the creation!
100 GDALDriverH driver = GDALGetDriverByName( "PDF" );
101 if ( !driver )
102 {
103 mErrorMessage = QObject::tr( "Cannot load GDAL PDF driver" );
104 return false;
105 }
106
107 const QString xmlFilePath = generateTemporaryFilepath( u"composition.xml"_s );
108 QFile file( xmlFilePath );
109 if ( file.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
110 {
111 QTextStream out( &file );
112 out << composition;
113 }
114 else
115 {
116 mErrorMessage = QObject::tr( "Could not create geospatial PDF composition file" );
117 return false;
118 }
119
120 char **papszOptions = CSLSetNameValue( nullptr, "COMPOSITION_FILE", xmlFilePath.toUtf8().constData() );
121
122 QStringList creationErrors;
123 CPLPushErrorHandlerEx( collectErrors, &creationErrors );
124
125 // return a non-null (fake) dataset in case of success, nullptr otherwise.
126 gdal::dataset_unique_ptr outputDataset( GDALCreate( driver, destinationFile.toUtf8().constData(), 0, 0, 0, GDT_Unknown, papszOptions ) );
127
128 CPLPopErrorHandler();
129 // Keep explicit comparison to avoid confusing cppcheck
130 const bool res = outputDataset.get() != nullptr;
131 if ( !res )
132 {
133 if ( creationErrors.size() == 1 )
134 {
135 mErrorMessage = QObject::tr( "Could not create PDF file: %1" ).arg( creationErrors.at( 0 ) );
136 }
137 else if ( !creationErrors.empty() )
138 {
139 mErrorMessage = QObject::tr( "Could not create PDF file. Received errors:\n" );
140 for ( const QString &error : std::as_const( creationErrors ) )
141 {
142 mErrorMessage += ( !mErrorMessage.isEmpty() ? u"\n"_s : QString() ) + error;
143 }
144 }
145 else
146 {
147 mErrorMessage = QObject::tr( "Could not create PDF file, but no error details are available" );
148 }
149 }
150 outputDataset.reset();
151
152 CSLDestroy( papszOptions );
153
154 return res;
155}
156
158{
159 return mTemporaryDir.filePath( QgsFileUtils::stringToSafeFilename( filename ) );
160}
161
163{
164 switch ( mode )
165 {
166 case QPainter::CompositionMode_SourceOver:
167 case QPainter::CompositionMode_Multiply:
168 case QPainter::CompositionMode_Screen:
169 case QPainter::CompositionMode_Overlay:
170 case QPainter::CompositionMode_Darken:
171 case QPainter::CompositionMode_Lighten:
172 case QPainter::CompositionMode_ColorDodge:
173 case QPainter::CompositionMode_ColorBurn:
174 case QPainter::CompositionMode_HardLight:
175 case QPainter::CompositionMode_SoftLight:
176 case QPainter::CompositionMode_Difference:
177 case QPainter::CompositionMode_Exclusion:
178 return true;
179
180 default:
181 break;
182 }
183
184 return false;
185}
186
188{
189 // because map layers may be rendered in parallel, we need a mutex here
190 QMutexLocker locker( &mMutex );
191
192 // collate all the features which belong to the same layer, replacing their geometries with the rendered feature bounds
193 QgsFeature f = feature.feature;
194 f.setGeometry( feature.renderedBounds );
195 mCollatedFeatures[group][layerId].append( f );
196}
197
198bool QgsAbstractGeospatialPdfExporter::saveTemporaryLayers()
199{
200 for ( auto groupIt = mCollatedFeatures.constBegin(); groupIt != mCollatedFeatures.constEnd(); ++groupIt )
201 {
202 for ( auto it = groupIt->constBegin(); it != groupIt->constEnd(); ++it )
203 {
204 const QString filePath = generateTemporaryFilepath( it.key() + groupIt.key() + u".gpkg"_s );
205
206 VectorComponentDetail detail = componentDetailForLayerId( it.key() );
207 detail.sourceVectorPath = filePath;
208 detail.group = groupIt.key();
209
210 // write out features to disk
211 const QgsFeatureList features = it.value();
212 QString layerName;
214 saveOptions.driverName = u"GPKG"_s;
216 std::unique_ptr< QgsVectorFileWriter > writer(
218 create( filePath, features.first().fields(), features.first().geometry().wkbType(), QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext(), saveOptions, QgsFeatureSink::RegeneratePrimaryKey, nullptr, &layerName )
219 );
220 if ( writer->hasError() )
221 {
222 mErrorMessage = writer->errorMessage();
223 QgsDebugError( mErrorMessage );
224 return false;
225 }
226 for ( const QgsFeature &feature : features )
227 {
228 QgsFeature f = feature;
229 if ( !writer->addFeature( f, QgsFeatureSink::FastInsert ) )
230 {
231 mErrorMessage = writer->errorMessage();
232 QgsDebugError( mErrorMessage );
233 return false;
234 }
235 }
236 detail.sourceVectorLayer = layerName;
237 mVectorComponents << detail;
238 }
239 }
240 return true;
241}
242
244void sortTreeNodeList( std::vector<std::unique_ptr<TreeNode>> &nodeList, QStringList layerOrder )
245{
246 std::sort( nodeList.begin(), nodeList.end(), [&layerOrder]( const std::unique_ptr< TreeNode > &a, const std::unique_ptr< TreeNode > &b ) -> bool {
247 const qsizetype indexA = layerOrder.indexOf( a->mapLayerId );
248 const qsizetype indexB = layerOrder.indexOf( b->mapLayerId );
249
250 if ( indexA >= 0 && indexB >= 0 )
251 return indexA < indexB;
252 else if ( indexA >= 0 )
253 return true;
254 else if ( indexB >= 0 )
255 return false;
256
257 return a->name.localeAwareCompare( b->name ) < 0;
258 } );
259}
260
261QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QList<ComponentLayerDetail> &components, const ExportDetails &details ) const
262{
263 QDomDocument doc;
264 QDomElement compositionElem = doc.createElement( u"PDFComposition"_s );
265
266 // metadata
267 createMetadataXmlSection( compositionElem, doc, details );
268
269 // pages
270 QDomElement page = doc.createElement( u"Page"_s );
271 // hardcode DPI of 72 to get correct page sizes in outputs -- refs discussion in https://github.com/OSGeo/gdal/pull/2961
272 const double pageWidthPdfUnits = std::ceil( details.pageSizeMm.width() / 25.4 * DPI_72 );
273 const double pageHeightPdfUnits = std::ceil( details.pageSizeMm.height() / 25.4 * DPI_72 );
274 createPageDimensionXmlSection( page, doc, pageWidthPdfUnits, pageHeightPdfUnits );
275
276 // georeferencing
277 createGeoreferencingXmlSection( page, doc, details, pageWidthPdfUnits, pageHeightPdfUnits );
278
279 // layer tree and content
280 QgsLayerTree *layerTreePointer = layerTree();
281 if ( details.useLayerTreeConfig && layerTreePointer )
282 {
283 createLayerTreeAndContentXmlSectionsFromLayerTree( layerTreePointer, compositionElem, page, doc, components, details );
284 }
285 else
286 {
287 createLayerTreeAndContentXmlSections( compositionElem, page, doc, components, details );
288 }
289
290 compositionElem.appendChild( page );
291 doc.appendChild( compositionElem );
292
293 QString composition;
294 QTextStream stream( &composition );
295 doc.save( stream, -1 );
296
297 return composition;
298}
299
300void QgsAbstractGeospatialPdfExporter::createMetadataXmlSection( QDomElement &compositionElem, QDomDocument &doc, const ExportDetails &details ) const
301{
302 // metadata tags
303 QDomElement metadata = doc.createElement( u"Metadata"_s );
304 if ( !details.author.isEmpty() )
305 {
306 QDomElement author = doc.createElement( u"Author"_s );
307 author.appendChild( doc.createTextNode( details.author ) );
308 metadata.appendChild( author );
309 }
310 if ( !details.producer.isEmpty() )
311 {
312 QDomElement producer = doc.createElement( u"Producer"_s );
313 producer.appendChild( doc.createTextNode( details.producer ) );
314 metadata.appendChild( producer );
315 }
316 if ( !details.creator.isEmpty() )
317 {
318 QDomElement creator = doc.createElement( u"Creator"_s );
319 creator.appendChild( doc.createTextNode( details.creator ) );
320 metadata.appendChild( creator );
321 }
322 if ( details.creationDateTime.isValid() )
323 {
324 QDomElement creationDate = doc.createElement( u"CreationDate"_s );
325 QString creationDateString = u"D:%1"_s.arg( details.creationDateTime.toString( u"yyyyMMddHHmmss"_s ) );
326#if QT_FEATURE_timezone > 0
327 if ( details.creationDateTime.timeZone().isValid() )
328 {
329 int offsetFromUtc = details.creationDateTime.timeZone().offsetFromUtc( details.creationDateTime );
330 creationDateString += ( offsetFromUtc >= 0 ) ? '+' : '-';
331 offsetFromUtc = std::abs( offsetFromUtc );
332 int offsetHours = offsetFromUtc / 3600;
333 int offsetMins = ( offsetFromUtc % 3600 ) / 60;
334 creationDateString += u"%1'%2'"_s.arg( offsetHours ).arg( offsetMins );
335 }
336#else
337 QgsDebugError( u"Qt is built without timezone support, skipping timezone for pdf export"_s );
338#endif
339 creationDate.appendChild( doc.createTextNode( creationDateString ) );
340 metadata.appendChild( creationDate );
341 }
342 if ( !details.subject.isEmpty() )
343 {
344 QDomElement subject = doc.createElement( u"Subject"_s );
345 subject.appendChild( doc.createTextNode( details.subject ) );
346 metadata.appendChild( subject );
347 }
348 if ( !details.title.isEmpty() )
349 {
350 QDomElement title = doc.createElement( u"Title"_s );
351 title.appendChild( doc.createTextNode( details.title ) );
352 metadata.appendChild( title );
353 }
354 if ( !details.keywords.empty() )
355 {
356 QStringList allKeywords;
357 for ( auto it = details.keywords.constBegin(); it != details.keywords.constEnd(); ++it )
358 {
359 allKeywords.append( u"%1: %2"_s.arg( it.key(), it.value().join( ',' ) ) );
360 }
361 QDomElement keywords = doc.createElement( u"Keywords"_s );
362 keywords.appendChild( doc.createTextNode( allKeywords.join( ';' ) ) );
363 metadata.appendChild( keywords );
364 }
365 compositionElem.appendChild( metadata );
366}
367
368void QgsAbstractGeospatialPdfExporter::createGeoreferencingXmlSection(
369 QDomElement &pageElem, QDomDocument &doc, const ExportDetails &details, const double pageWidthPdfUnits, const double pageHeightPdfUnits
370) const
371{
372 int i = 0;
373 for ( const QgsAbstractGeospatialPdfExporter::GeoReferencedSection &section : details.georeferencedSections )
374 {
375 QDomElement georeferencing = doc.createElement( u"Georeferencing"_s );
376 georeferencing.setAttribute( u"id"_s, u"georeferenced_%1"_s.arg( i++ ) );
377 georeferencing.setAttribute( u"ISO32000ExtensionFormat"_s, details.useIso32000ExtensionFormatGeoreferencing ? u"true"_s : u"false"_s );
378
379 if ( section.crs.isValid() )
380 {
381 QDomElement srs = doc.createElement( u"SRS"_s );
382 // not currently used by GDAL or the PDF spec, but exposed in the GDAL XML schema. Maybe something we'll need to consider down the track...
383 // srs.setAttribute( u"dataAxisToSRSAxisMapping"_s, u"2,1"_s );
384 if ( !section.crs.authid().isEmpty() && !section.crs.authid().startsWith( u"user"_s, Qt::CaseInsensitive ) )
385 {
386 srs.appendChild( doc.createTextNode( section.crs.authid() ) );
387 }
388 else
389 {
390 srs.appendChild( doc.createTextNode( section.crs.toWkt( Qgis::CrsWktVariant::PreferredGdal ) ) );
391 }
392 georeferencing.appendChild( srs );
393 }
394
395 if ( !section.pageBoundsPolygon.isEmpty() )
396 {
397 /*
398 Define a polygon / neatline in PDF units into which the
399 Measure tool will display coordinates.
400 If not specified, BoundingBox will be used instead.
401 If none of BoundingBox and BoundingPolygon are specified,
402 the whole PDF page will be assumed to be georeferenced.
403 */
404 QDomElement boundingPolygon = doc.createElement( u"BoundingPolygon"_s );
405
406 // transform to PDF coordinate space
407 QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(), -pageHeightPdfUnits / details.pageSizeMm.height() );
408
409 QgsPolygon p = section.pageBoundsPolygon;
410 p.transform( t );
411 boundingPolygon.appendChild( doc.createTextNode( p.asWkt() ) );
412
413 georeferencing.appendChild( boundingPolygon );
414 }
415 else
416 {
417 /* Define the viewport where georeferenced coordinates are available.
418 If not specified, the extent of BoundingPolygon will be used instead.
419 If none of BoundingBox and BoundingPolygon are specified,
420 the whole PDF page will be assumed to be georeferenced.
421 */
422 QDomElement boundingBox = doc.createElement( u"BoundingBox"_s );
423 boundingBox.setAttribute( u"x1"_s, qgsDoubleToString( section.pageBoundsMm.xMinimum() / 25.4 * DPI_72 ) );
424 boundingBox.setAttribute( u"y1"_s, qgsDoubleToString( section.pageBoundsMm.yMinimum() / 25.4 * DPI_72 ) );
425 boundingBox.setAttribute( u"x2"_s, qgsDoubleToString( section.pageBoundsMm.xMaximum() / 25.4 * DPI_72 ) );
426 boundingBox.setAttribute( u"y2"_s, qgsDoubleToString( section.pageBoundsMm.yMaximum() / 25.4 * DPI_72 ) );
427 georeferencing.appendChild( boundingBox );
428 }
429
430 for ( const ControlPoint &point : section.controlPoints )
431 {
432 QDomElement cp1 = doc.createElement( u"ControlPoint"_s );
433 cp1.setAttribute( u"x"_s, qgsDoubleToString( point.pagePoint.x() / 25.4 * DPI_72 ) );
434 cp1.setAttribute( u"y"_s, qgsDoubleToString( ( details.pageSizeMm.height() - point.pagePoint.y() ) / 25.4 * DPI_72 ) );
435 cp1.setAttribute( u"GeoX"_s, qgsDoubleToString( point.geoPoint.x() ) );
436 cp1.setAttribute( u"GeoY"_s, qgsDoubleToString( point.geoPoint.y() ) );
437 georeferencing.appendChild( cp1 );
438 }
439
440 pageElem.appendChild( georeferencing );
441 }
442}
443
444void QgsAbstractGeospatialPdfExporter::createPageDimensionXmlSection( QDomElement &pageElem, QDomDocument &doc, const double pageWidthPdfUnits, const double pageHeightPdfUnits ) const
445{
446 QDomElement dpi = doc.createElement( u"DPI"_s );
447 // hardcode DPI of 72 to get correct page sizes in outputs -- refs discussion in https://github.com/OSGeo/gdal/pull/2961
448 dpi.appendChild( doc.createTextNode( qgsDoubleToString( DPI_72 ) ) );
449 pageElem.appendChild( dpi );
450 // assumes DPI of 72, as noted above.
451 QDomElement width = doc.createElement( u"Width"_s );
452 width.appendChild( doc.createTextNode( qgsDoubleToString( pageWidthPdfUnits ) ) );
453 pageElem.appendChild( width );
454 QDomElement height = doc.createElement( u"Height"_s );
455 height.appendChild( doc.createTextNode( qgsDoubleToString( pageHeightPdfUnits ) ) );
456 pageElem.appendChild( height );
457}
458
459void QgsAbstractGeospatialPdfExporter::createLayerTreeAndContentXmlSections(
460 QDomElement &compositionElem, QDomElement &pageElem, QDomDocument &doc, const QList<ComponentLayerDetail> &components, const ExportDetails &details
461) const
462{
463 QSet< QString > createdLayerIds;
464 std::vector< std::unique_ptr< TreeNode > > rootGroups;
465 std::vector< std::unique_ptr< TreeNode > > rootLayers;
466 QMap< QString, TreeNode * > groupNameMap;
467
468 QStringList layerTreeGroupOrder = details.layerTreeGroupOrder;
469
470 // add any missing groups to end of order
471 // Missing groups from the explicitly set custom layer tree groups
472 for ( auto it = details.customLayerTreeGroups.constBegin(); it != details.customLayerTreeGroups.constEnd(); ++it )
473 {
474 if ( layerTreeGroupOrder.contains( it.value() ) )
475 continue;
476 layerTreeGroupOrder.append( it.value() );
477 }
478
479 // Missing groups from vector components
480 if ( details.includeFeatures )
481 {
482 for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
483 {
484 if ( !component.group.isEmpty() && !layerTreeGroupOrder.contains( component.group ) )
485 {
486 layerTreeGroupOrder.append( component.group );
487 }
488 }
489 }
490
491 // missing groups from other components
492 for ( const ComponentLayerDetail &component : components )
493 {
494 if ( !component.group.isEmpty() && !layerTreeGroupOrder.contains( component.group ) )
495 {
496 layerTreeGroupOrder.append( component.group );
497 }
498 }
499
500 // now we are confident that we have a definitive list of all the groups for the export
501 QMap< QString, TreeNode * > groupNameToTreeNode;
502 QMap< QString, TreeNode * > layerIdToTreeNode;
503
504 auto createGroup = [&details, &groupNameToTreeNode]( const QString &groupName ) -> std::unique_ptr< TreeNode > {
505 auto group = std::make_unique< TreeNode >();
506 const QString id = QUuid::createUuid().toString();
507 group->id = id;
508 groupNameToTreeNode[groupName] = group.get();
509
510 group->name = groupName;
511 group->initiallyVisible = true;
512 if ( details.mutuallyExclusiveGroups.contains( groupName ) )
513 group->mutuallyExclusiveGroupId = u"__mutually_exclusive_groups__"_s;
514 return group;
515 };
516
517 if ( details.includeFeatures )
518 {
519 for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
520 {
521 const QString destinationGroup = details.customLayerTreeGroups.value( component.mapLayerId, component.group );
522 const QString id = destinationGroup.isEmpty() ? component.mapLayerId : u"%1_%2"_s.arg( destinationGroup, component.mapLayerId );
523
524 auto layer = std::make_unique< TreeNode >();
525 layer->id = id;
526 layer->name = details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name;
527 layer->initiallyVisible = details.initialLayerVisibility.value( component.mapLayerId, true );
528 layer->mapLayerId = component.mapLayerId;
529
530 layerIdToTreeNode.insert( layer->id, layer.get() );
531 if ( !destinationGroup.isEmpty() )
532 {
533 if ( TreeNode *groupNode = groupNameMap.value( destinationGroup ) )
534 {
535 groupNode->addChild( std::move( layer ) );
536 }
537 else
538 {
539 std::unique_ptr< TreeNode > group = createGroup( destinationGroup );
540 group->addChild( std::move( layer ) );
541 groupNameMap.insert( destinationGroup, group.get() );
542 rootGroups.emplace_back( std::move( group ) );
543 }
544 }
545 else
546 {
547 rootLayers.emplace_back( std::move( layer ) );
548 }
549
550 createdLayerIds.insert( id );
551 }
552 }
553
554 // some PDF components may not be linked to vector components - e.g.
555 // - layers with labels but no features
556 // - raster layers
557 // - legends and other map content
558 for ( const ComponentLayerDetail &component : components )
559 {
560 const QString destinationGroup = details.customLayerTreeGroups.value( component.mapLayerId, component.group );
561 const QString id = destinationGroup.isEmpty() ? component.mapLayerId : u"%1_%2"_s.arg( destinationGroup, component.mapLayerId );
562
563 if ( !component.mapLayerId.isEmpty() && createdLayerIds.contains( id ) )
564 continue;
565
566 if ( destinationGroup.isEmpty() && component.mapLayerId.isEmpty() )
567 continue;
568
569 std::unique_ptr< TreeNode > mapLayerNode;
570 if ( !component.mapLayerId.isEmpty() )
571 {
572 mapLayerNode = std::make_unique< TreeNode >();
573 mapLayerNode->id = id;
574 mapLayerNode->name = details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId, component.name );
575 mapLayerNode->initiallyVisible = details.initialLayerVisibility.value( component.mapLayerId, true );
576 mapLayerNode->mapLayerId = component.mapLayerId;
577
578 layerIdToTreeNode.insert( mapLayerNode->id, mapLayerNode.get() );
579 }
580
581 if ( !destinationGroup.isEmpty() )
582 {
583 if ( TreeNode *groupNode = groupNameMap.value( destinationGroup ) )
584 {
585 if ( mapLayerNode )
586 groupNode->addChild( std::move( mapLayerNode ) );
587 }
588 else
589 {
590 std::unique_ptr< TreeNode > group = createGroup( destinationGroup );
591 if ( mapLayerNode )
592 group->addChild( std::move( mapLayerNode ) );
593 groupNameMap.insert( destinationGroup, group.get() );
594 rootGroups.emplace_back( std::move( group ) );
595 }
596 }
597 else
598 {
599 if ( mapLayerNode )
600 rootLayers.emplace_back( std::move( mapLayerNode ) );
601 }
602
603 if ( !component.mapLayerId.isEmpty() )
604 {
605 createdLayerIds.insert( id );
606 }
607 }
608
609 QDomElement contentElem = doc.createElement( u"Content"_s );
610 createContentXmlSection( contentElem, doc, groupNameToTreeNode, layerIdToTreeNode, components, details );
611
612 pageElem.appendChild( contentElem );
613
614 // layertree
615 QDomElement layerTree = doc.createElement( u"LayerTree"_s );
616 //layerTree.setAttribute( u"displayOnlyOnVisiblePages"_s, u"true"_s);
617
618 // groups are added first
619
620 // sort root groups in desired order
621 std::sort( rootGroups.begin(), rootGroups.end(), [&layerTreeGroupOrder]( const std::unique_ptr< TreeNode > &a, const std::unique_ptr< TreeNode > &b ) -> bool {
622 return layerTreeGroupOrder.indexOf( a->name ) < layerTreeGroupOrder.indexOf( b->name );
623 } );
624
625 // sort group children
626 for ( const std::unique_ptr<TreeNode> &rootGroup : rootGroups )
627 {
628 sortTreeNodeList( rootGroup->children, details.layerOrder );
629 }
630
631 bool haveFoundMutuallyExclusiveGroup = false;
632 for ( const auto &node : std::as_const( rootGroups ) )
633 {
634 if ( !node->mutuallyExclusiveGroupId.isEmpty() )
635 {
636 // only the first object in a mutually exclusive group is initially visible
637 node->initiallyVisible = !haveFoundMutuallyExclusiveGroup;
638 haveFoundMutuallyExclusiveGroup = true;
639 }
640 layerTree.appendChild( node->toElement( doc ) );
641 }
642
643 // filter out groups which don't have any content
644 layerTreeGroupOrder.erase(
645 std::remove_if( layerTreeGroupOrder.begin(), layerTreeGroupOrder.end(), [&details]( const QString &group ) { return details.customLayerTreeGroups.key( group ).isEmpty(); } ),
646 layerTreeGroupOrder.end()
647 );
648
649
650 // then top-level layers
651 sortTreeNodeList( rootLayers, details.layerOrder );
652
653 for ( const auto &node : std::as_const( rootLayers ) )
654 {
655 layerTree.appendChild( node->toElement( doc ) );
656 }
657
658 compositionElem.appendChild( layerTree );
659}
660
661void QgsAbstractGeospatialPdfExporter::createLayerTreeAndContentXmlSectionsFromLayerTree(
662 const QgsLayerTree *layerTree, QDomElement &compositionElem, QDomElement &pageElem, QDomDocument &doc, const QList<ComponentLayerDetail> &components, const ExportDetails &details
663) const
664{
665 QMap< QString, TreeNode * > groupNameToTreeNode;
666 QMap< QString, TreeNode * > layerIdToTreeNode;
667
668 // Add tree structure from QGIS layer tree to the intermediate TreeNode struct
669 std::unique_ptr< TreeNode > rootPdfNode = createPdfTreeNodes( groupNameToTreeNode, layerIdToTreeNode, layerTree );
670 rootPdfNode->isRootNode = true; // To skip the layer tree root from the PDF layer tree
671
672 // Add missing groups from other components
673 for ( const ComponentLayerDetail &component : components )
674 {
675 if ( !component.group.isEmpty() && !groupNameToTreeNode.contains( component.group ) )
676 {
677 auto pdfTreeGroup = std::make_unique< TreeNode >();
678 const QString id = QUuid::createUuid().toString();
679 pdfTreeGroup->id = id;
680 pdfTreeGroup->name = component.group;
681 pdfTreeGroup->initiallyVisible = true;
682 groupNameToTreeNode[pdfTreeGroup->name] = pdfTreeGroup.get();
683 rootPdfNode->addChild( std::move( pdfTreeGroup ) );
684 }
685 }
686
687 QDomElement contentElem = doc.createElement( u"Content"_s );
688 createContentXmlSection( contentElem, doc, groupNameToTreeNode, layerIdToTreeNode, components, details );
689
690 pageElem.appendChild( contentElem );
691
692 // PDF Layer Tree
693 QDomElement layerTreeElem = doc.createElement( u"LayerTree"_s );
694 rootPdfNode->toChildrenElements( doc, layerTreeElem );
695 compositionElem.appendChild( layerTreeElem );
696}
697
698void QgsAbstractGeospatialPdfExporter::createContentXmlSection(
699 QDomElement &contentElem,
700 QDomDocument &doc,
701 const QMap< QString, TreeNode * > &groupNameToTreeNode,
702 const QMap< QString, TreeNode * > &layerIdToTreeNode,
703 const QList<ComponentLayerDetail> &components,
704 const ExportDetails &details
705) const
706{
707 auto createPdfDatasetElement = [&doc]( const ComponentLayerDetail &component ) -> QDomElement {
708 QDomElement pdfDataset = doc.createElement( u"PDF"_s );
709 pdfDataset.setAttribute( u"dataset"_s, component.sourcePdfPath );
710 if ( component.opacity != 1.0 || component.compositionMode != QPainter::CompositionMode_SourceOver )
711 {
712 QDomElement blendingElement = doc.createElement( u"Blending"_s );
713 blendingElement.setAttribute( u"opacity"_s, component.opacity );
714 blendingElement.setAttribute( u"function"_s, compositionModeToString( component.compositionMode ) );
715
716 pdfDataset.appendChild( blendingElement );
717 }
718 return pdfDataset;
719 };
720
721 // PDF Content
722 for ( const ComponentLayerDetail &component : components )
723 {
724 if ( component.mapLayerId.isEmpty() && component.group.isEmpty() )
725 {
726 contentElem.appendChild( createPdfDatasetElement( component ) );
727 }
728 else if ( !component.mapLayerId.isEmpty() )
729 {
730 const QString destinationGroup = details.customLayerTreeGroups.value( component.mapLayerId, component.group );
731 const QString id = destinationGroup.isEmpty() ? component.mapLayerId : u"%1_%2"_s.arg( destinationGroup, component.mapLayerId );
732 if ( TreeNode *treeNode = layerIdToTreeNode.value( id ) )
733 {
734 QDomElement ifLayerOnElement = treeNode->createNestedIfLayerOnElements( doc, contentElem );
735 ifLayerOnElement.appendChild( createPdfDatasetElement( component ) );
736 }
737 }
738 else if ( TreeNode *groupNode = groupNameToTreeNode.value( component.group ) )
739 {
740 QDomElement ifGroupOn = groupNode->createIfLayerOnElement( doc, contentElem );
741 ifGroupOn.appendChild( createPdfDatasetElement( component ) );
742 }
743 }
744
745 // vector datasets (we "draw" these on top, just for debugging... but they are invisible, so are never really drawn!)
746 if ( details.includeFeatures )
747 {
748 for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
749 {
750 const QString destinationGroup = details.customLayerTreeGroups.value( component.mapLayerId, component.group );
751 const QString id = destinationGroup.isEmpty() ? component.mapLayerId : u"%1_%2"_s.arg( destinationGroup, component.mapLayerId );
752 if ( TreeNode *treeNode = layerIdToTreeNode.value( id ) )
753 {
754 QDomElement ifLayerOnElement = treeNode->createNestedIfLayerOnElements( doc, contentElem );
755
756 QDomElement vectorDataset = doc.createElement( u"Vector"_s );
757 vectorDataset.setAttribute( u"dataset"_s, component.sourceVectorPath );
758 vectorDataset.setAttribute( u"layer"_s, component.sourceVectorLayer );
759 vectorDataset.setAttribute( u"visible"_s, u"false"_s );
760 QDomElement logicalStructure = doc.createElement( u"LogicalStructure"_s );
761 logicalStructure.setAttribute( u"displayLayerName"_s, component.name );
762 if ( !component.displayAttribute.isEmpty() )
763 logicalStructure.setAttribute( u"fieldToDisplay"_s, component.displayAttribute );
764 vectorDataset.appendChild( logicalStructure );
765 ifLayerOnElement.appendChild( vectorDataset );
766 }
767 }
768 }
769}
770
771std::unique_ptr< TreeNode > QgsAbstractGeospatialPdfExporter::createPdfTreeNodes(
772 QMap< QString, TreeNode * > &groupNameToTreeNode, QMap< QString, TreeNode * > &layerIdToTreeNode, const QgsLayerTreeGroup *layerTreeGroup
773) const
774{
775 auto pdfTreeNodes = std::make_unique< TreeNode >();
776 const QString id = QUuid::createUuid().toString();
777 pdfTreeNodes->id = id;
778 pdfTreeNodes->name = layerTreeGroup->name();
779 pdfTreeNodes->initiallyVisible = layerTreeGroup->itemVisibilityChecked();
780
781 const QList<QgsLayerTreeNode *> groupChildren = layerTreeGroup->children();
782
783 for ( QgsLayerTreeNode *qgisNode : groupChildren )
784 {
785 switch ( qgisNode->nodeType() )
786 {
788 {
789 QgsLayerTreeLayer *layerTreeLayer = qobject_cast<QgsLayerTreeLayer *>( qgisNode );
790
791 // Skip invalid layers, tables and unknown geometry types, since they won't appear in the PDF
792 if ( !layerTreeLayer->layer()->isValid() )
793 break;
794
795 if ( layerTreeLayer->layer()->type() == Qgis::LayerType::Vector )
796 {
797 QgsVectorLayer *vectorLayer = qobject_cast< QgsVectorLayer * >( layerTreeLayer->layer() );
798 if ( vectorLayer->geometryType() == Qgis::GeometryType::Unknown || vectorLayer->geometryType() == Qgis::GeometryType::Null )
799 break;
800 }
801
802 auto pdfLayerNode = std::make_unique< TreeNode >();
803 pdfLayerNode->id = layerTreeLayer->layerId();
804 pdfLayerNode->name = layerTreeLayer->name();
805 pdfLayerNode->initiallyVisible = layerTreeLayer->itemVisibilityChecked();
806 pdfLayerNode->mapLayerId = layerTreeLayer->layerId();
807 layerIdToTreeNode.insert( pdfLayerNode->id, pdfLayerNode.get() );
808 pdfTreeNodes->addChild( std::move( pdfLayerNode ) );
809 break;
810 }
811
813 {
814 QgsLayerTreeGroup *childLayerTreeGroup = qobject_cast<QgsLayerTreeGroup *>( qgisNode );
815
816 // GroupLayers support
817 if ( QgsGroupLayer *groupLayer = childLayerTreeGroup->groupLayer() )
818 {
819 // We deal with it as another map layer
820 auto pdfLayerNode = std::make_unique< TreeNode >();
821 pdfLayerNode->id = groupLayer->id();
822 pdfLayerNode->name = childLayerTreeGroup->name();
823 pdfLayerNode->initiallyVisible = childLayerTreeGroup->itemVisibilityChecked();
824 pdfLayerNode->mapLayerId = groupLayer->id();
825 layerIdToTreeNode.insert( pdfLayerNode->id, pdfLayerNode.get() );
826 pdfTreeNodes->addChild( std::move( pdfLayerNode ) );
827 break;
828 }
829
830 // Skip empty groups
831 if ( !childLayerTreeGroup->children().empty() )
832 {
833 std::unique_ptr< TreeNode > pdfGroupNode = createPdfTreeNodes( groupNameToTreeNode, layerIdToTreeNode, childLayerTreeGroup );
834
835 // A group that is not empty in the QGIS layer tree, may be emptied
836 // if it only contais invalid and/or geometryless layers. Skip it!
837 if ( !pdfGroupNode->children.empty() )
838 {
839 pdfTreeNodes->addChild( std::move( pdfGroupNode ) );
840 }
841 }
842 break;
843 }
844
846 break;
847 }
848 }
849
850 // Now we know if our group is not empty. Add it to the groupNameToTreeNode then.
851 if ( !pdfTreeNodes->children.empty() )
852 {
853 groupNameToTreeNode[pdfTreeNodes->name] = pdfTreeNodes.get();
854 }
855
856 return pdfTreeNodes;
857}
858
859QString QgsAbstractGeospatialPdfExporter::compositionModeToString( QPainter::CompositionMode mode )
860{
861 switch ( mode )
862 {
863 case QPainter::CompositionMode_SourceOver:
864 return u"Normal"_s;
865
866 case QPainter::CompositionMode_Multiply:
867 return u"Multiply"_s;
868
869 case QPainter::CompositionMode_Screen:
870 return u"Screen"_s;
871
872 case QPainter::CompositionMode_Overlay:
873 return u"Overlay"_s;
874
875 case QPainter::CompositionMode_Darken:
876 return u"Darken"_s;
877
878 case QPainter::CompositionMode_Lighten:
879 return u"Lighten"_s;
880
881 case QPainter::CompositionMode_ColorDodge:
882 return u"ColorDodge"_s;
883
884 case QPainter::CompositionMode_ColorBurn:
885 return u"ColorBurn"_s;
886
887 case QPainter::CompositionMode_HardLight:
888 return u"HardLight"_s;
889
890 case QPainter::CompositionMode_SoftLight:
891 return u"SoftLight"_s;
892
893 case QPainter::CompositionMode_Difference:
894 return u"Difference"_s;
895
896 case QPainter::CompositionMode_Exclusion:
897 return u"Exclusion"_s;
898
899 default:
900 break;
901 }
902
903 QgsDebugError( u"Unsupported PDF blend mode %1"_s.arg( mode ) );
904 return u"Normal"_s;
905}
@ Unknown
Unknown types.
Definition qgis.h:383
@ Null
No geometry.
Definition qgis.h:384
@ Vector
Vector layer.
Definition qgis.h:207
@ PreferredGdal
Preferred format for conversion of CRS to WKT for use with the GDAL library.
Definition qgis.h:2582
@ NoSymbology
Export only data.
Definition qgis.h:6013
static bool compositionModeSupported(QPainter::CompositionMode mode)
Returns true if the specified composition mode is supported for layers during Geospatial PDF exports.
static QString geospatialPDFAvailabilityExplanation()
Returns a user-friendly, translated string explaining why Geospatial PDF export support is not availa...
static constexpr double DPI_72
Hardcode DPI of 72 to get correct page sizes in outputs.
bool finalize(const QList< QgsAbstractGeospatialPdfExporter::ComponentLayerDetail > &components, const QString &destinationFile, const ExportDetails &details)
To be called after the rendering operation is complete.
void pushRenderedFeature(const QString &layerId, const QgsAbstractGeospatialPdfExporter::RenderedFeature &feature, const QString &group=QString())
Called multiple times during the rendering operation, whenever a feature associated with the specifie...
static bool geospatialPDFCreationAvailable()
Returns true if the current QGIS build is capable of Geospatial PDF support.
QString generateTemporaryFilepath(const QString &filename) const
Returns a file path to use for temporary files required for Geospatial PDF creation.
Represents a coordinate reference system (CRS).
bool isValid() const
Returns whether this CRS is correctly initialized and usable.
QString toWkt(Qgis::CrsWktVariant variant=Qgis::CrsWktVariant::Wkt1Gdal, bool multiline=false, int indentationWidth=4) const
Returns a WKT representation of this CRS.
Contains information about the context in which a coordinate transform is executed.
bool isEmpty() const override
Returns true if the geometry is empty.
void transform(const QgsCoordinateTransform &ct, Qgis::TransformDirection d=Qgis::TransformDirection::Forward, bool transformZ=false) override
Transforms the geometry using a coordinate transform.
@ FastInsert
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
@ RegeneratePrimaryKey
This flag indicates, that a primary key field cannot be guaranteed to be unique and the sink should i...
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:60
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
static QString stringToSafeFilename(const QString &string)
Converts a string to a safe filename, replacing characters which are not safe for filenames with an '...
Layer tree group node serves as a container for layers and further groups.
QString name() const override
Returns the group's name.
QgsGroupLayer * groupLayer()
Returns a reference to the associated group layer, if the layer tree group will be treated as group l...
QString layerId() const
Returns the ID for the map layer associated with this node.
QString name() const override
Returns the layer's name.
QgsMapLayer * layer() const
Returns the map layer associated with this node.
@ NodeCustom
Leaf node pointing to a custom object.
@ NodeGroup
Container of other groups and layers.
@ NodeLayer
Leaf node pointing to a layer.
QList< QgsLayerTreeNode * > children()
Gets list of children of the node. Children are owned by the parent.
bool itemVisibilityChecked() const
Returns whether a node is checked (independently of its ancestors or children).
Namespace with helper functions for layer tree operations.
Qgis::LayerType type
Definition qgsmaplayer.h:93
QString asWkt(int precision=17) const override
Returns a WKT representation of the geometry.
double xMinimum
double yMinimum
double xMaximum
double yMaximum
Options to pass to QgsVectorFileWriter::writeAsVectorFormat().
Qgis::FeatureSymbologyExport symbologyExport
Symbology to export.
A convenience class for writing vector layers to disk based formats (e.g.
Q_INVOKABLE Qgis::GeometryType geometryType() const
Returns point, line or polygon.
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition qgis.h:6995
void sortTreeNodeList(std::vector< std::unique_ptr< TreeNode > > &nodeList, QStringList layerOrder)
Sort a tree node list according to layerOrder.
void CPL_STDCALL collectErrors(CPLErr, int, const char *msg)
QList< QgsFeature > QgsFeatureList
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63
#define QgsDebugError(str)
Definition qgslogger.h:59
Contains details of a particular input component to be used during PDF composition.
Contains details of a control point used during georeferencing Geospatial PDF outputs.
bool includeFeatures
true if feature vector information (such as attributes) should be exported.
QgsRectangle pageBoundsMm
Bounds of the georeferenced section on the page, in millimeters.
QgsCoordinateReferenceSystem crs
Coordinate reference system for georeferenced section.
QList< QgsAbstractGeospatialPdfExporter::ControlPoint > controlPoints
List of control points corresponding to this georeferenced section.
QgsPolygon pageBoundsPolygon
Bounds of the georeferenced section on the page, in millimeters, as a free-form polygon.
Contains information about a feature rendered inside the PDF.
QgsGeometry renderedBounds
Bounds, in PDF units, of rendered feature.
Contains information relating to a single PDF layer in the Geospatial PDF export.