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