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