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