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