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