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:
166 QMutexLocker locker( &mMutex );
171 mCollatedFeatures[ group ][ layerId ].append( f );
174 bool QgsAbstractGeoPdfExporter::saveTemporaryLayers()
176 for (
auto groupIt = mCollatedFeatures.constBegin(); groupIt != mCollatedFeatures.constEnd(); ++groupIt )
178 for (
auto it = groupIt->constBegin(); it != groupIt->constEnd(); ++it )
182 VectorComponentDetail detail = componentDetailForLayerId( it.key() );
183 detail.sourceVectorPath = filePath;
184 detail.group = groupIt.key();
190 saveOptions.
driverName = QStringLiteral(
"GPKG" );
193 if ( writer->hasError() )
195 mErrorMessage = writer->errorMessage();
204 mErrorMessage = writer->errorMessage();
209 detail.sourceVectorLayer = layerName;
210 mVectorComponents << detail;
216 QString QgsAbstractGeoPdfExporter::createCompositionXml(
const QList<ComponentLayerDetail> &components,
const ExportDetails &details )
220 QDomElement compositionElem = doc.createElement( QStringLiteral(
"PDFComposition" ) );
223 QDomElement metadata = doc.createElement( QStringLiteral(
"Metadata" ) );
224 if ( !details.author.isEmpty() )
226 QDomElement author = doc.createElement( QStringLiteral(
"Author" ) );
227 author.appendChild( doc.createTextNode( details.author ) );
228 metadata.appendChild( author );
230 if ( !details.producer.isEmpty() )
232 QDomElement producer = doc.createElement( QStringLiteral(
"Producer" ) );
233 producer.appendChild( doc.createTextNode( details.producer ) );
234 metadata.appendChild( producer );
236 if ( !details.creator.isEmpty() )
238 QDomElement creator = doc.createElement( QStringLiteral(
"Creator" ) );
239 creator.appendChild( doc.createTextNode( details.creator ) );
240 metadata.appendChild( creator );
242 if ( details.creationDateTime.isValid() )
244 QDomElement creationDate = doc.createElement( QStringLiteral(
"CreationDate" ) );
245 QString creationDateString = QStringLiteral(
"D:%1" ).arg( details.creationDateTime.toString( QStringLiteral(
"yyyyMMddHHmmss" ) ) );
246 if ( details.creationDateTime.timeZone().isValid() )
248 int offsetFromUtc = details.creationDateTime.timeZone().offsetFromUtc( details.creationDateTime );
249 creationDateString += ( offsetFromUtc >= 0 ) ?
'+' :
'-';
250 offsetFromUtc = std::abs( offsetFromUtc );
251 int offsetHours = offsetFromUtc / 3600;
252 int offsetMins = ( offsetFromUtc % 3600 ) / 60;
253 creationDateString += QStringLiteral(
"%1'%2'" ).arg( offsetHours ).arg( offsetMins );
255 creationDate.appendChild( doc.createTextNode( creationDateString ) );
256 metadata.appendChild( creationDate );
258 if ( !details.subject.isEmpty() )
260 QDomElement subject = doc.createElement( QStringLiteral(
"Subject" ) );
261 subject.appendChild( doc.createTextNode( details.subject ) );
262 metadata.appendChild( subject );
264 if ( !details.title.isEmpty() )
266 QDomElement title = doc.createElement( QStringLiteral(
"Title" ) );
267 title.appendChild( doc.createTextNode( details.title ) );
268 metadata.appendChild( title );
270 if ( !details.keywords.empty() )
272 QStringList allKeywords;
273 for (
auto it = details.keywords.constBegin(); it != details.keywords.constEnd(); ++it )
275 allKeywords.append( QStringLiteral(
"%1: %2" ).arg( it.key(), it.value().join(
',' ) ) );
277 QDomElement keywords = doc.createElement( QStringLiteral(
"Keywords" ) );
278 keywords.appendChild( doc.createTextNode( allKeywords.join(
';' ) ) );
279 metadata.appendChild( keywords );
281 compositionElem.appendChild( metadata );
283 QMap< QString, QSet< QString > > createdLayerIds;
284 QMap< QString, QDomElement > groupLayerMap;
285 QMap< QString, QString > customGroupNamesToIds;
287 QMultiMap< QString, QDomElement > pendingLayerTreeElements;
289 if ( details.includeFeatures )
291 for (
const VectorComponentDetail &component : qgis::as_const( mVectorComponents ) )
293 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
296 QDomElement layer = doc.createElement( QStringLiteral(
"Layer" ) );
297 layer.setAttribute( QStringLiteral(
"id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
298 layer.setAttribute( QStringLiteral(
"name" ), details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name );
299 layer.setAttribute( QStringLiteral(
"initiallyVisible" ), details.initialLayerVisibility.value( component.mapLayerId,
true ) ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
301 if ( !component.group.isEmpty() )
303 if ( groupLayerMap.contains( component.group ) )
305 groupLayerMap[ component.group ].appendChild( layer );
309 QDomElement group = doc.createElement( QStringLiteral(
"Layer" ) );
310 group.setAttribute( QStringLiteral(
"id" ), QStringLiteral(
"group_%1" ).arg( component.group ) );
311 group.setAttribute( QStringLiteral(
"name" ), component.group );
312 group.setAttribute( QStringLiteral(
"initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
313 group.setAttribute( QStringLiteral(
"mutuallyExclusiveGroupId" ), QStringLiteral(
"__mutually_exclusive_groups__" ) );
314 pendingLayerTreeElements.insert( component.mapLayerId, group );
315 group.appendChild( layer );
316 groupLayerMap[ component.group ] = group;
321 pendingLayerTreeElements.insert( component.mapLayerId, layer );
324 createdLayerIds[ component.group ].insert( component.mapLayerId );
328 for (
const ComponentLayerDetail &component : components )
330 if ( component.mapLayerId.isEmpty() || createdLayerIds.value( component.group ).contains( component.mapLayerId ) )
333 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
336 QDomElement layer = doc.createElement( QStringLiteral(
"Layer" ) );
337 layer.setAttribute( QStringLiteral(
"id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
338 layer.setAttribute( QStringLiteral(
"name" ), details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name );
339 layer.setAttribute( QStringLiteral(
"initiallyVisible" ), details.initialLayerVisibility.value( component.mapLayerId,
true ) ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
341 if ( !component.group.isEmpty() )
343 if ( groupLayerMap.contains( component.group ) )
345 groupLayerMap[ component.group ].appendChild( layer );
349 QDomElement group = doc.createElement( QStringLiteral(
"Layer" ) );
350 group.setAttribute( QStringLiteral(
"id" ), QStringLiteral(
"group_%1" ).arg( component.group ) );
351 group.setAttribute( QStringLiteral(
"name" ), component.group );
352 group.setAttribute( QStringLiteral(
"initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
353 group.setAttribute( QStringLiteral(
"mutuallyExclusiveGroupId" ), QStringLiteral(
"__mutually_exclusive_groups__" ) );
354 pendingLayerTreeElements.insert( component.mapLayerId, group );
355 group.appendChild( layer );
356 groupLayerMap[ component.group ] = group;
361 pendingLayerTreeElements.insert( component.mapLayerId, layer );
364 createdLayerIds[ component.group ].insert( component.mapLayerId );
368 QDomElement
layerTree = doc.createElement( QStringLiteral(
"LayerTree" ) );
372 for (
auto it = details.customLayerTreeGroups.constBegin(); it != details.customLayerTreeGroups.constEnd(); ++it )
374 if ( customGroupNamesToIds.contains( it.value() ) )
377 QDomElement layer = doc.createElement( QStringLiteral(
"Layer" ) );
378 const QString
id = QUuid::createUuid().toString();
379 customGroupNamesToIds[ it.value() ] = id;
380 layer.setAttribute( QStringLiteral(
"id" ),
id );
381 layer.setAttribute( QStringLiteral(
"name" ), it.value() );
382 layer.setAttribute( QStringLiteral(
"initiallyVisible" ), QStringLiteral(
"true" ) );
387 for (
const QString &layerId : details.layerOrder )
389 const QList< QDomElement> elements = pendingLayerTreeElements.values( layerId );
390 for (
const QDomElement &element : elements )
394 for (
auto it = pendingLayerTreeElements.constBegin(); it != pendingLayerTreeElements.constEnd(); ++it )
396 if ( details.layerOrder.contains( it.key() ) )
405 compositionElem.appendChild(
layerTree );
408 QDomElement page = doc.createElement( QStringLiteral(
"Page" ) );
409 QDomElement dpi = doc.createElement( QStringLiteral(
"DPI" ) );
410 dpi.appendChild( doc.createTextNode( QString::number( details.dpi ) ) );
411 page.appendChild( dpi );
413 QDomElement width = doc.createElement( QStringLiteral(
"Width" ) );
414 const double pageWidthPdfUnits = std::ceil( details.pageSizeMm.width() / 25.4 * 72 );
415 width.appendChild( doc.createTextNode( QString::number( pageWidthPdfUnits ) ) );
416 page.appendChild( width );
417 QDomElement height = doc.createElement( QStringLiteral(
"Height" ) );
418 const double pageHeightPdfUnits = std::ceil( details.pageSizeMm.height() / 25.4 * 72 );
419 height.appendChild( doc.createTextNode( QString::number( pageHeightPdfUnits ) ) );
420 page.appendChild( height );
427 QDomElement georeferencing = doc.createElement( QStringLiteral(
"Georeferencing" ) );
428 georeferencing.setAttribute( QStringLiteral(
"id" ), QStringLiteral(
"georeferenced_%1" ).arg( i++ ) );
429 georeferencing.setAttribute( QStringLiteral(
"OGCBestPracticeFormat" ), details.useOgcBestPracticeFormatGeoreferencing ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
430 georeferencing.setAttribute( QStringLiteral(
"ISO32000ExtensionFormat" ), details.useIso32000ExtensionFormatGeoreferencing ? QStringLiteral(
"true" ) : QStringLiteral(
"false" ) );
434 QDomElement srs = doc.createElement( QStringLiteral(
"SRS" ) );
437 if ( !section.
crs.
authid().startsWith( QStringLiteral(
"user" ), Qt::CaseInsensitive ) )
439 srs.appendChild( doc.createTextNode( section.
crs.
authid() ) );
445 georeferencing.appendChild( srs );
457 QDomElement boundingPolygon = doc.createElement( QStringLiteral(
"BoundingPolygon" ) );
460 QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(),
461 -pageHeightPdfUnits / details.pageSizeMm.height() );
465 boundingPolygon.appendChild( doc.createTextNode( p.
asWkt() ) );
467 georeferencing.appendChild( boundingPolygon );
476 QDomElement boundingBox = doc.createElement( QStringLiteral(
"BoundingBox" ) );
477 boundingBox.setAttribute( QStringLiteral(
"x1" ), QString::number( section.
pageBoundsMm.
xMinimum() / 25.4 * 72 ) );
478 boundingBox.setAttribute( QStringLiteral(
"y1" ), QString::number( section.
pageBoundsMm.
yMinimum() / 25.4 * 72 ) );
479 boundingBox.setAttribute( QStringLiteral(
"x2" ), QString::number( section.
pageBoundsMm.
xMaximum() / 25.4 * 72 ) );
480 boundingBox.setAttribute( QStringLiteral(
"y2" ), QString::number( section.
pageBoundsMm.
yMaximum() / 25.4 * 72 ) );
481 georeferencing.appendChild( boundingBox );
486 QDomElement cp1 = doc.createElement( QStringLiteral(
"ControlPoint" ) );
487 cp1.setAttribute( QStringLiteral(
"x" ), QString::number( point.pagePoint.x() / 25.4 * 72 ) );
488 cp1.setAttribute( QStringLiteral(
"y" ), QString::number( ( details.pageSizeMm.height() - point.pagePoint.y() ) / 25.4 * 72 ) );
489 cp1.setAttribute( QStringLiteral(
"GeoX" ), QString::number( point.geoPoint.x() ) );
490 cp1.setAttribute( QStringLiteral(
"GeoY" ), QString::number( point.geoPoint.y() ) );
491 georeferencing.appendChild( cp1 );
494 page.appendChild( georeferencing );
497 auto createPdfDatasetElement = [&doc](
const ComponentLayerDetail & component ) -> QDomElement
499 QDomElement pdfDataset = doc.createElement( QStringLiteral(
"PDF" ) );
500 pdfDataset.setAttribute( QStringLiteral(
"dataset" ), component.sourcePdfPath );
501 if ( component.opacity != 1.0 || component.compositionMode != QPainter::CompositionMode_SourceOver )
503 QDomElement blendingElement = doc.createElement( QStringLiteral(
"Blending" ) );
504 blendingElement.setAttribute( QStringLiteral(
"opacity" ), component.opacity );
505 blendingElement.setAttribute( QStringLiteral(
"function" ), compositionModeToString( component.compositionMode ) );
507 pdfDataset.appendChild( blendingElement );
513 QDomElement content = doc.createElement( QStringLiteral(
"Content" ) );
514 for (
const ComponentLayerDetail &component : components )
516 if ( component.mapLayerId.isEmpty() )
518 content.appendChild( createPdfDatasetElement( component ) );
520 else if ( !component.group.isEmpty() )
523 QDomElement ifGroupOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
524 ifGroupOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"group_%1" ).arg( component.group ) );
525 QDomElement ifLayerOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
526 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
527 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
528 else if ( component.group.isEmpty() )
529 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), component.mapLayerId );
531 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
533 ifLayerOn.appendChild( createPdfDatasetElement( component ) );
534 ifGroupOn.appendChild( ifLayerOn );
535 content.appendChild( ifGroupOn );
539 QDomElement ifLayerOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
540 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
541 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
542 else if ( component.group.isEmpty() )
543 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), component.mapLayerId );
545 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
546 ifLayerOn.appendChild( createPdfDatasetElement( component ) );
547 content.appendChild( ifLayerOn );
552 if ( details.includeFeatures )
554 for (
const VectorComponentDetail &component : qgis::as_const( mVectorComponents ) )
556 QDomElement ifLayerOn = doc.createElement( QStringLiteral(
"IfLayerOn" ) );
557 if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
558 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
559 else if ( component.group.isEmpty() )
560 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), component.mapLayerId );
562 ifLayerOn.setAttribute( QStringLiteral(
"layerId" ), QStringLiteral(
"%1_%2" ).arg( component.group, component.mapLayerId ) );
563 QDomElement vectorDataset = doc.createElement( QStringLiteral(
"Vector" ) );
564 vectorDataset.setAttribute( QStringLiteral(
"dataset" ), component.sourceVectorPath );
565 vectorDataset.setAttribute( QStringLiteral(
"layer" ), component.sourceVectorLayer );
566 vectorDataset.setAttribute( QStringLiteral(
"visible" ), QStringLiteral(
"false" ) );
567 QDomElement logicalStructure = doc.createElement( QStringLiteral(
"LogicalStructure" ) );
568 logicalStructure.setAttribute( QStringLiteral(
"displayLayerName" ), component.name );
569 if ( !component.displayAttribute.isEmpty() )
570 logicalStructure.setAttribute( QStringLiteral(
"fieldToDisplay" ), component.displayAttribute );
571 vectorDataset.appendChild( logicalStructure );
572 ifLayerOn.appendChild( vectorDataset );
573 content.appendChild( ifLayerOn );
577 page.appendChild( content );
578 compositionElem.appendChild( page );
580 doc.appendChild( compositionElem );
583 QTextStream stream( &composition );
584 doc.save( stream, -1 );
589 QString QgsAbstractGeoPdfExporter::compositionModeToString( QPainter::CompositionMode mode )
593 case QPainter::CompositionMode_SourceOver:
594 return QStringLiteral(
"Normal" );
596 case QPainter::CompositionMode_Multiply:
597 return QStringLiteral(
"Multiply" );
599 case QPainter::CompositionMode_Screen:
600 return QStringLiteral(
"Screen" );
602 case QPainter::CompositionMode_Overlay:
603 return QStringLiteral(
"Overlay" );
605 case QPainter::CompositionMode_Darken:
606 return QStringLiteral(
"Darken" );
608 case QPainter::CompositionMode_Lighten:
609 return QStringLiteral(
"Lighten" );
611 case QPainter::CompositionMode_ColorDodge:
612 return QStringLiteral(
"ColorDodge" );
614 case QPainter::CompositionMode_ColorBurn:
615 return QStringLiteral(
"ColorBurn" );
617 case QPainter::CompositionMode_HardLight:
618 return QStringLiteral(
"HardLight" );
620 case QPainter::CompositionMode_SoftLight:
621 return QStringLiteral(
"SoftLight" );
623 case QPainter::CompositionMode_Difference:
624 return QStringLiteral(
"Difference" );
626 case QPainter::CompositionMode_Exclusion:
627 return QStringLiteral(
"Exclusion" );
630 QgsDebugMsg( QStringLiteral(
"Unsupported PDF blend mode %1" ).arg( mode ) );
631 return QStringLiteral(
"Normal" );