QGIS API Documentation 3.41.0-Master (02257426e5a)
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 geospatial PDF 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 QgsAbstractGeospatialPdfExporter::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 geospatial PDF 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
157{
158 return mTemporaryDir.filePath( QgsFileUtils::stringToSafeFilename( filename ) );
159}
160
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
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 QgsAbstractGeospatialPdfExporter::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 QgsAbstractGeospatialPdfExporter::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 QgsAbstractGeospatialPdfExporter::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( "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 QgsAbstractGeospatialPdfExporter::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.
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.
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:6008
void CPL_STDCALL collectErrors(CPLErr, int, const char *msg)
QList< QgsFeature > QgsFeatureList
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:41
#define QgsDebugError(str)
Definition qgslogger.h:40
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.