28 #include "cpl_string.h"
31 #include <QMutexLocker>
32 #include <QDomDocument>
33 #include <QDomElement>
36 #include <QTextStream>
41 GDALDriverH hDriverMem = GDALGetDriverByName(
"PDF" );
47 const char *pHavePoppler = GDALGetMetadataItem( hDriverMem,
"HAVE_POPPLER",
nullptr );
48 if ( pHavePoppler && strstr( pHavePoppler,
"YES" ) )
51 const char *pHavePdfium = GDALGetMetadataItem( hDriverMem,
"HAVE_PDFIUM",
nullptr );
52 if ( pHavePdfium && strstr( pHavePdfium,
"YES" ) )
61 GDALDriverH hDriverMem = GDALGetDriverByName(
"PDF" );
64 return QObject::tr(
"No GDAL PDF driver available." );
67 const char *pHavePoppler = GDALGetMetadataItem( hDriverMem,
"HAVE_POPPLER",
nullptr );
68 if ( pHavePoppler && strstr( pHavePoppler,
"YES" ) )
71 const char *pHavePdfium = GDALGetMetadataItem( hDriverMem,
"HAVE_PDFIUM",
nullptr );
72 if ( pHavePdfium && strstr( pHavePdfium,
"YES" ) )
75 return QObject::tr(
"GDAL PDF driver was not built with PDF read support. A build with PDF read support is required for GeoPDF creation." );
83 const QString composition = createCompositionXml( components, details );
85 if ( composition.isEmpty() )
89 GDALDriverH driver = GDALGetDriverByName(
"PDF" );
92 mErrorMessage = QObject::tr(
"Cannot load GDAL PDF driver" );
97 QFile file( xmlFilePath );
98 if ( file.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
100 QTextStream out( &file );
101 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
102 out.setCodec(
"UTF-8" );
108 mErrorMessage = QObject::tr(
"Could not create GeoPDF composition file" );
112 char **papszOptions = CSLSetNameValue(
nullptr,
"COMPOSITION_FILE", xmlFilePath.toUtf8().constData() );
115 gdal::dataset_unique_ptr outputDataset( GDALCreate( driver, destinationFile.toUtf8().constData(), 0, 0, 0, GDT_Unknown, papszOptions ) );
116 bool res = outputDataset.get();
117 outputDataset.reset();
119 CSLDestroy( papszOptions );
126 return mTemporaryDir.filePath( filename );
133 case QPainter::CompositionMode_SourceOver:
134 case QPainter::CompositionMode_Multiply:
135 case QPainter::CompositionMode_Screen:
136 case QPainter::CompositionMode_Overlay:
137 case QPainter::CompositionMode_Darken:
138 case QPainter::CompositionMode_Lighten:
139 case QPainter::CompositionMode_ColorDodge:
140 case QPainter::CompositionMode_ColorBurn:
141 case QPainter::CompositionMode_HardLight:
142 case QPainter::CompositionMode_SoftLight:
143 case QPainter::CompositionMode_Difference:
144 case QPainter::CompositionMode_Exclusion:
157 QMutexLocker locker( &mMutex );
162 mCollatedFeatures[ group ][ layerId ].append( f );
165 bool QgsAbstractGeoPdfExporter::saveTemporaryLayers()
167 for (
auto groupIt = mCollatedFeatures.constBegin(); groupIt != mCollatedFeatures.constEnd(); ++groupIt )
169 for (
auto it = groupIt->constBegin(); it != groupIt->constEnd(); ++it )
173 VectorComponentDetail detail = componentDetailForLayerId( it.key() );
174 detail.sourceVectorPath = filePath;
175 detail.group = groupIt.key();
181 saveOptions.
driverName = QStringLiteral(
"GPKG" );
184 if ( writer->hasError() )
186 mErrorMessage = writer->errorMessage();
195 mErrorMessage = writer->errorMessage();
200 detail.sourceVectorLayer = layerName;
201 mVectorComponents << detail;
207 QString QgsAbstractGeoPdfExporter::createCompositionXml(
const QList<ComponentLayerDetail> &components,
const ExportDetails &details )
211 QDomElement compositionElem = doc.createElement( QStringLiteral(
"PDFComposition" ) );
214 QDomElement metadata = doc.createElement( QStringLiteral(
"Metadata" ) );
215 if ( !details.author.isEmpty() )
217 QDomElement author = doc.createElement( QStringLiteral(
"Author" ) );
218 author.appendChild( doc.createTextNode( details.author ) );
219 metadata.appendChild( author );
221 if ( !details.producer.isEmpty() )
223 QDomElement producer = doc.createElement( QStringLiteral(
"Producer" ) );
224 producer.appendChild( doc.createTextNode( details.producer ) );
225 metadata.appendChild( producer );
227 if ( !details.creator.isEmpty() )
229 QDomElement creator = doc.createElement( QStringLiteral(
"Creator" ) );
230 creator.appendChild( doc.createTextNode( details.creator ) );
231 metadata.appendChild( creator );
233 if ( details.creationDateTime.isValid() )
235 QDomElement creationDate = doc.createElement( QStringLiteral(
"CreationDate" ) );
236 QString creationDateString = QStringLiteral(
"D:%1" ).arg( details.creationDateTime.toString( QStringLiteral(
"yyyyMMddHHmmss" ) ) );
237 if ( details.creationDateTime.timeZone().isValid() )
239 int offsetFromUtc = details.creationDateTime.timeZone().offsetFromUtc( details.creationDateTime );
240 creationDateString += ( offsetFromUtc >= 0 ) ?
'+' :
'-';
241 offsetFromUtc = std::abs( offsetFromUtc );
242 int offsetHours = offsetFromUtc / 3600;
243 int offsetMins = ( offsetFromUtc % 3600 ) / 60;
244 creationDateString += QStringLiteral(
"%1'%2'" ).arg( offsetHours ).arg( offsetMins );
246 creationDate.appendChild( doc.createTextNode( creationDateString ) );
247 metadata.appendChild( creationDate );
249 if ( !details.subject.isEmpty() )
251 QDomElement subject = doc.createElement( QStringLiteral(
"Subject" ) );
252 subject.appendChild( doc.createTextNode( details.subject ) );
253 metadata.appendChild( subject );
255 if ( !details.title.isEmpty() )
257 QDomElement title = doc.createElement( QStringLiteral(
"Title" ) );
258 title.appendChild( doc.createTextNode( details.title ) );
259 metadata.appendChild( title );
261 if ( !details.keywords.empty() )
263 QStringList allKeywords;
264 for (
auto it = details.keywords.constBegin(); it != details.keywords.constEnd(); ++it )
266 allKeywords.append( QStringLiteral(
"%1: %2" ).arg( it.key(), it.value().join(
',' ) ) );
268 QDomElement keywords = doc.createElement( QStringLiteral(
"Keywords" ) );
269 keywords.appendChild( doc.createTextNode( allKeywords.join(
';' ) ) );
270 metadata.appendChild( keywords );
272 compositionElem.appendChild( metadata );
274 QMap< QString, QSet< QString > > createdLayerIds;
275 QMap< QString, QDomElement > groupLayerMap;
276 QMap< QString, QString > customGroupNamesToIds;
278 QMultiMap< QString, QDomElement > pendingLayerTreeElements;
280 if ( details.includeFeatures )
282 for (
const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
284 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
287 QDomElement layer = doc.createElement( QStringLiteral(
"Layer" ) );
288 layer.setAttribute( QStringLiteral(
"id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
289 layer.setAttribute( QStringLiteral(
"name" ), details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name );
290 layer.setAttribute( QStringLiteral(
"initiallyVisible" ), details.initialLayerVisibility.value( component.mapLayerId,
true ) ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
292 if ( !component.group.isEmpty() )
294 if ( groupLayerMap.contains( component.group ) )
296 groupLayerMap[ component.group ].appendChild( layer );
300 QDomElement group = doc.createElement( QStringLiteral(
"Layer" ) );
301 group.setAttribute( QStringLiteral(
"id" ), QStringLiteral(
"group_%1" ).arg( component.group ) );
302 group.setAttribute( QStringLiteral(
"name" ), component.group );
303 group.setAttribute( QStringLiteral(
"initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
304 group.setAttribute( QStringLiteral(
"mutuallyExclusiveGroupId" ), QStringLiteral(
"__mutually_exclusive_groups__" ) );
305 pendingLayerTreeElements.insert( component.mapLayerId, group );
306 group.appendChild( layer );
307 groupLayerMap[ component.group ] = group;
312 pendingLayerTreeElements.insert( component.mapLayerId, layer );
315 createdLayerIds[ component.group ].insert( component.mapLayerId );
319 for (
const ComponentLayerDetail &component : components )
321 if ( component.mapLayerId.isEmpty() || createdLayerIds.value( component.group ).contains( component.mapLayerId ) )
324 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
327 QDomElement layer = doc.createElement( QStringLiteral(
"Layer" ) );
328 layer.setAttribute( QStringLiteral(
"id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
329 layer.setAttribute( QStringLiteral(
"name" ), details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name );
330 layer.setAttribute( QStringLiteral(
"initiallyVisible" ), details.initialLayerVisibility.value( component.mapLayerId,
true ) ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
332 if ( !component.group.isEmpty() )
334 if ( groupLayerMap.contains( component.group ) )
336 groupLayerMap[ component.group ].appendChild( layer );
340 QDomElement group = doc.createElement( QStringLiteral(
"Layer" ) );
341 group.setAttribute( QStringLiteral(
"id" ), QStringLiteral(
"group_%1" ).arg( component.group ) );
342 group.setAttribute( QStringLiteral(
"name" ), component.group );
343 group.setAttribute( QStringLiteral(
"initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
344 group.setAttribute( QStringLiteral(
"mutuallyExclusiveGroupId" ), QStringLiteral(
"__mutually_exclusive_groups__" ) );
345 pendingLayerTreeElements.insert( component.mapLayerId, group );
346 group.appendChild( layer );
347 groupLayerMap[ component.group ] = group;
352 pendingLayerTreeElements.insert( component.mapLayerId, layer );
355 createdLayerIds[ component.group ].insert( component.mapLayerId );
359 QDomElement
layerTree = doc.createElement( QStringLiteral(
"LayerTree" ) );
363 for (
auto it = details.customLayerTreeGroups.constBegin(); it != details.customLayerTreeGroups.constEnd(); ++it )
365 if ( customGroupNamesToIds.contains( it.value() ) )
368 QDomElement layer = doc.createElement( QStringLiteral(
"Layer" ) );
369 const QString
id = QUuid::createUuid().toString();
370 customGroupNamesToIds[ it.value() ] = id;
371 layer.setAttribute( QStringLiteral(
"id" ),
id );
372 layer.setAttribute( QStringLiteral(
"name" ), it.value() );
373 layer.setAttribute( QStringLiteral(
"initiallyVisible" ), QStringLiteral(
"true" ) );
378 for (
const QString &layerId : details.layerOrder )
380 const QList< QDomElement> elements = pendingLayerTreeElements.values( layerId );
381 for (
const QDomElement &element : elements )
385 for (
auto it = pendingLayerTreeElements.constBegin(); it != pendingLayerTreeElements.constEnd(); ++it )
387 if ( details.layerOrder.contains( it.key() ) )
396 compositionElem.appendChild(
layerTree );
399 QDomElement page = doc.createElement( QStringLiteral(
"Page" ) );
400 QDomElement dpi = doc.createElement( QStringLiteral(
"DPI" ) );
403 page.appendChild( dpi );
405 QDomElement width = doc.createElement( QStringLiteral(
"Width" ) );
406 const double pageWidthPdfUnits = std::ceil( details.pageSizeMm.width() / 25.4 * 72 );
407 width.appendChild( doc.createTextNode(
qgsDoubleToString( pageWidthPdfUnits ) ) );
408 page.appendChild( width );
409 QDomElement height = doc.createElement( QStringLiteral(
"Height" ) );
410 const double pageHeightPdfUnits = std::ceil( details.pageSizeMm.height() / 25.4 * 72 );
411 height.appendChild( doc.createTextNode(
qgsDoubleToString( pageHeightPdfUnits ) ) );
412 page.appendChild( height );
419 QDomElement georeferencing = doc.createElement( QStringLiteral(
"Georeferencing" ) );
420 georeferencing.setAttribute( QStringLiteral(
"id" ), QStringLiteral(
"georeferenced_%1" ).arg( i++ ) );
421 georeferencing.setAttribute( QStringLiteral(
"OGCBestPracticeFormat" ), details.useOgcBestPracticeFormatGeoreferencing ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
422 georeferencing.setAttribute( QStringLiteral(
"ISO32000ExtensionFormat" ), details.useIso32000ExtensionFormatGeoreferencing ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
426 QDomElement srs = doc.createElement( QStringLiteral(
"SRS" ) );
429 if ( !section.
crs.
authid().startsWith( QStringLiteral(
"user" ), Qt::CaseInsensitive ) )
431 srs.appendChild( doc.createTextNode( section.
crs.
authid() ) );
437 georeferencing.appendChild( srs );
449 QDomElement boundingPolygon = doc.createElement( QStringLiteral(
"BoundingPolygon" ) );
452 QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(),
453 -pageHeightPdfUnits / details.pageSizeMm.height() );
457 boundingPolygon.appendChild( doc.createTextNode( p.
asWkt() ) );
459 georeferencing.appendChild( boundingPolygon );
468 QDomElement boundingBox = doc.createElement( QStringLiteral(
"BoundingBox" ) );
473 georeferencing.appendChild( boundingBox );
478 QDomElement cp1 = doc.createElement( QStringLiteral(
"ControlPoint" ) );
479 cp1.setAttribute( QStringLiteral(
"x" ),
qgsDoubleToString( point.pagePoint.x() / 25.4 * 72 ) );
480 cp1.setAttribute( QStringLiteral(
"y" ),
qgsDoubleToString( ( details.pageSizeMm.height() - point.pagePoint.y() ) / 25.4 * 72 ) );
481 cp1.setAttribute( QStringLiteral(
"GeoX" ),
qgsDoubleToString( point.geoPoint.x() ) );
482 cp1.setAttribute( QStringLiteral(
"GeoY" ),
qgsDoubleToString( point.geoPoint.y() ) );
483 georeferencing.appendChild( cp1 );
486 page.appendChild( georeferencing );
489 auto createPdfDatasetElement = [&doc](
const ComponentLayerDetail & component ) -> QDomElement
491 QDomElement pdfDataset = doc.createElement( QStringLiteral(
"PDF" ) );
492 pdfDataset.setAttribute( QStringLiteral(
"dataset" ), component.sourcePdfPath );
493 if ( component.opacity != 1.0 || component.compositionMode != QPainter::CompositionMode_SourceOver )
495 QDomElement blendingElement = doc.createElement( QStringLiteral(
"Blending" ) );
496 blendingElement.setAttribute( QStringLiteral(
"opacity" ), component.opacity );
497 blendingElement.setAttribute( QStringLiteral(
"function" ), compositionModeToString( component.compositionMode ) );
499 pdfDataset.appendChild( blendingElement );
505 QDomElement content = doc.createElement( QStringLiteral(
"Content" ) );
506 for (
const ComponentLayerDetail &component : components )
508 if ( component.mapLayerId.isEmpty() )
510 content.appendChild( createPdfDatasetElement( component ) );
512 else if ( !component.group.isEmpty() )
515 QDomElement ifGroupOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
516 ifGroupOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"group_%1" ).arg( component.group ) );
517 QDomElement ifLayerOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
518 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
519 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
520 else if ( component.group.isEmpty() )
521 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), component.mapLayerId );
523 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
525 ifLayerOn.appendChild( createPdfDatasetElement( component ) );
526 ifGroupOn.appendChild( ifLayerOn );
527 content.appendChild( ifGroupOn );
531 QDomElement ifLayerOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
532 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
533 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
534 else if ( component.group.isEmpty() )
535 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), component.mapLayerId );
537 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
538 ifLayerOn.appendChild( createPdfDatasetElement( component ) );
539 content.appendChild( ifLayerOn );
544 if ( details.includeFeatures )
546 for (
const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
548 QDomElement ifLayerOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
549 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
550 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
551 else if ( component.group.isEmpty() )
552 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), component.mapLayerId );
554 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
555 QDomElement vectorDataset = doc.createElement( QStringLiteral(
"Vector" ) );
556 vectorDataset.setAttribute( QStringLiteral(
"dataset" ), component.sourceVectorPath );
557 vectorDataset.setAttribute( QStringLiteral(
"layer" ), component.sourceVectorLayer );
558 vectorDataset.setAttribute( QStringLiteral(
"visible" ), QStringLiteral(
"false" ) );
559 QDomElement logicalStructure = doc.createElement( QStringLiteral(
"LogicalStructure" ) );
560 logicalStructure.setAttribute( QStringLiteral(
"displayLayerName" ), component.name );
561 if ( !component.displayAttribute.isEmpty() )
562 logicalStructure.setAttribute( QStringLiteral(
"fieldToDisplay" ), component.displayAttribute );
563 vectorDataset.appendChild( logicalStructure );
564 ifLayerOn.appendChild( vectorDataset );
565 content.appendChild( ifLayerOn );
569 page.appendChild( content );
570 compositionElem.appendChild( page );
572 doc.appendChild( compositionElem );
575 QTextStream stream( &composition );
576 doc.save( stream, -1 );
581 QString QgsAbstractGeoPdfExporter::compositionModeToString( QPainter::CompositionMode mode )
585 case QPainter::CompositionMode_SourceOver:
586 return QStringLiteral(
"Normal" );
588 case QPainter::CompositionMode_Multiply:
589 return QStringLiteral(
"Multiply" );
591 case QPainter::CompositionMode_Screen:
592 return QStringLiteral(
"Screen" );
594 case QPainter::CompositionMode_Overlay:
595 return QStringLiteral(
"Overlay" );
597 case QPainter::CompositionMode_Darken:
598 return QStringLiteral(
"Darken" );
600 case QPainter::CompositionMode_Lighten:
601 return QStringLiteral(
"Lighten" );
603 case QPainter::CompositionMode_ColorDodge:
604 return QStringLiteral(
"ColorDodge" );
606 case QPainter::CompositionMode_ColorBurn:
607 return QStringLiteral(
"ColorBurn" );
609 case QPainter::CompositionMode_HardLight:
610 return QStringLiteral(
"HardLight" );
612 case QPainter::CompositionMode_SoftLight:
613 return QStringLiteral(
"SoftLight" );
615 case QPainter::CompositionMode_Difference:
616 return QStringLiteral(
"Difference" );
618 case QPainter::CompositionMode_Exclusion:
619 return QStringLiteral(
"Exclusion" );
625 QgsDebugMsg( QStringLiteral(
"Unsupported PDF blend mode %1" ).arg( mode ) );
626 return QStringLiteral(
"Normal" );