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