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 &region ) 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 &region, 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 &region, 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