QGIS API Documentation  3.10.0-A Coruña (6c816b4204)
qgsabstractgeopdfexporter.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsabtractgeopdfexporter.cpp
3  --------------------------
4  begin : August 2019
5  copyright : (C) 2019 by Nyall Dawson
6  email : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 /***************************************************************************
9  * *
10  * This program is free software; you can redistribute it and/or modify *
11  * it under the terms of the GNU General Public License as published by *
12  * the Free Software Foundation; either version 2 of the License, or *
13  * (at your option) any later version. *
14  * *
15  ***************************************************************************/
16 
19 #include "qgsfeaturerequest.h"
20 #include "qgslogger.h"
21 #include "qgsgeometry.h"
22 #include "qgsvectorlayer.h"
23 #include "qgsvectorfilewriter.h"
24 
25 #include <gdal.h>
26 #include "qgsgdalutils.h"
27 #include "cpl_string.h"
28 
29 #include <QMutex>
30 #include <QMutexLocker>
31 #include <QDomDocument>
32 #include <QDomElement>
33 
34 
36 {
37 #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0)
38  return false;
39 #else
40 
41  // test if GDAL has read support in PDF driver
42  GDALDriverH hDriverMem = GDALGetDriverByName( "PDF" );
43  if ( !hDriverMem )
44  {
45  return false;
46  }
47 
48  const char *pHavePoppler = GDALGetMetadataItem( hDriverMem, "HAVE_POPPLER", nullptr );
49  if ( pHavePoppler && strstr( pHavePoppler, "YES" ) )
50  return true;
51 
52  const char *pHavePdfium = GDALGetMetadataItem( hDriverMem, "HAVE_PDFIUM", nullptr );
53  if ( pHavePdfium && strstr( pHavePdfium, "YES" ) )
54  return true;
55 
56  return false;
57 #endif
58 }
59 
61 {
62 #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0)
63  return QObject::tr( "GeoPDF creation requires GDAL version 3.0 or later." );
64 #else
65  // test if GDAL has read support in PDF driver
66  GDALDriverH hDriverMem = GDALGetDriverByName( "PDF" );
67  if ( !hDriverMem )
68  {
69  return QObject::tr( "No GDAL PDF driver available." );
70  }
71 
72  const char *pHavePoppler = GDALGetMetadataItem( hDriverMem, "HAVE_POPPLER", nullptr );
73  if ( pHavePoppler && strstr( pHavePoppler, "YES" ) )
74  return QString();
75 
76  const char *pHavePdfium = GDALGetMetadataItem( hDriverMem, "HAVE_PDFIUM", nullptr );
77  if ( pHavePdfium && strstr( pHavePdfium, "YES" ) )
78  return QString();
79 
80  return QObject::tr( "GDAL PDF driver was not built with PDF read support. A build with PDF read support is required for GeoPDF creation." );
81 #endif
82 }
83 
84 bool QgsAbstractGeoPdfExporter::finalize( const QList<ComponentLayerDetail> &components, const QString &destinationFile, const ExportDetails &details )
85 {
86  if ( details.includeFeatures && !saveTemporaryLayers() )
87  return false;
88 
89 #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0)
90  Q_UNUSED( components )
91  Q_UNUSED( destinationFile )
92  return false;
93 #else
94  const QString composition = createCompositionXml( components, details );
95  QgsDebugMsg( composition );
96  if ( composition.isEmpty() )
97  return false;
98 
99  // do the creation!
100  GDALDriverH driver = GDALGetDriverByName( "PDF" );
101  if ( !driver )
102  {
103  mErrorMessage = QObject::tr( "Cannot load GDAL PDF driver" );
104  return false;
105  }
106 
107  const QString xmlFilePath = generateTemporaryFilepath( QStringLiteral( "composition.xml" ) );
108  QFile file( xmlFilePath );
109  if ( file.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
110  {
111  QTextStream out( &file );
112  out << composition;
113  }
114  else
115  {
116  mErrorMessage = QObject::tr( "Could not create GeoPDF composition file" );
117  return false;
118  }
119 
120  char **papszOptions = CSLSetNameValue( nullptr, "COMPOSITION_FILE", xmlFilePath.toUtf8().constData() );
121 
122  // return a non-null (fake) dataset in case of success, nullptr otherwise.
123  gdal::dataset_unique_ptr outputDataset( GDALCreate( driver, destinationFile.toUtf8().constData(), 0, 0, 0, GDT_Unknown, papszOptions ) );
124  bool res = outputDataset.get();
125  outputDataset.reset();
126 
127  CSLDestroy( papszOptions );
128 
129  return res;
130 #endif
131 }
132 
133 QString QgsAbstractGeoPdfExporter::generateTemporaryFilepath( const QString &filename ) const
134 {
135  return mTemporaryDir.filePath( filename );
136 }
137 
138 
139 void QgsAbstractGeoPdfExporter::pushRenderedFeature( const QString &layerId, const QgsAbstractGeoPdfExporter::RenderedFeature &feature, const QString &group )
140 {
141  // because map layers may be rendered in parallel, we need a mutex here
142  QMutexLocker locker( &mMutex );
143 
144  // collate all the features which belong to the same layer, replacing their geometries with the rendered feature bounds
145  QgsFeature f = feature.feature;
146  f.setGeometry( feature.renderedBounds );
147  mCollatedFeatures[ group ][ layerId ].append( f );
148 }
149 
150 bool QgsAbstractGeoPdfExporter::saveTemporaryLayers()
151 {
152  for ( auto groupIt = mCollatedFeatures.constBegin(); groupIt != mCollatedFeatures.constEnd(); ++groupIt )
153  {
154  for ( auto it = groupIt->constBegin(); it != groupIt->constEnd(); ++it )
155  {
156  const QString filePath = generateTemporaryFilepath( it.key() + groupIt.key() + QStringLiteral( ".gpkg" ) );
157 
158  VectorComponentDetail detail = componentDetailForLayerId( it.key() );
159  detail.sourceVectorPath = filePath;
160  detail.group = groupIt.key();
161 
162  // write out features to disk
163  const QgsFeatureList features = it.value();
164  QString layerName;
165  QgsVectorFileWriter writer( filePath, QString(), features.first().fields(), features.first().geometry().wkbType(), QgsCoordinateReferenceSystem(), QStringLiteral( "GPKG" ), QStringList(), QStringList(), nullptr, QgsVectorFileWriter::NoSymbology, QgsFeatureSink::RegeneratePrimaryKey, &layerName );
166  if ( writer.hasError() )
167  {
168  mErrorMessage = writer.errorMessage();
169  QgsDebugMsg( mErrorMessage );
170  return false;
171  }
172  for ( const QgsFeature &feature : features )
173  {
174  QgsFeature f = feature;
175  if ( !writer.addFeature( f, QgsFeatureSink::FastInsert ) )
176  {
177  mErrorMessage = writer.errorMessage();
178  QgsDebugMsg( mErrorMessage );
179  return false;
180  }
181  }
182  detail.sourceVectorLayer = layerName;
183  mVectorComponents << detail;
184  }
185  }
186  return true;
187 }
188 
189 QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLayerDetail> &components, const ExportDetails &details )
190 {
191  QDomDocument doc;
192 
193  QDomElement compositionElem = doc.createElement( QStringLiteral( "PDFComposition" ) );
194 
195  // metadata tags
196  QDomElement metadata = doc.createElement( QStringLiteral( "Metadata" ) );
197  if ( !details.author.isEmpty() )
198  {
199  QDomElement author = doc.createElement( QStringLiteral( "Author" ) );
200  author.appendChild( doc.createTextNode( details.author ) );
201  metadata.appendChild( author );
202  }
203  if ( !details.producer.isEmpty() )
204  {
205  QDomElement producer = doc.createElement( QStringLiteral( "Producer" ) );
206  producer.appendChild( doc.createTextNode( details.producer ) );
207  metadata.appendChild( producer );
208  }
209  if ( !details.creator.isEmpty() )
210  {
211  QDomElement creator = doc.createElement( QStringLiteral( "Creator" ) );
212  creator.appendChild( doc.createTextNode( details.creator ) );
213  metadata.appendChild( creator );
214  }
215  if ( details.creationDateTime.isValid() )
216  {
217  QDomElement creationDate = doc.createElement( QStringLiteral( "CreationDate" ) );
218  QString creationDateString = QStringLiteral( "D:%1" ).arg( details.creationDateTime.toString( QStringLiteral( "yyyyMMddHHmmss" ) ) );
219  if ( details.creationDateTime.timeZone().isValid() )
220  {
221  int offsetFromUtc = details.creationDateTime.timeZone().offsetFromUtc( details.creationDateTime );
222  creationDateString += ( offsetFromUtc >= 0 ) ? '+' : '-';
223  offsetFromUtc = std::abs( offsetFromUtc );
224  int offsetHours = offsetFromUtc / 3600;
225  int offsetMins = ( offsetFromUtc % 3600 ) / 60;
226  creationDateString += QStringLiteral( "%1'%2'" ).arg( offsetHours ).arg( offsetMins );
227  }
228  creationDate.appendChild( doc.createTextNode( creationDateString ) );
229  metadata.appendChild( creationDate );
230  }
231  if ( !details.subject.isEmpty() )
232  {
233  QDomElement subject = doc.createElement( QStringLiteral( "Subject" ) );
234  subject.appendChild( doc.createTextNode( details.subject ) );
235  metadata.appendChild( subject );
236  }
237  if ( !details.title.isEmpty() )
238  {
239  QDomElement title = doc.createElement( QStringLiteral( "Title" ) );
240  title.appendChild( doc.createTextNode( details.title ) );
241  metadata.appendChild( title );
242  }
243  if ( !details.keywords.empty() )
244  {
245  QStringList allKeywords;
246  for ( auto it = details.keywords.constBegin(); it != details.keywords.constEnd(); ++it )
247  {
248  allKeywords.append( QStringLiteral( "%1: %2" ).arg( it.key(), it.value().join( ',' ) ) );
249  }
250  QDomElement keywords = doc.createElement( QStringLiteral( "Keywords" ) );
251  keywords.appendChild( doc.createTextNode( allKeywords.join( ';' ) ) );
252  metadata.appendChild( keywords );
253  }
254  compositionElem.appendChild( metadata );
255 
256  // layertree
257  QDomElement layerTree = doc.createElement( QStringLiteral( "LayerTree" ) );
258  //layerTree.setAttribute( QStringLiteral("displayOnlyOnVisiblePages"), QStringLiteral("true"));
259  QMap< QString, QSet< QString > > createdLayerIds;
260  QMap< QString, QDomElement > groupLayerMap;
261  QMap< QString, QString > customGroupNamesToIds;
262  if ( details.includeFeatures )
263  {
264  for ( const VectorComponentDetail &component : qgis::as_const( mVectorComponents ) )
265  {
266  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
267  continue;
268 
269  QDomElement layer = doc.createElement( QStringLiteral( "Layer" ) );
270  layer.setAttribute( QStringLiteral( "id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
271  layer.setAttribute( QStringLiteral( "name" ), component.name );
272  layer.setAttribute( QStringLiteral( "initiallyVisible" ), QStringLiteral( "true" ) );
273 
274  if ( !component.group.isEmpty() )
275  {
276  if ( groupLayerMap.contains( component.group ) )
277  {
278  groupLayerMap[ component.group ].appendChild( layer );
279  }
280  else
281  {
282  QDomElement group = doc.createElement( QStringLiteral( "Layer" ) );
283  group.setAttribute( QStringLiteral( "id" ), QStringLiteral( "group_%1" ).arg( component.group ) );
284  group.setAttribute( QStringLiteral( "name" ), component.group );
285  group.setAttribute( QStringLiteral( "initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
286  group.setAttribute( QStringLiteral( "mutuallyExclusiveGroupId" ), QStringLiteral( "__mutually_exclusive_groups__" ) );
287  layerTree.appendChild( group );
288  group.appendChild( layer );
289  groupLayerMap[ component.group ] = group;
290  }
291  }
292  else
293  {
294  layerTree.appendChild( layer );
295  }
296 
297  createdLayerIds[ component.group ].insert( component.mapLayerId );
298  }
299  }
300  // some PDF components may not be linked to vector components - e.g. layers with labels but no features
301  for ( const ComponentLayerDetail &component : components )
302  {
303  if ( component.mapLayerId.isEmpty() || createdLayerIds.value( component.group ).contains( component.mapLayerId ) )
304  continue;
305 
306  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
307  continue;
308 
309  QDomElement layer = doc.createElement( QStringLiteral( "Layer" ) );
310  layer.setAttribute( QStringLiteral( "id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
311  layer.setAttribute( QStringLiteral( "name" ), component.name );
312  layer.setAttribute( QStringLiteral( "initiallyVisible" ), QStringLiteral( "true" ) );
313 
314  if ( !component.group.isEmpty() )
315  {
316  if ( groupLayerMap.contains( component.group ) )
317  {
318  groupLayerMap[ component.group ].appendChild( layer );
319  }
320  else
321  {
322  QDomElement group = doc.createElement( QStringLiteral( "Layer" ) );
323  group.setAttribute( QStringLiteral( "id" ), QStringLiteral( "group_%1" ).arg( component.group ) );
324  group.setAttribute( QStringLiteral( "name" ), component.group );
325  group.setAttribute( QStringLiteral( "initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
326  group.setAttribute( QStringLiteral( "mutuallyExclusiveGroupId" ), QStringLiteral( "__mutually_exclusive_groups__" ) );
327  layerTree.appendChild( group );
328  group.appendChild( layer );
329  groupLayerMap[ component.group ] = group;
330  }
331  }
332  else
333  {
334  layerTree.appendChild( layer );
335  }
336 
337  createdLayerIds[ component.group ].insert( component.mapLayerId );
338  }
339 
340  // create custom layer tree entries
341  for ( auto it = details.customLayerTreeGroups.constBegin(); it != details.customLayerTreeGroups.constEnd(); ++it )
342  {
343  if ( customGroupNamesToIds.contains( it.value() ) )
344  continue;
345 
346  QDomElement layer = doc.createElement( QStringLiteral( "Layer" ) );
347  const QString id = QUuid::createUuid().toString();
348  customGroupNamesToIds[ it.value() ] = id;
349  layer.setAttribute( QStringLiteral( "id" ), id );
350  layer.setAttribute( QStringLiteral( "name" ), it.value() );
351  layer.setAttribute( QStringLiteral( "initiallyVisible" ), QStringLiteral( "true" ) );
352  layerTree.appendChild( layer );
353  }
354 
355  compositionElem.appendChild( layerTree );
356 
357  // pages
358  QDomElement page = doc.createElement( QStringLiteral( "Page" ) );
359  QDomElement dpi = doc.createElement( QStringLiteral( "DPI" ) );
360  dpi.appendChild( doc.createTextNode( QString::number( details.dpi ) ) );
361  page.appendChild( dpi );
362  // 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!
363  QDomElement width = doc.createElement( QStringLiteral( "Width" ) );
364  const double pageWidthPdfUnits = std::ceil( details.pageSizeMm.width() / 25.4 * 72 );
365  width.appendChild( doc.createTextNode( QString::number( pageWidthPdfUnits ) ) );
366  page.appendChild( width );
367  QDomElement height = doc.createElement( QStringLiteral( "Height" ) );
368  const double pageHeightPdfUnits = std::ceil( details.pageSizeMm.height() / 25.4 * 72 );
369  height.appendChild( doc.createTextNode( QString::number( pageHeightPdfUnits ) ) );
370  page.appendChild( height );
371 
372 
373  // georeferencing
374  int i = 0;
376  {
377  QDomElement georeferencing = doc.createElement( QStringLiteral( "Georeferencing" ) );
378  georeferencing.setAttribute( QStringLiteral( "id" ), QStringLiteral( "georeferenced_%1" ).arg( i++ ) );
379  georeferencing.setAttribute( QStringLiteral( "OGCBestPracticeFormat" ), details.useOgcBestPracticeFormatGeoreferencing ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
380  georeferencing.setAttribute( QStringLiteral( "ISO32000ExtensionFormat" ), details.useIso32000ExtensionFormatGeoreferencing ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
381 
382  if ( section.crs.isValid() )
383  {
384  QDomElement srs = doc.createElement( QStringLiteral( "SRS" ) );
385  // 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...
386  // srs.setAttribute( QStringLiteral( "dataAxisToSRSAxisMapping" ), QStringLiteral( "2,1" ) );
387  if ( !section.crs.authid().startsWith( QStringLiteral( "user" ), Qt::CaseInsensitive ) )
388  {
389  srs.appendChild( doc.createTextNode( section.crs.authid() ) );
390  }
391  else
392  {
393  srs.appendChild( doc.createTextNode( section.crs.toWkt( QgsCoordinateReferenceSystem::WKT2_2018 ) ) );
394  }
395  georeferencing.appendChild( srs );
396  }
397 
398  if ( !section.pageBoundsPolygon.isEmpty() )
399  {
400  /*
401  Define a polygon / neatline in PDF units into which the
402  Measure tool will display coordinates.
403  If not specified, BoundingBox will be used instead.
404  If none of BoundingBox and BoundingPolygon are specified,
405  the whole PDF page will be assumed to be georeferenced.
406  */
407  QDomElement boundingPolygon = doc.createElement( QStringLiteral( "BoundingPolygon" ) );
408 
409  // transform to PDF coordinate space
410  QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(),
411  -pageHeightPdfUnits / details.pageSizeMm.height() );
412 
413  QgsPolygon p = section.pageBoundsPolygon;
414  p.transform( t );
415  boundingPolygon.appendChild( doc.createTextNode( p.asWkt() ) );
416 
417  georeferencing.appendChild( boundingPolygon );
418  }
419  else
420  {
421  /* Define the viewport where georeferenced coordinates are available.
422  If not specified, the extent of BoundingPolygon will be used instead.
423  If none of BoundingBox and BoundingPolygon are specified,
424  the whole PDF page will be assumed to be georeferenced.
425  */
426  QDomElement boundingBox = doc.createElement( QStringLiteral( "BoundingBox" ) );
427  boundingBox.setAttribute( QStringLiteral( "x1" ), QString::number( section.pageBoundsMm.xMinimum() / 25.4 * 72 ) );
428  boundingBox.setAttribute( QStringLiteral( "y1" ), QString::number( section.pageBoundsMm.yMinimum() / 25.4 * 72 ) );
429  boundingBox.setAttribute( QStringLiteral( "x2" ), QString::number( section.pageBoundsMm.xMaximum() / 25.4 * 72 ) );
430  boundingBox.setAttribute( QStringLiteral( "y2" ), QString::number( section.pageBoundsMm.yMaximum() / 25.4 * 72 ) );
431  georeferencing.appendChild( boundingBox );
432  }
433 
434  for ( const ControlPoint &point : section.controlPoints )
435  {
436  QDomElement cp1 = doc.createElement( QStringLiteral( "ControlPoint" ) );
437  cp1.setAttribute( QStringLiteral( "x" ), QString::number( point.pagePoint.x() / 25.4 * 72 ) );
438  cp1.setAttribute( QStringLiteral( "y" ), QString::number( ( details.pageSizeMm.height() - point.pagePoint.y() ) / 25.4 * 72 ) );
439  cp1.setAttribute( QStringLiteral( "GeoX" ), QString::number( point.geoPoint.x() ) );
440  cp1.setAttribute( QStringLiteral( "GeoY" ), QString::number( point.geoPoint.y() ) );
441  georeferencing.appendChild( cp1 );
442  }
443 
444  page.appendChild( georeferencing );
445  }
446 
447  // content
448  QDomElement content = doc.createElement( QStringLiteral( "Content" ) );
449  for ( const ComponentLayerDetail &component : components )
450  {
451  if ( component.mapLayerId.isEmpty() )
452  {
453  QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
454  pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
455  content.appendChild( pdfDataset );
456  }
457  else if ( !component.group.isEmpty() )
458  {
459  // if content belongs to a group, we need nested "IfLayerOn" elements, one for the group and one for the layer
460  QDomElement ifGroupOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
461  ifGroupOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "group_%1" ).arg( component.group ) );
462  QDomElement ifLayerOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
463  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
464  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
465  else if ( component.group.isEmpty() )
466  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
467  else
468  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
469  QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
470  pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
471  ifLayerOn.appendChild( pdfDataset );
472  ifGroupOn.appendChild( ifLayerOn );
473  content.appendChild( ifGroupOn );
474  }
475  else
476  {
477  QDomElement ifLayerOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
478  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
479  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
480  else if ( component.group.isEmpty() )
481  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
482  else
483  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
484  QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
485  pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
486  ifLayerOn.appendChild( pdfDataset );
487  content.appendChild( ifLayerOn );
488  }
489  }
490 
491  // vector datasets (we "draw" these on top, just for debugging... but they are invisible, so are never really drawn!)
492  if ( details.includeFeatures )
493  {
494  for ( const VectorComponentDetail &component : qgis::as_const( mVectorComponents ) )
495  {
496  QDomElement ifLayerOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
497  if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
498  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
499  else if ( component.group.isEmpty() )
500  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
501  else
502  ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
503  QDomElement vectorDataset = doc.createElement( QStringLiteral( "Vector" ) );
504  vectorDataset.setAttribute( QStringLiteral( "dataset" ), component.sourceVectorPath );
505  vectorDataset.setAttribute( QStringLiteral( "layer" ), component.sourceVectorLayer );
506  vectorDataset.setAttribute( QStringLiteral( "visible" ), QStringLiteral( "false" ) );
507  QDomElement logicalStructure = doc.createElement( QStringLiteral( "LogicalStructure" ) );
508  logicalStructure.setAttribute( QStringLiteral( "displayLayerName" ), component.name );
509  if ( !component.displayAttribute.isEmpty() )
510  logicalStructure.setAttribute( QStringLiteral( "fieldToDisplay" ), component.displayAttribute );
511  vectorDataset.appendChild( logicalStructure );
512  ifLayerOn.appendChild( vectorDataset );
513  content.appendChild( ifLayerOn );
514  }
515  }
516 
517  page.appendChild( content );
518  compositionElem.appendChild( page );
519 
520  doc.appendChild( compositionElem );
521 
522  QString composition;
523  QTextStream stream( &composition );
524  doc.save( stream, -1 );
525 
526  return composition;
527 }
528 
QList< QgsAbstractGeoPdfExporter::GeoReferencedSection > georeferencedSections
List of georeferenced sections.
bool includeFeatures
true if feature vector information (such as attributes) should be exported.
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
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 asWkt(int precision=17) const override
Returns a WKT representation of the geometry.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QList< QgsFeature > QgsFeatureList
Definition: qgsfeature.h:571
double y
Definition: qgspointxy.h:48
QgsGeometry renderedBounds
Bounds, in PDF units, of rendered feature.
QgsLayerTree * layerTree(const QgsWmsRenderContext &context)
QgsAbstractMetadataBase::KeywordMap keywords
Metadata keyword map.
QSizeF pageSizeMm
Page size, in millimeters.
QMap< QString, QString > customLayerTreeGroups
Optional map of map layer ID to custom logical layer tree group in created PDF file.
A convenience class for writing vector files to disk.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:55
This flag indicates, that a primary key field cannot be guaranteed to be unique and the sink should i...
QDateTime creationDateTime
Metadata creation datetime.
void transform(const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d=QgsCoordinateTransform::ForwardTransform, bool transformZ=false) override SIP_THROW(QgsCsException)
Transforms the geometry using a coordinate transform.
Contains details of a control point used during georeferencing GeoPDF outputs.
bool useOgcBestPracticeFormatGeoreferencing
true if OGC "best practice" format georeferencing should be used.
QList< QgsAbstractGeoPdfExporter::ControlPoint > controlPoints
List of control points corresponding to this georeferenced section.
QgsCoordinateReferenceSystem crs
Coordinate reference system for georeferenced section.
Contains information about a feature rendered inside the PDF.
QString sourceVectorPath
File path to the (already created) vector dataset to use as the source for this component layer...
Contains information relating to a single PDF layer in the GeoPDF export.
QgsRectangle pageBoundsMm
Bounds of the georeferenced section on the page, in millimeters.
double x
Definition: qgspointxy.h:47
Full WKT2 string, conforming to ISO 19162:2018 / OGC 18-010, with all possible nodes and new keyword ...
double yMinimum() const
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:177
Contains details of a particular input component to be used during PDF composition.
double xMaximum() const
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:162
QString generateTemporaryFilepath(const QString &filename) const
Returns a file path to use for temporary files required for GeoPDF creation.
bool finalize(const QList< QgsAbstractGeoPdfExporter::ComponentLayerDetail > &components, const QString &destinationFile, const ExportDetails &details)
To be called after the rendering operation is complete.
bool useIso32000ExtensionFormatGeoreferencing
true if ISO3200 extension format georeferencing should be used.
QgsPolygon pageBoundsPolygon
Bounds of the georeferenced section on the page, in millimeters, as a free-form polygon.
This class represents a coordinate reference system (CRS).
void setGeometry(const QgsGeometry &geometry)
Set the feature&#39;s geometry.
Definition: qgsfeature.cpp:137
double xMinimum() const
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:167
double yMaximum() const
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:172
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.
Definition: qgsogrutils.h:134
QgsPointXY pagePoint
Coordinate on the page of the control point, in millimeters.
static QString geoPDFAvailabilityExplanation()
Returns a user-friendly, translated string explaining why GeoPDF export support is not available on t...
Polygon geometry type.
Definition: qgspolygon.h:31
QgsPointXY geoPoint
Georeferenced coordinate of the control point, in CRS units.
bool isEmpty() const override
Returns true if the geometry is empty.
QString authid() const
Returns the authority identifier for the CRS.
QString toWkt(WktVariant variant=WKT1_GDAL, bool multiline=false, int indentationWidth=4) const
Returns a WKT representation of this CRS.
static bool geoPDFCreationAvailable()
Returns true if the current QGIS build is capable of GeoPDF support.
bool isValid() const
Returns whether this CRS is correctly initialized and usable.