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