1 /***************************************************************************
2 qgslayoutexporter.cpp
3 -------------------
4 begin : October 2017
5 copyright : (C) 2017 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 "qgslayoutexporter.h"
18 #ifndef QT_NO_PRINTER
19
20 #include "qgslayout.h"
21 #include "qgslayoutitemmap.h"
22 #include "qgslayoutpagecollection.h"
23 #include "qgsogrutils.h"
24 #include "qgspaintenginehack.h"
25 #include "qgslayoutguidecollection.h"
26 #include "qgsabstractlayoutiterator.h"
27 #include "qgsfeedback.h"
28 #include "qgslayoutgeopdfexporter.h"
29 #include "qgslinestring.h"
30 #include <QImageWriter>
31 #include <QSize>
32 #include <QSvgGenerator>
33
34 #include "gdal.h"
35 #include "cpl_conv.h"
36
37 ///@cond PRIVATE
38 class LayoutContextPreviewSettingRestorer
39 {
40 public:
41
LayoutContextPreviewSettingRestorer(QgsLayout * layout)42 LayoutContextPreviewSettingRestorer( QgsLayout *layout )
43 : mLayout( layout )
44 , mPreviousSetting( layout->renderContext().mIsPreviewRender )
45 {
46 mLayout->renderContext().mIsPreviewRender = false;
47 }
48
~LayoutContextPreviewSettingRestorer()49 ~LayoutContextPreviewSettingRestorer()
50 {
51 mLayout->renderContext().mIsPreviewRender = mPreviousSetting;
52 }
53
54 LayoutContextPreviewSettingRestorer( const LayoutContextPreviewSettingRestorer &other ) = delete;
55 LayoutContextPreviewSettingRestorer &operator=( const LayoutContextPreviewSettingRestorer &other ) = delete;
56
57 private:
58 QgsLayout *mLayout = nullptr;
59 bool mPreviousSetting = false;
60 };
61
62 class LayoutGuideHider
63 {
64 public:
65
LayoutGuideHider(QgsLayout * layout)66 LayoutGuideHider( QgsLayout *layout )
67 : mLayout( layout )
68 {
69 const QList< QgsLayoutGuide * > guides = mLayout->guides().guides();
70 for ( QgsLayoutGuide *guide : guides )
71 {
72 mPrevVisibility.insert( guide, guide->item()->isVisible() );
73 guide->item()->setVisible( false );
74 }
75 }
76
~LayoutGuideHider()77 ~LayoutGuideHider()
78 {
79 for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
80 {
81 it.key()->item()->setVisible( it.value() );
82 }
83 }
84
85 LayoutGuideHider( const LayoutGuideHider &other ) = delete;
86 LayoutGuideHider &operator=( const LayoutGuideHider &other ) = delete;
87
88 private:
89 QgsLayout *mLayout = nullptr;
90 QHash< QgsLayoutGuide *, bool > mPrevVisibility;
91 };
92
93 class LayoutItemHider
94 {
95 public:
LayoutItemHider(const QList<QGraphicsItem * > & items)96 explicit LayoutItemHider( const QList<QGraphicsItem *> &items )
97 {
98 mItemsToIterate.reserve( items.count() );
99 for ( QGraphicsItem *item : items )
100 {
101 const bool isVisible = item->isVisible();
102 mPrevVisibility[item] = isVisible;
103 if ( isVisible )
104 mItemsToIterate.append( item );
105 if ( QgsLayoutItem *layoutItem = dynamic_cast< QgsLayoutItem * >( item ) )
106 layoutItem->setProperty( "wasVisible", isVisible );
107
108 item->hide();
109 }
110 }
111
hideAll()112 void hideAll()
113 {
114 for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
115 {
116 it.key()->hide();
117 }
118 }
119
~LayoutItemHider()120 ~LayoutItemHider()
121 {
122 for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
123 {
124 it.key()->setVisible( it.value() );
125 if ( QgsLayoutItem *layoutItem = dynamic_cast< QgsLayoutItem * >( it.key() ) )
126 layoutItem->setProperty( "wasVisible", QVariant() );
127 }
128 }
129
itemsToIterate() const130 QList< QGraphicsItem * > itemsToIterate() const { return mItemsToIterate; }
131
132 LayoutItemHider( const LayoutItemHider &other ) = delete;
133 LayoutItemHider &operator=( const LayoutItemHider &other ) = delete;
134
135 private:
136
137 QList<QGraphicsItem * > mItemsToIterate;
138 QHash<QGraphicsItem *, bool> mPrevVisibility;
139 };
140
141 ///@endcond PRIVATE
142
QgsLayoutExporter(QgsLayout * layout)143 QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout )
144 : mLayout( layout )
145 {
146
147 }
148
layout() const149 QgsLayout *QgsLayoutExporter::layout() const
150 {
151 return mLayout;
152 }
153
renderPage(QPainter * painter,int page) const154 void QgsLayoutExporter::renderPage( QPainter *painter, int page ) const
155 {
156 if ( !mLayout )
157 return;
158
159 if ( mLayout->pageCollection()->pageCount() <= page || page < 0 )
160 {
161 return;
162 }
163
164 QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( page );
165 if ( !pageItem )
166 {
167 return;
168 }
169
170 LayoutContextPreviewSettingRestorer restorer( mLayout );
171 ( void )restorer;
172
173 QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
174 renderRegion( painter, paperRect );
175 }
176
renderPageToImage(int page,QSize imageSize,double dpi) const177 QImage QgsLayoutExporter::renderPageToImage( int page, QSize imageSize, double dpi ) const
178 {
179 if ( !mLayout )
180 return QImage();
181
182 if ( mLayout->pageCollection()->pageCount() <= page || page < 0 )
183 {
184 return QImage();
185 }
186
187 QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( page );
188 if ( !pageItem )
189 {
190 return QImage();
191 }
192
193 LayoutContextPreviewSettingRestorer restorer( mLayout );
194 ( void )restorer;
195
196 QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
197
198 if ( imageSize.isValid() && ( !qgsDoubleNear( static_cast< double >( imageSize.width() ) / imageSize.height(),
199 paperRect.width() / paperRect.height(), 0.008 ) ) )
200 {
201 // specified image size is wrong aspect ratio for paper rect - so ignore it and just use dpi
202 // this can happen e.g. as a result of data defined page sizes
203 // see https://github.com/qgis/QGIS/issues/26422
204 imageSize = QSize();
205 }
206
207 return renderRegionToImage( paperRect, imageSize, dpi );
208 }
209
210 ///@cond PRIVATE
211 class LayoutItemCacheSettingRestorer
212 {
213 public:
214
LayoutItemCacheSettingRestorer(QgsLayout * layout)215 LayoutItemCacheSettingRestorer( QgsLayout *layout )
216 : mLayout( layout )
217 {
218 const QList< QGraphicsItem * > items = mLayout->items();
219 for ( QGraphicsItem *item : items )
220 {
221 mPrevCacheMode.insert( item, item->cacheMode() );
222 item->setCacheMode( QGraphicsItem::NoCache );
223 }
224 }
225
~LayoutItemCacheSettingRestorer()226 ~LayoutItemCacheSettingRestorer()
227 {
228 for ( auto it = mPrevCacheMode.constBegin(); it != mPrevCacheMode.constEnd(); ++it )
229 {
230 it.key()->setCacheMode( it.value() );
231 }
232 }
233
234 LayoutItemCacheSettingRestorer( const LayoutItemCacheSettingRestorer &other ) = delete;
235 LayoutItemCacheSettingRestorer &operator=( const LayoutItemCacheSettingRestorer &other ) = delete;
236
237 private:
238 QgsLayout *mLayout = nullptr;
239 QHash< QGraphicsItem *, QGraphicsItem::CacheMode > mPrevCacheMode;
240 };
241
242 ///@endcond PRIVATE
243
renderRegion(QPainter * painter,const QRectF & region) const244 void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) const
245 {
246 QPaintDevice *paintDevice = painter->device();
247 if ( !paintDevice || !mLayout )
248 {
249 return;
250 }
251
252 LayoutItemCacheSettingRestorer cacheRestorer( mLayout );
253 ( void )cacheRestorer;
254 LayoutContextPreviewSettingRestorer restorer( mLayout );
255 ( void )restorer;
256 LayoutGuideHider guideHider( mLayout );
257 ( void ) guideHider;
258
259 painter->setRenderHint( QPainter::Antialiasing, mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagAntialiasing );
260
261 mLayout->render( painter, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), region );
262 }
263
renderRegionToImage(const QRectF & region,QSize imageSize,double dpi) const264 QImage QgsLayoutExporter::renderRegionToImage( const QRectF ®ion, QSize imageSize, double dpi ) const
265 {
266 if ( !mLayout )
267 return QImage();
268
269 LayoutContextPreviewSettingRestorer restorer( mLayout );
270 ( void )restorer;
271
272 double resolution = mLayout->renderContext().dpi();
273 double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) );
274 if ( imageSize.isValid() )
275 {
276 //output size in pixels specified, calculate resolution using average of
277 //derived x/y dpi
278 resolution = ( imageSize.width() / region.width()
279 + imageSize.height() / region.height() ) / 2.0 * oneInchInLayoutUnits;
280 }
281 else if ( dpi > 0 )
282 {
283 //dpi overridden by function parameters
284 resolution = dpi;
285 }
286
287 int width = imageSize.isValid() ? imageSize.width()
288 : static_cast< int >( resolution * region.width() / oneInchInLayoutUnits );
289 int height = imageSize.isValid() ? imageSize.height()
290 : static_cast< int >( resolution * region.height() / oneInchInLayoutUnits );
291
292 QImage image( QSize( width, height ), QImage::Format_ARGB32 );
293 if ( !image.isNull() )
294 {
295 image.setDotsPerMeterX( static_cast< int >( std::round( resolution / 25.4 * 1000 ) ) );
296 image.setDotsPerMeterY( static_cast< int>( std::round( resolution / 25.4 * 1000 ) ) );
297 image.fill( Qt::transparent );
298 QPainter imagePainter( &image );
299 renderRegion( &imagePainter, region );
300 if ( !imagePainter.isActive() )
301 return QImage();
302 }
303
304 return image;
305 }
306
307 ///@cond PRIVATE
308 class LayoutContextSettingsRestorer
309 {
310 public:
311
312 Q_NOWARN_DEPRECATED_PUSH
LayoutContextSettingsRestorer(QgsLayout * layout)313 LayoutContextSettingsRestorer( QgsLayout *layout )
314 : mLayout( layout )
315 , mPreviousDpi( layout->renderContext().dpi() )
316 , mPreviousFlags( layout->renderContext().flags() )
317 , mPreviousTextFormat( layout->renderContext().textRenderFormat() )
318 , mPreviousExportLayer( layout->renderContext().currentExportLayer() )
319 , mPreviousSimplifyMethod( layout->renderContext().simplifyMethod() )
320 , mExportThemes( layout->renderContext().exportThemes() )
321 , mPredefinedScales( layout->renderContext().predefinedScales() )
322 {
323 }
324 Q_NOWARN_DEPRECATED_POP
325
~LayoutContextSettingsRestorer()326 ~LayoutContextSettingsRestorer()
327 {
328 mLayout->renderContext().setDpi( mPreviousDpi );
329 mLayout->renderContext().setFlags( mPreviousFlags );
330 mLayout->renderContext().setTextRenderFormat( mPreviousTextFormat );
331 Q_NOWARN_DEPRECATED_PUSH
332 mLayout->renderContext().setCurrentExportLayer( mPreviousExportLayer );
333 Q_NOWARN_DEPRECATED_POP
334 mLayout->renderContext().setSimplifyMethod( mPreviousSimplifyMethod );
335 mLayout->renderContext().setExportThemes( mExportThemes );
336 mLayout->renderContext().setPredefinedScales( mPredefinedScales );
337 }
338
339 LayoutContextSettingsRestorer( const LayoutContextSettingsRestorer &other ) = delete;
340 LayoutContextSettingsRestorer &operator=( const LayoutContextSettingsRestorer &other ) = delete;
341
342 private:
343 QgsLayout *mLayout = nullptr;
344 double mPreviousDpi = 0;
345 QgsLayoutRenderContext::Flags mPreviousFlags = QgsLayoutRenderContext::Flags();
346 QgsRenderContext::TextRenderFormat mPreviousTextFormat = QgsRenderContext::TextFormatAlwaysOutlines;
347 int mPreviousExportLayer = 0;
348 QgsVectorSimplifyMethod mPreviousSimplifyMethod;
349 QStringList mExportThemes;
350 QVector< double > mPredefinedScales;
351
352 };
353 ///@endcond PRIVATE
354
exportToImage(const QString & filePath,const QgsLayoutExporter::ImageExportSettings & s)355 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &s )
356 {
357 if ( !mLayout )
358 return PrintError;
359
360 ImageExportSettings settings = s;
361 if ( settings.dpi <= 0 )
362 settings.dpi = mLayout->renderContext().dpi();
363
364 mErrorFileName.clear();
365
366 int worldFilePageNo = -1;
367 if ( QgsLayoutItemMap *referenceMap = mLayout->referenceMap() )
368 {
369 worldFilePageNo = referenceMap->page();
370 }
371
372 QFileInfo fi( filePath );
373
374 PageExportDetails pageDetails;
375 pageDetails.directory = fi.path();
376 pageDetails.baseName = fi.completeBaseName();
377 pageDetails.extension = fi.suffix();
378
379 LayoutContextPreviewSettingRestorer restorer( mLayout );
380 ( void )restorer;
381 LayoutContextSettingsRestorer dpiRestorer( mLayout );
382 ( void )dpiRestorer;
383 mLayout->renderContext().setDpi( settings.dpi );
384 mLayout->renderContext().setFlags( settings.flags );
385 mLayout->renderContext().setPredefinedScales( settings.predefinedMapScales );
386
387 QList< int > pages;
388 if ( settings.pages.empty() )
389 {
390 for ( int page = 0; page < mLayout->pageCollection()->pageCount(); ++page )
391 pages << page;
392 }
393 else
394 {
395 for ( int page : qgis::as_const( settings.pages ) )
396 {
397 if ( page >= 0 && page < mLayout->pageCollection()->pageCount() )
398 pages << page;
399 }
400 }
401
402 for ( int page : qgis::as_const( pages ) )
403 {
404 if ( !mLayout->pageCollection()->shouldExportPage( page ) )
405 {
406 continue;
407 }
408
409 bool skip = false;
410 QRectF bounds;
411 QImage image = createImage( settings, page, bounds, skip );
412
413 if ( skip )
414 continue; // should skip this page, e.g. null size
415
416 pageDetails.page = page;
417 QString outputFilePath = generateFileName( pageDetails );
418
419 if ( image.isNull() )
420 {
421 mErrorFileName = outputFilePath;
422 return MemoryError;
423 }
424
425 if ( !saveImage( image, outputFilePath, pageDetails.extension, settings.exportMetadata ? mLayout->project() : nullptr ) )
426 {
427 mErrorFileName = outputFilePath;
428 return FileError;
429 }
430
431 const bool shouldGeoreference = ( page == worldFilePageNo );
432 if ( shouldGeoreference )
433 {
434 georeferenceOutputPrivate( outputFilePath, nullptr, bounds, settings.dpi, shouldGeoreference );
435
436 if ( settings.generateWorldFile )
437 {
438 // should generate world file for this page
439 double a, b, c, d, e, f;
440 if ( bounds.isValid() )
441 computeWorldFileParameters( bounds, a, b, c, d, e, f, settings.dpi );
442 else
443 computeWorldFileParameters( a, b, c, d, e, f, settings.dpi );
444
445 QFileInfo fi( outputFilePath );
446 // build the world file name
447 QString outputSuffix = fi.suffix();
448 QString worldFileName = fi.absolutePath() + '/' + fi.completeBaseName() + '.'
449 + outputSuffix.at( 0 ) + outputSuffix.at( fi.suffix().size() - 1 ) + 'w';
450
451 writeWorldFile( worldFileName, a, b, c, d, e, f );
452 }
453 }
454
455 }
456 return Success;
457 }
458
exportToImage(QgsAbstractLayoutIterator * iterator,const QString & baseFilePath,const QString & extension,const QgsLayoutExporter::ImageExportSettings & settings,QString & error,QgsFeedback * feedback)459 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QString &extension, const QgsLayoutExporter::ImageExportSettings &settings, QString &error, QgsFeedback *feedback )
460 {
461 error.clear();
462
463 if ( !iterator->beginRender() )
464 return IteratorError;
465
466 int total = iterator->count();
467 double step = total > 0 ? 100.0 / total : 100.0;
468 int i = 0;
469 while ( iterator->next() )
470 {
471 if ( feedback )
472 {
473 if ( total > 0 )
474 feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) );
475 else
476 feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) );
477 feedback->setProgress( step * i );
478 }
479 if ( feedback && feedback->isCanceled() )
480 {
481 iterator->endRender();
482 return Canceled;
483 }
484
485 QgsLayoutExporter exporter( iterator->layout() );
486 QString filePath = iterator->filePath( baseFilePath, extension );
487 ExportResult result = exporter.exportToImage( filePath, settings );
488 if ( result != Success )
489 {
490 if ( result == FileError )
491 error = QObject::tr( "Cannot write to %1. This file may be open in another application or may be an invalid path." ).arg( QDir::toNativeSeparators( filePath ) );
492 iterator->endRender();
493 return result;
494 }
495 i++;
496 }
497
498 if ( feedback )
499 {
500 feedback->setProgress( 100 );
501 }
502
503 iterator->endRender();
504 return Success;
505 }
506
exportToPdf(const QString & filePath,const QgsLayoutExporter::PdfExportSettings & s)507 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &s )
508 {
509 if ( !mLayout || mLayout->pageCollection()->pageCount() == 0 )
510 return PrintError;
511
512 PdfExportSettings settings = s;
513 if ( settings.dpi <= 0 )
514 settings.dpi = mLayout->renderContext().dpi();
515
516 mErrorFileName.clear();
517
518 LayoutContextPreviewSettingRestorer restorer( mLayout );
519 ( void )restorer;
520 LayoutContextSettingsRestorer contextRestorer( mLayout );
521 ( void )contextRestorer;
522 mLayout->renderContext().setDpi( settings.dpi );
523 mLayout->renderContext().setPredefinedScales( settings.predefinedMapScales );
524
525 if ( settings.simplifyGeometries )
526 {
527 mLayout->renderContext().setSimplifyMethod( createExportSimplifyMethod() );
528 }
529
530 std::unique_ptr< QgsLayoutGeoPdfExporter > geoPdfExporter;
531 if ( settings.writeGeoPdf || settings.exportLayersAsSeperateFiles ) //#spellok
532 geoPdfExporter = qgis::make_unique< QgsLayoutGeoPdfExporter >( mLayout );
533
534 mLayout->renderContext().setFlags( settings.flags );
535
536 // If we are not printing as raster, temporarily disable advanced effects
537 // as QPrinter does not support composition modes and can result
538 // in items missing from the output
539 mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.forceVectorOutput );
540 mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagForceVectorOutput, settings.forceVectorOutput );
541 mLayout->renderContext().setTextRenderFormat( settings.textRenderFormat );
542 mLayout->renderContext().setExportThemes( settings.exportThemes );
543
544 ExportResult result = Success;
545 if ( settings.writeGeoPdf || settings.exportLayersAsSeperateFiles ) //#spellok
546 {
547 mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagRenderLabelsByMapLayer, true );
548
549 // here we need to export layers to individual PDFs
550 PdfExportSettings subSettings = settings;
551 subSettings.writeGeoPdf = false;
552 subSettings.exportLayersAsSeperateFiles = false; //#spellok
553
554 const QList<QGraphicsItem *> items = mLayout->items( Qt::AscendingOrder );
555
556 QList< QgsLayoutGeoPdfExporter::ComponentLayerDetail > pdfComponents;
557
558 const QDir baseDir = settings.exportLayersAsSeperateFiles ? QFileInfo( filePath ).dir() : QDir(); //#spellok
559 const QString baseFileName = settings.exportLayersAsSeperateFiles ? QFileInfo( filePath ).completeBaseName() : QString(); //#spellok
560
561 auto exportFunc = [this, &subSettings, &pdfComponents, &geoPdfExporter, &settings, &baseDir, &baseFileName]( unsigned int layerId, const QgsLayoutItem::ExportLayerDetail & layerDetail )->QgsLayoutExporter::ExportResult
562 {
563 ExportResult layerExportResult = Success;
564 QPrinter printer;
565 QgsLayoutGeoPdfExporter::ComponentLayerDetail component;
566 component.name = layerDetail.name;
567 component.mapLayerId = layerDetail.mapLayerId;
568 component.opacity = layerDetail.opacity;
569 component.compositionMode = layerDetail.compositionMode;
570 component.group = layerDetail.mapTheme;
571 component.sourcePdfPath = settings.writeGeoPdf ? geoPdfExporter->generateTemporaryFilepath( QStringLiteral( "layer_%1.pdf" ).arg( layerId ) ) : baseDir.filePath( QStringLiteral( "%1_%2.pdf" ).arg( baseFileName ).arg( layerId, 4, 10, QChar( '0' ) ) );
572 pdfComponents << component;
573 preparePrintAsPdf( mLayout, printer, component.sourcePdfPath );
574 preparePrint( mLayout, printer, false );
575 QPainter p;
576 if ( !p.begin( &printer ) )
577 {
578 //error beginning print
579 return FileError;
580 }
581
582 layerExportResult = printPrivate( printer, p, false, subSettings.dpi, subSettings.rasterizeWholeImage );
583 p.end();
584 return layerExportResult;
585 };
586 result = handleLayeredExport( items, exportFunc );
587 if ( result != Success )
588 return result;
589
590 if ( settings.writeGeoPdf )
591 {
592 QgsAbstractGeoPdfExporter::ExportDetails details;
593 details.dpi = settings.dpi;
594 // TODO - multipages
595 QgsLayoutSize pageSize = mLayout->pageCollection()->page( 0 )->sizeWithUnits();
596 QgsLayoutSize pageSizeMM = mLayout->renderContext().measurementConverter().convert( pageSize, QgsUnitTypes::LayoutMillimeters );
597 details.pageSizeMm = pageSizeMM.toQSizeF();
598
599 if ( settings.exportMetadata )
600 {
601 // copy layout metadata to GeoPDF export settings
602 details.author = mLayout->project()->metadata().author();
603 details.producer = QStringLiteral( "QGIS %1" ).arg( Qgis::version() );
604 details.creator = QStringLiteral( "QGIS %1" ).arg( Qgis::version() );
605 details.creationDateTime = mLayout->project()->metadata().creationDateTime();
606 details.subject = mLayout->project()->metadata().abstract();
607 details.title = mLayout->project()->metadata().title();
608 details.keywords = mLayout->project()->metadata().keywords();
609 }
610
611 const QList< QgsMapLayer * > layers = mLayout->project()->mapLayers().values();
612 for ( const QgsMapLayer *layer : layers )
613 {
614 details.layerIdToPdfLayerTreeNameMap.insert( layer->id(), layer->name() );
615 }
616
617 if ( settings.appendGeoreference )
618 {
619 // setup georeferencing
620 QList< QgsLayoutItemMap * > maps;
621 mLayout->layoutItems( maps );
622 for ( QgsLayoutItemMap *map : qgis::as_const( maps ) )
623 {
624 QgsAbstractGeoPdfExporter::GeoReferencedSection georef;
625 georef.crs = map->crs();
626
627 const QPointF topLeft = map->mapToScene( QPointF( 0, 0 ) );
628 const QPointF topRight = map->mapToScene( QPointF( map->rect().width(), 0 ) );
629 const QPointF bottomLeft = map->mapToScene( QPointF( 0, map->rect().height() ) );
630 const QPointF bottomRight = map->mapToScene( QPointF( map->rect().width(), map->rect().height() ) );
631 const QgsLayoutPoint topLeftMm = mLayout->convertFromLayoutUnits( topLeft, QgsUnitTypes::LayoutMillimeters );
632 const QgsLayoutPoint topRightMm = mLayout->convertFromLayoutUnits( topRight, QgsUnitTypes::LayoutMillimeters );
633 const QgsLayoutPoint bottomLeftMm = mLayout->convertFromLayoutUnits( bottomLeft, QgsUnitTypes::LayoutMillimeters );
634 const QgsLayoutPoint bottomRightMm = mLayout->convertFromLayoutUnits( bottomRight, QgsUnitTypes::LayoutMillimeters );
635
636 georef.pageBoundsPolygon.setExteriorRing( new QgsLineString( QVector< QgsPointXY >() << QgsPointXY( topLeftMm.x(), topLeftMm.y() )
637 << QgsPointXY( topRightMm.x(), topRightMm.y() )
638 << QgsPointXY( bottomRightMm.x(), bottomRightMm.y() )
639 << QgsPointXY( bottomLeftMm.x(), bottomLeftMm.y() )
640 << QgsPointXY( topLeftMm.x(), topLeftMm.y() ) ) );
641
642 georef.controlPoints.reserve( 4 );
643 const QTransform t = map->layoutToMapCoordsTransform();
644 const QgsPointXY topLeftMap = t.map( topLeft );
645 const QgsPointXY topRightMap = t.map( topRight );
646 const QgsPointXY bottomLeftMap = t.map( bottomLeft );
647 const QgsPointXY bottomRightMap = t.map( bottomRight );
648
649 georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( topLeftMm.x(), topLeftMm.y() ), topLeftMap );
650 georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( topRightMm.x(), topRightMm.y() ), topRightMap );
651 georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( bottomLeftMm.x(), bottomLeftMm.y() ), bottomLeftMap );
652 georef.controlPoints << QgsAbstractGeoPdfExporter::ControlPoint( QgsPointXY( bottomRightMm.x(), bottomRightMm.y() ), bottomRightMap );
653 details.georeferencedSections << georef;
654 }
655 }
656
657 details.customLayerTreeGroups = geoPdfExporter->customLayerTreeGroups();
658 details.initialLayerVisibility = geoPdfExporter->initialLayerVisibility();
659 details.layerOrder = geoPdfExporter->layerOrder();
660 details.includeFeatures = settings.includeGeoPdfFeatures;
661 details.useOgcBestPracticeFormatGeoreferencing = settings.useOgcBestPracticeFormatGeoreferencing;
662 details.useIso32000ExtensionFormatGeoreferencing = settings.useIso32000ExtensionFormatGeoreferencing;
663
664 if ( !geoPdfExporter->finalize( pdfComponents, filePath, details ) )
665 result = PrintError;
666 }
667 else
668 {
669 result = Success;
670 }
671 }
672 else
673 {
674 QPrinter printer;
675 preparePrintAsPdf( mLayout, printer, filePath );
676 preparePrint( mLayout, printer, false );
677 QPainter p;
678 if ( !p.begin( &printer ) )
679 {
680 //error beginning print
681 return FileError;
682 }
683
684 result = printPrivate( printer, p, false, settings.dpi, settings.rasterizeWholeImage );
685 p.end();
686
687 bool shouldAppendGeoreference = settings.appendGeoreference && mLayout && mLayout->referenceMap() && mLayout->referenceMap()->page() == 0;
688 if ( settings.appendGeoreference || settings.exportMetadata )
689 {
690 georeferenceOutputPrivate( filePath, nullptr, QRectF(), settings.dpi, shouldAppendGeoreference, settings.exportMetadata );
691 }
692 }
693 return result;
694 }
695
exportToPdf(QgsAbstractLayoutIterator * iterator,const QString & fileName,const QgsLayoutExporter::PdfExportSettings & s,QString & error,QgsFeedback * feedback)696 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( QgsAbstractLayoutIterator *iterator, const QString &fileName, const QgsLayoutExporter::PdfExportSettings &s, QString &error, QgsFeedback *feedback )
697 {
698 error.clear();
699
700 if ( !iterator->beginRender() )
701 return IteratorError;
702
703 PdfExportSettings settings = s;
704
705 QPrinter printer;
706 QPainter p;
707
708 int total = iterator->count();
709 double step = total > 0 ? 100.0 / total : 100.0;
710 int i = 0;
711 bool first = true;
712 while ( iterator->next() )
713 {
714 if ( feedback )
715 {
716 if ( total > 0 )
717 feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) );
718 else
719 feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ) );
720 feedback->setProgress( step * i );
721 }
722 if ( feedback && feedback->isCanceled() )
723 {
724 iterator->endRender();
725 return Canceled;
726 }
727
728 if ( s.dpi <= 0 )
729 settings.dpi = iterator->layout()->renderContext().dpi();
730
731 LayoutContextPreviewSettingRestorer restorer( iterator->layout() );
732 ( void )restorer;
733 LayoutContextSettingsRestorer contextRestorer( iterator->layout() );
734 ( void )contextRestorer;
735 iterator->layout()->renderContext().setDpi( settings.dpi );
736
737 iterator->layout()->renderContext().setFlags( settings.flags );
738 iterator->layout()->renderContext().setPredefinedScales( settings.predefinedMapScales );
739
740 if ( settings.simplifyGeometries )
741 {
742 iterator->layout()->renderContext().setSimplifyMethod( createExportSimplifyMethod() );
743 }
744
745 // If we are not printing as raster, temporarily disable advanced effects
746 // as QPrinter does not support composition modes and can result
747 // in items missing from the output
748 iterator->layout()->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.forceVectorOutput );
749
750 iterator->layout()->renderContext().setFlag( QgsLayoutRenderContext::FlagForceVectorOutput, settings.forceVectorOutput );
751
752 iterator->layout()->renderContext().setTextRenderFormat( settings.textRenderFormat );
753
754 if ( first )
755 {
756 preparePrintAsPdf( iterator->layout(), printer, fileName );
757 preparePrint( iterator->layout(), printer, false );
758
759 if ( !p.begin( &printer ) )
760 {
761 //error beginning print
762 return PrintError;
763 }
764 }
765
766 QgsLayoutExporter exporter( iterator->layout() );
767
768 ExportResult result = exporter.printPrivate( printer, p, !first, settings.dpi, settings.rasterizeWholeImage );
769 if ( result != Success )
770 {
771 if ( result == FileError )
772 error = QObject::tr( "Cannot write to %1. This file may be open in another application or may be an invalid path." ).arg( QDir::toNativeSeparators( fileName ) );
773 iterator->endRender();
774 return result;
775 }
776 first = false;
777 i++;
778 }
779
780 if ( feedback )
781 {
782 feedback->setProgress( 100 );
783 }
784
785 iterator->endRender();
786 return Success;
787 }
788
exportToPdfs(QgsAbstractLayoutIterator * iterator,const QString & baseFilePath,const QgsLayoutExporter::PdfExportSettings & settings,QString & error,QgsFeedback * feedback)789 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdfs( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QgsLayoutExporter::PdfExportSettings &settings, QString &error, QgsFeedback *feedback )
790 {
791 error.clear();
792
793 if ( !iterator->beginRender() )
794 return IteratorError;
795
796 int total = iterator->count();
797 double step = total > 0 ? 100.0 / total : 100.0;
798 int i = 0;
799 while ( iterator->next() )
800 {
801 if ( feedback )
802 {
803 if ( total > 0 )
804 feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) );
805 else
806 feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) );
807 feedback->setProgress( step * i );
808 }
809 if ( feedback && feedback->isCanceled() )
810 {
811 iterator->endRender();
812 return Canceled;
813 }
814
815 QString filePath = iterator->filePath( baseFilePath, QStringLiteral( "pdf" ) );
816
817 QgsLayoutExporter exporter( iterator->layout() );
818 ExportResult result = exporter.exportToPdf( filePath, settings );
819 if ( result != Success )
820 {
821 if ( result == FileError )
822 error = QObject::tr( "Cannot write to %1. This file may be open in another application or may be an invalid path." ).arg( QDir::toNativeSeparators( filePath ) );
823 iterator->endRender();
824 return result;
825 }
826 i++;
827 }
828
829 if ( feedback )
830 {
831 feedback->setProgress( 100 );
832 }
833
834 iterator->endRender();
835 return Success;
836 }
837
print(QPrinter & printer,const QgsLayoutExporter::PrintExportSettings & s)838 QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QPrinter &printer, const QgsLayoutExporter::PrintExportSettings &s )
839 {
840 if ( !mLayout )
841 return PrintError;
842
843 QgsLayoutExporter::PrintExportSettings settings = s;
844 if ( settings.dpi <= 0 )
845 settings.dpi = mLayout->renderContext().dpi();
846
847 mErrorFileName.clear();
848
849 LayoutContextPreviewSettingRestorer restorer( mLayout );
850 ( void )restorer;
851 LayoutContextSettingsRestorer contextRestorer( mLayout );
852 ( void )contextRestorer;
853 mLayout->renderContext().setDpi( settings.dpi );
854
855 mLayout->renderContext().setFlags( settings.flags );
856 mLayout->renderContext().setPredefinedScales( settings.predefinedMapScales );
857 // If we are not printing as raster, temporarily disable advanced effects
858 // as QPrinter does not support composition modes and can result
859 // in items missing from the output
860 mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.rasterizeWholeImage );
861
862 preparePrint( mLayout, printer, true );
863 QPainter p;
864 if ( !p.begin( &printer ) )
865 {
866 //error beginning print
867 return PrintError;
868 }
869
870 ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasterizeWholeImage );
871 p.end();
872
873 return result;
874 }
875
print(QgsAbstractLayoutIterator * iterator,QPrinter & printer,const QgsLayoutExporter::PrintExportSettings & s,QString & error,QgsFeedback * feedback)876 QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QgsAbstractLayoutIterator *iterator, QPrinter &printer, const QgsLayoutExporter::PrintExportSettings &s, QString &error, QgsFeedback *feedback )
877 {
878 error.clear();
879
880 if ( !iterator->beginRender() )
881 return IteratorError;
882
883 PrintExportSettings settings = s;
884
885 QPainter p;
886
887 int total = iterator->count();
888 double step = total > 0 ? 100.0 / total : 100.0;
889 int i = 0;
890 bool first = true;
891 while ( iterator->next() )
892 {
893 if ( feedback )
894 {
895 if ( total > 0 )
896 feedback->setProperty( "progress", QObject::tr( "Printing %1 of %2" ).arg( i + 1 ).arg( total ) );
897 else
898 feedback->setProperty( "progress", QObject::tr( "Printing section %1" ).arg( i + 1 ).arg( total ) );
899 feedback->setProgress( step * i );
900 }
901 if ( feedback && feedback->isCanceled() )
902 {
903 iterator->endRender();
904 return Canceled;
905 }
906
907 if ( s.dpi <= 0 )
908 settings.dpi = iterator->layout()->renderContext().dpi();
909
910 LayoutContextPreviewSettingRestorer restorer( iterator->layout() );
911 ( void )restorer;
912 LayoutContextSettingsRestorer contextRestorer( iterator->layout() );
913 ( void )contextRestorer;
914 iterator->layout()->renderContext().setDpi( settings.dpi );
915
916 iterator->layout()->renderContext().setFlags( settings.flags );
917 iterator->layout()->renderContext().setPredefinedScales( settings.predefinedMapScales );
918
919 // If we are not printing as raster, temporarily disable advanced effects
920 // as QPrinter does not support composition modes and can result
921 // in items missing from the output
922 iterator->layout()->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.rasterizeWholeImage );
923
924 if ( first )
925 {
926 preparePrint( iterator->layout(), printer, true );
927
928 if ( !p.begin( &printer ) )
929 {
930 //error beginning print
931 return PrintError;
932 }
933 }
934
935 QgsLayoutExporter exporter( iterator->layout() );
936
937 ExportResult result = exporter.printPrivate( printer, p, !first, settings.dpi, settings.rasterizeWholeImage );
938 if ( result != Success )
939 {
940 iterator->endRender();
941 return result;
942 }
943 first = false;
944 i++;
945 }
946
947 if ( feedback )
948 {
949 feedback->setProgress( 100 );
950 }
951
952 iterator->endRender();
953 return Success;
954 }
955
exportToSvg(const QString & filePath,const QgsLayoutExporter::SvgExportSettings & s)956 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &s )
957 {
958 if ( !mLayout )
959 return PrintError;
960
961 SvgExportSettings settings = s;
962 if ( settings.dpi <= 0 )
963 settings.dpi = mLayout->renderContext().dpi();
964
965 mErrorFileName.clear();
966
967 LayoutContextPreviewSettingRestorer restorer( mLayout );
968 ( void )restorer;
969 LayoutContextSettingsRestorer contextRestorer( mLayout );
970 ( void )contextRestorer;
971 mLayout->renderContext().setDpi( settings.dpi );
972
973 mLayout->renderContext().setFlags( settings.flags );
974 mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagForceVectorOutput, settings.forceVectorOutput );
975 mLayout->renderContext().setTextRenderFormat( s.textRenderFormat );
976 mLayout->renderContext().setPredefinedScales( settings.predefinedMapScales );
977
978 if ( settings.simplifyGeometries )
979 {
980 mLayout->renderContext().setSimplifyMethod( createExportSimplifyMethod() );
981 }
982
983 QFileInfo fi( filePath );
984 PageExportDetails pageDetails;
985 pageDetails.directory = fi.path();
986 pageDetails.baseName = fi.baseName();
987 pageDetails.extension = fi.completeSuffix();
988
989 double inchesToLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) );
990
991 for ( int i = 0; i < mLayout->pageCollection()->pageCount(); ++i )
992 {
993 if ( !mLayout->pageCollection()->shouldExportPage( i ) )
994 {
995 continue;
996 }
997
998 pageDetails.page = i;
999 QString fileName = generateFileName( pageDetails );
1000
1001 QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( i );
1002 QRectF bounds;
1003 if ( settings.cropToContents )
1004 {
1005 if ( mLayout->pageCollection()->pageCount() == 1 )
1006 {
1007 // single page, so include everything
1008 bounds = mLayout->layoutBounds( true );
1009 }
1010 else
1011 {
1012 // multi page, so just clip to items on current page
1013 bounds = mLayout->pageItemBounds( i, true );
1014 }
1015 bounds = bounds.adjusted( -settings.cropMargins.left(),
1016 -settings.cropMargins.top(),
1017 settings.cropMargins.right(),
1018 settings.cropMargins.bottom() );
1019 }
1020 else
1021 {
1022 bounds = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
1023 }
1024
1025 //width in pixel
1026 int width = static_cast< int >( bounds.width() * settings.dpi / inchesToLayoutUnits );
1027 //height in pixel
1028 int height = static_cast< int >( bounds.height() * settings.dpi / inchesToLayoutUnits );
1029 if ( width == 0 || height == 0 )
1030 {
1031 //invalid size, skip this page
1032 continue;
1033 }
1034
1035 if ( settings.exportAsLayers )
1036 {
1037 mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagRenderLabelsByMapLayer, settings.exportLabelsToSeparateLayers );
1038 const QRectF paperRect = QRectF( pageItem->pos().x(),
1039 pageItem->pos().y(),
1040 pageItem->rect().width(),
1041 pageItem->rect().height() );
1042 QDomDocument svg;
1043 QDomNode svgDocRoot;
1044 const QList<QGraphicsItem *> items = mLayout->items( paperRect,
1045 Qt::IntersectsItemBoundingRect,
1046 Qt::AscendingOrder );
1047
1048 auto exportFunc = [this, &settings, width, height, i, bounds, fileName, &svg, &svgDocRoot]( unsigned int layerId, const QgsLayoutItem::ExportLayerDetail & layerDetail )->QgsLayoutExporter::ExportResult
1049 {
1050 return renderToLayeredSvg( settings, width, height, i, bounds, fileName, layerId, layerDetail.name, svg, svgDocRoot, settings.exportMetadata );
1051 };
1052 ExportResult res = handleLayeredExport( items, exportFunc );
1053 if ( res != Success )
1054 return res;
1055
1056 if ( settings.exportMetadata )
1057 appendMetadataToSvg( svg );
1058
1059 QFile out( fileName );
1060 bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
1061 if ( !openOk )
1062 {
1063 mErrorFileName = fileName;
1064 return FileError;
1065 }
1066
1067 out.write( svg.toByteArray() );
1068 }
1069 else
1070 {
1071 QBuffer svgBuffer;
1072 {
1073 QSvgGenerator generator;
1074 if ( settings.exportMetadata )
1075 {
1076 generator.setTitle( mLayout->project()->metadata().title() );
1077 generator.setDescription( mLayout->project()->metadata().abstract() );
1078 }
1079 generator.setOutputDevice( &svgBuffer );
1080 generator.setSize( QSize( width, height ) );
1081 generator.setViewBox( QRect( 0, 0, width, height ) );
1082 generator.setResolution( static_cast< int >( std::round( settings.dpi ) ) );
1083
1084 QPainter p;
1085 bool createOk = p.begin( &generator );
1086 if ( !createOk )
1087 {
1088 mErrorFileName = fileName;
1089 return FileError;
1090 }
1091
1092 if ( settings.cropToContents )
1093 renderRegion( &p, bounds );
1094 else
1095 renderPage( &p, i );
1096
1097 p.end();
1098 }
1099 {
1100 svgBuffer.close();
1101 svgBuffer.open( QIODevice::ReadOnly );
1102 QDomDocument svg;
1103 QString errorMsg;
1104 int errorLine;
1105 if ( ! svg.setContent( &svgBuffer, false, &errorMsg, &errorLine ) )
1106 {
1107 mErrorFileName = fileName;
1108 return SvgLayerError;
1109 }
1110
1111 if ( settings.exportMetadata )
1112 appendMetadataToSvg( svg );
1113
1114 QFile out( fileName );
1115 bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
1116 if ( !openOk )
1117 {
1118 mErrorFileName = fileName;
1119 return FileError;
1120 }
1121
1122 out.write( svg.toByteArray() );
1123 }
1124 }
1125 }
1126
1127 return Success;
1128 }
1129
exportToSvg(QgsAbstractLayoutIterator * iterator,const QString & baseFilePath,const QgsLayoutExporter::SvgExportSettings & settings,QString & error,QgsFeedback * feedback)1130 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QgsLayoutExporter::SvgExportSettings &settings, QString &error, QgsFeedback *feedback )
1131 {
1132 error.clear();
1133
1134 if ( !iterator->beginRender() )
1135 return IteratorError;
1136
1137 int total = iterator->count();
1138 double step = total > 0 ? 100.0 / total : 100.0;
1139 int i = 0;
1140 while ( iterator->next() )
1141 {
1142 if ( feedback )
1143 {
1144 if ( total > 0 )
1145 feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) );
1146 else
1147 feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) );
1148
1149 feedback->setProgress( step * i );
1150 }
1151 if ( feedback && feedback->isCanceled() )
1152 {
1153 iterator->endRender();
1154 return Canceled;
1155 }
1156
1157 QString filePath = iterator->filePath( baseFilePath, QStringLiteral( "svg" ) );
1158
1159 QgsLayoutExporter exporter( iterator->layout() );
1160 ExportResult result = exporter.exportToSvg( filePath, settings );
1161 if ( result != Success )
1162 {
1163 if ( result == FileError )
1164 error = QObject::tr( "Cannot write to %1. This file may be open in another application or may be an invalid path." ).arg( QDir::toNativeSeparators( filePath ) );
1165 iterator->endRender();
1166 return result;
1167 }
1168 i++;
1169 }
1170
1171 if ( feedback )
1172 {
1173 feedback->setProgress( 100 );
1174 }
1175
1176 iterator->endRender();
1177 return Success;
1178
1179 }
1180
preparePrintAsPdf(QgsLayout * layout,QPrinter & printer,const QString & filePath)1181 void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPrinter &printer, const QString &filePath )
1182 {
1183 printer.setOutputFileName( filePath );
1184 printer.setOutputFormat( QPrinter::PdfFormat );
1185
1186 updatePrinterPageSize( layout, printer, firstPageToBeExported( layout ) );
1187
1188 // TODO: add option for this in layout
1189 // May not work on Windows or non-X11 Linux. Works fine on Mac using QPrinter::NativeFormat
1190 //printer.setFontEmbeddingEnabled( true );
1191
1192 QgsPaintEngineHack::fixEngineFlags( printer.paintEngine() );
1193 }
1194
preparePrint(QgsLayout * layout,QPrinter & printer,bool setFirstPageSize)1195 void QgsLayoutExporter::preparePrint( QgsLayout *layout, QPrinter &printer, bool setFirstPageSize )
1196 {
1197 printer.setFullPage( true );
1198 printer.setColorMode( QPrinter::Color );
1199
1200 //set user-defined resolution
1201 printer.setResolution( static_cast< int>( std::round( layout->renderContext().dpi() ) ) );
1202
1203 if ( setFirstPageSize )
1204 {
1205 updatePrinterPageSize( layout, printer, firstPageToBeExported( layout ) );
1206 }
1207 }
1208
print(QPrinter & printer)1209 QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QPrinter &printer )
1210 {
1211 if ( mLayout->pageCollection()->pageCount() == 0 )
1212 return PrintError;
1213
1214 preparePrint( mLayout, printer, true );
1215 QPainter p;
1216 if ( !p.begin( &printer ) )
1217 {
1218 //error beginning print
1219 return PrintError;
1220 }
1221
1222 printPrivate( printer, p );
1223 p.end();
1224 return Success;
1225 }
1226
printPrivate(QPrinter & printer,QPainter & painter,bool startNewPage,double dpi,bool rasterize)1227 QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage, double dpi, bool rasterize )
1228 {
1229 //layout starts page numbering at 0
1230 int fromPage = ( printer.fromPage() < 1 ) ? 0 : printer.fromPage() - 1;
1231 int toPage = ( printer.toPage() < 1 ) ? mLayout->pageCollection()->pageCount() - 1 : printer.toPage() - 1;
1232
1233 bool pageExported = false;
1234 if ( rasterize )
1235 {
1236 for ( int i = fromPage; i <= toPage; ++i )
1237 {
1238 if ( !mLayout->pageCollection()->shouldExportPage( i ) )
1239 {
1240 continue;
1241 }
1242
1243 updatePrinterPageSize( mLayout, printer, i );
1244 if ( ( pageExported && i > fromPage ) || startNewPage )
1245 {
1246 printer.newPage();
1247 }
1248
1249 QImage image = renderPageToImage( i, QSize(), dpi );
1250 if ( !image.isNull() )
1251 {
1252 QRectF targetArea( 0, 0, image.width(), image.height() );
1253 painter.drawImage( targetArea, image, targetArea );
1254 }
1255 else
1256 {
1257 return MemoryError;
1258 }
1259 pageExported = true;
1260 }
1261 }
1262 else
1263 {
1264 for ( int i = fromPage; i <= toPage; ++i )
1265 {
1266 if ( !mLayout->pageCollection()->shouldExportPage( i ) )
1267 {
1268 continue;
1269 }
1270
1271 updatePrinterPageSize( mLayout, printer, i );
1272
1273 if ( ( pageExported && i > fromPage ) || startNewPage )
1274 {
1275 printer.newPage();
1276 }
1277 renderPage( &painter, i );
1278 pageExported = true;
1279 }
1280 }
1281 return Success;
1282 }
1283
updatePrinterPageSize(QgsLayout * layout,QPrinter & printer,int page)1284 void QgsLayoutExporter::updatePrinterPageSize( QgsLayout *layout, QPrinter &printer, int page )
1285 {
1286 QgsLayoutSize pageSize = layout->pageCollection()->page( page )->sizeWithUnits();
1287 QgsLayoutSize pageSizeMM = layout->renderContext().measurementConverter().convert( pageSize, QgsUnitTypes::LayoutMillimeters );
1288
1289 QPageLayout pageLayout( QPageSize( pageSizeMM.toQSizeF(), QPageSize::Millimeter ),
1290 QPageLayout::Portrait,
1291 QMarginsF( 0, 0, 0, 0 ) );
1292 pageLayout.setMode( QPageLayout::FullPageMode );
1293 printer.setPageLayout( pageLayout );
1294 printer.setFullPage( true );
1295 printer.setPageMargins( QMarginsF( 0, 0, 0, 0 ) );
1296 }
1297
renderToLayeredSvg(const SvgExportSettings & settings,double width,double height,int page,const QRectF & bounds,const QString & filename,unsigned int svgLayerId,const QString & layerName,QDomDocument & svg,QDomNode & svgDocRoot,bool includeMetadata) const1298 QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, const QRectF &bounds, const QString &filename, unsigned int svgLayerId, const QString &layerName, QDomDocument &svg, QDomNode &svgDocRoot, bool includeMetadata ) const
1299 {
1300 QBuffer svgBuffer;
1301 {
1302 QSvgGenerator generator;
1303 if ( includeMetadata )
1304 {
1305 if ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( mLayout.data() ) )
1306 generator.setTitle( l->name() );
1307 else if ( mLayout->project() )
1308 generator.setTitle( mLayout->project()->title() );
1309 }
1310
1311 generator.setOutputDevice( &svgBuffer );
1312 generator.setSize( QSize( static_cast< int >( std::round( width ) ),
1313 static_cast< int >( std::round( height ) ) ) );
1314 generator.setViewBox( QRect( 0, 0,
1315 static_cast< int >( std::round( width ) ),
1316 static_cast< int >( std::round( height ) ) ) );
1317 generator.setResolution( static_cast< int >( std::round( settings.dpi ) ) ); //because the rendering is done in mm, convert the dpi
1318
1319 QPainter svgPainter( &generator );
1320 if ( settings.cropToContents )
1321 renderRegion( &svgPainter, bounds );
1322 else
1323 renderPage( &svgPainter, page );
1324 }
1325
1326 // post-process svg output to create groups in a single svg file
1327 // we create inkscape layers since it's nice and clean and free
1328 // and fully svg compatible
1329 {
1330 svgBuffer.close();
1331 svgBuffer.open( QIODevice::ReadOnly );
1332 QDomDocument doc;
1333 QString errorMsg;
1334 int errorLine;
1335 if ( ! doc.setContent( &svgBuffer, false, &errorMsg, &errorLine ) )
1336 {
1337 mErrorFileName = filename;
1338 return SvgLayerError;
1339 }
1340 if ( 1 == svgLayerId )
1341 {
1342 svg = QDomDocument( doc.doctype() );
1343 svg.appendChild( svg.importNode( doc.firstChild(), false ) );
1344 svgDocRoot = svg.importNode( doc.elementsByTagName( QStringLiteral( "svg" ) ).at( 0 ), false );
1345 svgDocRoot.toElement().setAttribute( QStringLiteral( "xmlns:inkscape" ), QStringLiteral( "http://www.inkscape.org/namespaces/inkscape" ) );
1346 svg.appendChild( svgDocRoot );
1347 }
1348 QDomNode mainGroup = svg.importNode( doc.elementsByTagName( QStringLiteral( "g" ) ).at( 0 ), true );
1349 mainGroup.toElement().setAttribute( QStringLiteral( "id" ), layerName );
1350 mainGroup.toElement().setAttribute( QStringLiteral( "inkscape:label" ), layerName );
1351 mainGroup.toElement().setAttribute( QStringLiteral( "inkscape:groupmode" ), QStringLiteral( "layer" ) );
1352 QDomNode defs = svg.importNode( doc.elementsByTagName( QStringLiteral( "defs" ) ).at( 0 ), true );
1353 svgDocRoot.appendChild( defs );
1354 svgDocRoot.appendChild( mainGroup );
1355 }
1356 return Success;
1357 }
1358
appendMetadataToSvg(QDomDocument & svg) const1359 void QgsLayoutExporter::appendMetadataToSvg( QDomDocument &svg ) const
1360 {
1361 const QgsProjectMetadata &metadata = mLayout->project()->metadata();
1362 QDomElement metadataElement = svg.createElement( QStringLiteral( "metadata" ) );
1363 QDomElement rdfElement = svg.createElement( QStringLiteral( "rdf:RDF" ) );
1364 rdfElement.setAttribute( QStringLiteral( "xmlns:rdf" ), QStringLiteral( "http://www.w3.org/1999/02/22-rdf-syntax-ns#" ) );
1365 rdfElement.setAttribute( QStringLiteral( "xmlns:rdfs" ), QStringLiteral( "http://www.w3.org/2000/01/rdf-schema#" ) );
1366 rdfElement.setAttribute( QStringLiteral( "xmlns:dc" ), QStringLiteral( "http://purl.org/dc/elements/1.1/" ) );
1367 QDomElement descriptionElement = svg.createElement( QStringLiteral( "rdf:Description" ) );
1368 QDomElement workElement = svg.createElement( QStringLiteral( "cc:Work" ) );
1369 workElement.setAttribute( QStringLiteral( "rdf:about" ), QString() );
1370
1371 auto addTextNode = [&workElement, &descriptionElement, &svg]( const QString & tag, const QString & value )
1372 {
1373 // inkscape compatible
1374 QDomElement element = svg.createElement( tag );
1375 QDomText t = svg.createTextNode( value );
1376 element.appendChild( t );
1377 workElement.appendChild( element );
1378
1379 // svg spec compatible
1380 descriptionElement.setAttribute( tag, value );
1381 };
1382
1383 addTextNode( QStringLiteral( "dc:format" ), QStringLiteral( "image/svg+xml" ) );
1384 addTextNode( QStringLiteral( "dc:title" ), metadata.title() );
1385 addTextNode( QStringLiteral( "dc:date" ), metadata.creationDateTime().toString( Qt::ISODate ) );
1386 addTextNode( QStringLiteral( "dc:identifier" ), metadata.identifier() );
1387 addTextNode( QStringLiteral( "dc:description" ), metadata.abstract() );
1388
1389 auto addAgentNode = [&workElement, &descriptionElement, &svg]( const QString & tag, const QString & value )
1390 {
1391 // inkscape compatible
1392 QDomElement inkscapeElement = svg.createElement( tag );
1393 QDomElement agentElement = svg.createElement( QStringLiteral( "cc:Agent" ) );
1394 QDomElement titleElement = svg.createElement( QStringLiteral( "dc:title" ) );
1395 QDomText t = svg.createTextNode( value );
1396 titleElement.appendChild( t );
1397 agentElement.appendChild( titleElement );
1398 inkscapeElement.appendChild( agentElement );
1399 workElement.appendChild( inkscapeElement );
1400
1401 // svg spec compatible
1402 QDomElement bagElement = svg.createElement( QStringLiteral( "rdf:Bag" ) );
1403 QDomElement liElement = svg.createElement( QStringLiteral( "rdf:li" ) );
1404 t = svg.createTextNode( value );
1405 liElement.appendChild( t );
1406 bagElement.appendChild( liElement );
1407
1408 QDomElement element = svg.createElement( tag );
1409 element.appendChild( bagElement );
1410 descriptionElement.appendChild( element );
1411 };
1412
1413 addAgentNode( QStringLiteral( "dc:creator" ), metadata.author() );
1414 addAgentNode( QStringLiteral( "dc:publisher" ), QStringLiteral( "QGIS %1" ).arg( Qgis::version() ) );
1415
1416 // keywords
1417 {
1418 QDomElement element = svg.createElement( QStringLiteral( "dc:subject" ) );
1419 QDomElement bagElement = svg.createElement( QStringLiteral( "rdf:Bag" ) );
1420 QgsAbstractMetadataBase::KeywordMap keywords = metadata.keywords();
1421 for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
1422 {
1423 const QStringList words = it.value();
1424 for ( const QString &keyword : words )
1425 {
1426 QDomElement liElement = svg.createElement( QStringLiteral( "rdf:li" ) );
1427 QDomText t = svg.createTextNode( keyword );
1428 liElement.appendChild( t );
1429 bagElement.appendChild( liElement );
1430 }
1431 }
1432 element.appendChild( bagElement );
1433 workElement.appendChild( element );
1434 descriptionElement.appendChild( element );
1435 }
1436
1437 rdfElement.appendChild( descriptionElement );
1438 rdfElement.appendChild( workElement );
1439 metadataElement.appendChild( rdfElement );
1440 svg.documentElement().appendChild( metadataElement );
1441 svg.documentElement().setAttribute( QStringLiteral( "xmlns:cc" ), QStringLiteral( "http://creativecommons.org/ns#" ) );
1442 }
1443
computeGeoTransform(const QgsLayoutItemMap * map,const QRectF & region,double dpi) const1444 std::unique_ptr<double[]> QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF ®ion, double dpi ) const
1445 {
1446 if ( !map )
1447 map = mLayout->referenceMap();
1448
1449 if ( !map )
1450 return nullptr;
1451
1452 if ( dpi < 0 )
1453 dpi = mLayout->renderContext().dpi();
1454
1455 // calculate region of composition to export (in mm)
1456 QRectF exportRegion = region;
1457 if ( !exportRegion.isValid() )
1458 {
1459 int pageNumber = map->page();
1460
1461 QgsLayoutItemPage *page = mLayout->pageCollection()->page( pageNumber );
1462 double pageY = page->pos().y();
1463 QSizeF pageSize = page->rect().size();
1464 exportRegion = QRectF( 0, pageY, pageSize.width(), pageSize.height() );
1465 }
1466
1467 // map rectangle (in mm)
1468 QRectF mapItemSceneRect = map->mapRectToScene( map->rect() );
1469
1470 // destination width/height in mm
1471 double outputHeightMM = exportRegion.height();
1472 double outputWidthMM = exportRegion.width();
1473
1474 // map properties
1475 QgsRectangle mapExtent = map->extent();
1476 double mapXCenter = mapExtent.center().x();
1477 double mapYCenter = mapExtent.center().y();
1478 double alpha = - map->mapRotation() / 180 * M_PI;
1479 double sinAlpha = std::sin( alpha );
1480 double cosAlpha = std::cos( alpha );
1481
1482 // get the extent (in map units) for the exported region
1483 QPointF mapItemPos = map->pos();
1484 //adjust item position so it is relative to export region
1485 mapItemPos.rx() -= exportRegion.left();
1486 mapItemPos.ry() -= exportRegion.top();
1487
1488 // calculate extent of entire page in map units
1489 double xRatio = mapExtent.width() / mapItemSceneRect.width();
1490 double yRatio = mapExtent.height() / mapItemSceneRect.height();
1491 double xmin = mapExtent.xMinimum() - mapItemPos.x() * xRatio;
1492 double ymax = mapExtent.yMaximum() + mapItemPos.y() * yRatio;
1493 QgsRectangle paperExtent( xmin, ymax - outputHeightMM * yRatio, xmin + outputWidthMM * xRatio, ymax );
1494
1495 // calculate origin of page
1496 double X0 = paperExtent.xMinimum();
1497 double Y0 = paperExtent.yMaximum();
1498
1499 if ( !qgsDoubleNear( alpha, 0.0 ) )
1500 {
1501 // translate origin to account for map rotation
1502 double X1 = X0 - mapXCenter;
1503 double Y1 = Y0 - mapYCenter;
1504 double X2 = X1 * cosAlpha + Y1 * sinAlpha;
1505 double Y2 = -X1 * sinAlpha + Y1 * cosAlpha;
1506 X0 = X2 + mapXCenter;
1507 Y0 = Y2 + mapYCenter;
1508 }
1509
1510 // calculate scaling of pixels
1511 int pageWidthPixels = static_cast< int >( dpi * outputWidthMM / 25.4 );
1512 int pageHeightPixels = static_cast< int >( dpi * outputHeightMM / 25.4 );
1513 double pixelWidthScale = paperExtent.width() / pageWidthPixels;
1514 double pixelHeightScale = paperExtent.height() / pageHeightPixels;
1515
1516 // transform matrix
1517 std::unique_ptr<double[]> t( new double[6] );
1518 t[0] = X0;
1519 t[1] = cosAlpha * pixelWidthScale;
1520 t[2] = -sinAlpha * pixelWidthScale;
1521 t[3] = Y0;
1522 t[4] = -sinAlpha * pixelHeightScale;
1523 t[5] = -cosAlpha * pixelHeightScale;
1524
1525 return t;
1526 }
1527
writeWorldFile(const QString & worldFileName,double a,double b,double c,double d,double e,double f) const1528 void QgsLayoutExporter::writeWorldFile( const QString &worldFileName, double a, double b, double c, double d, double e, double f ) const
1529 {
1530 QFile worldFile( worldFileName );
1531 if ( !worldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
1532 {
1533 return;
1534 }
1535 QTextStream fout( &worldFile );
1536
1537 // QString::number does not use locale settings (for the decimal point)
1538 // which is what we want here
1539 fout << QString::number( a, 'f', 12 ) << "\r\n";
1540 fout << QString::number( d, 'f', 12 ) << "\r\n";
1541 fout << QString::number( b, 'f', 12 ) << "\r\n";
1542 fout << QString::number( e, 'f', 12 ) << "\r\n";
1543 fout << QString::number( c, 'f', 12 ) << "\r\n";
1544 fout << QString::number( f, 'f', 12 ) << "\r\n";
1545 }
1546
georeferenceOutput(const QString & file,QgsLayoutItemMap * map,const QRectF & exportRegion,double dpi) const1547 bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMap *map, const QRectF &exportRegion, double dpi ) const
1548 {
1549 return georeferenceOutputPrivate( file, map, exportRegion, dpi, false );
1550 }
1551
georeferenceOutputPrivate(const QString & file,QgsLayoutItemMap * map,const QRectF & exportRegion,double dpi,bool includeGeoreference,bool includeMetadata) const1552 bool QgsLayoutExporter::georeferenceOutputPrivate( const QString &file, QgsLayoutItemMap *map, const QRectF &exportRegion, double dpi, bool includeGeoreference, bool includeMetadata ) const
1553 {
1554 if ( !mLayout )
1555 return false;
1556
1557 if ( !map && includeGeoreference )
1558 map = mLayout->referenceMap();
1559
1560 std::unique_ptr<double[]> t;
1561
1562 if ( map && includeGeoreference )
1563 {
1564 if ( dpi < 0 )
1565 dpi = mLayout->renderContext().dpi();
1566
1567 t = computeGeoTransform( map, exportRegion, dpi );
1568 }
1569
1570 // important - we need to manually specify the DPI in advance, as GDAL will otherwise
1571 // assume a DPI of 150
1572 CPLSetConfigOption( "GDAL_PDF_DPI", QString::number( dpi ).toLocal8Bit().constData() );
1573 gdal::dataset_unique_ptr outputDS( GDALOpen( file.toLocal8Bit().constData(), GA_Update ) );
1574 if ( outputDS )
1575 {
1576 if ( t )
1577 GDALSetGeoTransform( outputDS.get(), t.get() );
1578
1579 if ( includeMetadata )
1580 {
1581 QString creationDateString;
1582 const QDateTime creationDateTime = mLayout->project()->metadata().creationDateTime();
1583 if ( creationDateTime.isValid() )
1584 {
1585 creationDateString = QStringLiteral( "D:%1" ).arg( mLayout->project()->metadata().creationDateTime().toString( QStringLiteral( "yyyyMMddHHmmss" ) ) );
1586 if ( creationDateTime.timeZone().isValid() )
1587 {
1588 int offsetFromUtc = creationDateTime.timeZone().offsetFromUtc( creationDateTime );
1589 creationDateString += ( offsetFromUtc >= 0 ) ? '+' : '-';
1590 offsetFromUtc = std::abs( offsetFromUtc );
1591 int offsetHours = offsetFromUtc / 3600;
1592 int offsetMins = ( offsetFromUtc % 3600 ) / 60;
1593 creationDateString += QStringLiteral( "%1'%2'" ).arg( offsetHours ).arg( offsetMins );
1594 }
1595 }
1596 GDALSetMetadataItem( outputDS.get(), "CREATION_DATE", creationDateString.toUtf8().constData(), nullptr );
1597
1598 GDALSetMetadataItem( outputDS.get(), "AUTHOR", mLayout->project()->metadata().author().toUtf8().constData(), nullptr );
1599 const QString creator = QStringLiteral( "QGIS %1" ).arg( Qgis::version() );
1600 GDALSetMetadataItem( outputDS.get(), "CREATOR", creator.toUtf8().constData(), nullptr );
1601 GDALSetMetadataItem( outputDS.get(), "PRODUCER", creator.toUtf8().constData(), nullptr );
1602 GDALSetMetadataItem( outputDS.get(), "SUBJECT", mLayout->project()->metadata().abstract().toUtf8().constData(), nullptr );
1603 GDALSetMetadataItem( outputDS.get(), "TITLE", mLayout->project()->metadata().title().toUtf8().constData(), nullptr );
1604
1605 const QgsAbstractMetadataBase::KeywordMap keywords = mLayout->project()->metadata().keywords();
1606 QStringList allKeywords;
1607 for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
1608 {
1609 allKeywords.append( QStringLiteral( "%1: %2" ).arg( it.key(), it.value().join( ',' ) ) );
1610 }
1611 const QString keywordString = allKeywords.join( ';' );
1612 GDALSetMetadataItem( outputDS.get(), "KEYWORDS", keywordString.toUtf8().constData(), nullptr );
1613 }
1614
1615 if ( t )
1616 GDALSetProjection( outputDS.get(), map->crs().toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED_GDAL ).toLocal8Bit().constData() );
1617 }
1618 CPLSetConfigOption( "GDAL_PDF_DPI", nullptr );
1619
1620 return true;
1621 }
1622
nameForLayerWithItems(const QList<QGraphicsItem * > & items,unsigned int layerId)1623 QString nameForLayerWithItems( const QList< QGraphicsItem * > &items, unsigned int layerId )
1624 {
1625 if ( items.count() == 1 )
1626 {
1627 if ( QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( items.at( 0 ) ) )
1628 {
1629 QString name = layoutItem->displayName();
1630 // cleanup default item ID format
1631 if ( name.startsWith( '<' ) && name.endsWith( '>' ) )
1632 name = name.mid( 1, name.length() - 2 );
1633 return name;
1634 }
1635 }
1636 else if ( items.count() > 1 )
1637 {
1638 QStringList currentLayerItemTypes;
1639 for ( QGraphicsItem *item : items )
1640 {
1641 if ( QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( item ) )
1642 {
1643 const QString itemType = QgsApplication::layoutItemRegistry()->itemMetadata( layoutItem->type() )->visibleName();
1644 const QString itemTypePlural = QgsApplication::layoutItemRegistry()->itemMetadata( layoutItem->type() )->visiblePluralName();
1645 if ( !currentLayerItemTypes.contains( itemType ) && !currentLayerItemTypes.contains( itemTypePlural ) )
1646 currentLayerItemTypes << itemType;
1647 else if ( currentLayerItemTypes.contains( itemType ) )
1648 {
1649 currentLayerItemTypes.replace( currentLayerItemTypes.indexOf( itemType ), itemTypePlural );
1650 }
1651 }
1652 else
1653 {
1654 if ( !currentLayerItemTypes.contains( QObject::tr( "Other" ) ) )
1655 currentLayerItemTypes.append( QObject::tr( "Other" ) );
1656 }
1657 }
1658 return currentLayerItemTypes.join( QLatin1String( ", " ) );
1659 }
1660 return QObject::tr( "Layer %1" ).arg( layerId );
1661 }
1662
handleLayeredExport(const QList<QGraphicsItem * > & items,const std::function<QgsLayoutExporter::ExportResult (unsigned int,const QgsLayoutItem::ExportLayerDetail &)> & exportFunc)1663 QgsLayoutExporter::ExportResult QgsLayoutExporter::handleLayeredExport( const QList<QGraphicsItem *> &items,
1664 const std::function<QgsLayoutExporter::ExportResult( unsigned int, const QgsLayoutItem::ExportLayerDetail & )> &exportFunc )
1665 {
1666 LayoutItemHider itemHider( items );
1667 ( void )itemHider;
1668
1669 int prevType = -1;
1670 QgsLayoutItem::ExportLayerBehavior prevItemBehavior = QgsLayoutItem::CanGroupWithAnyOtherItem;
1671 unsigned int layerId = 1;
1672 QgsLayoutItem::ExportLayerDetail layerDetails;
1673 itemHider.hideAll();
1674 const QList< QGraphicsItem * > itemsToIterate = itemHider.itemsToIterate();
1675 QList< QGraphicsItem * > currentLayerItems;
1676 for ( QGraphicsItem *item : itemsToIterate )
1677 {
1678 QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( item );
1679
1680 bool canPlaceInExistingLayer = false;
1681 if ( layoutItem )
1682 {
1683 switch ( layoutItem->exportLayerBehavior() )
1684 {
1685 case QgsLayoutItem::CanGroupWithAnyOtherItem:
1686 {
1687 switch ( prevItemBehavior )
1688 {
1689 case QgsLayoutItem::CanGroupWithAnyOtherItem:
1690 canPlaceInExistingLayer = true;
1691 break;
1692
1693 case QgsLayoutItem::CanGroupWithItemsOfSameType:
1694 canPlaceInExistingLayer = prevType == -1 || prevType == layoutItem->type();
1695 break;
1696
1697 case QgsLayoutItem::MustPlaceInOwnLayer:
1698 case QgsLayoutItem::ItemContainsSubLayers:
1699 canPlaceInExistingLayer = false;
1700 break;
1701 }
1702 break;
1703 }
1704
1705 case QgsLayoutItem::CanGroupWithItemsOfSameType:
1706 {
1707 switch ( prevItemBehavior )
1708 {
1709 case QgsLayoutItem::CanGroupWithAnyOtherItem:
1710 case QgsLayoutItem::CanGroupWithItemsOfSameType:
1711 canPlaceInExistingLayer = prevType == -1 || prevType == layoutItem->type();
1712 break;
1713
1714 case QgsLayoutItem::MustPlaceInOwnLayer:
1715 case QgsLayoutItem::ItemContainsSubLayers:
1716 canPlaceInExistingLayer = false;
1717 break;
1718 }
1719 break;
1720 }
1721
1722 case QgsLayoutItem::MustPlaceInOwnLayer:
1723 {
1724 canPlaceInExistingLayer = false;
1725 break;
1726 }
1727
1728 case QgsLayoutItem::ItemContainsSubLayers:
1729 canPlaceInExistingLayer = false;
1730 break;
1731 }
1732 prevItemBehavior = layoutItem->exportLayerBehavior();
1733 prevType = layoutItem->type();
1734 }
1735 else
1736 {
1737 prevItemBehavior = QgsLayoutItem::MustPlaceInOwnLayer;
1738 }
1739
1740 if ( canPlaceInExistingLayer )
1741 {
1742 currentLayerItems << item;
1743 item->show();
1744 }
1745 else
1746 {
1747 if ( !currentLayerItems.isEmpty() )
1748 {
1749 layerDetails.name = nameForLayerWithItems( currentLayerItems, layerId );
1750
1751 ExportResult result = exportFunc( layerId, layerDetails );
1752 if ( result != Success )
1753 return result;
1754 layerId++;
1755 currentLayerItems.clear();
1756 }
1757
1758 itemHider.hideAll();
1759 item->show();
1760
1761 if ( layoutItem && layoutItem->exportLayerBehavior() == QgsLayoutItem::ItemContainsSubLayers )
1762 {
1763 int layoutItemLayerIdx = 0;
1764 Q_NOWARN_DEPRECATED_PUSH
1765 mLayout->renderContext().setCurrentExportLayer( layoutItemLayerIdx );
1766 Q_NOWARN_DEPRECATED_POP
1767 layoutItem->startLayeredExport();
1768 while ( layoutItem->nextExportPart() )
1769 {
1770 Q_NOWARN_DEPRECATED_PUSH
1771 mLayout->renderContext().setCurrentExportLayer( layoutItemLayerIdx );
1772 Q_NOWARN_DEPRECATED_POP
1773
1774 layerDetails = layoutItem->exportLayerDetails();
1775 ExportResult result = exportFunc( layerId, layerDetails );
1776 if ( result != Success )
1777 return result;
1778 layerId++;
1779
1780 layoutItemLayerIdx++;
1781 }
1782 layerDetails.mapLayerId.clear();
1783 Q_NOWARN_DEPRECATED_PUSH
1784 mLayout->renderContext().setCurrentExportLayer( -1 );
1785 Q_NOWARN_DEPRECATED_POP
1786 layoutItem->stopLayeredExport();
1787 currentLayerItems.clear();
1788 }
1789 else
1790 {
1791 currentLayerItems << item;
1792 }
1793 }
1794 }
1795 if ( !currentLayerItems.isEmpty() )
1796 {
1797 layerDetails.name = nameForLayerWithItems( currentLayerItems, layerId );
1798 ExportResult result = exportFunc( layerId, layerDetails );
1799 if ( result != Success )
1800 return result;
1801 }
1802 return Success;
1803 }
1804
createExportSimplifyMethod()1805 QgsVectorSimplifyMethod QgsLayoutExporter::createExportSimplifyMethod()
1806 {
1807 QgsVectorSimplifyMethod simplifyMethod;
1808 simplifyMethod.setSimplifyHints( QgsVectorSimplifyMethod::GeometrySimplification );
1809 simplifyMethod.setForceLocalOptimization( true );
1810 // we use SnappedToGridGlobal, because it avoids gaps and slivers between previously adjacent polygons
1811 simplifyMethod.setSimplifyAlgorithm( QgsVectorSimplifyMethod::SnappedToGridGlobal );
1812 simplifyMethod.setThreshold( 0.1f ); // (pixels). We are quite conservative here. This could possibly be bumped all the way up to 1. But let's play it safe.
1813 return simplifyMethod;
1814 }
1815
computeWorldFileParameters(double & a,double & b,double & c,double & d,double & e,double & f,double dpi) const1816 void QgsLayoutExporter::computeWorldFileParameters( double &a, double &b, double &c, double &d, double &e, double &f, double dpi ) const
1817 {
1818 if ( !mLayout )
1819 return;
1820
1821 QgsLayoutItemMap *map = mLayout->referenceMap();
1822 if ( !map )
1823 {
1824 return;
1825 }
1826
1827 int pageNumber = map->page();
1828 QgsLayoutItemPage *page = mLayout->pageCollection()->page( pageNumber );
1829 double pageY = page->pos().y();
1830 QSizeF pageSize = page->rect().size();
1831 QRectF pageRect( 0, pageY, pageSize.width(), pageSize.height() );
1832 computeWorldFileParameters( pageRect, a, b, c, d, e, f, dpi );
1833 }
1834
computeWorldFileParameters(const QRectF & exportRegion,double & a,double & b,double & c,double & d,double & e,double & f,double dpi) const1835 void QgsLayoutExporter::computeWorldFileParameters( const QRectF &exportRegion, double &a, double &b, double &c, double &d, double &e, double &f, double dpi ) const
1836 {
1837 if ( !mLayout )
1838 return;
1839
1840 // World file parameters : affine transformation parameters from pixel coordinates to map coordinates
1841 QgsLayoutItemMap *map = mLayout->referenceMap();
1842 if ( !map )
1843 {
1844 return;
1845 }
1846
1847 double destinationHeight = exportRegion.height();
1848 double destinationWidth = exportRegion.width();
1849
1850 QRectF mapItemSceneRect = map->mapRectToScene( map->rect() );
1851 QgsRectangle mapExtent = map->extent();
1852
1853 double alpha = map->mapRotation() / 180 * M_PI;
1854
1855 double xRatio = mapExtent.width() / mapItemSceneRect.width();
1856 double yRatio = mapExtent.height() / mapItemSceneRect.height();
1857
1858 double xCenter = mapExtent.center().x();
1859 double yCenter = mapExtent.center().y();
1860
1861 // get the extent (in map units) for the region
1862 QPointF mapItemPos = map->pos();
1863 //adjust item position so it is relative to export region
1864 mapItemPos.rx() -= exportRegion.left();
1865 mapItemPos.ry() -= exportRegion.top();
1866
1867 double xmin = mapExtent.xMinimum() - mapItemPos.x() * xRatio;
1868 double ymax = mapExtent.yMaximum() + mapItemPos.y() * yRatio;
1869 QgsRectangle paperExtent( xmin, ymax - destinationHeight * yRatio, xmin + destinationWidth * xRatio, ymax );
1870
1871 double X0 = paperExtent.xMinimum();
1872 double Y0 = paperExtent.yMinimum();
1873
1874 if ( dpi < 0 )
1875 dpi = mLayout->renderContext().dpi();
1876
1877 int widthPx = static_cast< int >( dpi * destinationWidth / 25.4 );
1878 int heightPx = static_cast< int >( dpi * destinationHeight / 25.4 );
1879
1880 double Ww = paperExtent.width() / widthPx;
1881 double Hh = paperExtent.height() / heightPx;
1882
1883 // scaling matrix
1884 double s[6];
1885 s[0] = Ww;
1886 s[1] = 0;
1887 s[2] = X0;
1888 s[3] = 0;
1889 s[4] = -Hh;
1890 s[5] = Y0 + paperExtent.height();
1891
1892 // rotation matrix
1893 double r[6];
1894 r[0] = std::cos( alpha );
1895 r[1] = -std::sin( alpha );
1896 r[2] = xCenter * ( 1 - std::cos( alpha ) ) + yCenter * std::sin( alpha );
1897 r[3] = std::sin( alpha );
1898 r[4] = std::cos( alpha );
1899 r[5] = - xCenter * std::sin( alpha ) + yCenter * ( 1 - std::cos( alpha ) );
1900
1901 // result = rotation x scaling = rotation(scaling(X))
1902 a = r[0] * s[0] + r[1] * s[3];
1903 b = r[0] * s[1] + r[1] * s[4];
1904 c = r[0] * s[2] + r[1] * s[5] + r[2];
1905 d = r[3] * s[0] + r[4] * s[3];
1906 e = r[3] * s[1] + r[4] * s[4];
1907 f = r[3] * s[2] + r[4] * s[5] + r[5];
1908 }
1909
createImage(const QgsLayoutExporter::ImageExportSettings & settings,int page,QRectF & bounds,bool & skipPage) const1910 QImage QgsLayoutExporter::createImage( const QgsLayoutExporter::ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const
1911 {
1912 bounds = QRectF();
1913 skipPage = false;
1914
1915 if ( settings.cropToContents )
1916 {
1917 if ( mLayout->pageCollection()->pageCount() == 1 )
1918 {
1919 // single page, so include everything
1920 bounds = mLayout->layoutBounds( true );
1921 }
1922 else
1923 {
1924 // multi page, so just clip to items on current page
1925 bounds = mLayout->pageItemBounds( page, true );
1926 }
1927 if ( bounds.width() <= 0 || bounds.height() <= 0 )
1928 {
1929 //invalid size, skip page
1930 skipPage = true;
1931 return QImage();
1932 }
1933
1934 double pixelToLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutPixels ) );
1935 bounds = bounds.adjusted( -settings.cropMargins.left() * pixelToLayoutUnits,
1936 -settings.cropMargins.top() * pixelToLayoutUnits,
1937 settings.cropMargins.right() * pixelToLayoutUnits,
1938 settings.cropMargins.bottom() * pixelToLayoutUnits );
1939 return renderRegionToImage( bounds, QSize(), settings.dpi );
1940 }
1941 else
1942 {
1943 return renderPageToImage( page, settings.imageSize, settings.dpi );
1944 }
1945 }
1946
firstPageToBeExported(QgsLayout * layout)1947 int QgsLayoutExporter::firstPageToBeExported( QgsLayout *layout )
1948 {
1949 const int pageCount = layout->pageCollection()->pageCount();
1950 for ( int i = 0; i < pageCount; ++i )
1951 {
1952 if ( !layout->pageCollection()->shouldExportPage( i ) )
1953 {
1954 continue;
1955 }
1956
1957 return i;
1958 }
1959 return 0; // shouldn't really matter -- we aren't exporting ANY pages!
1960 }
1961
generateFileName(const PageExportDetails & details) const1962 QString QgsLayoutExporter::generateFileName( const PageExportDetails &details ) const
1963 {
1964 if ( details.page == 0 )
1965 {
1966 return details.directory + '/' + details.baseName + '.' + details.extension;
1967 }
1968 else
1969 {
1970 return details.directory + '/' + details.baseName + '_' + QString::number( details.page + 1 ) + '.' + details.extension;
1971 }
1972 }
1973
saveImage(const QImage & image,const QString & imageFilename,const QString & imageFormat,QgsProject * projectForMetadata)1974 bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat, QgsProject *projectForMetadata )
1975 {
1976 QImageWriter w( imageFilename, imageFormat.toLocal8Bit().constData() );
1977 if ( imageFormat.compare( QLatin1String( "tiff" ), Qt::CaseInsensitive ) == 0 || imageFormat.compare( QLatin1String( "tif" ), Qt::CaseInsensitive ) == 0 )
1978 {
1979 w.setCompression( 1 ); //use LZW compression
1980 }
1981 if ( projectForMetadata )
1982 {
1983 w.setText( QStringLiteral( "Author" ), projectForMetadata->metadata().author() );
1984 const QString creator = QStringLiteral( "QGIS %1" ).arg( Qgis::version() );
1985 w.setText( QStringLiteral( "Creator" ), creator );
1986 w.setText( QStringLiteral( "Producer" ), creator );
1987 w.setText( QStringLiteral( "Subject" ), projectForMetadata->metadata().abstract() );
1988 w.setText( QStringLiteral( "Created" ), projectForMetadata->metadata().creationDateTime().toString( Qt::ISODate ) );
1989 w.setText( QStringLiteral( "Title" ), projectForMetadata->metadata().title() );
1990
1991 const QgsAbstractMetadataBase::KeywordMap keywords = projectForMetadata->metadata().keywords();
1992 QStringList allKeywords;
1993 for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
1994 {
1995 allKeywords.append( QStringLiteral( "%1: %2" ).arg( it.key(), it.value().join( ',' ) ) );
1996 }
1997 const QString keywordString = allKeywords.join( ';' );
1998 w.setText( QStringLiteral( "Keywords" ), keywordString );
1999 }
2000 return w.write( image );
2001 }
2002
2003 #endif // ! QT_NO_PRINTER
2004