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