QGIS API Documentation 3.99.0-Master (26c88405ac0)
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
28#include <QDomDocument>
29#include <QDomElement>
30#include <QMutex>
31#include <QMutexLocker>
32#include <QTextStream>
33#include <QTimeZone>
34#include <QUuid>
35
37{
38 // test if GDAL has read support in PDF driver
39 GDALDriverH hDriverMem = GDALGetDriverByName( "PDF" );
40 if ( !hDriverMem )
41 {
42 return false;
43 }
44
45 const char *pHavePoppler = GDALGetMetadataItem( hDriverMem, "HAVE_POPPLER", nullptr );
46 if ( pHavePoppler && strstr( pHavePoppler, "YES" ) )
47 return true;
48
49 const char *pHavePdfium = GDALGetMetadataItem( hDriverMem, "HAVE_PDFIUM", nullptr );
50 if ( pHavePdfium && strstr( pHavePdfium, "YES" ) )
51 return true;
52
53 return false;
54}
55
57{
58 // test if GDAL has read support in PDF driver
59 GDALDriverH hDriverMem = GDALGetDriverByName( "PDF" );
60 if ( !hDriverMem )
61 {
62 return QObject::tr( "No GDAL PDF driver available." );
63 }
64
65 const char *pHavePoppler = GDALGetMetadataItem( hDriverMem, "HAVE_POPPLER", nullptr );
66 if ( pHavePoppler && strstr( pHavePoppler, "YES" ) )
67 return QString();
68
69 const char *pHavePdfium = GDALGetMetadataItem( hDriverMem, "HAVE_PDFIUM", nullptr );
70 if ( pHavePdfium && strstr( pHavePdfium, "YES" ) )
71 return QString();
72
73 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." );
74}
75
76void CPL_STDCALL collectErrors( CPLErr, int, const char *msg )
77{
78 QgsDebugError( QStringLiteral( "GDAL PDF creation error: %1 " ).arg( msg ) );
79 if ( QStringList *errorList = static_cast< QStringList * >( CPLGetErrorHandlerUserData() ) )
80 {
81 errorList->append( QString( msg ) );
82 }
83}
84
85bool QgsAbstractGeospatialPdfExporter::finalize( const QList<ComponentLayerDetail> &components, const QString &destinationFile, const ExportDetails &details )
86{
87 if ( details.includeFeatures && !saveTemporaryLayers() )
88 return false;
89
90 const QString composition = createCompositionXml( components, details );
91 QgsDebugMsgLevel( composition, 2 );
92 if ( composition.isEmpty() )
93 return false;
94
95 // do the creation!
96 GDALDriverH driver = GDALGetDriverByName( "PDF" );
97 if ( !driver )
98 {
99 mErrorMessage = QObject::tr( "Cannot load GDAL PDF driver" );
100 return false;
101 }
102
103 const QString xmlFilePath = generateTemporaryFilepath( QStringLiteral( "composition.xml" ) );
104 QFile file( xmlFilePath );
105 if ( file.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
106 {
107 QTextStream out( &file );
108#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
109 out.setCodec( "UTF-8" );
110#endif
111 out << composition;
112 }
113 else
114 {
115 mErrorMessage = QObject::tr( "Could not create geospatial PDF composition file" );
116 return false;
117 }
118
119 char **papszOptions = CSLSetNameValue( nullptr, "COMPOSITION_FILE", xmlFilePath.toUtf8().constData() );
120
121 QStringList creationErrors;
122 CPLPushErrorHandlerEx( collectErrors, &creationErrors );
123
124 // return a non-null (fake) dataset in case of success, nullptr otherwise.
125 gdal::dataset_unique_ptr outputDataset( GDALCreate( driver, destinationFile.toUtf8().constData(), 0, 0, 0, GDT_Unknown, papszOptions ) );
126
127 CPLPopErrorHandler();
128 // Keep explicit comparison to avoid confusing cppcheck
129 const bool res = outputDataset.get() != nullptr;
130 if ( !res )
131 {
132 if ( creationErrors.size() == 1 )
133 {
134 mErrorMessage = QObject::tr( "Could not create PDF file: %1" ).arg( creationErrors.at( 0 ) );
135 }
136 else if ( !creationErrors.empty() )
137 {
138 mErrorMessage = QObject::tr( "Could not create PDF file. Received errors:\n" );
139 for ( const QString &error : std::as_const( creationErrors ) )
140 {
141 mErrorMessage += ( !mErrorMessage.isEmpty() ? QStringLiteral( "\n" ) : QString() ) + error;
142 }
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() + QStringLiteral( ".gpkg" ) );
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 = QStringLiteral( "GPKG" );
216 std::unique_ptr< QgsVectorFileWriter > writer( QgsVectorFileWriter::create( filePath, features.first().fields(), features.first().geometry().wkbType(), QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext(), saveOptions, QgsFeatureSink::RegeneratePrimaryKey, nullptr, &layerName ) );
217 if ( writer->hasError() )
218 {
219 mErrorMessage = writer->errorMessage();
220 QgsDebugError( mErrorMessage );
221 return false;
222 }
223 for ( const QgsFeature &feature : features )
224 {
225 QgsFeature f = feature;
226 if ( !writer->addFeature( f, QgsFeatureSink::FastInsert ) )
227 {
228 mErrorMessage = writer->errorMessage();
229 QgsDebugError( mErrorMessage );
230 return false;
231 }
232 }
233 detail.sourceVectorLayer = layerName;
234 mVectorComponents << detail;
235 }
236 }
237 return true;
238}
239
241struct TreeNode
242{
243 QString id;
244 bool initiallyVisible = false;
245 QString name;
246 QString mutuallyExclusiveGroupId;
247 QString mapLayerId;
248 std::vector< std::unique_ptr< TreeNode > > children;
249 TreeNode *parent = nullptr;
250
251 void addChild( std::unique_ptr< TreeNode > child )
252 {
253 child->parent = this;
254 children.emplace_back( std::move( child ) );
255 }
256
257 QDomElement toElement( QDomDocument &doc ) const
258 {
259 QDomElement layerElement = doc.createElement( QStringLiteral( "Layer" ) );
260 layerElement.setAttribute( QStringLiteral( "id" ), id );
261 layerElement.setAttribute( QStringLiteral( "name" ), name );
262 layerElement.setAttribute( QStringLiteral( "initiallyVisible" ), initiallyVisible ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
263 if ( !mutuallyExclusiveGroupId.isEmpty() )
264 layerElement.setAttribute( QStringLiteral( "mutuallyExclusiveGroupId" ), mutuallyExclusiveGroupId );
265
266 for ( const auto &child : children )
267 {
268 layerElement.appendChild( child->toElement( doc ) );
269 }
270
271 return layerElement;
272 }
273
274 QDomElement createIfLayerOnElement( QDomDocument &doc, QDomElement &contentElement ) const
275 {
276 QDomElement element = doc.createElement( QStringLiteral( "IfLayerOn" ) );
277 element.setAttribute( QStringLiteral( "layerId" ), id );
278 contentElement.appendChild( element );
279 return element;
280 }
281
282 QDomElement createNestedIfLayerOnElements( QDomDocument &doc, QDomElement &contentElement ) const
283 {
284 TreeNode *currentParent = parent;
285 QDomElement finalElement = doc.createElement( QStringLiteral( "IfLayerOn" ) );
286 finalElement.setAttribute( QStringLiteral( "layerId" ), id );
287
288 QDomElement currentElement = finalElement;
289 while ( currentParent )
290 {
291 QDomElement ifGroupOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
292 ifGroupOn.setAttribute( QStringLiteral( "layerId" ), currentParent->id );
293 ifGroupOn.appendChild( currentElement );
294 currentElement = ifGroupOn;
295 currentParent = currentParent->parent;
296 }
297 contentElement.appendChild( currentElement );
298 return finalElement;
299 }
300};
302
303QString QgsAbstractGeospatialPdfExporter::createCompositionXml( const QList<ComponentLayerDetail> &components, const ExportDetails &details )
304{
305 QDomDocument doc;
306
307 QDomElement compositionElem = doc.createElement( QStringLiteral( "PDFComposition" ) );
308
309 // metadata tags
310 QDomElement metadata = doc.createElement( QStringLiteral( "Metadata" ) );
311 if ( !details.author.isEmpty() )
312 {
313 QDomElement author = doc.createElement( QStringLiteral( "Author" ) );
314 author.appendChild( doc.createTextNode( details.author ) );
315 metadata.appendChild( author );
316 }
317 if ( !details.producer.isEmpty() )
318 {
319 QDomElement producer = doc.createElement( QStringLiteral( "Producer" ) );
320 producer.appendChild( doc.createTextNode( details.producer ) );
321 metadata.appendChild( producer );
322 }
323 if ( !details.creator.isEmpty() )
324 {
325 QDomElement creator = doc.createElement( QStringLiteral( "Creator" ) );
326 creator.appendChild( doc.createTextNode( details.creator ) );
327 metadata.appendChild( creator );
328 }
329 if ( details.creationDateTime.isValid() )
330 {
331 QDomElement creationDate = doc.createElement( QStringLiteral( "CreationDate" ) );
332 QString creationDateString = QStringLiteral( "D:%1" ).arg( details.creationDateTime.toString( QStringLiteral( "yyyyMMddHHmmss" ) ) );
333 if ( details.creationDateTime.timeZone().isValid() )
334 {
335 int offsetFromUtc = details.creationDateTime.timeZone().offsetFromUtc( details.creationDateTime );
336 creationDateString += ( offsetFromUtc >= 0 ) ? '+' : '-';
337 offsetFromUtc = std::abs( offsetFromUtc );
338 int offsetHours = offsetFromUtc / 3600;
339 int offsetMins = ( offsetFromUtc % 3600 ) / 60;
340 creationDateString += QStringLiteral( "%1'%2'" ).arg( offsetHours ).arg( offsetMins );
341 }
342 creationDate.appendChild( doc.createTextNode( creationDateString ) );
343 metadata.appendChild( creationDate );
344 }
345 if ( !details.subject.isEmpty() )
346 {
347 QDomElement subject = doc.createElement( QStringLiteral( "Subject" ) );
348 subject.appendChild( doc.createTextNode( details.subject ) );
349 metadata.appendChild( subject );
350 }
351 if ( !details.title.isEmpty() )
352 {
353 QDomElement title = doc.createElement( QStringLiteral( "Title" ) );
354 title.appendChild( doc.createTextNode( details.title ) );
355 metadata.appendChild( title );
356 }
357 if ( !details.keywords.empty() )
358 {
359 QStringList allKeywords;
360 for ( auto it = details.keywords.constBegin(); it != details.keywords.constEnd(); ++it )
361 {
362 allKeywords.append( QStringLiteral( "%1: %2" ).arg( it.key(), it.value().join( ',' ) ) );
363 }
364 QDomElement keywords = doc.createElement( QStringLiteral( "Keywords" ) );
365 keywords.appendChild( doc.createTextNode( allKeywords.join( ';' ) ) );
366 metadata.appendChild( keywords );
367 }
368 compositionElem.appendChild( metadata );
369
370 QSet< QString > createdLayerIds;
371 std::vector< std::unique_ptr< TreeNode > > rootGroups;
372 std::vector< std::unique_ptr< TreeNode > > rootLayers;
373 QMap< QString, TreeNode * > groupNameMap;
374
375 QStringList layerTreeGroupOrder = details.layerTreeGroupOrder;
376
377 // add any missing groups to end of order
378 // Missing groups from the explicitly set custom layer tree groups
379 for ( auto it = details.customLayerTreeGroups.constBegin(); it != details.customLayerTreeGroups.constEnd(); ++it )
380 {
381 if ( layerTreeGroupOrder.contains( it.value() ) )
382 continue;
383 layerTreeGroupOrder.append( it.value() );
384 }
385
386 // Missing groups from vector components
387 if ( details.includeFeatures )
388 {
389 for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
390 {
391 if ( !component.group.isEmpty() && !layerTreeGroupOrder.contains( component.group ) )
392 {
393 layerTreeGroupOrder.append( component.group );
394 }
395 }
396 }
397
398 // missing groups from other components
399 for ( const ComponentLayerDetail &component : components )
400 {
401 if ( !component.group.isEmpty() && !layerTreeGroupOrder.contains( component.group ) )
402 {
403 layerTreeGroupOrder.append( component.group );
404 }
405 }
406 // now we are confident that we have a definitive list of all the groups for the export
407 QMap< QString, TreeNode * > groupNameToTreeNode;
408 QMap< QString, TreeNode * > layerIdToTreeNode;
409
410 auto createGroup = [&details, &groupNameToTreeNode]( const QString & groupName ) -> std::unique_ptr< TreeNode >
411 {
412 auto group = std::make_unique< TreeNode >();
413 const QString id = QUuid::createUuid().toString();
414 group->id = id;
415 groupNameToTreeNode[ groupName ] = group.get();
416
417 group->name = groupName;
418 group->initiallyVisible = true;
419 if ( details.mutuallyExclusiveGroups.contains( groupName ) )
420 group->mutuallyExclusiveGroupId = QStringLiteral( "__mutually_exclusive_groups__" );
421 return group;
422 };
423
424 if ( details.includeFeatures )
425 {
426 for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
427 {
428 const QString destinationGroup = details.customLayerTreeGroups.value( component.mapLayerId, component.group );
429
430 auto layer = std::make_unique< TreeNode >();
431 layer->id = destinationGroup.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( destinationGroup, component.mapLayerId );
432 layer->name = details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name;
433 layer->initiallyVisible = details.initialLayerVisibility.value( component.mapLayerId, true );
434 layer->mapLayerId = component.mapLayerId;
435
436 layerIdToTreeNode.insert( component.mapLayerId, layer.get() );
437 if ( !destinationGroup.isEmpty() )
438 {
439 if ( TreeNode *groupNode = groupNameMap.value( destinationGroup ) )
440 {
441 groupNode->addChild( std::move( layer ) );
442 }
443 else
444 {
445 std::unique_ptr< TreeNode > group = createGroup( destinationGroup );
446 group->addChild( std::move( layer ) );
447 groupNameMap.insert( destinationGroup, group.get() );
448 rootGroups.emplace_back( std::move( group ) );
449 }
450 }
451 else
452 {
453 rootLayers.emplace_back( std::move( layer ) );
454 }
455
456 createdLayerIds.insert( component.mapLayerId );
457 }
458 }
459
460 // some PDF components may not be linked to vector components - e.g.
461 // - layers with labels but no features
462 // - raster layers
463 // - legends and other map content
464 for ( const ComponentLayerDetail &component : components )
465 {
466 if ( !component.mapLayerId.isEmpty() && createdLayerIds.contains( component.mapLayerId ) )
467 continue;
468
469 const QString destinationGroup = details.customLayerTreeGroups.value( component.mapLayerId, component.group );
470 if ( destinationGroup.isEmpty() && component.mapLayerId.isEmpty() )
471 continue;
472
473 std::unique_ptr< TreeNode > mapLayerNode;
474 if ( !component.mapLayerId.isEmpty() )
475 {
476 mapLayerNode = std::make_unique< TreeNode >();
477 mapLayerNode->id = destinationGroup.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( destinationGroup, component.mapLayerId );
478 mapLayerNode->name = details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId, component.name );
479 mapLayerNode->initiallyVisible = details.initialLayerVisibility.value( component.mapLayerId, true );
480 mapLayerNode->mapLayerId = component.mapLayerId;
481
482 layerIdToTreeNode.insert( component.mapLayerId, mapLayerNode.get() );
483 }
484
485 if ( !destinationGroup.isEmpty() )
486 {
487 if ( TreeNode *groupNode = groupNameMap.value( destinationGroup ) )
488 {
489 if ( mapLayerNode )
490 groupNode->addChild( std::move( mapLayerNode ) );
491 }
492 else
493 {
494 std::unique_ptr< TreeNode > group = createGroup( destinationGroup );
495 if ( mapLayerNode )
496 group->addChild( std::move( mapLayerNode ) );
497 groupNameMap.insert( destinationGroup, group.get() );
498 rootGroups.emplace_back( std::move( group ) );
499 }
500 }
501 else
502 {
503 if ( mapLayerNode )
504 rootLayers.emplace_back( std::move( mapLayerNode ) );
505 }
506
507 if ( !component.mapLayerId.isEmpty() )
508 {
509 createdLayerIds.insert( component.mapLayerId );
510 }
511 }
512
513 // pages
514 QDomElement page = doc.createElement( QStringLiteral( "Page" ) );
515 QDomElement dpi = doc.createElement( QStringLiteral( "DPI" ) );
516 // hardcode DPI of 72 to get correct page sizes in outputs -- refs discussion in https://github.com/OSGeo/gdal/pull/2961
517 dpi.appendChild( doc.createTextNode( qgsDoubleToString( 72 ) ) );
518 page.appendChild( dpi );
519 // assumes DPI of 72, as noted above.
520 QDomElement width = doc.createElement( QStringLiteral( "Width" ) );
521 const double pageWidthPdfUnits = std::ceil( details.pageSizeMm.width() / 25.4 * 72 );
522 width.appendChild( doc.createTextNode( qgsDoubleToString( pageWidthPdfUnits ) ) );
523 page.appendChild( width );
524 QDomElement height = doc.createElement( QStringLiteral( "Height" ) );
525 const double pageHeightPdfUnits = std::ceil( details.pageSizeMm.height() / 25.4 * 72 );
526 height.appendChild( doc.createTextNode( qgsDoubleToString( pageHeightPdfUnits ) ) );
527 page.appendChild( height );
528
529 // georeferencing
530 int i = 0;
531 for ( const QgsAbstractGeospatialPdfExporter::GeoReferencedSection &section : details.georeferencedSections )
532 {
533 QDomElement georeferencing = doc.createElement( QStringLiteral( "Georeferencing" ) );
534 georeferencing.setAttribute( QStringLiteral( "id" ), QStringLiteral( "georeferenced_%1" ).arg( i++ ) );
535 georeferencing.setAttribute( QStringLiteral( "ISO32000ExtensionFormat" ), details.useIso32000ExtensionFormatGeoreferencing ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
536
537 if ( section.crs.isValid() )
538 {
539 QDomElement srs = doc.createElement( QStringLiteral( "SRS" ) );
540 // 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...
541 // srs.setAttribute( QStringLiteral( "dataAxisToSRSAxisMapping" ), QStringLiteral( "2,1" ) );
542 if ( !section.crs.authid().isEmpty() && !section.crs.authid().startsWith( QStringLiteral( "user" ), Qt::CaseInsensitive ) )
543 {
544 srs.appendChild( doc.createTextNode( section.crs.authid() ) );
545 }
546 else
547 {
548 srs.appendChild( doc.createTextNode( section.crs.toWkt( Qgis::CrsWktVariant::PreferredGdal ) ) );
549 }
550 georeferencing.appendChild( srs );
551 }
552
553 if ( !section.pageBoundsPolygon.isEmpty() )
554 {
555 /*
556 Define a polygon / neatline in PDF units into which the
557 Measure tool will display coordinates.
558 If not specified, BoundingBox will be used instead.
559 If none of BoundingBox and BoundingPolygon are specified,
560 the whole PDF page will be assumed to be georeferenced.
561 */
562 QDomElement boundingPolygon = doc.createElement( QStringLiteral( "BoundingPolygon" ) );
563
564 // transform to PDF coordinate space
565 QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(),
566 -pageHeightPdfUnits / details.pageSizeMm.height() );
567
568 QgsPolygon p = section.pageBoundsPolygon;
569 p.transform( t );
570 boundingPolygon.appendChild( doc.createTextNode( p.asWkt() ) );
571
572 georeferencing.appendChild( boundingPolygon );
573 }
574 else
575 {
576 /* Define the viewport where georeferenced coordinates are available.
577 If not specified, the extent of BoundingPolygon will be used instead.
578 If none of BoundingBox and BoundingPolygon are specified,
579 the whole PDF page will be assumed to be georeferenced.
580 */
581 QDomElement boundingBox = doc.createElement( QStringLiteral( "BoundingBox" ) );
582 boundingBox.setAttribute( QStringLiteral( "x1" ), qgsDoubleToString( section.pageBoundsMm.xMinimum() / 25.4 * 72 ) );
583 boundingBox.setAttribute( QStringLiteral( "y1" ), qgsDoubleToString( section.pageBoundsMm.yMinimum() / 25.4 * 72 ) );
584 boundingBox.setAttribute( QStringLiteral( "x2" ), qgsDoubleToString( section.pageBoundsMm.xMaximum() / 25.4 * 72 ) );
585 boundingBox.setAttribute( QStringLiteral( "y2" ), qgsDoubleToString( section.pageBoundsMm.yMaximum() / 25.4 * 72 ) );
586 georeferencing.appendChild( boundingBox );
587 }
588
589 for ( const ControlPoint &point : section.controlPoints )
590 {
591 QDomElement cp1 = doc.createElement( QStringLiteral( "ControlPoint" ) );
592 cp1.setAttribute( QStringLiteral( "x" ), qgsDoubleToString( point.pagePoint.x() / 25.4 * 72 ) );
593 cp1.setAttribute( QStringLiteral( "y" ), qgsDoubleToString( ( details.pageSizeMm.height() - point.pagePoint.y() ) / 25.4 * 72 ) );
594 cp1.setAttribute( QStringLiteral( "GeoX" ), qgsDoubleToString( point.geoPoint.x() ) );
595 cp1.setAttribute( QStringLiteral( "GeoY" ), qgsDoubleToString( point.geoPoint.y() ) );
596 georeferencing.appendChild( cp1 );
597 }
598
599 page.appendChild( georeferencing );
600 }
601
602 auto createPdfDatasetElement = [&doc]( const ComponentLayerDetail & component ) -> QDomElement
603 {
604 QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
605 pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
606 if ( component.opacity != 1.0 || component.compositionMode != QPainter::CompositionMode_SourceOver )
607 {
608 QDomElement blendingElement = doc.createElement( QStringLiteral( "Blending" ) );
609 blendingElement.setAttribute( QStringLiteral( "opacity" ), component.opacity );
610 blendingElement.setAttribute( QStringLiteral( "function" ), compositionModeToString( component.compositionMode ) );
611
612 pdfDataset.appendChild( blendingElement );
613 }
614 return pdfDataset;
615 };
616
617 // content
618 QDomElement content = doc.createElement( QStringLiteral( "Content" ) );
619 for ( const ComponentLayerDetail &component : components )
620 {
621 if ( component.mapLayerId.isEmpty() && component.group.isEmpty() )
622 {
623 content.appendChild( createPdfDatasetElement( component ) );
624 }
625 else if ( !component.mapLayerId.isEmpty() )
626 {
627 if ( TreeNode *treeNode = layerIdToTreeNode.value( component.mapLayerId ) )
628 {
629 QDomElement ifLayerOnElement = treeNode->createNestedIfLayerOnElements( doc, content );
630 ifLayerOnElement.appendChild( createPdfDatasetElement( component ) );
631 }
632 }
633 else if ( TreeNode *groupNode = groupNameToTreeNode.value( component.group ) )
634 {
635 QDomElement ifGroupOn = groupNode->createIfLayerOnElement( doc, content );
636 ifGroupOn.appendChild( createPdfDatasetElement( component ) );
637 }
638 }
639
640 // vector datasets (we "draw" these on top, just for debugging... but they are invisible, so are never really drawn!)
641 if ( details.includeFeatures )
642 {
643 for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
644 {
645 if ( TreeNode *treeNode = layerIdToTreeNode.value( component.mapLayerId ) )
646 {
647 QDomElement ifLayerOnElement = treeNode->createNestedIfLayerOnElements( doc, content );
648
649 QDomElement vectorDataset = doc.createElement( QStringLiteral( "Vector" ) );
650 vectorDataset.setAttribute( QStringLiteral( "dataset" ), component.sourceVectorPath );
651 vectorDataset.setAttribute( QStringLiteral( "layer" ), component.sourceVectorLayer );
652 vectorDataset.setAttribute( QStringLiteral( "visible" ), QStringLiteral( "false" ) );
653 QDomElement logicalStructure = doc.createElement( QStringLiteral( "LogicalStructure" ) );
654 logicalStructure.setAttribute( QStringLiteral( "displayLayerName" ), component.name );
655 if ( !component.displayAttribute.isEmpty() )
656 logicalStructure.setAttribute( QStringLiteral( "fieldToDisplay" ), component.displayAttribute );
657 vectorDataset.appendChild( logicalStructure );
658 ifLayerOnElement.appendChild( vectorDataset );
659 }
660 }
661 }
662
663 page.appendChild( content );
664
665 // layertree
666 QDomElement layerTree = doc.createElement( QStringLiteral( "LayerTree" ) );
667 //layerTree.setAttribute( QStringLiteral("displayOnlyOnVisiblePages"), QStringLiteral("true"));
668
669 // groups are added first
670
671 // sort root groups in desired order
672 std::sort( rootGroups.begin(), rootGroups.end(), [&layerTreeGroupOrder]( const std::unique_ptr< TreeNode > &a, const std::unique_ptr< TreeNode > &b ) -> bool
673 {
674 return layerTreeGroupOrder.indexOf( a->name ) < layerTreeGroupOrder.indexOf( b->name );
675 } );
676
677 bool haveFoundMutuallyExclusiveGroup = false;
678 for ( const auto &node : std::as_const( rootGroups ) )
679 {
680 if ( !node->mutuallyExclusiveGroupId.isEmpty() )
681 {
682 // only the first object in a mutually exclusive group is initially visible
683 node->initiallyVisible = !haveFoundMutuallyExclusiveGroup;
684 haveFoundMutuallyExclusiveGroup = true;
685 }
686 layerTree.appendChild( node->toElement( doc ) );
687 }
688
689 // filter out groups which don't have any content
690 layerTreeGroupOrder.erase( std::remove_if( layerTreeGroupOrder.begin(), layerTreeGroupOrder.end(), [&details]( const QString & group )
691 {
692 return details.customLayerTreeGroups.key( group ).isEmpty();
693 } ), layerTreeGroupOrder.end() );
694
695
696 // then top-level layers
697 std::sort( rootLayers.begin(), rootLayers.end(), [&details]( const std::unique_ptr< TreeNode > &a, const std::unique_ptr< TreeNode > &b ) -> bool
698 {
699 const int indexA = details.layerOrder.indexOf( a->mapLayerId );
700 const int indexB = details.layerOrder.indexOf( b->mapLayerId );
701
702 if ( indexA >= 0 && indexB >= 0 )
703 return indexA < indexB;
704 else if ( indexA >= 0 )
705 return true;
706 else if ( indexB >= 0 )
707 return false;
708
709 return a->name.localeAwareCompare( b->name ) < 0;
710 } );
711
712 for ( const auto &node : std::as_const( rootLayers ) )
713 {
714 layerTree.appendChild( node->toElement( doc ) );
715 }
716
717 compositionElem.appendChild( layerTree );
718 compositionElem.appendChild( page );
719
720 doc.appendChild( compositionElem );
721
722 QString composition;
723 QTextStream stream( &composition );
724 doc.save( stream, -1 );
725
726 return composition;
727}
728
729QString QgsAbstractGeospatialPdfExporter::compositionModeToString( QPainter::CompositionMode mode )
730{
731 switch ( mode )
732 {
733 case QPainter::CompositionMode_SourceOver:
734 return QStringLiteral( "Normal" );
735
736 case QPainter::CompositionMode_Multiply:
737 return QStringLiteral( "Multiply" );
738
739 case QPainter::CompositionMode_Screen:
740 return QStringLiteral( "Screen" );
741
742 case QPainter::CompositionMode_Overlay:
743 return QStringLiteral( "Overlay" );
744
745 case QPainter::CompositionMode_Darken:
746 return QStringLiteral( "Darken" );
747
748 case QPainter::CompositionMode_Lighten:
749 return QStringLiteral( "Lighten" );
750
751 case QPainter::CompositionMode_ColorDodge:
752 return QStringLiteral( "ColorDodge" );
753
754 case QPainter::CompositionMode_ColorBurn:
755 return QStringLiteral( "ColorBurn" );
756
757 case QPainter::CompositionMode_HardLight:
758 return QStringLiteral( "HardLight" );
759
760 case QPainter::CompositionMode_SoftLight:
761 return QStringLiteral( "SoftLight" );
762
763 case QPainter::CompositionMode_Difference:
764 return QStringLiteral( "Difference" );
765
766 case QPainter::CompositionMode_Exclusion:
767 return QStringLiteral( "Exclusion" );
768
769 default:
770 break;
771 }
772
773 QgsDebugError( QStringLiteral( "Unsupported PDF blend mode %1" ).arg( mode ) );
774 return QStringLiteral( "Normal" );
775}
776
@ PreferredGdal
Preferred format for conversion of CRS to WKT for use with the GDAL library.
Definition qgis.h:2441
@ NoSymbology
Export only data.
Definition qgis.h:5558
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...
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:58
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 '...
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.
static QgsVectorFileWriter * create(const QString &fileName, const QgsFields &fields, Qgis::WkbType geometryType, const QgsCoordinateReferenceSystem &srs, const QgsCoordinateTransformContext &transformContext, const QgsVectorFileWriter::SaveVectorOptions &options, QgsFeatureSink::SinkFlags sinkFlags=QgsFeatureSink::SinkFlags(), QString *newFilename=nullptr, QString *newLayer=nullptr)
Create a new vector file writer.
QgsLayerTree * layerTree(const QgsWmsRenderContext &context)
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:6524
void CPL_STDCALL collectErrors(CPLErr, int, const char *msg)
QList< QgsFeature > QgsFeatureList
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:61
#define QgsDebugError(str)
Definition qgslogger.h:57
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.