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 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:
167 QMutexLocker locker( &mMutex );
172 mCollatedFeatures[ group ][ layerId ].append( f );
175 bool QgsAbstractGeoPdfExporter::saveTemporaryLayers()
177 for (
auto groupIt = mCollatedFeatures.constBegin(); groupIt != mCollatedFeatures.constEnd(); ++groupIt )
179 for (
auto it = groupIt->constBegin(); it != groupIt->constEnd(); ++it )
183 VectorComponentDetail detail = componentDetailForLayerId( it.key() );
184 detail.sourceVectorPath = filePath;
185 detail.group = groupIt.key();
191 saveOptions.
driverName = QStringLiteral(
"GPKG" );
194 if ( writer->hasError() )
196 mErrorMessage = writer->errorMessage();
205 mErrorMessage = writer->errorMessage();
210 detail.sourceVectorLayer = layerName;
211 mVectorComponents << detail;
217 QString QgsAbstractGeoPdfExporter::createCompositionXml(
const QList<ComponentLayerDetail> &components,
const ExportDetails &details )
221 QDomElement compositionElem = doc.createElement( QStringLiteral(
"PDFComposition" ) );
224 QDomElement metadata = doc.createElement( QStringLiteral(
"Metadata" ) );
225 if ( !details.author.isEmpty() )
227 QDomElement author = doc.createElement( QStringLiteral(
"Author" ) );
228 author.appendChild( doc.createTextNode( details.author ) );
229 metadata.appendChild( author );
231 if ( !details.producer.isEmpty() )
233 QDomElement producer = doc.createElement( QStringLiteral(
"Producer" ) );
234 producer.appendChild( doc.createTextNode( details.producer ) );
235 metadata.appendChild( producer );
237 if ( !details.creator.isEmpty() )
239 QDomElement creator = doc.createElement( QStringLiteral(
"Creator" ) );
240 creator.appendChild( doc.createTextNode( details.creator ) );
241 metadata.appendChild( creator );
243 if ( details.creationDateTime.isValid() )
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() )
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 );
256 creationDate.appendChild( doc.createTextNode( creationDateString ) );
257 metadata.appendChild( creationDate );
259 if ( !details.subject.isEmpty() )
261 QDomElement subject = doc.createElement( QStringLiteral(
"Subject" ) );
262 subject.appendChild( doc.createTextNode( details.subject ) );
263 metadata.appendChild( subject );
265 if ( !details.title.isEmpty() )
267 QDomElement title = doc.createElement( QStringLiteral(
"Title" ) );
268 title.appendChild( doc.createTextNode( details.title ) );
269 metadata.appendChild( title );
271 if ( !details.keywords.empty() )
273 QStringList allKeywords;
274 for (
auto it = details.keywords.constBegin(); it != details.keywords.constEnd(); ++it )
276 allKeywords.append( QStringLiteral(
"%1: %2" ).arg( it.key(), it.value().join(
',' ) ) );
278 QDomElement keywords = doc.createElement( QStringLiteral(
"Keywords" ) );
279 keywords.appendChild( doc.createTextNode( allKeywords.join(
';' ) ) );
280 metadata.appendChild( keywords );
282 compositionElem.appendChild( metadata );
284 QMap< QString, QSet< QString > > createdLayerIds;
285 QMap< QString, QDomElement > groupLayerMap;
286 QMap< QString, QString > customGroupNamesToIds;
288 QMultiMap< QString, QDomElement > pendingLayerTreeElements;
290 if ( details.includeFeatures )
292 for (
const VectorComponentDetail &component : qgis::as_const( mVectorComponents ) )
294 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
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" ) );
302 if ( !component.group.isEmpty() )
304 if ( groupLayerMap.contains( component.group ) )
306 groupLayerMap[ component.group ].appendChild( layer );
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;
322 pendingLayerTreeElements.insert( component.mapLayerId, layer );
325 createdLayerIds[ component.group ].insert( component.mapLayerId );
329 for (
const ComponentLayerDetail &component : components )
331 if ( component.mapLayerId.isEmpty() || createdLayerIds.value( component.group ).contains( component.mapLayerId ) )
334 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
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" ) );
342 if ( !component.group.isEmpty() )
344 if ( groupLayerMap.contains( component.group ) )
346 groupLayerMap[ component.group ].appendChild( layer );
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;
362 pendingLayerTreeElements.insert( component.mapLayerId, layer );
365 createdLayerIds[ component.group ].insert( component.mapLayerId );
369 QDomElement
layerTree = doc.createElement( QStringLiteral(
"LayerTree" ) );
373 for (
auto it = details.customLayerTreeGroups.constBegin(); it != details.customLayerTreeGroups.constEnd(); ++it )
375 if ( customGroupNamesToIds.contains( it.value() ) )
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" ) );
388 for (
const QString &layerId : details.layerOrder )
390 const QList< QDomElement> elements = pendingLayerTreeElements.values( layerId );
391 for (
const QDomElement &element : elements )
395 for (
auto it = pendingLayerTreeElements.constBegin(); it != pendingLayerTreeElements.constEnd(); ++it )
397 if ( details.layerOrder.contains( it.key() ) )
406 compositionElem.appendChild(
layerTree );
409 QDomElement page = doc.createElement( QStringLiteral(
"Page" ) );
410 QDomElement dpi = doc.createElement( QStringLiteral(
"DPI" ) );
413 page.appendChild( dpi );
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 );
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" ) );
436 QDomElement srs = doc.createElement( QStringLiteral(
"SRS" ) );
439 if ( !section.
crs.
authid().startsWith( QStringLiteral(
"user" ), Qt::CaseInsensitive ) )
441 srs.appendChild( doc.createTextNode( section.
crs.
authid() ) );
447 georeferencing.appendChild( srs );
459 QDomElement boundingPolygon = doc.createElement( QStringLiteral(
"BoundingPolygon" ) );
462 QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(),
463 -pageHeightPdfUnits / details.pageSizeMm.height() );
467 boundingPolygon.appendChild( doc.createTextNode( p.
asWkt() ) );
469 georeferencing.appendChild( boundingPolygon );
478 QDomElement boundingBox = doc.createElement( QStringLiteral(
"BoundingBox" ) );
483 georeferencing.appendChild( boundingBox );
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 );
496 page.appendChild( georeferencing );
499 auto createPdfDatasetElement = [&doc](
const ComponentLayerDetail & component ) -> QDomElement
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 )
505 QDomElement blendingElement = doc.createElement( QStringLiteral(
"Blending" ) );
506 blendingElement.setAttribute( QStringLiteral(
"opacity" ), component.opacity );
507 blendingElement.setAttribute( QStringLiteral(
"function" ), compositionModeToString( component.compositionMode ) );
509 pdfDataset.appendChild( blendingElement );
515 QDomElement content = doc.createElement( QStringLiteral(
"Content" ) );
516 for (
const ComponentLayerDetail &component : components )
518 if ( component.mapLayerId.isEmpty() )
520 content.appendChild( createPdfDatasetElement( component ) );
522 else if ( !component.group.isEmpty() )
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 );
533 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
535 ifLayerOn.appendChild( createPdfDatasetElement( component ) );
536 ifGroupOn.appendChild( ifLayerOn );
537 content.appendChild( ifGroupOn );
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 );
547 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
548 ifLayerOn.appendChild( createPdfDatasetElement( component ) );
549 content.appendChild( ifLayerOn );
554 if ( details.includeFeatures )
556 for (
const VectorComponentDetail &component : qgis::as_const( mVectorComponents ) )
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 );
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 );
579 page.appendChild( content );
580 compositionElem.appendChild( page );
582 doc.appendChild( compositionElem );
585 QTextStream stream( &composition );
586 doc.save( stream, -1 );
591 QString QgsAbstractGeoPdfExporter::compositionModeToString( QPainter::CompositionMode mode )
595 case QPainter::CompositionMode_SourceOver:
596 return QStringLiteral(
"Normal" );
598 case QPainter::CompositionMode_Multiply:
599 return QStringLiteral(
"Multiply" );
601 case QPainter::CompositionMode_Screen:
602 return QStringLiteral(
"Screen" );
604 case QPainter::CompositionMode_Overlay:
605 return QStringLiteral(
"Overlay" );
607 case QPainter::CompositionMode_Darken:
608 return QStringLiteral(
"Darken" );
610 case QPainter::CompositionMode_Lighten:
611 return QStringLiteral(
"Lighten" );
613 case QPainter::CompositionMode_ColorDodge:
614 return QStringLiteral(
"ColorDodge" );
616 case QPainter::CompositionMode_ColorBurn:
617 return QStringLiteral(
"ColorBurn" );
619 case QPainter::CompositionMode_HardLight:
620 return QStringLiteral(
"HardLight" );
622 case QPainter::CompositionMode_SoftLight:
623 return QStringLiteral(
"SoftLight" );
625 case QPainter::CompositionMode_Difference:
626 return QStringLiteral(
"Difference" );
628 case QPainter::CompositionMode_Exclusion:
629 return QStringLiteral(
"Exclusion" );
635 QgsDebugMsg( QStringLiteral(
"Unsupported PDF blend mode %1" ).arg( mode ) );
636 return QStringLiteral(
"Normal" );