QGIS API Documentation  3.2.0-Bonn (bc43194)
qgslayoutexporter.cpp
Go to the documentation of this file.
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 #include "qgslayout.h"
19 #include "qgslayoutitemmap.h"
21 #include "qgsogrutils.h"
22 #include "qgspaintenginehack.h"
25 #include "qgsfeedback.h"
26 #include <QImageWriter>
27 #include <QSize>
28 #include <QSvgGenerator>
29 
30 #include "gdal.h"
31 #include "cpl_conv.h"
32 
34 class LayoutContextPreviewSettingRestorer
35 {
36  public:
37 
38  LayoutContextPreviewSettingRestorer( QgsLayout *layout )
39  : mLayout( layout )
40  , mPreviousSetting( layout->renderContext().mIsPreviewRender )
41  {
42  mLayout->renderContext().mIsPreviewRender = false;
43  }
44 
45  ~LayoutContextPreviewSettingRestorer()
46  {
47  mLayout->renderContext().mIsPreviewRender = mPreviousSetting;
48  }
49 
50  private:
51  QgsLayout *mLayout = nullptr;
52  bool mPreviousSetting = false;
53 };
54 
55 class LayoutGuideHider
56 {
57  public:
58 
59  LayoutGuideHider( QgsLayout *layout )
60  : mLayout( layout )
61  {
62  const QList< QgsLayoutGuide * > guides = mLayout->guides().guides();
63  for ( QgsLayoutGuide *guide : guides )
64  {
65  mPrevVisibility.insert( guide, guide->item()->isVisible() );
66  guide->item()->setVisible( false );
67  }
68  }
69 
70  ~LayoutGuideHider()
71  {
72  for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
73  {
74  it.key()->item()->setVisible( it.value() );
75  }
76  }
77 
78  private:
79  QgsLayout *mLayout = nullptr;
80  QHash< QgsLayoutGuide *, bool > mPrevVisibility;
81 };
82 
83 class LayoutItemHider
84 {
85  public:
86  explicit LayoutItemHider( const QList<QGraphicsItem *> &items )
87  {
88  for ( QGraphicsItem *item : items )
89  {
90  mPrevVisibility[item] = item->isVisible();
91  item->hide();
92  }
93  }
94 
95  void hideAll()
96  {
97  for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
98  {
99  it.key()->hide();
100  }
101  }
102 
103  ~LayoutItemHider()
104  {
105  for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
106  {
107  it.key()->setVisible( it.value() );
108  }
109  }
110 
111  private:
112 
113  QHash<QGraphicsItem *, bool> mPrevVisibility;
114 };
115 
117 
119  : mLayout( layout )
120 {
121 
122 }
123 
125 {
126  return mLayout;
127 }
128 
129 void QgsLayoutExporter::renderPage( QPainter *painter, int page ) const
130 {
131  if ( !mLayout )
132  return;
133 
134  if ( mLayout->pageCollection()->pageCount() <= page || page < 0 )
135  {
136  return;
137  }
138 
139  QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( page );
140  if ( !pageItem )
141  {
142  return;
143  }
144 
145  LayoutContextPreviewSettingRestorer restorer( mLayout );
146  ( void )restorer;
147 
148  QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
149  renderRegion( painter, paperRect );
150 }
151 
152 QImage QgsLayoutExporter::renderPageToImage( int page, QSize imageSize, double dpi ) const
153 {
154  if ( !mLayout )
155  return QImage();
156 
157  if ( mLayout->pageCollection()->pageCount() <= page || page < 0 )
158  {
159  return QImage();
160  }
161 
162  QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( page );
163  if ( !pageItem )
164  {
165  return QImage();
166  }
167 
168  LayoutContextPreviewSettingRestorer restorer( mLayout );
169  ( void )restorer;
170 
171  QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
172 
173  if ( imageSize.isValid() && ( !qgsDoubleNear( static_cast< double >( imageSize.width() ) / imageSize.height(),
174  paperRect.width() / paperRect.height(), 0.008 ) ) )
175  {
176  // specified image size is wrong aspect ratio for paper rect - so ignore it and just use dpi
177  // this can happen e.g. as a result of data defined page sizes
178  // see https://issues.qgis.org/issues/18534
179  imageSize = QSize();
180  }
181 
182  return renderRegionToImage( paperRect, imageSize, dpi );
183 }
184 
186 class LayoutItemCacheSettingRestorer
187 {
188  public:
189 
190  LayoutItemCacheSettingRestorer( QgsLayout *layout )
191  : mLayout( layout )
192  {
193  const QList< QGraphicsItem * > items = mLayout->items();
194  for ( QGraphicsItem *item : items )
195  {
196  mPrevCacheMode.insert( item, item->cacheMode() );
197  item->setCacheMode( QGraphicsItem::NoCache );
198  }
199  }
200 
201  ~LayoutItemCacheSettingRestorer()
202  {
203  for ( auto it = mPrevCacheMode.constBegin(); it != mPrevCacheMode.constEnd(); ++it )
204  {
205  it.key()->setCacheMode( it.value() );
206  }
207  }
208 
209  private:
210  QgsLayout *mLayout = nullptr;
211  QHash< QGraphicsItem *, QGraphicsItem::CacheMode > mPrevCacheMode;
212 };
213 
215 
216 void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region ) const
217 {
218  QPaintDevice *paintDevice = painter->device();
219  if ( !paintDevice || !mLayout )
220  {
221  return;
222  }
223 
224  LayoutItemCacheSettingRestorer cacheRestorer( mLayout );
225  ( void )cacheRestorer;
226  LayoutContextPreviewSettingRestorer restorer( mLayout );
227  ( void )restorer;
228  LayoutGuideHider guideHider( mLayout );
229  ( void ) guideHider;
230 
231  painter->setRenderHint( QPainter::Antialiasing, mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagAntialiasing );
232 
233  mLayout->render( painter, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), region );
234 }
235 
236 QImage QgsLayoutExporter::renderRegionToImage( const QRectF &region, QSize imageSize, double dpi ) const
237 {
238  if ( !mLayout )
239  return QImage();
240 
241  LayoutContextPreviewSettingRestorer restorer( mLayout );
242  ( void )restorer;
243 
244  double resolution = mLayout->renderContext().dpi();
245  double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) );
246  if ( imageSize.isValid() )
247  {
248  //output size in pixels specified, calculate resolution using average of
249  //derived x/y dpi
250  resolution = ( imageSize.width() / region.width()
251  + imageSize.height() / region.height() ) / 2.0 * oneInchInLayoutUnits;
252  }
253  else if ( dpi > 0 )
254  {
255  //dpi overridden by function parameters
256  resolution = dpi;
257  }
258 
259  int width = imageSize.isValid() ? imageSize.width()
260  : static_cast< int >( resolution * region.width() / oneInchInLayoutUnits );
261  int height = imageSize.isValid() ? imageSize.height()
262  : static_cast< int >( resolution * region.height() / oneInchInLayoutUnits );
263 
264  QImage image( QSize( width, height ), QImage::Format_ARGB32 );
265  if ( !image.isNull() )
266  {
267  image.setDotsPerMeterX( resolution / 25.4 * 1000 );
268  image.setDotsPerMeterY( resolution / 25.4 * 1000 );
269  image.fill( Qt::transparent );
270  QPainter imagePainter( &image );
271  renderRegion( &imagePainter, region );
272  if ( !imagePainter.isActive() )
273  return QImage();
274  }
275 
276  return image;
277 }
278 
280 class LayoutContextSettingsRestorer
281 {
282  public:
283 
284  LayoutContextSettingsRestorer( QgsLayout *layout )
285  : mLayout( layout )
286  , mPreviousDpi( layout->renderContext().dpi() )
287  , mPreviousFlags( layout->renderContext().flags() )
288  , mPreviousExportLayer( layout->renderContext().currentExportLayer() )
289  {
290  }
291 
292  ~LayoutContextSettingsRestorer()
293  {
294  mLayout->renderContext().setDpi( mPreviousDpi );
295  mLayout->renderContext().setFlags( mPreviousFlags );
296  mLayout->renderContext().setCurrentExportLayer( mPreviousExportLayer );
297  }
298 
299  private:
300  QgsLayout *mLayout = nullptr;
301  double mPreviousDpi = 0;
302  QgsLayoutRenderContext::Flags mPreviousFlags = nullptr;
303  int mPreviousExportLayer = 0;
304 };
306 
308 {
309  if ( !mLayout )
310  return PrintError;
311 
312  ImageExportSettings settings = s;
313  if ( settings.dpi <= 0 )
314  settings.dpi = mLayout->renderContext().dpi();
315 
316  mErrorFileName.clear();
317 
318  int worldFilePageNo = -1;
319  if ( QgsLayoutItemMap *referenceMap = mLayout->referenceMap() )
320  {
321  worldFilePageNo = referenceMap->page();
322  }
323 
324  QFileInfo fi( filePath );
325 
326  PageExportDetails pageDetails;
327  pageDetails.directory = fi.path();
328  pageDetails.baseName = fi.baseName();
329  pageDetails.extension = fi.completeSuffix();
330 
331  LayoutContextPreviewSettingRestorer restorer( mLayout );
332  ( void )restorer;
333  LayoutContextSettingsRestorer dpiRestorer( mLayout );
334  ( void )dpiRestorer;
335  mLayout->renderContext().setDpi( settings.dpi );
336  mLayout->renderContext().setFlags( settings.flags );
337 
338  QList< int > pages;
339  if ( settings.pages.empty() )
340  {
341  for ( int page = 0; page < mLayout->pageCollection()->pageCount(); ++page )
342  pages << page;
343  }
344  else
345  {
346  for ( int page : qgis::as_const( settings.pages ) )
347  {
348  if ( page >= 0 && page < mLayout->pageCollection()->pageCount() )
349  pages << page;
350  }
351  }
352 
353  for ( int page : qgis::as_const( pages ) )
354  {
355  if ( !mLayout->pageCollection()->shouldExportPage( page ) )
356  {
357  continue;
358  }
359 
360  bool skip = false;
361  QRectF bounds;
362  QImage image = createImage( settings, page, bounds, skip );
363 
364  if ( skip )
365  continue; // should skip this page, e.g. null size
366 
367  pageDetails.page = page;
368  QString outputFilePath = generateFileName( pageDetails );
369 
370  if ( image.isNull() )
371  {
372  mErrorFileName = outputFilePath;
373  return MemoryError;
374  }
375 
376  if ( !saveImage( image, outputFilePath, pageDetails.extension, settings.exportMetadata ? mLayout->project() : nullptr ) )
377  {
378  mErrorFileName = outputFilePath;
379  return FileError;
380  }
381 
382  const bool shouldGeoreference = ( page == worldFilePageNo );
383  if ( shouldGeoreference )
384  {
385  georeferenceOutputPrivate( outputFilePath, nullptr, bounds, settings.dpi, shouldGeoreference );
386 
387  if ( settings.generateWorldFile )
388  {
389  // should generate world file for this page
390  double a, b, c, d, e, f;
391  if ( bounds.isValid() )
392  computeWorldFileParameters( bounds, a, b, c, d, e, f, settings.dpi );
393  else
394  computeWorldFileParameters( a, b, c, d, e, f, settings.dpi );
395 
396  QFileInfo fi( outputFilePath );
397  // build the world file name
398  QString outputSuffix = fi.suffix();
399  QString worldFileName = fi.absolutePath() + '/' + fi.baseName() + '.'
400  + outputSuffix.at( 0 ) + outputSuffix.at( fi.suffix().size() - 1 ) + 'w';
401 
402  writeWorldFile( worldFileName, a, b, c, d, e, f );
403  }
404  }
405 
406  }
407  return Success;
408 }
409 
410 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QString &extension, const QgsLayoutExporter::ImageExportSettings &settings, QString &error, QgsFeedback *feedback )
411 {
412  error.clear();
413 
414  if ( !iterator->beginRender() )
415  return IteratorError;
416 
417  int total = iterator->count();
418  double step = total > 0 ? 100.0 / total : 100.0;
419  int i = 0;
420  while ( iterator->next() )
421  {
422  if ( feedback )
423  {
424  if ( total > 0 )
425  feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) );
426  else
427  feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) );
428  feedback->setProgress( step * i );
429  }
430  if ( feedback && feedback->isCanceled() )
431  {
432  iterator->endRender();
433  return Canceled;
434  }
435 
436  QgsLayoutExporter exporter( iterator->layout() );
437  QString filePath = iterator->filePath( baseFilePath, extension );
438  ExportResult result = exporter.exportToImage( filePath, settings );
439  if ( result != Success )
440  {
441  if ( result == FileError )
442  error = QObject::tr( "Cannot write to %1. This file may be open in another application." ).arg( filePath );
443  iterator->endRender();
444  return result;
445  }
446  i++;
447  }
448 
449  if ( feedback )
450  {
451  feedback->setProgress( 100 );
452  }
453 
454  iterator->endRender();
455  return Success;
456 }
457 
459 {
460  if ( !mLayout )
461  return PrintError;
462 
463  PdfExportSettings settings = s;
464  if ( settings.dpi <= 0 )
465  settings.dpi = mLayout->renderContext().dpi();
466 
467  mErrorFileName.clear();
468 
469  LayoutContextPreviewSettingRestorer restorer( mLayout );
470  ( void )restorer;
471  LayoutContextSettingsRestorer contextRestorer( mLayout );
472  ( void )contextRestorer;
473  mLayout->renderContext().setDpi( settings.dpi );
474 
475  // If we are not printing as raster, temporarily disable advanced effects
476  // as QPrinter does not support composition modes and can result
477  // in items missing from the output
478  mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.forceVectorOutput );
479 
480  mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagForceVectorOutput, settings.forceVectorOutput );
481 
482  QPrinter printer;
483  preparePrintAsPdf( mLayout, printer, filePath );
484  preparePrint( mLayout, printer, false );
485  QPainter p;
486  if ( !p.begin( &printer ) )
487  {
488  //error beginning print
489  return PrintError;
490  }
491 
492  ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasterizeWholeImage );
493  p.end();
494 
495  const bool shouldGeoreference = mLayout->pageCollection()->pageCount() == 1;
496  if ( shouldGeoreference || settings.exportMetadata )
497  {
498  georeferenceOutputPrivate( filePath, nullptr, QRectF(), settings.dpi, shouldGeoreference, settings.exportMetadata );
499  }
500  return result;
501 }
502 
504 {
505  error.clear();
506 
507  if ( !iterator->beginRender() )
508  return IteratorError;
509 
510  PdfExportSettings settings = s;
511 
512  QPrinter printer;
513  QPainter p;
514 
515  int total = iterator->count();
516  double step = total > 0 ? 100.0 / total : 100.0;
517  int i = 0;
518  bool first = true;
519  while ( iterator->next() )
520  {
521  if ( feedback )
522  {
523  if ( total > 0 )
524  feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) );
525  else
526  feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) );
527  feedback->setProgress( step * i );
528  }
529  if ( feedback && feedback->isCanceled() )
530  {
531  iterator->endRender();
532  return Canceled;
533  }
534 
535  if ( s.dpi <= 0 )
536  settings.dpi = iterator->layout()->renderContext().dpi();
537 
538  LayoutContextPreviewSettingRestorer restorer( iterator->layout() );
539  ( void )restorer;
540  LayoutContextSettingsRestorer contextRestorer( iterator->layout() );
541  ( void )contextRestorer;
542  iterator->layout()->renderContext().setDpi( settings.dpi );
543 
544  // If we are not printing as raster, temporarily disable advanced effects
545  // as QPrinter does not support composition modes and can result
546  // in items missing from the output
548 
550 
551  if ( first )
552  {
553  preparePrintAsPdf( iterator->layout(), printer, fileName );
554  preparePrint( iterator->layout(), printer, false );
555 
556  if ( !p.begin( &printer ) )
557  {
558  //error beginning print
559  return PrintError;
560  }
561  }
562 
563  QgsLayoutExporter exporter( iterator->layout() );
564 
565  ExportResult result = exporter.printPrivate( printer, p, !first, settings.dpi, settings.rasterizeWholeImage );
566  if ( result != Success )
567  {
568  if ( result == FileError )
569  error = QObject::tr( "Cannot write to %1. This file may be open in another application." ).arg( fileName );
570  iterator->endRender();
571  return result;
572  }
573  first = false;
574  i++;
575  }
576 
577  if ( feedback )
578  {
579  feedback->setProgress( 100 );
580  }
581 
582  iterator->endRender();
583  return Success;
584 }
585 
586 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdfs( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QgsLayoutExporter::PdfExportSettings &settings, QString &error, QgsFeedback *feedback )
587 {
588  error.clear();
589 
590  if ( !iterator->beginRender() )
591  return IteratorError;
592 
593  int total = iterator->count();
594  double step = total > 0 ? 100.0 / total : 100.0;
595  int i = 0;
596  while ( iterator->next() )
597  {
598  if ( feedback )
599  {
600  if ( total > 0 )
601  feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) );
602  else
603  feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) );
604  feedback->setProgress( step * i );
605  }
606  if ( feedback && feedback->isCanceled() )
607  {
608  iterator->endRender();
609  return Canceled;
610  }
611 
612  QString filePath = iterator->filePath( baseFilePath, QStringLiteral( "pdf" ) );
613 
614  QgsLayoutExporter exporter( iterator->layout() );
615  ExportResult result = exporter.exportToPdf( filePath, settings );
616  if ( result != Success )
617  {
618  if ( result == FileError )
619  error = QObject::tr( "Cannot write to %1. This file may be open in another application." ).arg( filePath );
620  iterator->endRender();
621  return result;
622  }
623  i++;
624  }
625 
626  if ( feedback )
627  {
628  feedback->setProgress( 100 );
629  }
630 
631  iterator->endRender();
632  return Success;
633 }
634 
636 {
637  if ( !mLayout )
638  return PrintError;
639 
641  if ( settings.dpi <= 0 )
642  settings.dpi = mLayout->renderContext().dpi();
643 
644  mErrorFileName.clear();
645 
646  LayoutContextPreviewSettingRestorer restorer( mLayout );
647  ( void )restorer;
648  LayoutContextSettingsRestorer contextRestorer( mLayout );
649  ( void )contextRestorer;
650  mLayout->renderContext().setDpi( settings.dpi );
651 
652  // If we are not printing as raster, temporarily disable advanced effects
653  // as QPrinter does not support composition modes and can result
654  // in items missing from the output
655  mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.rasterizeWholeImage );
656 
657  preparePrint( mLayout, printer, true );
658  QPainter p;
659  if ( !p.begin( &printer ) )
660  {
661  //error beginning print
662  return PrintError;
663  }
664 
665  ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasterizeWholeImage );
666  p.end();
667 
668  return result;
669 }
670 
672 {
673  error.clear();
674 
675  if ( !iterator->beginRender() )
676  return IteratorError;
677 
678  PrintExportSettings settings = s;
679 
680  QPainter p;
681 
682  int total = iterator->count();
683  double step = total > 0 ? 100.0 / total : 100.0;
684  int i = 0;
685  bool first = true;
686  while ( iterator->next() )
687  {
688  if ( feedback )
689  {
690  if ( total > 0 )
691  feedback->setProperty( "progress", QObject::tr( "Printing %1 of %2" ).arg( i + 1 ).arg( total ) );
692  else
693  feedback->setProperty( "progress", QObject::tr( "Printing section %1" ).arg( i + 1 ).arg( total ) );
694  feedback->setProgress( step * i );
695  }
696  if ( feedback && feedback->isCanceled() )
697  {
698  iterator->endRender();
699  return Canceled;
700  }
701 
702  if ( s.dpi <= 0 )
703  settings.dpi = iterator->layout()->renderContext().dpi();
704 
705  LayoutContextPreviewSettingRestorer restorer( iterator->layout() );
706  ( void )restorer;
707  LayoutContextSettingsRestorer contextRestorer( iterator->layout() );
708  ( void )contextRestorer;
709  iterator->layout()->renderContext().setDpi( settings.dpi );
710 
711  // If we are not printing as raster, temporarily disable advanced effects
712  // as QPrinter does not support composition modes and can result
713  // in items missing from the output
715 
716  if ( first )
717  {
718  preparePrint( iterator->layout(), printer, true );
719 
720  if ( !p.begin( &printer ) )
721  {
722  //error beginning print
723  return PrintError;
724  }
725  }
726 
727  QgsLayoutExporter exporter( iterator->layout() );
728 
729  ExportResult result = exporter.printPrivate( printer, p, !first, settings.dpi, settings.rasterizeWholeImage );
730  if ( result != Success )
731  {
732  iterator->endRender();
733  return result;
734  }
735  first = false;
736  i++;
737  }
738 
739  if ( feedback )
740  {
741  feedback->setProgress( 100 );
742  }
743 
744  iterator->endRender();
745  return Success;
746 }
747 
749 {
750  if ( !mLayout )
751  return PrintError;
752 
753  SvgExportSettings settings = s;
754  if ( settings.dpi <= 0 )
755  settings.dpi = mLayout->renderContext().dpi();
756 
757  mErrorFileName.clear();
758 
759  LayoutContextPreviewSettingRestorer restorer( mLayout );
760  ( void )restorer;
761  LayoutContextSettingsRestorer contextRestorer( mLayout );
762  ( void )contextRestorer;
763  mLayout->renderContext().setDpi( settings.dpi );
764 
765  mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagForceVectorOutput, settings.forceVectorOutput );
766 
767  QFileInfo fi( filePath );
768  PageExportDetails pageDetails;
769  pageDetails.directory = fi.path();
770  pageDetails.baseName = fi.baseName();
771  pageDetails.extension = fi.completeSuffix();
772 
773  double inchesToLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) );
774 
775  for ( int i = 0; i < mLayout->pageCollection()->pageCount(); ++i )
776  {
777  if ( !mLayout->pageCollection()->shouldExportPage( i ) )
778  {
779  continue;
780  }
781 
782  pageDetails.page = i;
783  QString fileName = generateFileName( pageDetails );
784 
785  QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( i );
786  QRectF bounds;
787  if ( settings.cropToContents )
788  {
789  if ( mLayout->pageCollection()->pageCount() == 1 )
790  {
791  // single page, so include everything
792  bounds = mLayout->layoutBounds( true );
793  }
794  else
795  {
796  // multi page, so just clip to items on current page
797  bounds = mLayout->pageItemBounds( i, true );
798  }
799  bounds = bounds.adjusted( -settings.cropMargins.left(),
800  -settings.cropMargins.top(),
801  settings.cropMargins.right(),
802  settings.cropMargins.bottom() );
803  }
804  else
805  {
806  bounds = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() );
807  }
808 
809  //width in pixel
810  int width = ( int )( bounds.width() * settings.dpi / inchesToLayoutUnits );
811  //height in pixel
812  int height = ( int )( bounds.height() * settings.dpi / inchesToLayoutUnits );
813  if ( width == 0 || height == 0 )
814  {
815  //invalid size, skip this page
816  continue;
817  }
818 
819  if ( settings.exportAsLayers )
820  {
821  const QRectF paperRect = QRectF( pageItem->pos().x(),
822  pageItem->pos().y(),
823  pageItem->rect().width(),
824  pageItem->rect().height() );
825  QDomDocument svg;
826  QDomNode svgDocRoot;
827  const QList<QGraphicsItem *> items = mLayout->items( paperRect,
828  Qt::IntersectsItemBoundingRect,
829  Qt::AscendingOrder );
830 
831  LayoutItemHider itemHider( items );
832  ( void )itemHider;
833 
834  int layoutItemLayerIdx = 0;
835  auto it = items.constBegin();
836  for ( unsigned svgLayerId = 1; it != items.constEnd(); ++svgLayerId )
837  {
838  itemHider.hideAll();
839  QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( *it );
840  QString layerName = QObject::tr( "Layer %1" ).arg( svgLayerId );
841  if ( layoutItem && layoutItem->numberExportLayers() > 0 )
842  {
843  layoutItem->show();
844  mLayout->renderContext().setCurrentExportLayer( layoutItemLayerIdx );
845  ++layoutItemLayerIdx;
846  }
847  else
848  {
849  // show all items until the next item that renders on a separate layer
850  for ( ; it != items.constEnd(); ++it )
851  {
852  layoutItem = dynamic_cast<QgsLayoutItem *>( *it );
853  if ( layoutItem && layoutItem->numberExportLayers() > 0 )
854  {
855  break;
856  }
857  else
858  {
859  ( *it )->show();
860  }
861  }
862  }
863 
864  ExportResult result = renderToLayeredSvg( settings, width, height, i, bounds, fileName, svgLayerId, layerName, svg, svgDocRoot, settings.exportMetadata );
865  if ( result != Success )
866  return result;
867 
868  if ( layoutItem && layoutItem->numberExportLayers() > 0 && layoutItem->numberExportLayers() == layoutItemLayerIdx ) // restore and pass to next item
869  {
870  mLayout->renderContext().setCurrentExportLayer( -1 );
871  layoutItemLayerIdx = 0;
872  ++it;
873  }
874  }
875 
876  if ( settings.exportMetadata )
877  appendMetadataToSvg( svg );
878 
879  QFile out( fileName );
880  bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
881  if ( !openOk )
882  {
883  mErrorFileName = fileName;
884  return FileError;
885  }
886 
887  out.write( svg.toByteArray() );
888  }
889  else
890  {
891  QBuffer svgBuffer;
892  {
893  QSvgGenerator generator;
894  if ( settings.exportMetadata )
895  {
896  generator.setTitle( mLayout->project()->metadata().title() );
897  generator.setDescription( mLayout->project()->metadata().abstract() );
898  }
899  generator.setOutputDevice( &svgBuffer );
900  generator.setSize( QSize( width, height ) );
901  generator.setViewBox( QRect( 0, 0, width, height ) );
902  generator.setResolution( settings.dpi );
903 
904  QPainter p;
905  bool createOk = p.begin( &generator );
906  if ( !createOk )
907  {
908  mErrorFileName = fileName;
909  return FileError;
910  }
911 
912  if ( settings.cropToContents )
913  renderRegion( &p, bounds );
914  else
915  renderPage( &p, i );
916 
917  p.end();
918  }
919  {
920  svgBuffer.close();
921  svgBuffer.open( QIODevice::ReadOnly );
922  QDomDocument svg;
923  QString errorMsg;
924  int errorLine;
925  if ( ! svg.setContent( &svgBuffer, false, &errorMsg, &errorLine ) )
926  {
927  mErrorFileName = fileName;
928  return SvgLayerError;
929  }
930 
931  if ( settings.exportMetadata )
932  appendMetadataToSvg( svg );
933 
934  QFile out( fileName );
935  bool openOk = out.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate );
936  if ( !openOk )
937  {
938  mErrorFileName = fileName;
939  return FileError;
940  }
941 
942  out.write( svg.toByteArray() );
943  }
944  }
945  }
946 
947  return Success;
948 }
949 
950 QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QgsLayoutExporter::SvgExportSettings &settings, QString &error, QgsFeedback *feedback )
951 {
952  error.clear();
953 
954  if ( !iterator->beginRender() )
955  return IteratorError;
956 
957  int total = iterator->count();
958  double step = total > 0 ? 100.0 / total : 100.0;
959  int i = 0;
960  while ( iterator->next() )
961  {
962  if ( feedback )
963  {
964  if ( total > 0 )
965  feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) );
966  else
967  feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) );
968 
969  feedback->setProgress( step * i );
970  }
971  if ( feedback && feedback->isCanceled() )
972  {
973  iterator->endRender();
974  return Canceled;
975  }
976 
977  QString filePath = iterator->filePath( baseFilePath, QStringLiteral( "svg" ) );
978 
979  QgsLayoutExporter exporter( iterator->layout() );
980  ExportResult result = exporter.exportToSvg( filePath, settings );
981  if ( result != Success )
982  {
983  if ( result == FileError )
984  error = QObject::tr( "Cannot write to %1. This file may be open in another application." ).arg( filePath );
985  iterator->endRender();
986  return result;
987  }
988  i++;
989  }
990 
991  if ( feedback )
992  {
993  feedback->setProgress( 100 );
994  }
995 
996  iterator->endRender();
997  return Success;
998 
999 }
1000 
1001 void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPrinter &printer, const QString &filePath )
1002 {
1003  printer.setOutputFileName( filePath );
1004  // setOutputFormat should come after setOutputFileName, which auto-sets format to QPrinter::PdfFormat.
1005  // [LS] This should be QPrinter::NativeFormat for Mac, otherwise fonts are not embed-able
1006  // and text is not searchable; however, there are several bugs with <= Qt 4.8.5, 5.1.1, 5.2.0:
1007  // https://bugreports.qt.io/browse/QTBUG-10094 - PDF font embedding fails
1008  // https://bugreports.qt.io/browse/QTBUG-33583 - PDF output converts text to outline
1009  // Also an issue with PDF paper size using QPrinter::NativeFormat on Mac (always outputs portrait letter-size)
1010  printer.setOutputFormat( QPrinter::PdfFormat );
1011 
1012  updatePrinterPageSize( layout, printer, 0 );
1013 
1014  // TODO: add option for this in layout
1015  // May not work on Windows or non-X11 Linux. Works fine on Mac using QPrinter::NativeFormat
1016  //printer.setFontEmbeddingEnabled( true );
1017 
1018  QgsPaintEngineHack::fixEngineFlags( printer.paintEngine() );
1019 }
1020 
1021 void QgsLayoutExporter::preparePrint( QgsLayout *layout, QPrinter &printer, bool setFirstPageSize )
1022 {
1023  printer.setFullPage( true );
1024  printer.setColorMode( QPrinter::Color );
1025 
1026  //set user-defined resolution
1027  printer.setResolution( layout->renderContext().dpi() );
1028 
1029  if ( setFirstPageSize )
1030  {
1031  updatePrinterPageSize( layout, printer, 0 );
1032  }
1033 }
1034 
1036 {
1037  preparePrint( mLayout, printer, true );
1038  QPainter p;
1039  if ( !p.begin( &printer ) )
1040  {
1041  //error beginning print
1042  return PrintError;
1043  }
1044 
1045  printPrivate( printer, p );
1046  p.end();
1047  return Success;
1048 }
1049 
1050 QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage, double dpi, bool rasterize )
1051 {
1052  //layout starts page numbering at 0
1053  int fromPage = ( printer.fromPage() < 1 ) ? 0 : printer.fromPage() - 1;
1054  int toPage = ( printer.toPage() < 1 ) ? mLayout->pageCollection()->pageCount() - 1 : printer.toPage() - 1;
1055 
1056  bool pageExported = false;
1057  if ( rasterize )
1058  {
1059  for ( int i = fromPage; i <= toPage; ++i )
1060  {
1061  if ( !mLayout->pageCollection()->shouldExportPage( i ) )
1062  {
1063  continue;
1064  }
1065 
1066  updatePrinterPageSize( mLayout, printer, i );
1067  if ( ( pageExported && i > fromPage ) || startNewPage )
1068  {
1069  printer.newPage();
1070  }
1071 
1072  QImage image = renderPageToImage( i, QSize(), dpi );
1073  if ( !image.isNull() )
1074  {
1075  QRectF targetArea( 0, 0, image.width(), image.height() );
1076  painter.drawImage( targetArea, image, targetArea );
1077  }
1078  else
1079  {
1080  return MemoryError;
1081  }
1082  pageExported = true;
1083  }
1084  }
1085  else
1086  {
1087  for ( int i = fromPage; i <= toPage; ++i )
1088  {
1089  if ( !mLayout->pageCollection()->shouldExportPage( i ) )
1090  {
1091  continue;
1092  }
1093 
1094  updatePrinterPageSize( mLayout, printer, i );
1095 
1096  if ( ( pageExported && i > fromPage ) || startNewPage )
1097  {
1098  printer.newPage();
1099  }
1100  renderPage( &painter, i );
1101  pageExported = true;
1102  }
1103  }
1104  return Success;
1105 }
1106 
1107 void QgsLayoutExporter::updatePrinterPageSize( QgsLayout *layout, QPrinter &printer, int page )
1108 {
1109  //must set orientation to portrait before setting paper size, otherwise size will be flipped
1110  //for landscape sized outputs (#11352)
1111  printer.setOrientation( QPrinter::Portrait );
1112  QgsLayoutSize pageSize = layout->pageCollection()->page( page )->sizeWithUnits();
1114  printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter );
1115 }
1116 
1117 QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds, const QString &filename, int svgLayerId, const QString &layerName, QDomDocument &svg, QDomNode &svgDocRoot, bool includeMetadata ) const
1118 {
1119  QBuffer svgBuffer;
1120  {
1121  QSvgGenerator generator;
1122  if ( includeMetadata )
1123  {
1124  if ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( mLayout.data() ) )
1125  generator.setTitle( l->name() );
1126  else if ( mLayout->project() )
1127  generator.setTitle( mLayout->project()->title() );
1128  }
1129 
1130  generator.setOutputDevice( &svgBuffer );
1131  generator.setSize( QSize( width, height ) );
1132  generator.setViewBox( QRect( 0, 0, width, height ) );
1133  generator.setResolution( settings.dpi ); //because the rendering is done in mm, convert the dpi
1134 
1135  QPainter svgPainter( &generator );
1136  if ( settings.cropToContents )
1137  renderRegion( &svgPainter, bounds );
1138  else
1139  renderPage( &svgPainter, page );
1140  }
1141 
1142  // post-process svg output to create groups in a single svg file
1143  // we create inkscape layers since it's nice and clean and free
1144  // and fully svg compatible
1145  {
1146  svgBuffer.close();
1147  svgBuffer.open( QIODevice::ReadOnly );
1148  QDomDocument doc;
1149  QString errorMsg;
1150  int errorLine;
1151  if ( ! doc.setContent( &svgBuffer, false, &errorMsg, &errorLine ) )
1152  {
1153  mErrorFileName = filename;
1154  return SvgLayerError;
1155  }
1156  if ( 1 == svgLayerId )
1157  {
1158  svg = QDomDocument( doc.doctype() );
1159  svg.appendChild( svg.importNode( doc.firstChild(), false ) );
1160  svgDocRoot = svg.importNode( doc.elementsByTagName( QStringLiteral( "svg" ) ).at( 0 ), false );
1161  svgDocRoot.toElement().setAttribute( QStringLiteral( "xmlns:inkscape" ), QStringLiteral( "http://www.inkscape.org/namespaces/inkscape" ) );
1162  svg.appendChild( svgDocRoot );
1163  }
1164  QDomNode mainGroup = svg.importNode( doc.elementsByTagName( QStringLiteral( "g" ) ).at( 0 ), true );
1165  mainGroup.toElement().setAttribute( QStringLiteral( "id" ), layerName );
1166  mainGroup.toElement().setAttribute( QStringLiteral( "inkscape:label" ), layerName );
1167  mainGroup.toElement().setAttribute( QStringLiteral( "inkscape:groupmode" ), QStringLiteral( "layer" ) );
1168  QDomNode defs = svg.importNode( doc.elementsByTagName( QStringLiteral( "defs" ) ).at( 0 ), true );
1169  svgDocRoot.appendChild( defs );
1170  svgDocRoot.appendChild( mainGroup );
1171  }
1172  return Success;
1173 }
1174 
1175 void QgsLayoutExporter::appendMetadataToSvg( QDomDocument &svg ) const
1176 {
1177  const QgsProjectMetadata &metadata = mLayout->project()->metadata();
1178  QDomElement metadataElement = svg.createElement( QStringLiteral( "metadata" ) );
1179  metadataElement.setAttribute( QStringLiteral( "id" ), QStringLiteral( "qgismetadata" ) );
1180  QDomElement rdfElement = svg.createElement( QStringLiteral( "rdf:RDF" ) );
1181  QDomElement workElement = svg.createElement( QStringLiteral( "cc:Work" ) );
1182 
1183  auto addTextNode = [&workElement, &svg]( const QString & tag, const QString & value )
1184  {
1185  QDomElement element = svg.createElement( tag );
1186  QDomText t = svg.createTextNode( value );
1187  element.appendChild( t );
1188  workElement.appendChild( element );
1189  };
1190 
1191  addTextNode( QStringLiteral( "dc:format" ), QStringLiteral( "image/svg+xml" ) );
1192  addTextNode( QStringLiteral( "dc:title" ), metadata.title() );
1193  addTextNode( QStringLiteral( "dc:date" ), metadata.creationDateTime().toString( Qt::ISODate ) );
1194  addTextNode( QStringLiteral( "dc:identifier" ), metadata.identifier() );
1195  addTextNode( QStringLiteral( "dc:description" ), metadata.abstract() );
1196 
1197  auto addAgentNode = [&workElement, &svg]( const QString & tag, const QString & value )
1198  {
1199  QDomElement element = svg.createElement( tag );
1200  QDomElement agentElement = svg.createElement( QStringLiteral( "cc:Agent" ) );
1201  QDomElement titleElement = svg.createElement( QStringLiteral( "dc:title" ) );
1202  QDomText t = svg.createTextNode( value );
1203  titleElement.appendChild( t );
1204  agentElement.appendChild( titleElement );
1205  element.appendChild( agentElement );
1206  workElement.appendChild( element );
1207  };
1208 
1209  addAgentNode( QStringLiteral( "dc:creator" ), metadata.author() );
1210  addAgentNode( QStringLiteral( "dc:publisher" ), QStringLiteral( "QGIS %1" ).arg( Qgis::QGIS_VERSION ) );
1211 
1212  // keywords
1213  {
1214  QDomElement element = svg.createElement( QStringLiteral( "dc:subject" ) );
1215  QDomElement bagElement = svg.createElement( QStringLiteral( "rdf:Bag" ) );
1216  QgsAbstractMetadataBase::KeywordMap keywords = metadata.keywords();
1217  for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
1218  {
1219  const QStringList words = it.value();
1220  for ( const QString &keyword : words )
1221  {
1222  QDomElement liElement = svg.createElement( QStringLiteral( "rdf:li" ) );
1223  QDomText t = svg.createTextNode( keyword );
1224  liElement.appendChild( t );
1225  bagElement.appendChild( liElement );
1226  }
1227  }
1228  element.appendChild( bagElement );
1229  workElement.appendChild( element );
1230  }
1231 
1232  rdfElement.appendChild( workElement );
1233  metadataElement.appendChild( rdfElement );
1234  svg.documentElement().appendChild( metadataElement );
1235 }
1236 
1237 std::unique_ptr<double[]> QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF &region, double dpi ) const
1238 {
1239  if ( !map )
1240  map = mLayout->referenceMap();
1241 
1242  if ( !map )
1243  return nullptr;
1244 
1245  if ( dpi < 0 )
1246  dpi = mLayout->renderContext().dpi();
1247 
1248  // calculate region of composition to export (in mm)
1249  QRectF exportRegion = region;
1250  if ( !exportRegion.isValid() )
1251  {
1252  int pageNumber = map->page();
1253 
1254  QgsLayoutItemPage *page = mLayout->pageCollection()->page( pageNumber );
1255  double pageY = page->pos().y();
1256  QSizeF pageSize = page->rect().size();
1257  exportRegion = QRectF( 0, pageY, pageSize.width(), pageSize.height() );
1258  }
1259 
1260  // map rectangle (in mm)
1261  QRectF mapItemSceneRect = map->mapRectToScene( map->rect() );
1262 
1263  // destination width/height in mm
1264  double outputHeightMM = exportRegion.height();
1265  double outputWidthMM = exportRegion.width();
1266 
1267  // map properties
1268  QgsRectangle mapExtent = map->extent();
1269  double mapXCenter = mapExtent.center().x();
1270  double mapYCenter = mapExtent.center().y();
1271  double alpha = - map->mapRotation() / 180 * M_PI;
1272  double sinAlpha = std::sin( alpha );
1273  double cosAlpha = std::cos( alpha );
1274 
1275  // get the extent (in map units) for the exported region
1276  QPointF mapItemPos = map->pos();
1277  //adjust item position so it is relative to export region
1278  mapItemPos.rx() -= exportRegion.left();
1279  mapItemPos.ry() -= exportRegion.top();
1280 
1281  // calculate extent of entire page in map units
1282  double xRatio = mapExtent.width() / mapItemSceneRect.width();
1283  double yRatio = mapExtent.height() / mapItemSceneRect.height();
1284  double xmin = mapExtent.xMinimum() - mapItemPos.x() * xRatio;
1285  double ymax = mapExtent.yMaximum() + mapItemPos.y() * yRatio;
1286  QgsRectangle paperExtent( xmin, ymax - outputHeightMM * yRatio, xmin + outputWidthMM * xRatio, ymax );
1287 
1288  // calculate origin of page
1289  double X0 = paperExtent.xMinimum();
1290  double Y0 = paperExtent.yMaximum();
1291 
1292  if ( !qgsDoubleNear( alpha, 0.0 ) )
1293  {
1294  // translate origin to account for map rotation
1295  double X1 = X0 - mapXCenter;
1296  double Y1 = Y0 - mapYCenter;
1297  double X2 = X1 * cosAlpha + Y1 * sinAlpha;
1298  double Y2 = -X1 * sinAlpha + Y1 * cosAlpha;
1299  X0 = X2 + mapXCenter;
1300  Y0 = Y2 + mapYCenter;
1301  }
1302 
1303  // calculate scaling of pixels
1304  int pageWidthPixels = static_cast< int >( dpi * outputWidthMM / 25.4 );
1305  int pageHeightPixels = static_cast< int >( dpi * outputHeightMM / 25.4 );
1306  double pixelWidthScale = paperExtent.width() / pageWidthPixels;
1307  double pixelHeightScale = paperExtent.height() / pageHeightPixels;
1308 
1309  // transform matrix
1310  std::unique_ptr<double[]> t( new double[6] );
1311  t[0] = X0;
1312  t[1] = cosAlpha * pixelWidthScale;
1313  t[2] = -sinAlpha * pixelWidthScale;
1314  t[3] = Y0;
1315  t[4] = -sinAlpha * pixelHeightScale;
1316  t[5] = -cosAlpha * pixelHeightScale;
1317 
1318  return t;
1319 }
1320 
1321 void QgsLayoutExporter::writeWorldFile( const QString &worldFileName, double a, double b, double c, double d, double e, double f ) const
1322 {
1323  QFile worldFile( worldFileName );
1324  if ( !worldFile.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
1325  {
1326  return;
1327  }
1328  QTextStream fout( &worldFile );
1329 
1330  // QString::number does not use locale settings (for the decimal point)
1331  // which is what we want here
1332  fout << QString::number( a, 'f', 12 ) << "\r\n";
1333  fout << QString::number( d, 'f', 12 ) << "\r\n";
1334  fout << QString::number( b, 'f', 12 ) << "\r\n";
1335  fout << QString::number( e, 'f', 12 ) << "\r\n";
1336  fout << QString::number( c, 'f', 12 ) << "\r\n";
1337  fout << QString::number( f, 'f', 12 ) << "\r\n";
1338 }
1339 
1340 bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMap *map, const QRectF &exportRegion, double dpi ) const
1341 {
1342  return georeferenceOutputPrivate( file, map, exportRegion, dpi, false );
1343 }
1344 
1345 bool QgsLayoutExporter::georeferenceOutputPrivate( const QString &file, QgsLayoutItemMap *map, const QRectF &exportRegion, double dpi, bool includeGeoreference, bool includeMetadata ) const
1346 {
1347  if ( !mLayout )
1348  return false;
1349 
1350  if ( !map && includeGeoreference )
1351  map = mLayout->referenceMap();
1352 
1353  std::unique_ptr<double[]> t;
1354 
1355  if ( map && includeGeoreference )
1356  {
1357  if ( dpi < 0 )
1358  dpi = mLayout->renderContext().dpi();
1359 
1360  t = computeGeoTransform( map, exportRegion, dpi );
1361  }
1362 
1363  // important - we need to manually specify the DPI in advance, as GDAL will otherwise
1364  // assume a DPI of 150
1365  CPLSetConfigOption( "GDAL_PDF_DPI", QString::number( dpi ).toLocal8Bit().constData() );
1366  gdal::dataset_unique_ptr outputDS( GDALOpen( file.toLocal8Bit().constData(), GA_Update ) );
1367  if ( outputDS )
1368  {
1369  if ( t )
1370  GDALSetGeoTransform( outputDS.get(), t.get() );
1371 
1372  if ( includeMetadata )
1373  {
1374  QString creationDateString;
1375  const QDateTime creationDateTime = mLayout->project()->metadata().creationDateTime();
1376  if ( creationDateTime.isValid() )
1377  {
1378  creationDateString = QStringLiteral( "D:%1" ).arg( mLayout->project()->metadata().creationDateTime().toString( QStringLiteral( "yyyyMMddHHmmss" ) ) );
1379  if ( creationDateTime.timeZone().isValid() )
1380  {
1381  int offsetFromUtc = creationDateTime.timeZone().offsetFromUtc( creationDateTime );
1382  creationDateString += ( offsetFromUtc >= 0 ) ? '+' : '-';
1383  offsetFromUtc = std::abs( offsetFromUtc );
1384  int offsetHours = offsetFromUtc / 3600;
1385  int offsetMins = ( offsetFromUtc % 3600 ) / 60;
1386  creationDateString += QStringLiteral( "%1'%2'" ).arg( offsetHours ).arg( offsetMins );
1387  }
1388  }
1389  GDALSetMetadataItem( outputDS.get(), "CREATION_DATE", creationDateString.toLocal8Bit().constData(), nullptr );
1390 
1391  GDALSetMetadataItem( outputDS.get(), "AUTHOR", mLayout->project()->metadata().author().toLocal8Bit().constData(), nullptr );
1392  const QString creator = QStringLiteral( "QGIS %1" ).arg( Qgis::QGIS_VERSION );
1393  GDALSetMetadataItem( outputDS.get(), "CREATOR", creator.toLocal8Bit().constData(), nullptr );
1394  GDALSetMetadataItem( outputDS.get(), "PRODUCER", creator.toLocal8Bit().constData(), nullptr );
1395  GDALSetMetadataItem( outputDS.get(), "SUBJECT", mLayout->project()->metadata().abstract().toLocal8Bit().constData(), nullptr );
1396  GDALSetMetadataItem( outputDS.get(), "TITLE", mLayout->project()->metadata().title().toLocal8Bit().constData(), nullptr );
1397 
1398  const QgsAbstractMetadataBase::KeywordMap keywords = mLayout->project()->metadata().keywords();
1399  QStringList allKeywords;
1400  for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
1401  {
1402  allKeywords.append( QStringLiteral( "%1: %2" ).arg( it.key(), it.value().join( ',' ) ) );
1403  }
1404  const QString keywordString = allKeywords.join( ';' );
1405  GDALSetMetadataItem( outputDS.get(), "KEYWORDS", keywordString.toLocal8Bit().constData(), nullptr );
1406  }
1407 
1408  if ( t )
1409  GDALSetProjection( outputDS.get(), map->crs().toWkt().toLocal8Bit().constData() );
1410  }
1411  CPLSetConfigOption( "GDAL_PDF_DPI", nullptr );
1412 
1413  return true;
1414 }
1415 
1416 void QgsLayoutExporter::computeWorldFileParameters( double &a, double &b, double &c, double &d, double &e, double &f, double dpi ) const
1417 {
1418  if ( !mLayout )
1419  return;
1420 
1421  QgsLayoutItemMap *map = mLayout->referenceMap();
1422  if ( !map )
1423  {
1424  return;
1425  }
1426 
1427  int pageNumber = map->page();
1428  QgsLayoutItemPage *page = mLayout->pageCollection()->page( pageNumber );
1429  double pageY = page->pos().y();
1430  QSizeF pageSize = page->rect().size();
1431  QRectF pageRect( 0, pageY, pageSize.width(), pageSize.height() );
1432  computeWorldFileParameters( pageRect, a, b, c, d, e, f, dpi );
1433 }
1434 
1435 void QgsLayoutExporter::computeWorldFileParameters( const QRectF &exportRegion, double &a, double &b, double &c, double &d, double &e, double &f, double dpi ) const
1436 {
1437  if ( !mLayout )
1438  return;
1439 
1440  // World file parameters : affine transformation parameters from pixel coordinates to map coordinates
1441  QgsLayoutItemMap *map = mLayout->referenceMap();
1442  if ( !map )
1443  {
1444  return;
1445  }
1446 
1447  double destinationHeight = exportRegion.height();
1448  double destinationWidth = exportRegion.width();
1449 
1450  QRectF mapItemSceneRect = map->mapRectToScene( map->rect() );
1451  QgsRectangle mapExtent = map->extent();
1452 
1453  double alpha = map->mapRotation() / 180 * M_PI;
1454 
1455  double xRatio = mapExtent.width() / mapItemSceneRect.width();
1456  double yRatio = mapExtent.height() / mapItemSceneRect.height();
1457 
1458  double xCenter = mapExtent.center().x();
1459  double yCenter = mapExtent.center().y();
1460 
1461  // get the extent (in map units) for the region
1462  QPointF mapItemPos = map->pos();
1463  //adjust item position so it is relative to export region
1464  mapItemPos.rx() -= exportRegion.left();
1465  mapItemPos.ry() -= exportRegion.top();
1466 
1467  double xmin = mapExtent.xMinimum() - mapItemPos.x() * xRatio;
1468  double ymax = mapExtent.yMaximum() + mapItemPos.y() * yRatio;
1469  QgsRectangle paperExtent( xmin, ymax - destinationHeight * yRatio, xmin + destinationWidth * xRatio, ymax );
1470 
1471  double X0 = paperExtent.xMinimum();
1472  double Y0 = paperExtent.yMinimum();
1473 
1474  if ( dpi < 0 )
1475  dpi = mLayout->renderContext().dpi();
1476 
1477  int widthPx = static_cast< int >( dpi * destinationWidth / 25.4 );
1478  int heightPx = static_cast< int >( dpi * destinationHeight / 25.4 );
1479 
1480  double Ww = paperExtent.width() / widthPx;
1481  double Hh = paperExtent.height() / heightPx;
1482 
1483  // scaling matrix
1484  double s[6];
1485  s[0] = Ww;
1486  s[1] = 0;
1487  s[2] = X0;
1488  s[3] = 0;
1489  s[4] = -Hh;
1490  s[5] = Y0 + paperExtent.height();
1491 
1492  // rotation matrix
1493  double r[6];
1494  r[0] = std::cos( alpha );
1495  r[1] = -std::sin( alpha );
1496  r[2] = xCenter * ( 1 - std::cos( alpha ) ) + yCenter * std::sin( alpha );
1497  r[3] = std::sin( alpha );
1498  r[4] = std::cos( alpha );
1499  r[5] = - xCenter * std::sin( alpha ) + yCenter * ( 1 - std::cos( alpha ) );
1500 
1501  // result = rotation x scaling = rotation(scaling(X))
1502  a = r[0] * s[0] + r[1] * s[3];
1503  b = r[0] * s[1] + r[1] * s[4];
1504  c = r[0] * s[2] + r[1] * s[5] + r[2];
1505  d = r[3] * s[0] + r[4] * s[3];
1506  e = r[3] * s[1] + r[4] * s[4];
1507  f = r[3] * s[2] + r[4] * s[5] + r[5];
1508 }
1509 
1510 QImage QgsLayoutExporter::createImage( const QgsLayoutExporter::ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const
1511 {
1512  bounds = QRectF();
1513  skipPage = false;
1514 
1515  if ( settings.cropToContents )
1516  {
1517  if ( mLayout->pageCollection()->pageCount() == 1 )
1518  {
1519  // single page, so include everything
1520  bounds = mLayout->layoutBounds( true );
1521  }
1522  else
1523  {
1524  // multi page, so just clip to items on current page
1525  bounds = mLayout->pageItemBounds( page, true );
1526  }
1527  if ( bounds.width() <= 0 || bounds.height() <= 0 )
1528  {
1529  //invalid size, skip page
1530  skipPage = true;
1531  return QImage();
1532  }
1533 
1534  double pixelToLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutPixels ) );
1535  bounds = bounds.adjusted( -settings.cropMargins.left() * pixelToLayoutUnits,
1536  -settings.cropMargins.top() * pixelToLayoutUnits,
1537  settings.cropMargins.right() * pixelToLayoutUnits,
1538  settings.cropMargins.bottom() * pixelToLayoutUnits );
1539  return renderRegionToImage( bounds, QSize(), settings.dpi );
1540  }
1541  else
1542  {
1543  return renderPageToImage( page, settings.imageSize, settings.dpi );
1544  }
1545 }
1546 
1548 {
1549  if ( details.page == 0 )
1550  {
1551  return details.directory + '/' + details.baseName + '.' + details.extension;
1552  }
1553  else
1554  {
1555  return details.directory + '/' + details.baseName + '_' + QString::number( details.page + 1 ) + '.' + details.extension;
1556  }
1557 }
1558 
1559 bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat, QgsProject *projectForMetadata )
1560 {
1561  QImageWriter w( imageFilename, imageFormat.toLocal8Bit().constData() );
1562  if ( imageFormat.compare( QLatin1String( "tiff" ), Qt::CaseInsensitive ) == 0 || imageFormat.compare( QLatin1String( "tif" ), Qt::CaseInsensitive ) == 0 )
1563  {
1564  w.setCompression( 1 ); //use LZW compression
1565  }
1566  if ( projectForMetadata )
1567  {
1568  w.setText( "Author", projectForMetadata->metadata().author() );
1569  const QString creator = QStringLiteral( "QGIS %1" ).arg( Qgis::QGIS_VERSION );
1570  w.setText( "Creator", creator );
1571  w.setText( "Producer", creator );
1572  w.setText( "Subject", projectForMetadata->metadata().abstract() );
1573  w.setText( "Created", projectForMetadata->metadata().creationDateTime().toString( Qt::ISODate ) );
1574  w.setText( "Title", projectForMetadata->metadata().title() );
1575 
1576  const QgsAbstractMetadataBase::KeywordMap keywords = projectForMetadata->metadata().keywords();
1577  QStringList allKeywords;
1578  for ( auto it = keywords.constBegin(); it != keywords.constEnd(); ++it )
1579  {
1580  allKeywords.append( QStringLiteral( "%1: %2" ).arg( it.key(), it.value().join( ',' ) ) );
1581  }
1582  const QString keywordString = allKeywords.join( ';' );
1583  w.setText( "Keywords", keywordString );
1584  }
1585  return w.write( image );
1586 }
1587 
bool cropToContents
Set to true if image should be cropped so only parts of the layout containing items are exported...
double right() const
Returns the right margin.
Definition: qgsmargins.h:84
void setDpi(double dpi)
Sets the dpi for outputting the layout.
A rectangle specified with double values.
Definition: qgsrectangle.h:40
void renderRegion(QPainter *painter, const QRectF &region) const
Renders a region from the layout to a painter.
static const QString QGIS_VERSION
Version string.
Definition: qgis.h:63
Contains settings relating to printing layouts.
QgsAbstractMetadataBase::KeywordMap keywords() const
Returns the keywords map, which is a set of descriptive keywords associated with the resource...
Base class for graphical items within a QgsLayout.
QgsMargins cropMargins
Crop to content margins, in layout units.
Unable to allocate memory required to export.
virtual bool endRender()=0
Ends the render, performing any required cleanup tasks.
void setFlag(QgsLayoutRenderContext::Flag flag, bool on=true)
Enables or disables a particular rendering flag for the layout.
QgsLayoutMeasurement convert(const QgsLayoutMeasurement &measurement, QgsUnitTypes::LayoutUnit targetUnits) const
Converts a measurement from one unit to another.
Contains the configuration for a single snap guide used by a layout.
Could not write to destination file, likely due to a lock held by another application.
ExportResult exportToImage(const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings)
Exports the layout to the filePath, using the specified export settings.
double y
Definition: qgspointxy.h:48
bool exportMetadata
Indicates whether SVG export should include RDF metadata generated from the layout&#39;s project&#39;s metada...
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:251
QgsLayoutSize sizeWithUnits() const
Returns the item&#39;s current size, including units.
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition: qgsfeedback.h:63
virtual bool next()=0
Iterates to next feature, returning false if no more features exist to iterate over.
QgsMargins cropMargins
Crop to content margins, in pixels.
bool forceVectorOutput
Set to true to force vector object exports, even when the resultant appearance will differ from the l...
bool rasterizeWholeImage
Set to true to force whole layout to be rasterized while exporting.
QString author() const
Returns the project author string.
ExportResult print(QPrinter &printer, const QgsLayoutExporter::PrintExportSettings &settings)
Prints the layout to a printer, using the specified export settings.
QString title() const
Returns the human readable name of the resource, typically displayed in search results.
QgsLayoutRenderContext & renderContext()
Returns a reference to the layout&#39;s render context, which stores information relating to the current ...
Definition: qgslayout.cpp:355
int currentExportLayer() const
Returns the current item layer to draw while exporting.
bool georeferenceOutput(const QString &file, QgsLayoutItemMap *referenceMap=nullptr, const QRectF &exportRegion=QRectF(), double dpi=-1) const
Georeferences a file (image of PDF) exported from the layout.
QList< int > pages
List of specific pages to export, or an empty list to export all pages.
QgsLayoutExporter(QgsLayout *layout)
Constructor for QgsLayoutExporter, for the specified layout.
Base class for feedback objects to be used for cancelation of something running in a worker thread...
Definition: qgsfeedback.h:44
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
QImage renderRegionToImage(const QRectF &region, QSize imageSize=QSize(), double dpi=-1) const
Renders a region of the layout to an image.
QSizeF toQSizeF() const
Converts the layout size to a QSizeF.
Contains details of a page being exported by the class.
QSize imageSize
Manual size in pixels for output image.
QgsRectangle extent() const
Returns the current map extent.
Layout graphical items for displaying a map.
QgsLayoutItemPage * page(int pageNumber)
Returns a specific page (by pageNumber) from the collection.
This class provides a method of storing measurements for use in QGIS layouts using a variety of diffe...
virtual QString generateFileName(const PageExportDetails &details) const
Generates the file name for a page during export.
bool exportAsLayers
Set to true to export as a layered SVG file.
An abstract base class for QgsLayout based classes which can be exported by QgsLayoutExporter.
double dpi
Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.
QgsLayoutPageCollection * pageCollection()
Returns a pointer to the layout&#39;s page collection, which stores and manages page items in the layout...
Definition: qgslayout.cpp:455
QgsProjectMetadata metadata
Definition: qgsproject.h:97
void computeWorldFileParameters(double &a, double &b, double &c, double &d, double &e, double &f, double dpi=-1) const
Compute world file parameters.
double bottom() const
Returns the bottom margin.
Definition: qgsmargins.h:90
QgsLayoutRenderContext::Flags flags() const
Returns the current combination of flags used for rendering the layout.
double dpi() const
Returns the dpi for outputting the layout.
double width() const
Returns the width of the rectangle.
Definition: qgsrectangle.h:201
bool rasterizeWholeImage
Set to true to force whole layout to be rasterized while exporting.
double dpi
Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.
bool forceVectorOutput
Set to true to force vector object exports, even when the resultant appearance will differ from the l...
virtual int numberExportLayers() const
Returns the number of layers that this item requires for exporting during layered exports (e...
virtual int count()=0
Returns the number of features to iterate over.
QDateTime creationDateTime() const
Returns the project&#39;s creation date/timestamp.
QString extension
File suffix/extension (without the leading &#39;.&#39;)
virtual QgsLayout * layout()=0
Returns the layout associated with the iterator.
double top() const
Returns the top margin.
Definition: qgsmargins.h:78
Reads and writes project states.
Definition: qgsproject.h:85
QString baseName
Base part of filename (i.e. file name without extension or &#39;.&#39;)
Could not create layered SVG file.
int page() const
Returns the page the item is currently on, with the first page returning 0.
ExportResult exportToSvg(const QString &filePath, const QgsLayoutExporter::SvgExportSettings &settings)
Exports the layout as an SVG to the filePath, using the specified export settings.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:49
QString abstract() const
Returns a free-form description of the resource.
double x
Definition: qgspointxy.h:47
Use antialiasing when drawing items.
double yMinimum() const
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:176
Handles rendering and exports of layouts to various formats.
Contains settings relating to exporting layouts to PDF.
bool generateWorldFile
Set to true to generate an external world file alongside exported images.
double mapRotation(QgsLayoutObject::PropertyValueType valueType=QgsLayoutObject::EvaluatedValue) const
Returns the rotation used for drawing the map within the layout item, in degrees clockwise.
Force output in vector format where possible, even if items require rasterization to keep their corre...
const QgsLayoutMeasurementConverter & measurementConverter() const
Returns the layout measurement converter to be used in the layout.
Contains settings relating to exporting layouts to raster images.
static ExportResult exportToPdfs(QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QgsLayoutExporter::PdfExportSettings &settings, QString &error, QgsFeedback *feedback=nullptr)
Exports a layout iterator to multiple PDF files, with the specified export settings.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
QgsCoordinateReferenceSystem crs() const
Returns coordinate reference system used for rendering the map.
Enable advanced effects such as blend modes.
double dpi
Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.
ExportResult exportToPdf(const QString &filePath, const QgsLayoutExporter::PdfExportSettings &settings)
Exports the layout as a PDF to the filePath, using the specified export settings. ...
QMap< QString, QStringList > KeywordMap
Map of vocabulary string to keyword list.
static void fixEngineFlags(QPaintEngine *engine)
QString toWkt() const
Returns a WKT representation of this CRS.
int page
Page number, where 0 = first page.
double xMinimum() const
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:166
bool exportMetadata
Indicates whether PDF export should include metadata generated from the layout&#39;s project&#39;s metadata...
double yMaximum() const
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:171
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.
Definition: qgsogrutils.h:134
bool exportMetadata
Indicates whether image export should include metadata generated from the layout&#39;s project&#39;s metadata...
Could not start printing to destination device.
QImage renderPageToImage(int page, QSize imageSize=QSize(), double dpi=-1) const
Renders a full page to an image.
QgsPointXY center() const
Returns the center point of the rectangle.
Definition: qgsrectangle.h:229
bool cropToContents
Set to true if image should be cropped so only parts of the layout containing items are exported...
This class provides a method of storing sizes, consisting of a width and height, for use in QGIS layo...
Definition: qgslayoutsize.h:40
Interface for master layout type objects, such as print layouts and reports.
double dpi
Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.
Contains settings relating to exporting layouts to SVG.
virtual bool beginRender()=0
Called when rendering begins, before iteration commences.
double left() const
Returns the left margin.
Definition: qgsmargins.h:72
QString identifier() const
A reference, URI, URL or some other mechanism to identify the resource.
Export was successful.
QgsLayout * layout() const
Returns the layout linked to this exporter.
virtual QString filePath(const QString &baseFilePath, const QString &extension)=0
Returns the file path for the current feature, based on a specified base file path and extension...
double height() const
Returns the height of the rectangle.
Definition: qgsrectangle.h:208
Item representing the paper in a layout.
QgsLayoutRenderContext::Flags flags
Layout context flags, which control how the export will be created.
A structured metadata store for a map layer.
ExportResult
Result codes for exporting layouts.
void renderPage(QPainter *painter, int page) const
Renders a full page to a destination painter.
Error iterating over layout.