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