28 #include "cpl_string.h" 31 #include <QMutexLocker> 32 #include <QDomDocument> 33 #include <QDomElement> 38 #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0) 43 GDALDriverH hDriverMem = GDALGetDriverByName(
"PDF" );
49 const char *pHavePoppler = GDALGetMetadataItem( hDriverMem,
"HAVE_POPPLER",
nullptr );
50 if ( pHavePoppler && strstr( pHavePoppler,
"YES" ) )
53 const char *pHavePdfium = GDALGetMetadataItem( hDriverMem,
"HAVE_PDFIUM",
nullptr );
54 if ( pHavePdfium && strstr( pHavePdfium,
"YES" ) )
63 #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0) 64 return QObject::tr(
"GeoPDF creation requires GDAL version 3.0 or later." );
67 GDALDriverH hDriverMem = GDALGetDriverByName(
"PDF" );
70 return QObject::tr(
"No GDAL PDF driver available." );
73 const char *pHavePoppler = GDALGetMetadataItem( hDriverMem,
"HAVE_POPPLER",
nullptr );
74 if ( pHavePoppler && strstr( pHavePoppler,
"YES" ) )
77 const char *pHavePdfium = GDALGetMetadataItem( hDriverMem,
"HAVE_PDFIUM",
nullptr );
78 if ( pHavePdfium && strstr( pHavePdfium,
"YES" ) )
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." );
90 #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0) 91 Q_UNUSED( components )
92 Q_UNUSED( destinationFile )
95 const QString composition = createCompositionXml( components, details );
97 if ( composition.isEmpty() )
101 GDALDriverH driver = GDALGetDriverByName(
"PDF" );
104 mErrorMessage = QObject::tr(
"Cannot load GDAL PDF driver" );
109 QFile file( xmlFilePath );
110 if ( file.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
112 QTextStream out( &file );
117 mErrorMessage = QObject::tr(
"Could not create GeoPDF composition file" );
121 char **papszOptions = CSLSetNameValue(
nullptr,
"COMPOSITION_FILE", xmlFilePath.toUtf8().constData() );
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();
128 CSLDestroy( papszOptions );
136 return mTemporaryDir.filePath( filename );
143 QMutexLocker locker( &mMutex );
148 mCollatedFeatures[ group ][ layerId ].append( f );
151 bool QgsAbstractGeoPdfExporter::saveTemporaryLayers()
153 for (
auto groupIt = mCollatedFeatures.constBegin(); groupIt != mCollatedFeatures.constEnd(); ++groupIt )
155 for (
auto it = groupIt->constBegin(); it != groupIt->constEnd(); ++it )
161 detail.group = groupIt.key();
167 saveOptions.
driverName = QStringLiteral(
"GPKG" );
170 if ( writer->hasError() )
172 mErrorMessage = writer->errorMessage();
181 mErrorMessage = writer->errorMessage();
186 detail.sourceVectorLayer = layerName;
187 mVectorComponents << detail;
193 QString QgsAbstractGeoPdfExporter::createCompositionXml(
const QList<ComponentLayerDetail> &components,
const ExportDetails &details )
197 QDomElement compositionElem = doc.createElement( QStringLiteral(
"PDFComposition" ) );
200 QDomElement metadata = doc.createElement( QStringLiteral(
"Metadata" ) );
201 if ( !details.
author.isEmpty() )
203 QDomElement author = doc.createElement( QStringLiteral(
"Author" ) );
204 author.appendChild( doc.createTextNode( details.
author ) );
205 metadata.appendChild( author );
209 QDomElement producer = doc.createElement( QStringLiteral(
"Producer" ) );
210 producer.appendChild( doc.createTextNode( details.
producer ) );
211 metadata.appendChild( producer );
213 if ( !details.
creator.isEmpty() )
215 QDomElement creator = doc.createElement( QStringLiteral(
"Creator" ) );
216 creator.appendChild( doc.createTextNode( details.
creator ) );
217 metadata.appendChild( creator );
221 QDomElement creationDate = doc.createElement( QStringLiteral(
"CreationDate" ) );
222 QString creationDateString = QStringLiteral(
"D:%1" ).arg( details.
creationDateTime.toString( QStringLiteral(
"yyyyMMddHHmmss" ) ) );
226 creationDateString += ( offsetFromUtc >= 0 ) ?
'+' :
'-';
227 offsetFromUtc = std::abs( offsetFromUtc );
228 int offsetHours = offsetFromUtc / 3600;
229 int offsetMins = ( offsetFromUtc % 3600 ) / 60;
230 creationDateString += QStringLiteral(
"%1'%2'" ).arg( offsetHours ).arg( offsetMins );
232 creationDate.appendChild( doc.createTextNode( creationDateString ) );
233 metadata.appendChild( creationDate );
235 if ( !details.
subject.isEmpty() )
237 QDomElement subject = doc.createElement( QStringLiteral(
"Subject" ) );
238 subject.appendChild( doc.createTextNode( details.
subject ) );
239 metadata.appendChild( subject );
241 if ( !details.
title.isEmpty() )
243 QDomElement title = doc.createElement( QStringLiteral(
"Title" ) );
244 title.appendChild( doc.createTextNode( details.
title ) );
245 metadata.appendChild( title );
249 QStringList allKeywords;
250 for (
auto it = details.
keywords.constBegin(); it != details.
keywords.constEnd(); ++it )
252 allKeywords.append( QStringLiteral(
"%1: %2" ).arg( it.key(), it.value().join(
',' ) ) );
254 QDomElement keywords = doc.createElement( QStringLiteral(
"Keywords" ) );
255 keywords.appendChild( doc.createTextNode( allKeywords.join(
';' ) ) );
256 metadata.appendChild( keywords );
258 compositionElem.appendChild( metadata );
261 QDomElement
layerTree = doc.createElement( QStringLiteral(
"LayerTree" ) );
263 QMap< QString, QSet< QString > > createdLayerIds;
264 QMap< QString, QDomElement > groupLayerMap;
265 QMap< QString, QString > customGroupNamesToIds;
273 QDomElement layer = doc.createElement( QStringLiteral(
"Layer" ) );
274 layer.setAttribute( QStringLiteral(
"id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
275 layer.setAttribute( QStringLiteral(
"name" ), component.name );
276 layer.setAttribute( QStringLiteral(
"initiallyVisible" ), QStringLiteral(
"true" ) );
278 if ( !component.group.isEmpty() )
280 if ( groupLayerMap.contains( component.group ) )
282 groupLayerMap[ component.group ].appendChild( layer );
286 QDomElement group = doc.createElement( QStringLiteral(
"Layer" ) );
287 group.setAttribute( QStringLiteral(
"id" ), QStringLiteral(
"group_%1" ).arg( component.group ) );
288 group.setAttribute( QStringLiteral(
"name" ), component.group );
289 group.setAttribute( QStringLiteral(
"initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
290 group.setAttribute( QStringLiteral(
"mutuallyExclusiveGroupId" ), QStringLiteral(
"__mutually_exclusive_groups__" ) );
291 layerTree.appendChild( group );
292 group.appendChild( layer );
293 groupLayerMap[ component.group ] = group;
298 layerTree.appendChild( layer );
301 createdLayerIds[ component.group ].insert( component.mapLayerId );
307 if ( component.mapLayerId.isEmpty() || createdLayerIds.value( component.group ).contains( component.mapLayerId ) )
313 QDomElement layer = doc.createElement( QStringLiteral(
"Layer" ) );
314 layer.setAttribute( QStringLiteral(
"id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
315 layer.setAttribute( QStringLiteral(
"name" ), component.name );
316 layer.setAttribute( QStringLiteral(
"initiallyVisible" ), QStringLiteral(
"true" ) );
318 if ( !component.group.isEmpty() )
320 if ( groupLayerMap.contains( component.group ) )
322 groupLayerMap[ component.group ].appendChild( layer );
326 QDomElement group = doc.createElement( QStringLiteral(
"Layer" ) );
327 group.setAttribute( QStringLiteral(
"id" ), QStringLiteral(
"group_%1" ).arg( component.group ) );
328 group.setAttribute( QStringLiteral(
"name" ), component.group );
329 group.setAttribute( QStringLiteral(
"initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
330 group.setAttribute( QStringLiteral(
"mutuallyExclusiveGroupId" ), QStringLiteral(
"__mutually_exclusive_groups__" ) );
331 layerTree.appendChild( group );
332 group.appendChild( layer );
333 groupLayerMap[ component.group ] = group;
338 layerTree.appendChild( layer );
341 createdLayerIds[ component.group ].insert( component.mapLayerId );
347 if ( customGroupNamesToIds.contains( it.value() ) )
350 QDomElement layer = doc.createElement( QStringLiteral(
"Layer" ) );
351 const QString
id = QUuid::createUuid().toString();
352 customGroupNamesToIds[ it.value() ] = id;
353 layer.setAttribute( QStringLiteral(
"id" ),
id );
354 layer.setAttribute( QStringLiteral(
"name" ), it.value() );
355 layer.setAttribute( QStringLiteral(
"initiallyVisible" ), QStringLiteral(
"true" ) );
356 layerTree.appendChild( layer );
359 compositionElem.appendChild( layerTree );
362 QDomElement page = doc.createElement( QStringLiteral(
"Page" ) );
363 QDomElement dpi = doc.createElement( QStringLiteral(
"DPI" ) );
364 dpi.appendChild( doc.createTextNode( QString::number( details.
dpi ) ) );
365 page.appendChild( dpi );
367 QDomElement width = doc.createElement( QStringLiteral(
"Width" ) );
368 const double pageWidthPdfUnits = std::ceil( details.
pageSizeMm.width() / 25.4 * 72 );
369 width.appendChild( doc.createTextNode( QString::number( pageWidthPdfUnits ) ) );
370 page.appendChild( width );
371 QDomElement height = doc.createElement( QStringLiteral(
"Height" ) );
372 const double pageHeightPdfUnits = std::ceil( details.
pageSizeMm.height() / 25.4 * 72 );
373 height.appendChild( doc.createTextNode( QString::number( pageHeightPdfUnits ) ) );
374 page.appendChild( height );
381 QDomElement georeferencing = doc.createElement( QStringLiteral(
"Georeferencing" ) );
382 georeferencing.setAttribute( QStringLiteral(
"id" ), QStringLiteral(
"georeferenced_%1" ).arg( i++ ) );
388 QDomElement srs = doc.createElement( QStringLiteral(
"SRS" ) );
391 if ( !section.
crs.
authid().startsWith( QStringLiteral(
"user" ), Qt::CaseInsensitive ) )
393 srs.appendChild( doc.createTextNode( section.
crs.
authid() ) );
399 georeferencing.appendChild( srs );
411 QDomElement boundingPolygon = doc.createElement( QStringLiteral(
"BoundingPolygon" ) );
414 QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.
pageSizeMm.width(),
415 -pageHeightPdfUnits / details.
pageSizeMm.height() );
419 boundingPolygon.appendChild( doc.createTextNode( p.
asWkt() ) );
421 georeferencing.appendChild( boundingPolygon );
430 QDomElement boundingBox = doc.createElement( QStringLiteral(
"BoundingBox" ) );
431 boundingBox.setAttribute( QStringLiteral(
"x1" ), QString::number( section.
pageBoundsMm.
xMinimum() / 25.4 * 72 ) );
432 boundingBox.setAttribute( QStringLiteral(
"y1" ), QString::number( section.
pageBoundsMm.
yMinimum() / 25.4 * 72 ) );
433 boundingBox.setAttribute( QStringLiteral(
"x2" ), QString::number( section.
pageBoundsMm.
xMaximum() / 25.4 * 72 ) );
434 boundingBox.setAttribute( QStringLiteral(
"y2" ), QString::number( section.
pageBoundsMm.
yMaximum() / 25.4 * 72 ) );
435 georeferencing.appendChild( boundingBox );
440 QDomElement cp1 = doc.createElement( QStringLiteral(
"ControlPoint" ) );
441 cp1.setAttribute( QStringLiteral(
"x" ), QString::number( point.
pagePoint.
x() / 25.4 * 72 ) );
442 cp1.setAttribute( QStringLiteral(
"y" ), QString::number( ( details.
pageSizeMm.height() - point.
pagePoint.
y() ) / 25.4 * 72 ) );
443 cp1.setAttribute( QStringLiteral(
"GeoX" ), QString::number( point.
geoPoint.
x() ) );
444 cp1.setAttribute( QStringLiteral(
"GeoY" ), QString::number( point.
geoPoint.
y() ) );
445 georeferencing.appendChild( cp1 );
448 page.appendChild( georeferencing );
452 QDomElement content = doc.createElement( QStringLiteral(
"Content" ) );
455 if ( component.mapLayerId.isEmpty() )
457 QDomElement pdfDataset = doc.createElement( QStringLiteral(
"PDF" ) );
458 pdfDataset.setAttribute( QStringLiteral(
"dataset" ), component.sourcePdfPath );
459 content.appendChild( pdfDataset );
461 else if ( !component.group.isEmpty() )
464 QDomElement ifGroupOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
465 ifGroupOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"group_%1" ).arg( component.group ) );
466 QDomElement ifLayerOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
468 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), customGroupNamesToIds.value( details.
customLayerTreeGroups.value( component.mapLayerId ) ) );
469 else if ( component.group.isEmpty() )
470 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), component.mapLayerId );
472 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
473 QDomElement pdfDataset = doc.createElement( QStringLiteral(
"PDF" ) );
474 pdfDataset.setAttribute( QStringLiteral(
"dataset" ), component.sourcePdfPath );
475 ifLayerOn.appendChild( pdfDataset );
476 ifGroupOn.appendChild( ifLayerOn );
477 content.appendChild( ifGroupOn );
481 QDomElement ifLayerOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
483 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), customGroupNamesToIds.value( details.
customLayerTreeGroups.value( component.mapLayerId ) ) );
484 else if ( component.group.isEmpty() )
485 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), component.mapLayerId );
487 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
488 QDomElement pdfDataset = doc.createElement( QStringLiteral(
"PDF" ) );
489 pdfDataset.setAttribute( QStringLiteral(
"dataset" ), component.sourcePdfPath );
490 ifLayerOn.appendChild( pdfDataset );
491 content.appendChild( ifLayerOn );
500 QDomElement ifLayerOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
502 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), customGroupNamesToIds.value( details.
customLayerTreeGroups.value( component.mapLayerId ) ) );
503 else if ( component.group.isEmpty() )
504 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), component.mapLayerId );
506 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
507 QDomElement vectorDataset = doc.createElement( QStringLiteral(
"Vector" ) );
508 vectorDataset.setAttribute( QStringLiteral(
"dataset" ), component.sourceVectorPath );
509 vectorDataset.setAttribute( QStringLiteral(
"layer" ), component.sourceVectorLayer );
510 vectorDataset.setAttribute( QStringLiteral(
"visible" ), QStringLiteral(
"false" ) );
511 QDomElement logicalStructure = doc.createElement( QStringLiteral(
"LogicalStructure" ) );
512 logicalStructure.setAttribute( QStringLiteral(
"displayLayerName" ), component.name );
513 if ( !component.displayAttribute.isEmpty() )
514 logicalStructure.setAttribute( QStringLiteral(
"fieldToDisplay" ), component.displayAttribute );
515 vectorDataset.appendChild( logicalStructure );
516 ifLayerOn.appendChild( vectorDataset );
517 content.appendChild( ifLayerOn );
521 page.appendChild( content );
522 compositionElem.appendChild( page );
524 doc.appendChild( compositionElem );
527 QTextStream stream( &composition );
528 doc.save( stream, -1 );
QList< QgsAbstractGeoPdfExporter::GeoReferencedSection > georeferencedSections
List of georeferenced sections.
bool includeFeatures
true if feature vector information (such as attributes) should be exported.
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=nullptr, QString *newFilename=nullptr, QString *newLayer=nullptr)
Create a new vector file writer.
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
QString author
Metadata author tag.
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.
QList< QgsFeature > QgsFeatureList
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.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
This flag indicates, that a primary key field cannot be guaranteed to be unique and the sink should i...
QDateTime creationDateTime
Metadata creation datetime.
QgsVectorFileWriter::SymbologyExport symbologyExport
Symbology to export.
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.
Options to pass to writeAsVectorFormat()
bool useOgcBestPracticeFormatGeoreferencing
true if OGC "best practice" format georeferencing should be used.
QString creator
Metadata creator tag.
QgsFeature feature
Rendered feature.
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...
QString subject
Metadata subject tag.
Contains information relating to a single PDF layer in the GeoPDF export.
QString driverName
OGR driver to use.
QgsRectangle pageBoundsMm
Bounds of the georeferenced section on the page, in millimeters.
Contains information about the context in which a coordinate transform is executed.
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).
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).
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.
QString title
Metadata title tag.
This class represents a coordinate reference system (CRS).
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
double xMinimum() const
Returns the x minimum value (left side of rectangle).
QString producer
Metadata producer tag.
double yMaximum() const
Returns the y maximum value (top side of rectangle).
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.
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...
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.