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