QGIS API Documentation 3.99.0-Master (2fe06baccd8)
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 QT_FEATURE_timezone > 0
334 if ( details.creationDateTime.timeZone().isValid() )
335 {
336 int offsetFromUtc = details.creationDateTime.timeZone().offsetFromUtc( details.creationDateTime );
337 creationDateString += ( offsetFromUtc >= 0 ) ? '+' : '-';
338 offsetFromUtc = std::abs( offsetFromUtc );
339 int offsetHours = offsetFromUtc / 3600;
340 int offsetMins = ( offsetFromUtc % 3600 ) / 60;
341 creationDateString += QStringLiteral( "%1'%2'" ).arg( offsetHours ).arg( offsetMins );
342 }
343#else
344 QgsDebugError( QStringLiteral( "Qt is built without timezone support, skipping timezone for pdf export" ) );
345#endif
346 creationDate.appendChild( doc.createTextNode( creationDateString ) );
347 metadata.appendChild( creationDate );
348 }
349 if ( !details.subject.isEmpty() )
350 {
351 QDomElement subject = doc.createElement( QStringLiteral( "Subject" ) );
352 subject.appendChild( doc.createTextNode( details.subject ) );
353 metadata.appendChild( subject );
354 }
355 if ( !details.title.isEmpty() )
356 {
357 QDomElement title = doc.createElement( QStringLiteral( "Title" ) );
358 title.appendChild( doc.createTextNode( details.title ) );
359 metadata.appendChild( title );
360 }
361 if ( !details.keywords.empty() )
362 {
363 QStringList allKeywords;
364 for ( auto it = details.keywords.constBegin(); it != details.keywords.constEnd(); ++it )
365 {
366 allKeywords.append( QStringLiteral( "%1: %2" ).arg( it.key(), it.value().join( ',' ) ) );
367 }
368 QDomElement keywords = doc.createElement( QStringLiteral( "Keywords" ) );
369 keywords.appendChild( doc.createTextNode( allKeywords.join( ';' ) ) );
370 metadata.appendChild( keywords );
371 }
372 compositionElem.appendChild( metadata );
373
374 QSet< QString > createdLayerIds;
375 std::vector< std::unique_ptr< TreeNode > > rootGroups;
376 std::vector< std::unique_ptr< TreeNode > > rootLayers;
377 QMap< QString, TreeNode * > groupNameMap;
378
379 QStringList layerTreeGroupOrder = details.layerTreeGroupOrder;
380
381 // add any missing groups to end of order
382 // Missing groups from the explicitly set custom layer tree groups
383 for ( auto it = details.customLayerTreeGroups.constBegin(); it != details.customLayerTreeGroups.constEnd(); ++it )
384 {
385 if ( layerTreeGroupOrder.contains( it.value() ) )
386 continue;
387 layerTreeGroupOrder.append( it.value() );
388 }
389
390 // Missing groups from vector components
391 if ( details.includeFeatures )
392 {
393 for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
394 {
395 if ( !component.group.isEmpty() && !layerTreeGroupOrder.contains( component.group ) )
396 {
397 layerTreeGroupOrder.append( component.group );
398 }
399 }
400 }
401
402 // missing groups from other components
403 for ( const ComponentLayerDetail &component : components )
404 {
405 if ( !component.group.isEmpty() && !layerTreeGroupOrder.contains( component.group ) )
406 {
407 layerTreeGroupOrder.append( component.group );
408 }
409 }
410 // now we are confident that we have a definitive list of all the groups for the export
411 QMap< QString, TreeNode * > groupNameToTreeNode;
412 QMap< QString, TreeNode * > layerIdToTreeNode;
413
414 auto createGroup = [&details, &groupNameToTreeNode]( const QString & groupName ) -> std::unique_ptr< TreeNode >
415 {
416 auto group = std::make_unique< TreeNode >();
417 const QString id = QUuid::createUuid().toString();
418 group->id = id;
419 groupNameToTreeNode[ groupName ] = group.get();
420
421 group->name = groupName;
422 group->initiallyVisible = true;
423 if ( details.mutuallyExclusiveGroups.contains( groupName ) )
424 group->mutuallyExclusiveGroupId = QStringLiteral( "__mutually_exclusive_groups__" );
425 return group;
426 };
427
428 if ( details.includeFeatures )
429 {
430 for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
431 {
432 const QString destinationGroup = details.customLayerTreeGroups.value( component.mapLayerId, component.group );
433
434 auto layer = std::make_unique< TreeNode >();
435 layer->id = destinationGroup.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( destinationGroup, component.mapLayerId );
436 layer->name = details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name;
437 layer->initiallyVisible = details.initialLayerVisibility.value( component.mapLayerId, true );
438 layer->mapLayerId = component.mapLayerId;
439
440 layerIdToTreeNode.insert( component.mapLayerId, layer.get() );
441 if ( !destinationGroup.isEmpty() )
442 {
443 if ( TreeNode *groupNode = groupNameMap.value( destinationGroup ) )
444 {
445 groupNode->addChild( std::move( layer ) );
446 }
447 else
448 {
449 std::unique_ptr< TreeNode > group = createGroup( destinationGroup );
450 group->addChild( std::move( layer ) );
451 groupNameMap.insert( destinationGroup, group.get() );
452 rootGroups.emplace_back( std::move( group ) );
453 }
454 }
455 else
456 {
457 rootLayers.emplace_back( std::move( layer ) );
458 }
459
460 createdLayerIds.insert( component.mapLayerId );
461 }
462 }
463
464 // some PDF components may not be linked to vector components - e.g.
465 // - layers with labels but no features
466 // - raster layers
467 // - legends and other map content
468 for ( const ComponentLayerDetail &component : components )
469 {
470 if ( !component.mapLayerId.isEmpty() && createdLayerIds.contains( component.mapLayerId ) )
471 continue;
472
473 const QString destinationGroup = details.customLayerTreeGroups.value( component.mapLayerId, component.group );
474 if ( destinationGroup.isEmpty() && component.mapLayerId.isEmpty() )
475 continue;
476
477 std::unique_ptr< TreeNode > mapLayerNode;
478 if ( !component.mapLayerId.isEmpty() )
479 {
480 mapLayerNode = std::make_unique< TreeNode >();
481 mapLayerNode->id = destinationGroup.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( destinationGroup, component.mapLayerId );
482 mapLayerNode->name = details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId, component.name );
483 mapLayerNode->initiallyVisible = details.initialLayerVisibility.value( component.mapLayerId, true );
484 mapLayerNode->mapLayerId = component.mapLayerId;
485
486 layerIdToTreeNode.insert( component.mapLayerId, mapLayerNode.get() );
487 }
488
489 if ( !destinationGroup.isEmpty() )
490 {
491 if ( TreeNode *groupNode = groupNameMap.value( destinationGroup ) )
492 {
493 if ( mapLayerNode )
494 groupNode->addChild( std::move( mapLayerNode ) );
495 }
496 else
497 {
498 std::unique_ptr< TreeNode > group = createGroup( destinationGroup );
499 if ( mapLayerNode )
500 group->addChild( std::move( mapLayerNode ) );
501 groupNameMap.insert( destinationGroup, group.get() );
502 rootGroups.emplace_back( std::move( group ) );
503 }
504 }
505 else
506 {
507 if ( mapLayerNode )
508 rootLayers.emplace_back( std::move( mapLayerNode ) );
509 }
510
511 if ( !component.mapLayerId.isEmpty() )
512 {
513 createdLayerIds.insert( component.mapLayerId );
514 }
515 }
516
517 // pages
518 QDomElement page = doc.createElement( QStringLiteral( "Page" ) );
519 QDomElement dpi = doc.createElement( QStringLiteral( "DPI" ) );
520 // hardcode DPI of 72 to get correct page sizes in outputs -- refs discussion in https://github.com/OSGeo/gdal/pull/2961
521 dpi.appendChild( doc.createTextNode( qgsDoubleToString( 72 ) ) );
522 page.appendChild( dpi );
523 // assumes DPI of 72, as noted above.
524 QDomElement width = doc.createElement( QStringLiteral( "Width" ) );
525 const double pageWidthPdfUnits = std::ceil( details.pageSizeMm.width() / 25.4 * 72 );
526 width.appendChild( doc.createTextNode( qgsDoubleToString( pageWidthPdfUnits ) ) );
527 page.appendChild( width );
528 QDomElement height = doc.createElement( QStringLiteral( "Height" ) );
529 const double pageHeightPdfUnits = std::ceil( details.pageSizeMm.height() / 25.4 * 72 );
530 height.appendChild( doc.createTextNode( qgsDoubleToString( pageHeightPdfUnits ) ) );
531 page.appendChild( height );
532
533 // georeferencing
534 int i = 0;
535 for ( const QgsAbstractGeospatialPdfExporter::GeoReferencedSection &section : details.georeferencedSections )
536 {
537 QDomElement georeferencing = doc.createElement( QStringLiteral( "Georeferencing" ) );
538 georeferencing.setAttribute( QStringLiteral( "id" ), QStringLiteral( "georeferenced_%1" ).arg( i++ ) );
539 georeferencing.setAttribute( QStringLiteral( "ISO32000ExtensionFormat" ), details.useIso32000ExtensionFormatGeoreferencing ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
540
541 if ( section.crs.isValid() )
542 {
543 QDomElement srs = doc.createElement( QStringLiteral( "SRS" ) );
544 // 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...
545 // srs.setAttribute( QStringLiteral( "dataAxisToSRSAxisMapping" ), QStringLiteral( "2,1" ) );
546 if ( !section.crs.authid().isEmpty() && !section.crs.authid().startsWith( QStringLiteral( "user" ), Qt::CaseInsensitive ) )
547 {
548 srs.appendChild( doc.createTextNode( section.crs.authid() ) );
549 }
550 else
551 {
552 srs.appendChild( doc.createTextNode( section.crs.toWkt( Qgis::CrsWktVariant::PreferredGdal ) ) );
553 }
554 georeferencing.appendChild( srs );
555 }
556
557 if ( !section.pageBoundsPolygon.isEmpty() )
558 {
559 /*
560 Define a polygon / neatline in PDF units into which the
561 Measure tool will display coordinates.
562 If not specified, BoundingBox will be used instead.
563 If none of BoundingBox and BoundingPolygon are specified,
564 the whole PDF page will be assumed to be georeferenced.
565 */
566 QDomElement boundingPolygon = doc.createElement( QStringLiteral( "BoundingPolygon" ) );
567
568 // transform to PDF coordinate space
569 QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(),
570 -pageHeightPdfUnits / details.pageSizeMm.height() );
571
572 QgsPolygon p = section.pageBoundsPolygon;
573 p.transform( t );
574 boundingPolygon.appendChild( doc.createTextNode( p.asWkt() ) );
575
576 georeferencing.appendChild( boundingPolygon );
577 }
578 else
579 {
580 /* Define the viewport where georeferenced coordinates are available.
581 If not specified, the extent of BoundingPolygon will be used instead.
582 If none of BoundingBox and BoundingPolygon are specified,
583 the whole PDF page will be assumed to be georeferenced.
584 */
585 QDomElement boundingBox = doc.createElement( QStringLiteral( "BoundingBox" ) );
586 boundingBox.setAttribute( QStringLiteral( "x1" ), qgsDoubleToString( section.pageBoundsMm.xMinimum() / 25.4 * 72 ) );
587 boundingBox.setAttribute( QStringLiteral( "y1" ), qgsDoubleToString( section.pageBoundsMm.yMinimum() / 25.4 * 72 ) );
588 boundingBox.setAttribute( QStringLiteral( "x2" ), qgsDoubleToString( section.pageBoundsMm.xMaximum() / 25.4 * 72 ) );
589 boundingBox.setAttribute( QStringLiteral( "y2" ), qgsDoubleToString( section.pageBoundsMm.yMaximum() / 25.4 * 72 ) );
590 georeferencing.appendChild( boundingBox );
591 }
592
593 for ( const ControlPoint &point : section.controlPoints )
594 {
595 QDomElement cp1 = doc.createElement( QStringLiteral( "ControlPoint" ) );
596 cp1.setAttribute( QStringLiteral( "x" ), qgsDoubleToString( point.pagePoint.x() / 25.4 * 72 ) );
597 cp1.setAttribute( QStringLiteral( "y" ), qgsDoubleToString( ( details.pageSizeMm.height() - point.pagePoint.y() ) / 25.4 * 72 ) );
598 cp1.setAttribute( QStringLiteral( "GeoX" ), qgsDoubleToString( point.geoPoint.x() ) );
599 cp1.setAttribute( QStringLiteral( "GeoY" ), qgsDoubleToString( point.geoPoint.y() ) );
600 georeferencing.appendChild( cp1 );
601 }
602
603 page.appendChild( georeferencing );
604 }
605
606 auto createPdfDatasetElement = [&doc]( const ComponentLayerDetail & component ) -> QDomElement
607 {
608 QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
609 pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
610 if ( component.opacity != 1.0 || component.compositionMode != QPainter::CompositionMode_SourceOver )
611 {
612 QDomElement blendingElement = doc.createElement( QStringLiteral( "Blending" ) );
613 blendingElement.setAttribute( QStringLiteral( "opacity" ), component.opacity );
614 blendingElement.setAttribute( QStringLiteral( "function" ), compositionModeToString( component.compositionMode ) );
615
616 pdfDataset.appendChild( blendingElement );
617 }
618 return pdfDataset;
619 };
620
621 // content
622 QDomElement content = doc.createElement( QStringLiteral( "Content" ) );
623 for ( const ComponentLayerDetail &component : components )
624 {
625 if ( component.mapLayerId.isEmpty() && component.group.isEmpty() )
626 {
627 content.appendChild( createPdfDatasetElement( component ) );
628 }
629 else if ( !component.mapLayerId.isEmpty() )
630 {
631 if ( TreeNode *treeNode = layerIdToTreeNode.value( component.mapLayerId ) )
632 {
633 QDomElement ifLayerOnElement = treeNode->createNestedIfLayerOnElements( doc, content );
634 ifLayerOnElement.appendChild( createPdfDatasetElement( component ) );
635 }
636 }
637 else if ( TreeNode *groupNode = groupNameToTreeNode.value( component.group ) )
638 {
639 QDomElement ifGroupOn = groupNode->createIfLayerOnElement( doc, content );
640 ifGroupOn.appendChild( createPdfDatasetElement( component ) );
641 }
642 }
643
644 // vector datasets (we "draw" these on top, just for debugging... but they are invisible, so are never really drawn!)
645 if ( details.includeFeatures )
646 {
647 for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
648 {
649 if ( TreeNode *treeNode = layerIdToTreeNode.value( component.mapLayerId ) )
650 {
651 QDomElement ifLayerOnElement = treeNode->createNestedIfLayerOnElements( doc, content );
652
653 QDomElement vectorDataset = doc.createElement( QStringLiteral( "Vector" ) );
654 vectorDataset.setAttribute( QStringLiteral( "dataset" ), component.sourceVectorPath );
655 vectorDataset.setAttribute( QStringLiteral( "layer" ), component.sourceVectorLayer );
656 vectorDataset.setAttribute( QStringLiteral( "visible" ), QStringLiteral( "false" ) );
657 QDomElement logicalStructure = doc.createElement( QStringLiteral( "LogicalStructure" ) );
658 logicalStructure.setAttribute( QStringLiteral( "displayLayerName" ), component.name );
659 if ( !component.displayAttribute.isEmpty() )
660 logicalStructure.setAttribute( QStringLiteral( "fieldToDisplay" ), component.displayAttribute );
661 vectorDataset.appendChild( logicalStructure );
662 ifLayerOnElement.appendChild( vectorDataset );
663 }
664 }
665 }
666
667 page.appendChild( content );
668
669 // layertree
670 QDomElement layerTree = doc.createElement( QStringLiteral( "LayerTree" ) );
671 //layerTree.setAttribute( QStringLiteral("displayOnlyOnVisiblePages"), QStringLiteral("true"));
672
673 // groups are added first
674
675 // sort root groups in desired order
676 std::sort( rootGroups.begin(), rootGroups.end(), [&layerTreeGroupOrder]( const std::unique_ptr< TreeNode > &a, const std::unique_ptr< TreeNode > &b ) -> bool
677 {
678 return layerTreeGroupOrder.indexOf( a->name ) < layerTreeGroupOrder.indexOf( b->name );
679 } );
680
681 bool haveFoundMutuallyExclusiveGroup = false;
682 for ( const auto &node : std::as_const( rootGroups ) )
683 {
684 if ( !node->mutuallyExclusiveGroupId.isEmpty() )
685 {
686 // only the first object in a mutually exclusive group is initially visible
687 node->initiallyVisible = !haveFoundMutuallyExclusiveGroup;
688 haveFoundMutuallyExclusiveGroup = true;
689 }
690 layerTree.appendChild( node->toElement( doc ) );
691 }
692
693 // filter out groups which don't have any content
694 layerTreeGroupOrder.erase( std::remove_if( layerTreeGroupOrder.begin(), layerTreeGroupOrder.end(), [&details]( const QString & group )
695 {
696 return details.customLayerTreeGroups.key( group ).isEmpty();
697 } ), layerTreeGroupOrder.end() );
698
699
700 // then top-level layers
701 std::sort( rootLayers.begin(), rootLayers.end(), [&details]( const std::unique_ptr< TreeNode > &a, const std::unique_ptr< TreeNode > &b ) -> bool
702 {
703 const int indexA = details.layerOrder.indexOf( a->mapLayerId );
704 const int indexB = details.layerOrder.indexOf( b->mapLayerId );
705
706 if ( indexA >= 0 && indexB >= 0 )
707 return indexA < indexB;
708 else if ( indexA >= 0 )
709 return true;
710 else if ( indexB >= 0 )
711 return false;
712
713 return a->name.localeAwareCompare( b->name ) < 0;
714 } );
715
716 for ( const auto &node : std::as_const( rootLayers ) )
717 {
718 layerTree.appendChild( node->toElement( doc ) );
719 }
720
721 compositionElem.appendChild( layerTree );
722 compositionElem.appendChild( page );
723
724 doc.appendChild( compositionElem );
725
726 QString composition;
727 QTextStream stream( &composition );
728 doc.save( stream, -1 );
729
730 return composition;
731}
732
733QString QgsAbstractGeospatialPdfExporter::compositionModeToString( QPainter::CompositionMode mode )
734{
735 switch ( mode )
736 {
737 case QPainter::CompositionMode_SourceOver:
738 return QStringLiteral( "Normal" );
739
740 case QPainter::CompositionMode_Multiply:
741 return QStringLiteral( "Multiply" );
742
743 case QPainter::CompositionMode_Screen:
744 return QStringLiteral( "Screen" );
745
746 case QPainter::CompositionMode_Overlay:
747 return QStringLiteral( "Overlay" );
748
749 case QPainter::CompositionMode_Darken:
750 return QStringLiteral( "Darken" );
751
752 case QPainter::CompositionMode_Lighten:
753 return QStringLiteral( "Lighten" );
754
755 case QPainter::CompositionMode_ColorDodge:
756 return QStringLiteral( "ColorDodge" );
757
758 case QPainter::CompositionMode_ColorBurn:
759 return QStringLiteral( "ColorBurn" );
760
761 case QPainter::CompositionMode_HardLight:
762 return QStringLiteral( "HardLight" );
763
764 case QPainter::CompositionMode_SoftLight:
765 return QStringLiteral( "SoftLight" );
766
767 case QPainter::CompositionMode_Difference:
768 return QStringLiteral( "Difference" );
769
770 case QPainter::CompositionMode_Exclusion:
771 return QStringLiteral( "Exclusion" );
772
773 default:
774 break;
775 }
776
777 QgsDebugError( QStringLiteral( "Unsupported PDF blend mode %1" ).arg( mode ) );
778 return QStringLiteral( "Normal" );
779}
780
@ 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.