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