QGIS API Documentation  3.16.0-Hannover (43b64b13f3)
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  break;
159  }
160 
161  return false;
162 }
163 
164 void QgsAbstractGeoPdfExporter::pushRenderedFeature( const QString &layerId, const QgsAbstractGeoPdfExporter::RenderedFeature &feature, const QString &group )
165 {
166  // because map layers may be rendered in parallel, we need a mutex here
167  QMutexLocker locker( &mMutex );
168 
169  // collate all the features which belong to the same layer, replacing their geometries with the rendered feature bounds
170  QgsFeature f = feature.feature;
171  f.setGeometry( feature.renderedBounds );
172  mCollatedFeatures[ group ][ layerId ].append( f );
173 }
174 
175 bool QgsAbstractGeoPdfExporter::saveTemporaryLayers()
176 {
177  for ( auto groupIt = mCollatedFeatures.constBegin(); groupIt != mCollatedFeatures.constEnd(); ++groupIt )
178  {
179  for ( auto it = groupIt->constBegin(); it != groupIt->constEnd(); ++it )
180  {
181  const QString filePath = generateTemporaryFilepath( it.key() + groupIt.key() + QStringLiteral( ".gpkg" ) );
182 
183  VectorComponentDetail detail = componentDetailForLayerId( it.key() );
184  detail.sourceVectorPath = filePath;
185  detail.group = groupIt.key();
186 
187  // write out features to disk
188  const QgsFeatureList features = it.value();
189  QString layerName;
191  saveOptions.driverName = QStringLiteral( "GPKG" );
193  std::unique_ptr< QgsVectorFileWriter > writer( QgsVectorFileWriter::create( filePath, features.first().fields(), features.first().geometry().wkbType(), QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext(), saveOptions, QgsFeatureSink::RegeneratePrimaryKey, nullptr, &layerName ) );
194  if ( writer->hasError() )
195  {
196  mErrorMessage = writer->errorMessage();
197  QgsDebugMsg( mErrorMessage );
198  return false;
199  }
200  for ( const QgsFeature &feature : features )
201  {
202  QgsFeature f = feature;
203  if ( !writer->addFeature( f, QgsFeatureSink::FastInsert ) )
204  {
205  mErrorMessage = writer->errorMessage();
206  QgsDebugMsg( mErrorMessage );
207  return false;
208  }
209  }
210  detail.sourceVectorLayer = layerName;
211  mVectorComponents << detail;
212  }
213  }
214  return true;
215 }
216 
217 QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLayerDetail> &components, const ExportDetails &details )
218 {
219  QDomDocument doc;
220 
221  QDomElement compositionElem = doc.createElement( QStringLiteral( "PDFComposition" ) );
222 
223  // metadata tags
224  QDomElement metadata = doc.createElement( QStringLiteral( "Metadata" ) );
225  if ( !details.author.isEmpty() )
226  {
227  QDomElement author = doc.createElement( QStringLiteral( "Author" ) );
228  author.appendChild( doc.createTextNode( details.author ) );
229  metadata.appendChild( author );
230  }
231  if ( !details.producer.isEmpty() )
232  {
233  QDomElement producer = doc.createElement( QStringLiteral( "Producer" ) );
234  producer.appendChild( doc.createTextNode( details.producer ) );
235  metadata.appendChild( producer );
236  }
237  if ( !details.creator.isEmpty() )
238  {
239  QDomElement creator = doc.createElement( QStringLiteral( "Creator" ) );
240  creator.appendChild( doc.createTextNode( details.creator ) );
241  metadata.appendChild( creator );
242  }
243  if ( details.creationDateTime.isValid() )
244  {
245  QDomElement creationDate = doc.createElement( QStringLiteral( "CreationDate" ) );
246  QString creationDateString = QStringLiteral( "D:%1" ).arg( details.creationDateTime.toString( QStringLiteral( "yyyyMMddHHmmss" ) ) );
247  if ( details.creationDateTime.timeZone().isValid() )
248  {
249  int offsetFromUtc = details.creationDateTime.timeZone().offsetFromUtc( details.creationDateTime );
250  creationDateString += ( offsetFromUtc >= 0 ) ? '+' : '-';
251  offsetFromUtc = std::abs( offsetFromUtc );
252  int offsetHours = offsetFromUtc / 3600;
253  int offsetMins = ( offsetFromUtc % 3600 ) / 60;
254  creationDateString += QStringLiteral( "%1'%2'" ).arg( offsetHours ).arg( offsetMins );
255  }
256  creationDate.appendChild( doc.createTextNode( creationDateString ) );
257  metadata.appendChild( creationDate );
258  }
259  if ( !details.subject.isEmpty() )
260  {
261  QDomElement subject = doc.createElement( QStringLiteral( "Subject" ) );
262  subject.appendChild( doc.createTextNode( details.subject ) );
263  metadata.appendChild( subject );
264  }
265  if ( !details.title.isEmpty() )
266  {
267  QDomElement title = doc.createElement( QStringLiteral( "Title" ) );
268  title.appendChild( doc.createTextNode( details.title ) );
269  metadata.appendChild( title );
270  }
271  if ( !details.keywords.empty() )
272  {
273  QStringList allKeywords;
274  for ( auto it = details.keywords.constBegin(); it != details.keywords.constEnd(); ++it )
275  {
276  allKeywords.append( QStringLiteral( "%1: %2" ).arg( it.key(), it.value().join( ',' ) ) );
277  }
278  QDomElement keywords = doc.createElement( QStringLiteral( "Keywords" ) );
279  keywords.appendChild( doc.createTextNode( allKeywords.join( ';' ) ) );
280  metadata.appendChild( keywords );
281  }
282  compositionElem.appendChild( metadata );
283 
284  QMap< QString, QSet< QString > > createdLayerIds;
285  QMap< QString, QDomElement > groupLayerMap;
286  QMap< QString, QString > customGroupNamesToIds;
287 
288  QMultiMap< QString, QDomElement > pendingLayerTreeElements;
289 
290  if ( details.includeFeatures )
291  {
292  for ( const VectorComponentDetail &component : qgis::as_const( mVectorComponents ) )
293  {
294  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
295  continue;
296 
297  QDomElement layer = doc.createElement( QStringLiteral( "Layer" ) );
298  layer.setAttribute( QStringLiteral( "id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
299  layer.setAttribute( QStringLiteral( "name" ), details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name );
300  layer.setAttribute( QStringLiteral( "initiallyVisible" ), details.initialLayerVisibility.value( component.mapLayerId, true ) ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
301 
302  if ( !component.group.isEmpty() )
303  {
304  if ( groupLayerMap.contains( component.group ) )
305  {
306  groupLayerMap[ component.group ].appendChild( layer );
307  }
308  else
309  {
310  QDomElement group = doc.createElement( QStringLiteral( "Layer" ) );
311  group.setAttribute( QStringLiteral( "id" ), QStringLiteral( "group_%1" ).arg( component.group ) );
312  group.setAttribute( QStringLiteral( "name" ), component.group );
313  group.setAttribute( QStringLiteral( "initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
314  group.setAttribute( QStringLiteral( "mutuallyExclusiveGroupId" ), QStringLiteral( "__mutually_exclusive_groups__" ) );
315  pendingLayerTreeElements.insert( component.mapLayerId, group );
316  group.appendChild( layer );
317  groupLayerMap[ component.group ] = group;
318  }
319  }
320  else
321  {
322  pendingLayerTreeElements.insert( component.mapLayerId, layer );
323  }
324 
325  createdLayerIds[ component.group ].insert( component.mapLayerId );
326  }
327  }
328  // some PDF components may not be linked to vector components - e.g. layers with labels but no features (or raster layers)
329  for ( const ComponentLayerDetail &component : components )
330  {
331  if ( component.mapLayerId.isEmpty() || createdLayerIds.value( component.group ).contains( component.mapLayerId ) )
332  continue;
333 
334  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
335  continue;
336 
337  QDomElement layer = doc.createElement( QStringLiteral( "Layer" ) );
338  layer.setAttribute( QStringLiteral( "id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
339  layer.setAttribute( QStringLiteral( "name" ), details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name );
340  layer.setAttribute( QStringLiteral( "initiallyVisible" ), details.initialLayerVisibility.value( component.mapLayerId, true ) ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
341 
342  if ( !component.group.isEmpty() )
343  {
344  if ( groupLayerMap.contains( component.group ) )
345  {
346  groupLayerMap[ component.group ].appendChild( layer );
347  }
348  else
349  {
350  QDomElement group = doc.createElement( QStringLiteral( "Layer" ) );
351  group.setAttribute( QStringLiteral( "id" ), QStringLiteral( "group_%1" ).arg( component.group ) );
352  group.setAttribute( QStringLiteral( "name" ), component.group );
353  group.setAttribute( QStringLiteral( "initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
354  group.setAttribute( QStringLiteral( "mutuallyExclusiveGroupId" ), QStringLiteral( "__mutually_exclusive_groups__" ) );
355  pendingLayerTreeElements.insert( component.mapLayerId, group );
356  group.appendChild( layer );
357  groupLayerMap[ component.group ] = group;
358  }
359  }
360  else
361  {
362  pendingLayerTreeElements.insert( component.mapLayerId, layer );
363  }
364 
365  createdLayerIds[ component.group ].insert( component.mapLayerId );
366  }
367 
368  // layertree
369  QDomElement layerTree = doc.createElement( QStringLiteral( "LayerTree" ) );
370  //layerTree.setAttribute( QStringLiteral("displayOnlyOnVisiblePages"), QStringLiteral("true"));
371 
372  // create custom layer tree entries
373  for ( auto it = details.customLayerTreeGroups.constBegin(); it != details.customLayerTreeGroups.constEnd(); ++it )
374  {
375  if ( customGroupNamesToIds.contains( it.value() ) )
376  continue;
377 
378  QDomElement layer = doc.createElement( QStringLiteral( "Layer" ) );
379  const QString id = QUuid::createUuid().toString();
380  customGroupNamesToIds[ it.value() ] = id;
381  layer.setAttribute( QStringLiteral( "id" ), id );
382  layer.setAttribute( QStringLiteral( "name" ), it.value() );
383  layer.setAttribute( QStringLiteral( "initiallyVisible" ), QStringLiteral( "true" ) );
384  layerTree.appendChild( layer );
385  }
386 
387  // start by adding layer tree elements with known layer orders
388  for ( const QString &layerId : details.layerOrder )
389  {
390  const QList< QDomElement> elements = pendingLayerTreeElements.values( layerId );
391  for ( const QDomElement &element : elements )
392  layerTree.appendChild( element );
393  }
394  // then add all the rest (those we don't have an explicit order for)
395  for ( auto it = pendingLayerTreeElements.constBegin(); it != pendingLayerTreeElements.constEnd(); ++it )
396  {
397  if ( details.layerOrder.contains( it.key() ) )
398  {
399  // already added this one, just above...
400  continue;
401  }
402 
403  layerTree.appendChild( it.value() );
404  }
405 
406  compositionElem.appendChild( layerTree );
407 
408  // pages
409  QDomElement page = doc.createElement( QStringLiteral( "Page" ) );
410  QDomElement dpi = doc.createElement( QStringLiteral( "DPI" ) );
411  // hardcode DPI of 72 to get correct page sizes in outputs -- refs discussion in https://github.com/OSGeo/gdal/pull/2961
412  dpi.appendChild( doc.createTextNode( qgsDoubleToString( 72 ) ) );
413  page.appendChild( dpi );
414  // assumes DPI of 72, as noted above.
415  QDomElement width = doc.createElement( QStringLiteral( "Width" ) );
416  const double pageWidthPdfUnits = std::ceil( details.pageSizeMm.width() / 25.4 * 72 );
417  width.appendChild( doc.createTextNode( qgsDoubleToString( pageWidthPdfUnits ) ) );
418  page.appendChild( width );
419  QDomElement height = doc.createElement( QStringLiteral( "Height" ) );
420  const double pageHeightPdfUnits = std::ceil( details.pageSizeMm.height() / 25.4 * 72 );
421  height.appendChild( doc.createTextNode( qgsDoubleToString( pageHeightPdfUnits ) ) );
422  page.appendChild( height );
423 
424 
425  // georeferencing
426  int i = 0;
427  for ( const QgsAbstractGeoPdfExporter::GeoReferencedSection &section : details.georeferencedSections )
428  {
429  QDomElement georeferencing = doc.createElement( QStringLiteral( "Georeferencing" ) );
430  georeferencing.setAttribute( QStringLiteral( "id" ), QStringLiteral( "georeferenced_%1" ).arg( i++ ) );
431  georeferencing.setAttribute( QStringLiteral( "OGCBestPracticeFormat" ), details.useOgcBestPracticeFormatGeoreferencing ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
432  georeferencing.setAttribute( QStringLiteral( "ISO32000ExtensionFormat" ), details.useIso32000ExtensionFormatGeoreferencing ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
433 
434  if ( section.crs.isValid() )
435  {
436  QDomElement srs = doc.createElement( QStringLiteral( "SRS" ) );
437  // 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...
438  // srs.setAttribute( QStringLiteral( "dataAxisToSRSAxisMapping" ), QStringLiteral( "2,1" ) );
439  if ( !section.crs.authid().startsWith( QStringLiteral( "user" ), Qt::CaseInsensitive ) )
440  {
441  srs.appendChild( doc.createTextNode( section.crs.authid() ) );
442  }
443  else
444  {
445  srs.appendChild( doc.createTextNode( section.crs.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED_GDAL ) ) );
446  }
447  georeferencing.appendChild( srs );
448  }
449 
450  if ( !section.pageBoundsPolygon.isEmpty() )
451  {
452  /*
453  Define a polygon / neatline in PDF units into which the
454  Measure tool will display coordinates.
455  If not specified, BoundingBox will be used instead.
456  If none of BoundingBox and BoundingPolygon are specified,
457  the whole PDF page will be assumed to be georeferenced.
458  */
459  QDomElement boundingPolygon = doc.createElement( QStringLiteral( "BoundingPolygon" ) );
460 
461  // transform to PDF coordinate space
462  QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(),
463  -pageHeightPdfUnits / details.pageSizeMm.height() );
464 
465  QgsPolygon p = section.pageBoundsPolygon;
466  p.transform( t );
467  boundingPolygon.appendChild( doc.createTextNode( p.asWkt() ) );
468 
469  georeferencing.appendChild( boundingPolygon );
470  }
471  else
472  {
473  /* Define the viewport where georeferenced coordinates are available.
474  If not specified, the extent of BoundingPolygon will be used instead.
475  If none of BoundingBox and BoundingPolygon are specified,
476  the whole PDF page will be assumed to be georeferenced.
477  */
478  QDomElement boundingBox = doc.createElement( QStringLiteral( "BoundingBox" ) );
479  boundingBox.setAttribute( QStringLiteral( "x1" ), qgsDoubleToString( section.pageBoundsMm.xMinimum() / 25.4 * 72 ) );
480  boundingBox.setAttribute( QStringLiteral( "y1" ), qgsDoubleToString( section.pageBoundsMm.yMinimum() / 25.4 * 72 ) );
481  boundingBox.setAttribute( QStringLiteral( "x2" ), qgsDoubleToString( section.pageBoundsMm.xMaximum() / 25.4 * 72 ) );
482  boundingBox.setAttribute( QStringLiteral( "y2" ), qgsDoubleToString( section.pageBoundsMm.yMaximum() / 25.4 * 72 ) );
483  georeferencing.appendChild( boundingBox );
484  }
485 
486  for ( const ControlPoint &point : section.controlPoints )
487  {
488  QDomElement cp1 = doc.createElement( QStringLiteral( "ControlPoint" ) );
489  cp1.setAttribute( QStringLiteral( "x" ), qgsDoubleToString( point.pagePoint.x() / 25.4 * 72 ) );
490  cp1.setAttribute( QStringLiteral( "y" ), qgsDoubleToString( ( details.pageSizeMm.height() - point.pagePoint.y() ) / 25.4 * 72 ) );
491  cp1.setAttribute( QStringLiteral( "GeoX" ), qgsDoubleToString( point.geoPoint.x() ) );
492  cp1.setAttribute( QStringLiteral( "GeoY" ), qgsDoubleToString( point.geoPoint.y() ) );
493  georeferencing.appendChild( cp1 );
494  }
495 
496  page.appendChild( georeferencing );
497  }
498 
499  auto createPdfDatasetElement = [&doc]( const ComponentLayerDetail & component ) -> QDomElement
500  {
501  QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
502  pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
503  if ( component.opacity != 1.0 || component.compositionMode != QPainter::CompositionMode_SourceOver )
504  {
505  QDomElement blendingElement = doc.createElement( QStringLiteral( "Blending" ) );
506  blendingElement.setAttribute( QStringLiteral( "opacity" ), component.opacity );
507  blendingElement.setAttribute( QStringLiteral( "function" ), compositionModeToString( component.compositionMode ) );
508 
509  pdfDataset.appendChild( blendingElement );
510  }
511  return pdfDataset;
512  };
513 
514  // content
515  QDomElement content = doc.createElement( QStringLiteral( "Content" ) );
516  for ( const ComponentLayerDetail &component : components )
517  {
518  if ( component.mapLayerId.isEmpty() )
519  {
520  content.appendChild( createPdfDatasetElement( component ) );
521  }
522  else if ( !component.group.isEmpty() )
523  {
524  // if content belongs to a group, we need nested "IfLayerOn" elements, one for the group and one for the layer
525  QDomElement ifGroupOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
526  ifGroupOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "group_%1" ).arg( component.group ) );
527  QDomElement ifLayerOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
528  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
529  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
530  else if ( component.group.isEmpty() )
531  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
532  else
533  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
534 
535  ifLayerOn.appendChild( createPdfDatasetElement( component ) );
536  ifGroupOn.appendChild( ifLayerOn );
537  content.appendChild( ifGroupOn );
538  }
539  else
540  {
541  QDomElement ifLayerOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
542  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
543  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
544  else if ( component.group.isEmpty() )
545  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
546  else
547  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
548  ifLayerOn.appendChild( createPdfDatasetElement( component ) );
549  content.appendChild( ifLayerOn );
550  }
551  }
552 
553  // vector datasets (we "draw" these on top, just for debugging... but they are invisible, so are never really drawn!)
554  if ( details.includeFeatures )
555  {
556  for ( const VectorComponentDetail &component : qgis::as_const( mVectorComponents ) )
557  {
558  QDomElement ifLayerOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
559  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
560  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
561  else if ( component.group.isEmpty() )
562  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
563  else
564  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
565  QDomElement vectorDataset = doc.createElement( QStringLiteral( "Vector" ) );
566  vectorDataset.setAttribute( QStringLiteral( "dataset" ), component.sourceVectorPath );
567  vectorDataset.setAttribute( QStringLiteral( "layer" ), component.sourceVectorLayer );
568  vectorDataset.setAttribute( QStringLiteral( "visible" ), QStringLiteral( "false" ) );
569  QDomElement logicalStructure = doc.createElement( QStringLiteral( "LogicalStructure" ) );
570  logicalStructure.setAttribute( QStringLiteral( "displayLayerName" ), component.name );
571  if ( !component.displayAttribute.isEmpty() )
572  logicalStructure.setAttribute( QStringLiteral( "fieldToDisplay" ), component.displayAttribute );
573  vectorDataset.appendChild( logicalStructure );
574  ifLayerOn.appendChild( vectorDataset );
575  content.appendChild( ifLayerOn );
576  }
577  }
578 
579  page.appendChild( content );
580  compositionElem.appendChild( page );
581 
582  doc.appendChild( compositionElem );
583 
584  QString composition;
585  QTextStream stream( &composition );
586  doc.save( stream, -1 );
587 
588  return composition;
589 }
590 
591 QString QgsAbstractGeoPdfExporter::compositionModeToString( QPainter::CompositionMode mode )
592 {
593  switch ( mode )
594  {
595  case QPainter::CompositionMode_SourceOver:
596  return QStringLiteral( "Normal" );
597 
598  case QPainter::CompositionMode_Multiply:
599  return QStringLiteral( "Multiply" );
600 
601  case QPainter::CompositionMode_Screen:
602  return QStringLiteral( "Screen" );
603 
604  case QPainter::CompositionMode_Overlay:
605  return QStringLiteral( "Overlay" );
606 
607  case QPainter::CompositionMode_Darken:
608  return QStringLiteral( "Darken" );
609 
610  case QPainter::CompositionMode_Lighten:
611  return QStringLiteral( "Lighten" );
612 
613  case QPainter::CompositionMode_ColorDodge:
614  return QStringLiteral( "ColorDodge" );
615 
616  case QPainter::CompositionMode_ColorBurn:
617  return QStringLiteral( "ColorBurn" );
618 
619  case QPainter::CompositionMode_HardLight:
620  return QStringLiteral( "HardLight" );
621 
622  case QPainter::CompositionMode_SoftLight:
623  return QStringLiteral( "SoftLight" );
624 
625  case QPainter::CompositionMode_Difference:
626  return QStringLiteral( "Difference" );
627 
628  case QPainter::CompositionMode_Exclusion:
629  return QStringLiteral( "Exclusion" );
630 
631  default:
632  break;
633  }
634 
635  QgsDebugMsg( QStringLiteral( "Unsupported PDF blend mode %1" ).arg( mode ) );
636  return QStringLiteral( "Normal" );
637 }
638 
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:150
QgsCoordinateTransformContext
Contains information about the context in which a coordinate transform is executed.
Definition: qgscoordinatetransformcontext.h:58
QgsVectorFileWriter::SaveVectorOptions
Options to pass to writeAsVectorFormat()
Definition: qgsvectorfilewriter.h:446
qgsfeaturerequest.h
QgsPolygon
Polygon geometry type.
Definition: qgspolygon.h:34
QgsWms::layerTree
QgsLayerTree * layerTree(const QgsWmsRenderContext &context)
Definition: qgswmsgetlegendgraphics.cpp:307
QgsCoordinateReferenceSystem::WKT_PREFERRED_GDAL
@ WKT_PREFERRED_GDAL
Preferred format for conversion of CRS to WKT for use with the GDAL library.
Definition: qgscoordinatereferencesystem.h:681
qgscoordinatetransformcontext.h
qgsgdalutils.h
QgsRectangle::yMinimum
double yMinimum() const SIP_HOLDGIL
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:177
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:164
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:198
QgsVectorFileWriter::NoSymbology
@ NoSymbology
Definition: qgsvectorfilewriter.h:183
QgsDebugMsg
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QgsAbstractGeoPdfExporter::GeoReferencedSection
Definition: qgsabstractgeopdfexporter.h:164
QgsCurvePolygon::isEmpty
bool isEmpty() const override SIP_HOLDGIL
Returns true if the geometry is empty.
Definition: qgscurvepolygon.cpp:918
qgsDoubleToString
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition: qgis.h:275
QgsRectangle::xMaximum
double xMaximum() const SIP_HOLDGIL
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:162
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:139
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:818
QgsAbstractGeoPdfExporter::RenderedFeature
Contains information about a feature rendered inside the PDF.
Definition: qgsabstractgeopdfexporter.h:85
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:1321
QgsFeatureList
QList< QgsFeature > QgsFeatureList
Definition: qgsfeature.h:583
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:1954
QgsCoordinateReferenceSystem::isValid
bool isValid() const
Returns whether this CRS is correctly initialized and usable.
Definition: qgscoordinatereferencesystem.cpp:924
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
QgsRectangle::xMinimum
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:167
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
QgsCoordinateReferenceSystem
This class represents a coordinate reference system (CRS).
Definition: qgscoordinatereferencesystem.h:206
QgsCurvePolygon::asWkt
QString asWkt(int precision=17) const override
Returns a WKT representation of the geometry.
Definition: qgscurvepolygon.cpp:324
qgsvectorlayer.h
QgsRectangle::yMaximum
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:172
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
QgsFeature
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:56
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:487
qgsvectorfilewriter.h
QgsVectorFileWriter::SaveVectorOptions::driverName
QString driverName
OGR driver to use.
Definition: qgsvectorfilewriter.h:454
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