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