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