QGIS API Documentation  3.25.0-Master (dec16ba68b)
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 
78 bool 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 
124 QString QgsAbstractGeoPdfExporter::generateTemporaryFilepath( const QString &filename ) const
125 {
126  return mTemporaryDir.filePath( filename );
127 }
128 
129 bool 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 
154 void 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 
165 bool 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 
207 QString 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 
581 QString 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:163
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:139
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition: qgis.h:1942
QList< QgsFeature > QgsFeatureList
Definition: qgsfeature.h:882
#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.