Branch data Line data Source code
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 : :
17 : : #include "qgsabstractgeopdfexporter.h"
18 : : #include "qgscoordinatetransformcontext.h"
19 : : #include "qgsrenderedfeaturehandlerinterface.h"
20 : : #include "qgsfeaturerequest.h"
21 : : #include "qgslogger.h"
22 : : #include "qgsgeometry.h"
23 : : #include "qgsvectorlayer.h"
24 : : #include "qgsvectorfilewriter.h"
25 : :
26 : : #include <gdal.h>
27 : : #include "qgsgdalutils.h"
28 : : #include "cpl_string.h"
29 : :
30 : : #include <QMutex>
31 : : #include <QMutexLocker>
32 : : #include <QDomDocument>
33 : : #include <QDomElement>
34 : : #include <QTimeZone>
35 : : #include <QUuid>
36 : : #include <QTextStream>
37 : :
38 : 0 : bool QgsAbstractGeoPdfExporter::geoPDFCreationAvailable()
39 : : {
40 : : // test if GDAL has read support in PDF driver
41 : 0 : GDALDriverH hDriverMem = GDALGetDriverByName( "PDF" );
42 : 0 : if ( !hDriverMem )
43 : : {
44 : 0 : return false;
45 : : }
46 : :
47 : 0 : const char *pHavePoppler = GDALGetMetadataItem( hDriverMem, "HAVE_POPPLER", nullptr );
48 : 0 : if ( pHavePoppler && strstr( pHavePoppler, "YES" ) )
49 : 0 : return true;
50 : :
51 : 0 : const char *pHavePdfium = GDALGetMetadataItem( hDriverMem, "HAVE_PDFIUM", nullptr );
52 : 0 : if ( pHavePdfium && strstr( pHavePdfium, "YES" ) )
53 : 0 : return true;
54 : :
55 : 0 : return false;
56 : 0 : }
57 : :
58 : 0 : QString QgsAbstractGeoPdfExporter::geoPDFAvailabilityExplanation()
59 : : {
60 : : // test if GDAL has read support in PDF driver
61 : 0 : GDALDriverH hDriverMem = GDALGetDriverByName( "PDF" );
62 : 0 : if ( !hDriverMem )
63 : : {
64 : 0 : return QObject::tr( "No GDAL PDF driver available." );
65 : : }
66 : :
67 : 0 : const char *pHavePoppler = GDALGetMetadataItem( hDriverMem, "HAVE_POPPLER", nullptr );
68 : 0 : if ( pHavePoppler && strstr( pHavePoppler, "YES" ) )
69 : 0 : return QString();
70 : :
71 : 0 : const char *pHavePdfium = GDALGetMetadataItem( hDriverMem, "HAVE_PDFIUM", nullptr );
72 : 0 : if ( pHavePdfium && strstr( pHavePdfium, "YES" ) )
73 : 0 : return QString();
74 : :
75 : 0 : return QObject::tr( "GDAL PDF driver was not built with PDF read support. A build with PDF read support is required for GeoPDF creation." );
76 : 0 : }
77 : :
78 : 0 : bool QgsAbstractGeoPdfExporter::finalize( const QList<ComponentLayerDetail> &components, const QString &destinationFile, const ExportDetails &details )
79 : : {
80 : 0 : if ( details.includeFeatures && !saveTemporaryLayers() )
81 : 0 : return false;
82 : :
83 : 0 : const QString composition = createCompositionXml( components, details );
84 : 0 : QgsDebugMsg( composition );
85 : 0 : if ( composition.isEmpty() )
86 : 0 : return false;
87 : :
88 : : // do the creation!
89 : 0 : GDALDriverH driver = GDALGetDriverByName( "PDF" );
90 : 0 : if ( !driver )
91 : : {
92 : 0 : mErrorMessage = QObject::tr( "Cannot load GDAL PDF driver" );
93 : 0 : return false;
94 : : }
95 : :
96 : 0 : const QString xmlFilePath = generateTemporaryFilepath( QStringLiteral( "composition.xml" ) );
97 : 0 : QFile file( xmlFilePath );
98 : 0 : if ( file.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
99 : : {
100 : 0 : QTextStream out( &file );
101 : : #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
102 : 0 : out.setCodec( "UTF-8" );
103 : : #endif
104 : 0 : out << composition;
105 : 0 : }
106 : : else
107 : : {
108 : 0 : mErrorMessage = QObject::tr( "Could not create GeoPDF composition file" );
109 : 0 : return false;
110 : : }
111 : :
112 : 0 : char **papszOptions = CSLSetNameValue( nullptr, "COMPOSITION_FILE", xmlFilePath.toUtf8().constData() );
113 : :
114 : : // return a non-null (fake) dataset in case of success, nullptr otherwise.
115 : 0 : gdal::dataset_unique_ptr outputDataset( GDALCreate( driver, destinationFile.toUtf8().constData(), 0, 0, 0, GDT_Unknown, papszOptions ) );
116 : 0 : bool res = outputDataset.get();
117 : 0 : outputDataset.reset();
118 : :
119 : 0 : CSLDestroy( papszOptions );
120 : :
121 : 0 : return res;
122 : 0 : }
123 : :
124 : 0 : QString QgsAbstractGeoPdfExporter::generateTemporaryFilepath( const QString &filename ) const
125 : : {
126 : 0 : return mTemporaryDir.filePath( filename );
127 : : }
128 : :
129 : 0 : bool QgsAbstractGeoPdfExporter::compositionModeSupported( QPainter::CompositionMode mode )
130 : : {
131 : 0 : switch ( mode )
132 : : {
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:
145 : 0 : return true;
146 : :
147 : : default:
148 : 0 : break;
149 : : }
150 : :
151 : 0 : return false;
152 : 0 : }
153 : :
154 : 0 : void QgsAbstractGeoPdfExporter::pushRenderedFeature( const QString &layerId, const QgsAbstractGeoPdfExporter::RenderedFeature &feature, const QString &group )
155 : : {
156 : : // because map layers may be rendered in parallel, we need a mutex here
157 : 0 : QMutexLocker locker( &mMutex );
158 : :
159 : : // collate all the features which belong to the same layer, replacing their geometries with the rendered feature bounds
160 : 0 : QgsFeature f = feature.feature;
161 : 0 : f.setGeometry( feature.renderedBounds );
162 : 0 : mCollatedFeatures[ group ][ layerId ].append( f );
163 : 0 : }
164 : :
165 : 0 : bool QgsAbstractGeoPdfExporter::saveTemporaryLayers()
166 : : {
167 : 0 : for ( auto groupIt = mCollatedFeatures.constBegin(); groupIt != mCollatedFeatures.constEnd(); ++groupIt )
168 : : {
169 : 0 : for ( auto it = groupIt->constBegin(); it != groupIt->constEnd(); ++it )
170 : : {
171 : 0 : const QString filePath = generateTemporaryFilepath( it.key() + groupIt.key() + QStringLiteral( ".gpkg" ) );
172 : :
173 : 0 : VectorComponentDetail detail = componentDetailForLayerId( it.key() );
174 : 0 : detail.sourceVectorPath = filePath;
175 : 0 : detail.group = groupIt.key();
176 : :
177 : : // write out features to disk
178 : 0 : const QgsFeatureList features = it.value();
179 : 0 : QString layerName;
180 : 0 : QgsVectorFileWriter::SaveVectorOptions saveOptions;
181 : 0 : saveOptions.driverName = QStringLiteral( "GPKG" );
182 : 0 : saveOptions.symbologyExport = QgsVectorFileWriter::NoSymbology;
183 : 0 : std::unique_ptr< QgsVectorFileWriter > writer( QgsVectorFileWriter::create( filePath, features.first().fields(), features.first().geometry().wkbType(), QgsCoordinateReferenceSystem(), QgsCoordinateTransformContext(), saveOptions, QgsFeatureSink::RegeneratePrimaryKey, nullptr, &layerName ) );
184 : 0 : if ( writer->hasError() )
185 : : {
186 : 0 : mErrorMessage = writer->errorMessage();
187 : 0 : QgsDebugMsg( mErrorMessage );
188 : 0 : return false;
189 : : }
190 : 0 : for ( const QgsFeature &feature : features )
191 : : {
192 : 0 : QgsFeature f = feature;
193 : 0 : if ( !writer->addFeature( f, QgsFeatureSink::FastInsert ) )
194 : : {
195 : 0 : mErrorMessage = writer->errorMessage();
196 : 0 : QgsDebugMsg( mErrorMessage );
197 : 0 : return false;
198 : : }
199 : 0 : }
200 : 0 : detail.sourceVectorLayer = layerName;
201 : 0 : mVectorComponents << detail;
202 : 0 : }
203 : 0 : }
204 : 0 : return true;
205 : 0 : }
206 : :
207 : 0 : QString QgsAbstractGeoPdfExporter::createCompositionXml( const QList<ComponentLayerDetail> &components, const ExportDetails &details )
208 : : {
209 : 0 : QDomDocument doc;
210 : :
211 : 0 : QDomElement compositionElem = doc.createElement( QStringLiteral( "PDFComposition" ) );
212 : :
213 : : // metadata tags
214 : 0 : QDomElement metadata = doc.createElement( QStringLiteral( "Metadata" ) );
215 : 0 : if ( !details.author.isEmpty() )
216 : : {
217 : 0 : QDomElement author = doc.createElement( QStringLiteral( "Author" ) );
218 : 0 : author.appendChild( doc.createTextNode( details.author ) );
219 : 0 : metadata.appendChild( author );
220 : 0 : }
221 : 0 : if ( !details.producer.isEmpty() )
222 : : {
223 : 0 : QDomElement producer = doc.createElement( QStringLiteral( "Producer" ) );
224 : 0 : producer.appendChild( doc.createTextNode( details.producer ) );
225 : 0 : metadata.appendChild( producer );
226 : 0 : }
227 : 0 : if ( !details.creator.isEmpty() )
228 : : {
229 : 0 : QDomElement creator = doc.createElement( QStringLiteral( "Creator" ) );
230 : 0 : creator.appendChild( doc.createTextNode( details.creator ) );
231 : 0 : metadata.appendChild( creator );
232 : 0 : }
233 : 0 : if ( details.creationDateTime.isValid() )
234 : : {
235 : 0 : QDomElement creationDate = doc.createElement( QStringLiteral( "CreationDate" ) );
236 : 0 : QString creationDateString = QStringLiteral( "D:%1" ).arg( details.creationDateTime.toString( QStringLiteral( "yyyyMMddHHmmss" ) ) );
237 : 0 : if ( details.creationDateTime.timeZone().isValid() )
238 : : {
239 : 0 : int offsetFromUtc = details.creationDateTime.timeZone().offsetFromUtc( details.creationDateTime );
240 : 0 : creationDateString += ( offsetFromUtc >= 0 ) ? '+' : '-';
241 : 0 : offsetFromUtc = std::abs( offsetFromUtc );
242 : 0 : int offsetHours = offsetFromUtc / 3600;
243 : 0 : int offsetMins = ( offsetFromUtc % 3600 ) / 60;
244 : 0 : creationDateString += QStringLiteral( "%1'%2'" ).arg( offsetHours ).arg( offsetMins );
245 : 0 : }
246 : 0 : creationDate.appendChild( doc.createTextNode( creationDateString ) );
247 : 0 : metadata.appendChild( creationDate );
248 : 0 : }
249 : 0 : if ( !details.subject.isEmpty() )
250 : : {
251 : 0 : QDomElement subject = doc.createElement( QStringLiteral( "Subject" ) );
252 : 0 : subject.appendChild( doc.createTextNode( details.subject ) );
253 : 0 : metadata.appendChild( subject );
254 : 0 : }
255 : 0 : if ( !details.title.isEmpty() )
256 : : {
257 : 0 : QDomElement title = doc.createElement( QStringLiteral( "Title" ) );
258 : 0 : title.appendChild( doc.createTextNode( details.title ) );
259 : 0 : metadata.appendChild( title );
260 : 0 : }
261 : 0 : if ( !details.keywords.empty() )
262 : : {
263 : 0 : QStringList allKeywords;
264 : 0 : for ( auto it = details.keywords.constBegin(); it != details.keywords.constEnd(); ++it )
265 : : {
266 : 0 : allKeywords.append( QStringLiteral( "%1: %2" ).arg( it.key(), it.value().join( ',' ) ) );
267 : 0 : }
268 : 0 : QDomElement keywords = doc.createElement( QStringLiteral( "Keywords" ) );
269 : 0 : keywords.appendChild( doc.createTextNode( allKeywords.join( ';' ) ) );
270 : 0 : metadata.appendChild( keywords );
271 : 0 : }
272 : 0 : compositionElem.appendChild( metadata );
273 : :
274 : 0 : QMap< QString, QSet< QString > > createdLayerIds;
275 : 0 : QMap< QString, QDomElement > groupLayerMap;
276 : 0 : QMap< QString, QString > customGroupNamesToIds;
277 : :
278 : 0 : QMultiMap< QString, QDomElement > pendingLayerTreeElements;
279 : :
280 : 0 : if ( details.includeFeatures )
281 : : {
282 : 0 : for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
283 : : {
284 : 0 : if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
285 : 0 : continue;
286 : :
287 : 0 : QDomElement layer = doc.createElement( QStringLiteral( "Layer" ) );
288 : 0 : layer.setAttribute( QStringLiteral( "id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
289 : 0 : layer.setAttribute( QStringLiteral( "name" ), details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name );
290 : 0 : layer.setAttribute( QStringLiteral( "initiallyVisible" ), details.initialLayerVisibility.value( component.mapLayerId, true ) ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
291 : :
292 : 0 : if ( !component.group.isEmpty() )
293 : : {
294 : 0 : if ( groupLayerMap.contains( component.group ) )
295 : : {
296 : 0 : groupLayerMap[ component.group ].appendChild( layer );
297 : 0 : }
298 : : else
299 : : {
300 : 0 : QDomElement group = doc.createElement( QStringLiteral( "Layer" ) );
301 : 0 : group.setAttribute( QStringLiteral( "id" ), QStringLiteral( "group_%1" ).arg( component.group ) );
302 : 0 : group.setAttribute( QStringLiteral( "name" ), component.group );
303 : 0 : group.setAttribute( QStringLiteral( "initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
304 : 0 : group.setAttribute( QStringLiteral( "mutuallyExclusiveGroupId" ), QStringLiteral( "__mutually_exclusive_groups__" ) );
305 : 0 : pendingLayerTreeElements.insert( component.mapLayerId, group );
306 : 0 : group.appendChild( layer );
307 : 0 : groupLayerMap[ component.group ] = group;
308 : 0 : }
309 : 0 : }
310 : : else
311 : : {
312 : 0 : pendingLayerTreeElements.insert( component.mapLayerId, layer );
313 : : }
314 : :
315 : 0 : createdLayerIds[ component.group ].insert( component.mapLayerId );
316 : 0 : }
317 : 0 : }
318 : : // some PDF components may not be linked to vector components - e.g. layers with labels but no features (or raster layers)
319 : 0 : for ( const ComponentLayerDetail &component : components )
320 : : {
321 : 0 : if ( component.mapLayerId.isEmpty() || createdLayerIds.value( component.group ).contains( component.mapLayerId ) )
322 : 0 : continue;
323 : :
324 : 0 : if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
325 : 0 : continue;
326 : :
327 : 0 : QDomElement layer = doc.createElement( QStringLiteral( "Layer" ) );
328 : 0 : layer.setAttribute( QStringLiteral( "id" ), component.group.isEmpty() ? component.mapLayerId : QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
329 : 0 : layer.setAttribute( QStringLiteral( "name" ), details.layerIdToPdfLayerTreeNameMap.contains( component.mapLayerId ) ? details.layerIdToPdfLayerTreeNameMap.value( component.mapLayerId ) : component.name );
330 : 0 : layer.setAttribute( QStringLiteral( "initiallyVisible" ), details.initialLayerVisibility.value( component.mapLayerId, true ) ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
331 : :
332 : 0 : if ( !component.group.isEmpty() )
333 : : {
334 : 0 : if ( groupLayerMap.contains( component.group ) )
335 : : {
336 : 0 : groupLayerMap[ component.group ].appendChild( layer );
337 : 0 : }
338 : : else
339 : : {
340 : 0 : QDomElement group = doc.createElement( QStringLiteral( "Layer" ) );
341 : 0 : group.setAttribute( QStringLiteral( "id" ), QStringLiteral( "group_%1" ).arg( component.group ) );
342 : 0 : group.setAttribute( QStringLiteral( "name" ), component.group );
343 : 0 : group.setAttribute( QStringLiteral( "initiallyVisible" ), groupLayerMap.empty() ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
344 : 0 : group.setAttribute( QStringLiteral( "mutuallyExclusiveGroupId" ), QStringLiteral( "__mutually_exclusive_groups__" ) );
345 : 0 : pendingLayerTreeElements.insert( component.mapLayerId, group );
346 : 0 : group.appendChild( layer );
347 : 0 : groupLayerMap[ component.group ] = group;
348 : 0 : }
349 : 0 : }
350 : : else
351 : : {
352 : 0 : pendingLayerTreeElements.insert( component.mapLayerId, layer );
353 : : }
354 : :
355 : 0 : createdLayerIds[ component.group ].insert( component.mapLayerId );
356 : 0 : }
357 : :
358 : : // layertree
359 : 0 : QDomElement layerTree = doc.createElement( QStringLiteral( "LayerTree" ) );
360 : : //layerTree.setAttribute( QStringLiteral("displayOnlyOnVisiblePages"), QStringLiteral("true"));
361 : :
362 : : // create custom layer tree entries
363 : 0 : for ( auto it = details.customLayerTreeGroups.constBegin(); it != details.customLayerTreeGroups.constEnd(); ++it )
364 : : {
365 : 0 : if ( customGroupNamesToIds.contains( it.value() ) )
366 : 0 : continue;
367 : :
368 : 0 : QDomElement layer = doc.createElement( QStringLiteral( "Layer" ) );
369 : 0 : const QString id = QUuid::createUuid().toString();
370 : 0 : customGroupNamesToIds[ it.value() ] = id;
371 : 0 : layer.setAttribute( QStringLiteral( "id" ), id );
372 : 0 : layer.setAttribute( QStringLiteral( "name" ), it.value() );
373 : 0 : layer.setAttribute( QStringLiteral( "initiallyVisible" ), QStringLiteral( "true" ) );
374 : 0 : layerTree.appendChild( layer );
375 : 0 : }
376 : :
377 : : // start by adding layer tree elements with known layer orders
378 : 0 : for ( const QString &layerId : details.layerOrder )
379 : : {
380 : 0 : const QList< QDomElement> elements = pendingLayerTreeElements.values( layerId );
381 : 0 : for ( const QDomElement &element : elements )
382 : 0 : layerTree.appendChild( element );
383 : 0 : }
384 : : // then add all the rest (those we don't have an explicit order for)
385 : 0 : for ( auto it = pendingLayerTreeElements.constBegin(); it != pendingLayerTreeElements.constEnd(); ++it )
386 : : {
387 : 0 : if ( details.layerOrder.contains( it.key() ) )
388 : : {
389 : : // already added this one, just above...
390 : 0 : continue;
391 : : }
392 : :
393 : 0 : layerTree.appendChild( it.value() );
394 : 0 : }
395 : :
396 : 0 : compositionElem.appendChild( layerTree );
397 : :
398 : : // pages
399 : 0 : QDomElement page = doc.createElement( QStringLiteral( "Page" ) );
400 : 0 : QDomElement dpi = doc.createElement( QStringLiteral( "DPI" ) );
401 : : // hardcode DPI of 72 to get correct page sizes in outputs -- refs discussion in https://github.com/OSGeo/gdal/pull/2961
402 : 0 : dpi.appendChild( doc.createTextNode( qgsDoubleToString( 72 ) ) );
403 : 0 : page.appendChild( dpi );
404 : : // assumes DPI of 72, as noted above.
405 : 0 : QDomElement width = doc.createElement( QStringLiteral( "Width" ) );
406 : 0 : const double pageWidthPdfUnits = std::ceil( details.pageSizeMm.width() / 25.4 * 72 );
407 : 0 : width.appendChild( doc.createTextNode( qgsDoubleToString( pageWidthPdfUnits ) ) );
408 : 0 : page.appendChild( width );
409 : 0 : QDomElement height = doc.createElement( QStringLiteral( "Height" ) );
410 : 0 : const double pageHeightPdfUnits = std::ceil( details.pageSizeMm.height() / 25.4 * 72 );
411 : 0 : height.appendChild( doc.createTextNode( qgsDoubleToString( pageHeightPdfUnits ) ) );
412 : 0 : page.appendChild( height );
413 : :
414 : :
415 : : // georeferencing
416 : 0 : int i = 0;
417 : 0 : for ( const QgsAbstractGeoPdfExporter::GeoReferencedSection §ion : details.georeferencedSections )
418 : : {
419 : 0 : QDomElement georeferencing = doc.createElement( QStringLiteral( "Georeferencing" ) );
420 : 0 : georeferencing.setAttribute( QStringLiteral( "id" ), QStringLiteral( "georeferenced_%1" ).arg( i++ ) );
421 : 0 : georeferencing.setAttribute( QStringLiteral( "OGCBestPracticeFormat" ), details.useOgcBestPracticeFormatGeoreferencing ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
422 : 0 : georeferencing.setAttribute( QStringLiteral( "ISO32000ExtensionFormat" ), details.useIso32000ExtensionFormatGeoreferencing ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
423 : :
424 : 0 : if ( section.crs.isValid() )
425 : : {
426 : 0 : QDomElement srs = doc.createElement( QStringLiteral( "SRS" ) );
427 : : // 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...
428 : : // srs.setAttribute( QStringLiteral( "dataAxisToSRSAxisMapping" ), QStringLiteral( "2,1" ) );
429 : 0 : if ( !section.crs.authid().startsWith( QStringLiteral( "user" ), Qt::CaseInsensitive ) )
430 : : {
431 : 0 : srs.appendChild( doc.createTextNode( section.crs.authid() ) );
432 : 0 : }
433 : : else
434 : : {
435 : 0 : srs.appendChild( doc.createTextNode( section.crs.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED_GDAL ) ) );
436 : : }
437 : 0 : georeferencing.appendChild( srs );
438 : 0 : }
439 : :
440 : 0 : if ( !section.pageBoundsPolygon.isEmpty() )
441 : : {
442 : : /*
443 : : Define a polygon / neatline in PDF units into which the
444 : : Measure tool will display coordinates.
445 : : If not specified, BoundingBox will be used instead.
446 : : If none of BoundingBox and BoundingPolygon are specified,
447 : : the whole PDF page will be assumed to be georeferenced.
448 : : */
449 : 0 : QDomElement boundingPolygon = doc.createElement( QStringLiteral( "BoundingPolygon" ) );
450 : :
451 : : // transform to PDF coordinate space
452 : 0 : QTransform t = QTransform::fromTranslate( 0, pageHeightPdfUnits ).scale( pageWidthPdfUnits / details.pageSizeMm.width(),
453 : 0 : -pageHeightPdfUnits / details.pageSizeMm.height() );
454 : :
455 : 0 : QgsPolygon p = section.pageBoundsPolygon;
456 : 0 : p.transform( t );
457 : 0 : boundingPolygon.appendChild( doc.createTextNode( p.asWkt() ) );
458 : :
459 : 0 : georeferencing.appendChild( boundingPolygon );
460 : 0 : }
461 : : else
462 : : {
463 : : /* Define the viewport where georeferenced coordinates are available.
464 : : If not specified, the extent of BoundingPolygon will be used instead.
465 : : If none of BoundingBox and BoundingPolygon are specified,
466 : : the whole PDF page will be assumed to be georeferenced.
467 : : */
468 : 0 : QDomElement boundingBox = doc.createElement( QStringLiteral( "BoundingBox" ) );
469 : 0 : boundingBox.setAttribute( QStringLiteral( "x1" ), qgsDoubleToString( section.pageBoundsMm.xMinimum() / 25.4 * 72 ) );
470 : 0 : boundingBox.setAttribute( QStringLiteral( "y1" ), qgsDoubleToString( section.pageBoundsMm.yMinimum() / 25.4 * 72 ) );
471 : 0 : boundingBox.setAttribute( QStringLiteral( "x2" ), qgsDoubleToString( section.pageBoundsMm.xMaximum() / 25.4 * 72 ) );
472 : 0 : boundingBox.setAttribute( QStringLiteral( "y2" ), qgsDoubleToString( section.pageBoundsMm.yMaximum() / 25.4 * 72 ) );
473 : 0 : georeferencing.appendChild( boundingBox );
474 : 0 : }
475 : :
476 : 0 : for ( const ControlPoint &point : section.controlPoints )
477 : : {
478 : 0 : QDomElement cp1 = doc.createElement( QStringLiteral( "ControlPoint" ) );
479 : 0 : cp1.setAttribute( QStringLiteral( "x" ), qgsDoubleToString( point.pagePoint.x() / 25.4 * 72 ) );
480 : 0 : cp1.setAttribute( QStringLiteral( "y" ), qgsDoubleToString( ( details.pageSizeMm.height() - point.pagePoint.y() ) / 25.4 * 72 ) );
481 : 0 : cp1.setAttribute( QStringLiteral( "GeoX" ), qgsDoubleToString( point.geoPoint.x() ) );
482 : 0 : cp1.setAttribute( QStringLiteral( "GeoY" ), qgsDoubleToString( point.geoPoint.y() ) );
483 : 0 : georeferencing.appendChild( cp1 );
484 : 0 : }
485 : :
486 : 0 : page.appendChild( georeferencing );
487 : 0 : }
488 : :
489 : 0 : auto createPdfDatasetElement = [&doc]( const ComponentLayerDetail & component ) -> QDomElement
490 : : {
491 : 0 : QDomElement pdfDataset = doc.createElement( QStringLiteral( "PDF" ) );
492 : 0 : pdfDataset.setAttribute( QStringLiteral( "dataset" ), component.sourcePdfPath );
493 : 0 : if ( component.opacity != 1.0 || component.compositionMode != QPainter::CompositionMode_SourceOver )
494 : : {
495 : 0 : QDomElement blendingElement = doc.createElement( QStringLiteral( "Blending" ) );
496 : 0 : blendingElement.setAttribute( QStringLiteral( "opacity" ), component.opacity );
497 : 0 : blendingElement.setAttribute( QStringLiteral( "function" ), compositionModeToString( component.compositionMode ) );
498 : :
499 : 0 : pdfDataset.appendChild( blendingElement );
500 : 0 : }
501 : 0 : return pdfDataset;
502 : 0 : };
503 : :
504 : : // content
505 : 0 : QDomElement content = doc.createElement( QStringLiteral( "Content" ) );
506 : 0 : for ( const ComponentLayerDetail &component : components )
507 : : {
508 : 0 : if ( component.mapLayerId.isEmpty() )
509 : : {
510 : 0 : content.appendChild( createPdfDatasetElement( component ) );
511 : 0 : }
512 : 0 : else if ( !component.group.isEmpty() )
513 : : {
514 : : // if content belongs to a group, we need nested "IfLayerOn" elements, one for the group and one for the layer
515 : 0 : QDomElement ifGroupOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
516 : 0 : ifGroupOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "group_%1" ).arg( component.group ) );
517 : 0 : QDomElement ifLayerOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
518 : 0 : if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
519 : 0 : ifLayerOn.setAttribute( QStringLiteral( "layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
520 : 0 : else if ( component.group.isEmpty() )
521 : 0 : ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
522 : : else
523 : 0 : ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
524 : :
525 : 0 : ifLayerOn.appendChild( createPdfDatasetElement( component ) );
526 : 0 : ifGroupOn.appendChild( ifLayerOn );
527 : 0 : content.appendChild( ifGroupOn );
528 : 0 : }
529 : : else
530 : : {
531 : 0 : QDomElement ifLayerOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
532 : 0 : if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
533 : 0 : ifLayerOn.setAttribute( QStringLiteral( "layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
534 : 0 : else if ( component.group.isEmpty() )
535 : 0 : ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
536 : : else
537 : 0 : ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
538 : 0 : ifLayerOn.appendChild( createPdfDatasetElement( component ) );
539 : 0 : content.appendChild( ifLayerOn );
540 : 0 : }
541 : : }
542 : :
543 : : // vector datasets (we "draw" these on top, just for debugging... but they are invisible, so are never really drawn!)
544 : 0 : if ( details.includeFeatures )
545 : : {
546 : 0 : for ( const VectorComponentDetail &component : std::as_const( mVectorComponents ) )
547 : : {
548 : 0 : QDomElement ifLayerOn = doc.createElement( QStringLiteral( "IfLayerOn" ) );
549 : 0 : if ( details.customLayerTreeGroups.contains( component.mapLayerId ) )
550 : 0 : ifLayerOn.setAttribute( QStringLiteral( "layerId" ), customGroupNamesToIds.value( details.customLayerTreeGroups.value( component.mapLayerId ) ) );
551 : 0 : else if ( component.group.isEmpty() )
552 : 0 : ifLayerOn.setAttribute( QStringLiteral( "layerId" ), component.mapLayerId );
553 : : else
554 : 0 : ifLayerOn.setAttribute( QStringLiteral( "layerId" ), QStringLiteral( "%1_%2" ).arg( component.group, component.mapLayerId ) );
555 : 0 : QDomElement vectorDataset = doc.createElement( QStringLiteral( "Vector" ) );
556 : 0 : vectorDataset.setAttribute( QStringLiteral( "dataset" ), component.sourceVectorPath );
557 : 0 : vectorDataset.setAttribute( QStringLiteral( "layer" ), component.sourceVectorLayer );
558 : 0 : vectorDataset.setAttribute( QStringLiteral( "visible" ), QStringLiteral( "false" ) );
559 : 0 : QDomElement logicalStructure = doc.createElement( QStringLiteral( "LogicalStructure" ) );
560 : 0 : logicalStructure.setAttribute( QStringLiteral( "displayLayerName" ), component.name );
561 : 0 : if ( !component.displayAttribute.isEmpty() )
562 : 0 : logicalStructure.setAttribute( QStringLiteral( "fieldToDisplay" ), component.displayAttribute );
563 : 0 : vectorDataset.appendChild( logicalStructure );
564 : 0 : ifLayerOn.appendChild( vectorDataset );
565 : 0 : content.appendChild( ifLayerOn );
566 : 0 : }
567 : 0 : }
568 : :
569 : 0 : page.appendChild( content );
570 : 0 : compositionElem.appendChild( page );
571 : :
572 : 0 : doc.appendChild( compositionElem );
573 : :
574 : 0 : QString composition;
575 : 0 : QTextStream stream( &composition );
576 : 0 : doc.save( stream, -1 );
577 : :
578 : 0 : return composition;
579 : 0 : }
580 : :
581 : 0 : QString QgsAbstractGeoPdfExporter::compositionModeToString( QPainter::CompositionMode mode )
582 : : {
583 : 0 : switch ( mode )
584 : : {
585 : : case QPainter::CompositionMode_SourceOver:
586 : 0 : return QStringLiteral( "Normal" );
587 : :
588 : : case QPainter::CompositionMode_Multiply:
589 : 0 : return QStringLiteral( "Multiply" );
590 : :
591 : : case QPainter::CompositionMode_Screen:
592 : 0 : return QStringLiteral( "Screen" );
593 : :
594 : : case QPainter::CompositionMode_Overlay:
595 : 0 : return QStringLiteral( "Overlay" );
596 : :
597 : : case QPainter::CompositionMode_Darken:
598 : 0 : return QStringLiteral( "Darken" );
599 : :
600 : : case QPainter::CompositionMode_Lighten:
601 : 0 : return QStringLiteral( "Lighten" );
602 : :
603 : : case QPainter::CompositionMode_ColorDodge:
604 : 0 : return QStringLiteral( "ColorDodge" );
605 : :
606 : : case QPainter::CompositionMode_ColorBurn:
607 : 0 : return QStringLiteral( "ColorBurn" );
608 : :
609 : : case QPainter::CompositionMode_HardLight:
610 : 0 : return QStringLiteral( "HardLight" );
611 : :
612 : : case QPainter::CompositionMode_SoftLight:
613 : 0 : return QStringLiteral( "SoftLight" );
614 : :
615 : : case QPainter::CompositionMode_Difference:
616 : 0 : return QStringLiteral( "Difference" );
617 : :
618 : : case QPainter::CompositionMode_Exclusion:
619 : 0 : return QStringLiteral( "Exclusion" );
620 : :
621 : : default:
622 : 0 : break;
623 : : }
624 : :
625 : 0 : QgsDebugMsg( QStringLiteral( "Unsupported PDF blend mode %1" ).arg( mode ) );
626 : 0 : return QStringLiteral( "Normal" );
627 : 0 : }
628 : :
|