From fb712d7d65e8991bb11078c1e25961d18f564fa6 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 5 Dec 2017 15:28:41 +1000 Subject: [PATCH 01/56] Decouple QgsLayoutExporter from QgsLayout --- src/core/layout/qgslayout.cpp | 6 ------ src/core/layout/qgslayout.h | 7 ------- src/core/qgsmultirenderchecker.cpp | 3 ++- tests/src/python/qgslayoutchecker.py | 5 +++-- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index f26243fe1271..253669e1cfa3 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -32,7 +32,6 @@ QgsLayout::QgsLayout( QgsProject *project ) , mGridSettings( this ) , mPageCollection( new QgsLayoutPageCollection( this ) ) , mUndoStack( new QgsLayoutUndoStack( this ) ) - , mExporter( QgsLayoutExporter( this ) ) { // just to make sure - this should be the default, but maybe it'll change in some future Qt version... setBackgroundBrush( Qt::NoBrush ); @@ -112,11 +111,6 @@ QgsLayoutModel *QgsLayout::itemsModel() return mItemsModel.get(); } -QgsLayoutExporter &QgsLayout::exporter() -{ - return mExporter; -} - void QgsLayout::setName( const QString &name ) { mName = name; diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index e4b4938bf463..40991d3d5884 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -93,12 +93,6 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext */ QgsLayoutModel *itemsModel(); - /** - * Returns the layout's exporter, which is used for rendering the layout and exporting - * to various formats. - */ - QgsLayoutExporter &exporter(); - /** * Returns the layout's name. * \see setName() @@ -608,7 +602,6 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext std::unique_ptr< QgsLayoutPageCollection > mPageCollection; std::unique_ptr< QgsLayoutUndoStack > mUndoStack; - QgsLayoutExporter mExporter; //! List of multiframe objects QList mMultiFrames; diff --git a/src/core/qgsmultirenderchecker.cpp b/src/core/qgsmultirenderchecker.cpp index a8d8f24a8f24..6116912f5e6f 100644 --- a/src/core/qgsmultirenderchecker.cpp +++ b/src/core/qgsmultirenderchecker.cpp @@ -217,7 +217,8 @@ bool QgsLayoutChecker::testLayout( QString &checkedReport, int page, int pixelDi outputImage.setDotsPerMeterY( mDotsPerMeter ); drawBackground( &outputImage ); QPainter p( &outputImage ); - mLayout->exporter().renderPage( &p, page ); + QgsLayoutExporter exporter( mLayout ); + exporter.renderPage( &p, page ); p.end(); QString renderedFilePath = QDir::tempPath() + '/' + QFileInfo( mTestName ).baseName() + "_rendered.png"; diff --git a/tests/src/python/qgslayoutchecker.py b/tests/src/python/qgslayoutchecker.py index b34e8a73d8f3..ab51c6d0a337 100644 --- a/tests/src/python/qgslayoutchecker.py +++ b/tests/src/python/qgslayoutchecker.py @@ -19,7 +19,7 @@ from qgis.PyQt.QtCore import QSize, QDir, QFileInfo from qgis.PyQt.QtGui import QImage, QPainter -from qgis.core import QgsMultiRenderChecker, QgsLayout +from qgis.core import QgsMultiRenderChecker, QgsLayoutExporter class QgsLayoutChecker(QgsMultiRenderChecker): @@ -47,7 +47,8 @@ def testLayout(self, page=0, pixelDiff=0): outputImage.setDotsPerMeterY(self.dots_per_meter) QgsMultiRenderChecker.drawBackground(outputImage) p = QPainter(outputImage) - self.layout.exporter().renderPage(p, page) + exporter = QgsLayoutExporter(self.layout) + exporter.renderPage(p, page) p.end() renderedFilePath = QDir.tempPath() + QDir.separator() + QFileInfo(self.test_name).baseName() + "_rendered.png" From c496b3bcca4d6e162531197ce7fc6953a77517ae Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 5 Dec 2017 15:55:48 +1000 Subject: [PATCH 02/56] Forward declare some classes --- src/app/layout/qgslayoutaddpagesdialog.cpp | 1 + src/app/layout/qgslayoutappmenuprovider.cpp | 2 ++ src/app/layout/qgslayoutdesignerdialog.cpp | 2 ++ src/app/layout/qgslayoutguidewidget.cpp | 2 ++ src/app/layout/qgslayouthtmlwidget.cpp | 1 + src/app/layout/qgslayoutmapwidget.cpp | 1 + src/app/layout/qgslayoutpagepropertieswidget.cpp | 2 ++ src/app/layout/qgslayoutpolygonwidget.cpp | 1 + src/app/layout/qgslayoutpolylinewidget.cpp | 1 + src/app/layout/qgslayoutpropertieswidget.cpp | 1 + src/core/layout/qgslayout.cpp | 1 + src/core/layout/qgslayout.h | 4 ++-- src/core/layout/qgslayoutaligner.cpp | 1 + src/core/layout/qgslayoutexporter.cpp | 2 ++ src/core/layout/qgslayoutgridsettings.cpp | 2 ++ src/core/layout/qgslayoutguidecollection.cpp | 2 ++ src/core/layout/qgslayoutitem.cpp | 2 ++ src/core/layout/qgslayoutitemgroup.cpp | 2 ++ src/core/layout/qgslayoutitempage.cpp | 2 ++ src/core/layout/qgslayoutitemundocommand.cpp | 1 + src/core/layout/qgslayoutmultiframe.cpp | 2 ++ src/core/layout/qgslayoutpagecollection.cpp | 2 ++ src/core/layout/qgslayoutpagecollection.h | 2 ++ src/core/layout/qgslayoutsnapper.cpp | 1 + src/core/layout/qgslayouttable.cpp | 1 + src/gui/layout/qgslayoutitemwidget.cpp | 1 + src/gui/layout/qgslayoutmousehandles.cpp | 1 + src/gui/layout/qgslayoutnewitempropertiesdialog.cpp | 2 +- src/gui/layout/qgslayoutruler.cpp | 1 + src/gui/layout/qgslayoutview.cpp | 2 ++ src/gui/layout/qgslayoutviewtooladditem.cpp | 1 + src/gui/layout/qgslayoutviewtooleditnodes.cpp | 1 + src/gui/layout/qgslayoutviewtoolmoveitemcontent.cpp | 1 + tests/src/core/testqgslayout.cpp | 2 ++ tests/src/core/testqgslayoutitem.cpp | 2 ++ tests/src/core/testqgslayoutitemgroup.cpp | 1 + tests/src/core/testqgslayoutmultiframe.cpp | 2 ++ tests/src/core/testqgslayoutpage.cpp | 1 + 38 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/app/layout/qgslayoutaddpagesdialog.cpp b/src/app/layout/qgslayoutaddpagesdialog.cpp index da64823feedb..73aa578006c3 100644 --- a/src/app/layout/qgslayoutaddpagesdialog.cpp +++ b/src/app/layout/qgslayoutaddpagesdialog.cpp @@ -18,6 +18,7 @@ #include "qgssettings.h" #include "qgslayout.h" #include "qgslayoutmeasurementconverter.h" +#include "qgslayoutpagecollection.h" QgsLayoutAddPagesDialog::QgsLayoutAddPagesDialog( QWidget *parent, Qt::WindowFlags flags ) : QDialog( parent, flags ) diff --git a/src/app/layout/qgslayoutappmenuprovider.cpp b/src/app/layout/qgslayoutappmenuprovider.cpp index 972a47c086f3..179e996a17fa 100644 --- a/src/app/layout/qgslayoutappmenuprovider.cpp +++ b/src/app/layout/qgslayoutappmenuprovider.cpp @@ -18,6 +18,8 @@ #include "qgslayoutitemgroup.h" #include "qgslayoutdesignerdialog.h" #include "qgslayout.h" +#include "qgslayoutundostack.h" +#include "qgslayoutpagecollection.h" #include #include diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index b3c5837c86e4..68bb18b1b29c 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -48,6 +48,8 @@ #include "qgslayoutitemslistview.h" #include "qgsproject.h" #include "qgsbusyindicatordialog.h" +#include "qgslayoutundostack.h" +#include "qgslayoutpagecollection.h" #include #include #include diff --git a/src/app/layout/qgslayoutguidewidget.cpp b/src/app/layout/qgslayoutguidewidget.cpp index 7151794d6ad7..fdbdc706d894 100644 --- a/src/app/layout/qgslayoutguidewidget.cpp +++ b/src/app/layout/qgslayoutguidewidget.cpp @@ -19,6 +19,8 @@ #include "qgslayoutview.h" #include "qgsdoublespinbox.h" #include "qgslayoutunitscombobox.h" +#include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" QgsLayoutGuideWidget::QgsLayoutGuideWidget( QWidget *parent, QgsLayout *layout, QgsLayoutView *layoutView ) : QgsPanelWidget( parent ) diff --git a/src/app/layout/qgslayouthtmlwidget.cpp b/src/app/layout/qgslayouthtmlwidget.cpp index 182013a485cb..a0b67b6fe9ec 100644 --- a/src/app/layout/qgslayouthtmlwidget.cpp +++ b/src/app/layout/qgslayouthtmlwidget.cpp @@ -20,6 +20,7 @@ #include "qgscodeeditorhtml.h" #include "qgscodeeditorcss.h" #include "qgssettings.h" +#include "qgslayoutundostack.h" #include diff --git a/src/app/layout/qgslayoutmapwidget.cpp b/src/app/layout/qgslayoutmapwidget.cpp index 42caa66209a7..fb57b8d57784 100644 --- a/src/app/layout/qgslayoutmapwidget.cpp +++ b/src/app/layout/qgslayoutmapwidget.cpp @@ -28,6 +28,7 @@ #include "qgssymbollayerutils.h" #include "qgslayoutmapgridwidget.h" #include "qgsstyle.h" +#include "qgslayoutundostack.h" #include #include diff --git a/src/app/layout/qgslayoutpagepropertieswidget.cpp b/src/app/layout/qgslayoutpagepropertieswidget.cpp index 449900ebaae7..3518998222db 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.cpp +++ b/src/app/layout/qgslayoutpagepropertieswidget.cpp @@ -18,6 +18,8 @@ #include "qgspagesizeregistry.h" #include "qgslayoutitempage.h" #include "qgslayout.h" +#include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" QgsLayoutPagePropertiesWidget::QgsLayoutPagePropertiesWidget( QWidget *parent, QgsLayoutItem *layoutItem ) : QgsLayoutItemBaseWidget( parent, layoutItem ) diff --git a/src/app/layout/qgslayoutpolygonwidget.cpp b/src/app/layout/qgslayoutpolygonwidget.cpp index a89cc77052c9..45aaa420506d 100644 --- a/src/app/layout/qgslayoutpolygonwidget.cpp +++ b/src/app/layout/qgslayoutpolygonwidget.cpp @@ -20,6 +20,7 @@ #include "qgslayout.h" #include "qgssymbollayerutils.h" #include "qgslayoutitemregistry.h" +#include "qgslayoutundostack.h" QgsLayoutPolygonWidget::QgsLayoutPolygonWidget( QgsLayoutItemPolygon *polygon ) : QgsLayoutItemBaseWidget( nullptr, polygon ) diff --git a/src/app/layout/qgslayoutpolylinewidget.cpp b/src/app/layout/qgslayoutpolylinewidget.cpp index 35a3478fb5aa..dfc675c5d95b 100644 --- a/src/app/layout/qgslayoutpolylinewidget.cpp +++ b/src/app/layout/qgslayoutpolylinewidget.cpp @@ -20,6 +20,7 @@ #include "qgssymbollayerutils.h" #include "qgslayoutitemregistry.h" #include "qgslayout.h" +#include "qgslayoutundostack.h" #include QgsLayoutPolylineWidget::QgsLayoutPolylineWidget( QgsLayoutItemPolyline *polyline ) diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index 554d39ff8264..2426d27446dd 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -17,6 +17,7 @@ #include "qgslayoutpropertieswidget.h" #include "qgslayout.h" #include "qgslayoutsnapper.h" +#include "qgslayoutpagecollection.h" QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout *layout ) : QgsPanelWidget( parent ) diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 253669e1cfa3..4ca83b5a40ac 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -25,6 +25,7 @@ #include "qgslayoutitemgroup.h" #include "qgslayoutitemgroupundocommand.h" #include "qgslayoutmultiframe.h" +#include "qgslayoutundostack.h" QgsLayout::QgsLayout( QgsProject *project ) : mProject( project ) diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 40991d3d5884..43780a19de9e 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -21,15 +21,15 @@ #include "qgslayoutcontext.h" #include "qgslayoutsnapper.h" #include "qgsexpressioncontextgenerator.h" -#include "qgslayoutpagecollection.h" #include "qgslayoutgridsettings.h" #include "qgslayoutguidecollection.h" -#include "qgslayoutundostack.h" #include "qgslayoutexporter.h" class QgsLayoutItemMap; class QgsLayoutModel; class QgsLayoutMultiFrame; +class QgsLayoutPageCollection; +class QgsLayoutUndoStack; /** * \ingroup core diff --git a/src/core/layout/qgslayoutaligner.cpp b/src/core/layout/qgslayoutaligner.cpp index 2724b26739da..79ec7c6a12ca 100644 --- a/src/core/layout/qgslayoutaligner.cpp +++ b/src/core/layout/qgslayoutaligner.cpp @@ -17,6 +17,7 @@ #include "qgslayoutaligner.h" #include "qgslayoutitem.h" #include "qgslayout.h" +#include "qgslayoutundostack.h" void QgsLayoutAligner::alignItems( QgsLayout *layout, const QList &items, QgsLayoutAligner::Alignment alignment ) { diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index ef17711f27f5..475dc758e8c3 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -16,6 +16,8 @@ #include "qgslayoutexporter.h" #include "qgslayout.h" +#include "qgslayoutitemmap.h" +#include "qgslayoutpagecollection.h" QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout ) : mLayout( layout ) diff --git a/src/core/layout/qgslayoutgridsettings.cpp b/src/core/layout/qgslayoutgridsettings.cpp index 0d382ddaf28c..3b4f8e6c3ba8 100644 --- a/src/core/layout/qgslayoutgridsettings.cpp +++ b/src/core/layout/qgslayoutgridsettings.cpp @@ -18,6 +18,8 @@ #include "qgsreadwritecontext.h" #include "qgslayout.h" #include "qgsproject.h" +#include "qgslayoutundostack.h" +#include "qgslayoutpagecollection.h" QgsLayoutGridSettings::QgsLayoutGridSettings( QgsLayout *layout ) : mGridResolution( QgsLayoutMeasurement( 10 ) ) diff --git a/src/core/layout/qgslayoutguidecollection.cpp b/src/core/layout/qgslayoutguidecollection.cpp index abad410b639c..366e20498558 100644 --- a/src/core/layout/qgslayoutguidecollection.cpp +++ b/src/core/layout/qgslayoutguidecollection.cpp @@ -18,6 +18,8 @@ #include "qgslayout.h" #include "qgsproject.h" #include "qgsreadwritecontext.h" +#include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" #include diff --git a/src/core/layout/qgslayoutitem.cpp b/src/core/layout/qgslayoutitem.cpp index f9150b2925e3..f4e7fa285d7e 100644 --- a/src/core/layout/qgslayoutitem.cpp +++ b/src/core/layout/qgslayoutitem.cpp @@ -24,6 +24,8 @@ #include "qgslayoutitemgroup.h" #include "qgspainting.h" #include "qgslayouteffect.h" +#include "qgslayoutundostack.h" +#include "qgslayoutpagecollection.h" #include #include #include diff --git a/src/core/layout/qgslayoutitemgroup.cpp b/src/core/layout/qgslayoutitemgroup.cpp index da4e18546d57..9d8e630ecab5 100644 --- a/src/core/layout/qgslayoutitemgroup.cpp +++ b/src/core/layout/qgslayoutitemgroup.cpp @@ -18,6 +18,8 @@ #include "qgslayoutitemregistry.h" #include "qgslayout.h" #include "qgslayoututils.h" +#include "qgslayoutundostack.h" +#include "qgslayoutpagecollection.h" QgsLayoutItemGroup::QgsLayoutItemGroup( QgsLayout *layout ) : QgsLayoutItem( layout ) diff --git a/src/core/layout/qgslayoutitempage.cpp b/src/core/layout/qgslayoutitempage.cpp index fca287f1a193..c47b7f48833d 100644 --- a/src/core/layout/qgslayoutitempage.cpp +++ b/src/core/layout/qgslayoutitempage.cpp @@ -20,6 +20,8 @@ #include "qgspagesizeregistry.h" #include "qgssymbollayerutils.h" #include "qgslayoutitemundocommand.h" +#include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" #include #include diff --git a/src/core/layout/qgslayoutitemundocommand.cpp b/src/core/layout/qgslayoutitemundocommand.cpp index dae1dd7e3014..9869f97122e3 100644 --- a/src/core/layout/qgslayoutitemundocommand.cpp +++ b/src/core/layout/qgslayoutitemundocommand.cpp @@ -20,6 +20,7 @@ #include "qgsreadwritecontext.h" #include "qgslayout.h" #include "qgsproject.h" +#include "qgslayoutundostack.h" ///@cond PRIVATE QgsLayoutItemUndoCommand::QgsLayoutItemUndoCommand( QgsLayoutItem *item, const QString &text, int id, QUndoCommand *parent ) diff --git a/src/core/layout/qgslayoutmultiframe.cpp b/src/core/layout/qgslayoutmultiframe.cpp index dfcfb1fc8f00..58fe45a68ecc 100644 --- a/src/core/layout/qgslayoutmultiframe.cpp +++ b/src/core/layout/qgslayoutmultiframe.cpp @@ -17,6 +17,8 @@ #include "qgslayoutmultiframeundocommand.h" #include "qgslayoutframe.h" #include "qgslayout.h" +#include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" #include QgsLayoutMultiFrame::QgsLayoutMultiFrame( QgsLayout *layout ) diff --git a/src/core/layout/qgslayoutpagecollection.cpp b/src/core/layout/qgslayoutpagecollection.cpp index 252593bfbbf9..ebefd4141c05 100644 --- a/src/core/layout/qgslayoutpagecollection.cpp +++ b/src/core/layout/qgslayoutpagecollection.cpp @@ -20,6 +20,8 @@ #include "qgsproject.h" #include "qgslayoutitemundocommand.h" #include "qgssymbollayerutils.h" +#include "qgslayoutframe.h" +#include "qgslayoutundostack.h" QgsLayoutPageCollection::QgsLayoutPageCollection( QgsLayout *layout ) : QObject( layout ) diff --git a/src/core/layout/qgslayoutpagecollection.h b/src/core/layout/qgslayoutpagecollection.h index e97798cb0159..0f44d3dc91d6 100644 --- a/src/core/layout/qgslayoutpagecollection.h +++ b/src/core/layout/qgslayoutpagecollection.h @@ -20,7 +20,9 @@ #include "qgis_core.h" #include "qgis_sip.h" #include "qgssymbol.h" +#include "qgslayout.h" #include "qgslayoutitempage.h" +#include "qgslayoutitem.h" #include "qgslayoutserializableobject.h" #include #include diff --git a/src/core/layout/qgslayoutsnapper.cpp b/src/core/layout/qgslayoutsnapper.cpp index 89ab08c7eb38..a42804e10e96 100644 --- a/src/core/layout/qgslayoutsnapper.cpp +++ b/src/core/layout/qgslayoutsnapper.cpp @@ -18,6 +18,7 @@ #include "qgslayout.h" #include "qgsreadwritecontext.h" #include "qgsproject.h" +#include "qgslayoutpagecollection.h" QgsLayoutSnapper::QgsLayoutSnapper( QgsLayout *layout ) : mLayout( layout ) diff --git a/src/core/layout/qgslayouttable.cpp b/src/core/layout/qgslayouttable.cpp index 12303690d0e9..dac33081a9f8 100644 --- a/src/core/layout/qgslayouttable.cpp +++ b/src/core/layout/qgslayouttable.cpp @@ -23,6 +23,7 @@ #include "qgslayoutframe.h" #include "qgsfontutils.h" #include "qgssettings.h" +#include "qgslayoutpagecollection.h" // // QgsLayoutTableStyle diff --git a/src/gui/layout/qgslayoutitemwidget.cpp b/src/gui/layout/qgslayoutitemwidget.cpp index 61bdfdaa2a07..2535991855f5 100644 --- a/src/gui/layout/qgslayoutitemwidget.cpp +++ b/src/gui/layout/qgslayoutitemwidget.cpp @@ -17,6 +17,7 @@ #include "qgspropertyoverridebutton.h" #include "qgslayout.h" #include "qgsproject.h" +#include "qgslayoutundostack.h" // // QgsLayoutConfigObject diff --git a/src/gui/layout/qgslayoutmousehandles.cpp b/src/gui/layout/qgslayoutmousehandles.cpp index e2a1d116d67e..93668d353084 100644 --- a/src/gui/layout/qgslayoutmousehandles.cpp +++ b/src/gui/layout/qgslayoutmousehandles.cpp @@ -26,6 +26,7 @@ #include "qgslayoutviewtoolselect.h" #include "qgslayoutsnapper.h" #include "qgslayoutitemgroup.h" +#include "qgslayoutundostack.h" #include #include #include diff --git a/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp b/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp index a8e96acbbfb0..867769d6d38c 100644 --- a/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp +++ b/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp @@ -16,7 +16,7 @@ #include "qgslayoutnewitempropertiesdialog.h" #include "qgssettings.h" #include "qgslayout.h" - +#include "qgslayoutpagecollection.h" QgsLayoutItemPropertiesDialog::QgsLayoutItemPropertiesDialog( QWidget *parent, Qt::WindowFlags flags ) : QDialog( parent, flags ) diff --git a/src/gui/layout/qgslayoutruler.cpp b/src/gui/layout/qgslayoutruler.cpp index d2c6cebc7ee9..f682fd3b0a55 100644 --- a/src/gui/layout/qgslayoutruler.cpp +++ b/src/gui/layout/qgslayoutruler.cpp @@ -17,6 +17,7 @@ #include "qgis.h" #include "qgslayoutview.h" #include "qgslogger.h" +#include "qgslayoutpagecollection.h" #include #include #include diff --git a/src/gui/layout/qgslayoutview.cpp b/src/gui/layout/qgslayoutview.cpp index b3faf042a671..913db7d14628 100644 --- a/src/gui/layout/qgslayoutview.cpp +++ b/src/gui/layout/qgslayoutview.cpp @@ -31,6 +31,8 @@ #include "qgslayoutitemundocommand.h" #include "qgsproject.h" #include "qgslayoutitemgroup.h" +#include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" #include #include #include diff --git a/src/gui/layout/qgslayoutviewtooladditem.cpp b/src/gui/layout/qgslayoutviewtooladditem.cpp index 6e5f33e889b9..f54a79c8611b 100644 --- a/src/gui/layout/qgslayoutviewtooladditem.cpp +++ b/src/gui/layout/qgslayoutviewtooladditem.cpp @@ -25,6 +25,7 @@ #include "qgslayoutitemguiregistry.h" #include "qgslayoutnewitempropertiesdialog.h" #include "qgssettings.h" +#include "qgslayoutundostack.h" #include #include #include diff --git a/src/gui/layout/qgslayoutviewtooleditnodes.cpp b/src/gui/layout/qgslayoutviewtooleditnodes.cpp index eb995fa7877b..606aa917f604 100644 --- a/src/gui/layout/qgslayoutviewtooleditnodes.cpp +++ b/src/gui/layout/qgslayoutviewtooleditnodes.cpp @@ -18,6 +18,7 @@ #include "qgslayoutview.h" #include "qgslayout.h" #include "qgslayoutitemnodeitem.h" +#include "qgslayoutundostack.h" QgsLayoutViewToolEditNodes::QgsLayoutViewToolEditNodes( QgsLayoutView *view ) : QgsLayoutViewTool( view, tr( "Select" ) ) diff --git a/src/gui/layout/qgslayoutviewtoolmoveitemcontent.cpp b/src/gui/layout/qgslayoutviewtoolmoveitemcontent.cpp index f66c82876b41..e5a97015d3bf 100644 --- a/src/gui/layout/qgslayoutviewtoolmoveitemcontent.cpp +++ b/src/gui/layout/qgslayoutviewtoolmoveitemcontent.cpp @@ -19,6 +19,7 @@ #include "qgslayout.h" #include "qgslayoutitemnodeitem.h" #include "qgssettings.h" +#include "qgslayoutundostack.h" QgsLayoutViewToolMoveItemContent::QgsLayoutViewToolMoveItemContent( QgsLayoutView *view ) : QgsLayoutViewTool( view, tr( "Select" ) ) diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index 6dc7fe7f1039..4f504dfd500a 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -20,6 +20,8 @@ #include "qgsproject.h" #include "qgslayoutitemmap.h" #include "qgslayoutitemshape.h" +#include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" class TestQgsLayout: public QObject { diff --git a/tests/src/core/testqgslayoutitem.cpp b/tests/src/core/testqgslayoutitem.cpp index 93c8010123da..42ed5f41e5f5 100644 --- a/tests/src/core/testqgslayoutitem.cpp +++ b/tests/src/core/testqgslayoutitem.cpp @@ -27,6 +27,8 @@ #include "qgslayoutitemshape.h" #include "qgslayouteffect.h" #include "qgsfillsymbollayer.h" +#include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" #include #include #include diff --git a/tests/src/core/testqgslayoutitemgroup.cpp b/tests/src/core/testqgslayoutitemgroup.cpp index 055ff66c1252..3cd93c042c09 100644 --- a/tests/src/core/testqgslayoutitemgroup.cpp +++ b/tests/src/core/testqgslayoutitemgroup.cpp @@ -26,6 +26,7 @@ #include "qgslogger.h" #include "qgsproject.h" #include "qgsfillsymbollayer.h" +#include "qgslayoutundostack.h" #include #include diff --git a/tests/src/core/testqgslayoutmultiframe.cpp b/tests/src/core/testqgslayoutmultiframe.cpp index 4308b00196ff..f77a6a782033 100644 --- a/tests/src/core/testqgslayoutmultiframe.cpp +++ b/tests/src/core/testqgslayoutmultiframe.cpp @@ -23,6 +23,8 @@ #include "qgsapplication.h" #include "qgsproject.h" #include "qgslayoutitemhtml.h" +#include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" #include #include "qgstest.h" diff --git a/tests/src/core/testqgslayoutpage.cpp b/tests/src/core/testqgslayoutpage.cpp index 09bcbff51cdf..962a3a1f57a4 100644 --- a/tests/src/core/testqgslayoutpage.cpp +++ b/tests/src/core/testqgslayoutpage.cpp @@ -25,6 +25,7 @@ #include "qgsfillsymbollayer.h" #include "qgslinesymbollayer.h" #include "qgsmultirenderchecker.h" +#include "qgslayoutpagecollection.h" #include #include "qgstest.h" From a3c9dc39233fe8090293699c61a803d038f91c2a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 5 Dec 2017 15:56:33 +1000 Subject: [PATCH 03/56] Remember window position --- src/gui/layout/qgslayoutnewitempropertiesdialog.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp b/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp index 867769d6d38c..ffcd56365f56 100644 --- a/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp +++ b/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp @@ -17,12 +17,15 @@ #include "qgssettings.h" #include "qgslayout.h" #include "qgslayoutpagecollection.h" +#include "qgsgui.h" QgsLayoutItemPropertiesDialog::QgsLayoutItemPropertiesDialog( QWidget *parent, Qt::WindowFlags flags ) : QDialog( parent, flags ) { setupUi( this ); + QgsGui::instance()->enableAutoGeometryRestore( this ); + //make button exclusive QButtonGroup *buttonGroup = new QButtonGroup( this ); buttonGroup->addButton( mUpperLeftRadioButton ); From f3fcb68ec45fb1ab5ddee749870396e0ac5a5de4 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 5 Dec 2017 16:01:13 +1000 Subject: [PATCH 04/56] Port method to retrieve items of a set type on a page --- src/core/layout/qgslayoutpagecollection.h | 24 ++++++++++ tests/src/core/testqgslayout.cpp | 53 ++++++++++++++++++++--- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/core/layout/qgslayoutpagecollection.h b/src/core/layout/qgslayoutpagecollection.h index 0f44d3dc91d6..8f47f3c6fe51 100644 --- a/src/core/layout/qgslayoutpagecollection.h +++ b/src/core/layout/qgslayoutpagecollection.h @@ -106,6 +106,30 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri */ QList< QgsLayoutItem *> itemsOnPage( int page ) const; + /** + * Returns layout items of a specific type on a specified \a page. + * \note not available in Python bindings. + */ + template void itemsOnPage( QList &itemList, int page ) const SIP_SKIP + { + itemList.clear(); + const QList graphicsItemList = mLayout->items(); + for ( QGraphicsItem *graphicsItem : graphicsItemList ) + { + T *item = dynamic_cast( graphicsItem ); + if ( item && item->page() == page ) + { + itemList.push_back( item ); + } + } + } + + /** + * Returns whether the specified \a page number should be included in exports of the layouts. + * \see pageIsEmpty() + */ + bool shouldExportPage( int page ) const; + /** * Adds a \a page to the collection. Ownership of the \a page is transferred * to the collection, and the page will automatically be added to the collection's diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index 4f504dfd500a..b2fb086f14af 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -22,6 +22,8 @@ #include "qgslayoutitemshape.h" #include "qgslayoutpagecollection.h" #include "qgslayoutundostack.h" +#include "qgslayoutitemlabel.h" +#include "qgslayoutitempolyline.h" class TestQgsLayout: public QObject { @@ -521,13 +523,13 @@ void TestQgsLayout::itemsOnPage() page3->setPageSize( "A4" ); l.pageCollection()->addPage( page3 ); - QgsLayoutItemShape *label1 = new QgsLayoutItemShape( &l ); + QgsLayoutItemLabel *label1 = new QgsLayoutItemLabel( &l ); l.addLayoutItem( label1 ); label1->attemptMove( QgsLayoutPoint( 10, 10 ), true, false, 0 ); - QgsLayoutItemShape *label2 = new QgsLayoutItemShape( &l ); + QgsLayoutItemLabel *label2 = new QgsLayoutItemLabel( &l ); l.addLayoutItem( label2 ); label2->attemptMove( QgsLayoutPoint( 10, 10 ), true, false, 0 ); - QgsLayoutItemShape *label3 = new QgsLayoutItemShape( &l ); + QgsLayoutItemLabel *label3 = new QgsLayoutItemLabel( &l ); l.addLayoutItem( label3 ); label3->attemptMove( QgsLayoutPoint( 10, 10 ), true, false, 1 ); QgsLayoutItemShape *shape1 = new QgsLayoutItemShape( &l ); @@ -536,10 +538,10 @@ void TestQgsLayout::itemsOnPage() QgsLayoutItemShape *shape2 = new QgsLayoutItemShape( &l ); l.addLayoutItem( shape2 ); shape2->attemptMove( QgsLayoutPoint( 10, 10 ), true, false, 1 ); - QgsLayoutItemShape *arrow1 = new QgsLayoutItemShape( &l ); + QgsLayoutItemPolyline *arrow1 = new QgsLayoutItemPolyline( &l ); l.addLayoutItem( arrow1 ); arrow1->attemptMove( QgsLayoutPoint( 10, 10 ), true, false, 2 ); - QgsLayoutItemShape *arrow2 = new QgsLayoutItemShape( &l ); + QgsLayoutItemPolyline *arrow2 = new QgsLayoutItemPolyline( &l ); l.addLayoutItem( arrow2 ); arrow2->attemptMove( QgsLayoutPoint( 10, 10 ), true, false, 2 ); @@ -554,6 +556,40 @@ void TestQgsLayout::itemsOnPage() //should be 3 items on page 3 QCOMPARE( items.length(), 3 ); + //check fetching specific item types + QList labels; + l.pageCollection()->itemsOnPage( labels, 0 ); + //should be 2 labels on page 1 + QCOMPARE( labels.length(), 2 ); + l.pageCollection()->itemsOnPage( labels, 1 ); + //should be 1 label on page 2 + QCOMPARE( labels.length(), 1 ); + l.pageCollection()->itemsOnPage( labels, 2 ); + //should be no label on page 3 + QCOMPARE( labels.length(), 0 ); + + QList shapes; + l.pageCollection()->itemsOnPage( shapes, 0 ); + //should be 1 shapes on page 1 + QCOMPARE( shapes.length(), 1 ); + l.pageCollection()->itemsOnPage( shapes, 1 ); + //should be 1 shapes on page 2 + QCOMPARE( shapes.length(), 1 ); + l.pageCollection()->itemsOnPage( shapes, 2 ); + //should be no shapes on page 3 + QCOMPARE( shapes.length(), 0 ); + + QList arrows; + l.pageCollection()->itemsOnPage( arrows, 0 ); + //should be no arrows on page 1 + QCOMPARE( arrows.length(), 0 ); + l.pageCollection()->itemsOnPage( arrows, 1 ); + //should be no arrows on page 2 + QCOMPARE( arrows.length(), 0 ); + l.pageCollection()->itemsOnPage( arrows, 2 ); + //should be 2 arrows on page 3 + QCOMPARE( arrows.length(), 2 ); + l.removeLayoutItem( label1 ); l.removeLayoutItem( label2 ); l.removeLayoutItem( label3 ); @@ -569,6 +605,13 @@ void TestQgsLayout::itemsOnPage() QCOMPARE( items.length(), 1 ); items = l.pageCollection()->itemsOnPage( 2 ); QCOMPARE( items.length(), 1 ); + + l.pageCollection()->itemsOnPage( labels, 0 ); + QCOMPARE( labels.length(), 0 ); + l.pageCollection()->itemsOnPage( labels, 1 ); + QCOMPARE( labels.length(), 0 ); + l.pageCollection()->itemsOnPage( labels, 2 ); + QCOMPARE( labels.length(), 0 ); } void TestQgsLayout::pageIsEmpty() From 5828f5d343132fd18b6f2e5f0a803fe34baa6c62 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 5 Dec 2017 16:14:38 +1000 Subject: [PATCH 05/56] Port method to determine whether page should be exported --- src/core/layout/qgslayoutpagecollection.cpp | 22 ++++++++++ src/core/layout/qgslayoutpagecollection.h | 1 + tests/src/core/testqgslayout.cpp | 47 +++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/src/core/layout/qgslayoutpagecollection.cpp b/src/core/layout/qgslayoutpagecollection.cpp index ebefd4141c05..1b09511fb2a0 100644 --- a/src/core/layout/qgslayoutpagecollection.cpp +++ b/src/core/layout/qgslayoutpagecollection.cpp @@ -368,6 +368,28 @@ QList QgsLayoutPageCollection::itemsOnPage( int page ) const return itemList; } +bool QgsLayoutPageCollection::shouldExportPage( int page ) const +{ + if ( page >= mPages.count() || page < 0 ) + { + //page number out of range, of course we shouldn't export it - stop smoking crack! + return false; + } + + //check all frame items on page + QList frames; + itemsOnPage( frames, page ); + for ( QgsLayoutFrame *frame : qgis::as_const( frames ) ) + { + if ( frame->hidePageIfEmpty() && frame->isEmpty() ) + { + //frame is set to hide page if empty, and frame is empty, so we don't want to export this page + return false; + } + } + return true; +} + void QgsLayoutPageCollection::addPage( QgsLayoutItemPage *page ) { if ( !mBlockUndoCommands ) diff --git a/src/core/layout/qgslayoutpagecollection.h b/src/core/layout/qgslayoutpagecollection.h index 8f47f3c6fe51..579da36eeed6 100644 --- a/src/core/layout/qgslayoutpagecollection.h +++ b/src/core/layout/qgslayoutpagecollection.h @@ -98,6 +98,7 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri /** * Returns whether a given \a page index is empty, ie, it contains no items except for the background * paper item. + * \see shouldExportPage() */ bool pageIsEmpty( int page ) const; diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index b2fb086f14af..0b4c5945837b 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -24,6 +24,8 @@ #include "qgslayoutundostack.h" #include "qgslayoutitemlabel.h" #include "qgslayoutitempolyline.h" +#include "qgslayoutitemhtml.h" +#include "qgslayoutframe.h" class TestQgsLayout: public QObject { @@ -47,6 +49,7 @@ class TestQgsLayout: public QObject void layoutItemByUuid(); void undoRedoOccurred(); void itemsOnPage(); //test fetching matching items on a set page + void shouldExportPage(); void pageIsEmpty(); void clear(); @@ -614,6 +617,50 @@ void TestQgsLayout::itemsOnPage() QCOMPARE( labels.length(), 0 ); } +void TestQgsLayout::shouldExportPage() +{ + QgsProject proj; + QgsLayout l( &proj ); + QgsLayoutItemPage *page = new QgsLayoutItemPage( &l ); + page->setPageSize( "A4" ); + l.pageCollection()->addPage( page ); + QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); + page2->setPageSize( "A4" ); + l.pageCollection()->addPage( page2 ); + + QgsLayoutItemHtml *htmlItem = new QgsLayoutItemHtml( &l ); + //frame on page 1 + QgsLayoutFrame *frame1 = new QgsLayoutFrame( &l, htmlItem ); + frame1->attemptSetSceneRect( QRectF( 0, 0, 100, 100 ) ); + //frame on page 2 + QgsLayoutFrame *frame2 = new QgsLayoutFrame( &l, htmlItem ); + frame2->attemptSetSceneRect( QRectF( 0, 320, 100, 100 ) ); + frame2->setHidePageIfEmpty( true ); + htmlItem->addFrame( frame1 ); + htmlItem->addFrame( frame2 ); + htmlItem->setContentMode( QgsLayoutItemHtml::ManualHtml ); + //short content, so frame 2 should be empty + htmlItem->setHtml( QStringLiteral( "

Test manual html

" ) ); + htmlItem->loadHtml(); + + QVERIFY( l.pageCollection()->shouldExportPage( 0 ) ); + QVERIFY( !l.pageCollection()->shouldExportPage( 1 ) ); + + //long content, so frame 2 should not be empty + htmlItem->setHtml( QStringLiteral( "

Test manual html

" ) ); + htmlItem->loadHtml(); + + QVERIFY( l.pageCollection()->shouldExportPage( 0 ) ); + QVERIFY( l.pageCollection()->shouldExportPage( 1 ) ); + + //...and back again... + htmlItem->setHtml( QStringLiteral( "

Test manual html

" ) ); + htmlItem->loadHtml(); + + QVERIFY( l.pageCollection()->shouldExportPage( 0 ) ); + QVERIFY( !l.pageCollection()->shouldExportPage( 1 ) ); +} + void TestQgsLayout::pageIsEmpty() { QgsProject proj; From ecfacdf2fc6992252f261354c85b0b8d8baaafbc Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 6 Dec 2017 07:47:47 +1000 Subject: [PATCH 06/56] Avoid Qt warning --- src/gui/layout/qgslayoutviewtoolselect.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/layout/qgslayoutviewtoolselect.cpp b/src/gui/layout/qgslayoutviewtoolselect.cpp index d8534fb9690c..301787c62eb6 100644 --- a/src/gui/layout/qgslayoutviewtoolselect.cpp +++ b/src/gui/layout/qgslayoutviewtoolselect.cpp @@ -287,7 +287,8 @@ QgsLayoutMouseHandles *QgsLayoutViewToolSelect::mouseHandles() void QgsLayoutViewToolSelect::setLayout( QgsLayout *layout ) { // existing handles are owned by previous layout - mMouseHandles->deleteLater(); + if ( mMouseHandles ) + mMouseHandles->deleteLater(); //add mouse selection handles to layout, and initially hide mMouseHandles = new QgsLayoutMouseHandles( layout, view() ); From f4f5f75b807d82f48d7e3f5e737ad2775254360d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 6 Dec 2017 07:48:22 +1000 Subject: [PATCH 07/56] Remove incorrect TransferThis annotation --- python/core/layout/qgslayoutitempage.sip | 2 +- src/core/layout/qgslayoutitempage.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/core/layout/qgslayoutitempage.sip b/python/core/layout/qgslayoutitempage.sip index 835355d09ace..47641e68ddaa 100644 --- a/python/core/layout/qgslayoutitempage.sip +++ b/python/core/layout/qgslayoutitempage.sip @@ -34,7 +34,7 @@ class QgsLayoutItemPage : QgsLayoutItem UndoPageSymbol, }; - explicit QgsLayoutItemPage( QgsLayout *layout /TransferThis/ ); + explicit QgsLayoutItemPage( QgsLayout *layout ); %Docstring Constructor for QgsLayoutItemPage, with the specified parent ``layout``. %End diff --git a/src/core/layout/qgslayoutitempage.h b/src/core/layout/qgslayoutitempage.h index 742edcdc0173..4aa90f160fe3 100644 --- a/src/core/layout/qgslayoutitempage.h +++ b/src/core/layout/qgslayoutitempage.h @@ -74,7 +74,7 @@ class CORE_EXPORT QgsLayoutItemPage : public QgsLayoutItem /** * Constructor for QgsLayoutItemPage, with the specified parent \a layout. */ - explicit QgsLayoutItemPage( QgsLayout *layout SIP_TRANSFERTHIS ); + explicit QgsLayoutItemPage( QgsLayout *layout ); /** * Returns a new page item for the specified \a layout. From fe5bd47eb06f38676ff4481dfbbc0acc06d42d0c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 6 Dec 2017 07:49:07 +1000 Subject: [PATCH 08/56] Work on resizing layouts to item bounds --- .../core/layout/qgslayoutpagecollection.sip | 10 ++ src/app/layout/qgslayoutpropertieswidget.cpp | 49 ++++++ src/app/layout/qgslayoutpropertieswidget.h | 2 + src/core/layout/qgslayoutpagecollection.cpp | 71 +++++++++ src/core/layout/qgslayoutpagecollection.h | 10 ++ src/ui/layout/qgslayoutwidgetbase.ui | 146 ++++++++++++++++-- tests/src/core/testqgslayout.cpp | 58 +++---- 7 files changed, 308 insertions(+), 38 deletions(-) diff --git a/python/core/layout/qgslayoutpagecollection.sip b/python/core/layout/qgslayoutpagecollection.sip index 42235f0f3f39..adef181b59da 100644 --- a/python/core/layout/qgslayoutpagecollection.sip +++ b/python/core/layout/qgslayoutpagecollection.sip @@ -282,6 +282,16 @@ Returns the space between pages, in layout units. Returns the size of the page shadow, in layout units. %End + void resizeToContents( const QgsMargins &margins, QgsUnitTypes::LayoutUnit marginUnits ); +%Docstring + Resizes the layout to a single page which fits the current contents of the layout. + + Calling this method resets the number of pages to 1, with the size set to the + minimum size required to fit all existing layout items. Items will also be + repositioned so that the new top-left bounds of the layout is at the point + (marginLeft, marginTop). An optional margin can be specified. +%End + virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const; %Docstring diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index 2426d27446dd..76e91c403e7f 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -18,11 +18,14 @@ #include "qgslayout.h" #include "qgslayoutsnapper.h" #include "qgslayoutpagecollection.h" +#include "qgslayoutundostack.h" QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout *layout ) : QgsPanelWidget( parent ) , mLayout( layout ) { + Q_ASSERT( mLayout ); + setupUi( this ); setPanelTitle( tr( "Layout properties" ) ); blockSignals( true ); @@ -42,6 +45,30 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout connect( mGridResolutionSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::gridResolutionChanged ); connect( mOffsetXSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::gridOffsetXChanged ); connect( mOffsetYSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::gridOffsetYChanged ); + + double leftMargin = mLayout->customProperty( QStringLiteral( "resizeToContentsLeftMargin" ) ).toDouble(); + double topMargin = mLayout->customProperty( QStringLiteral( "resizeToContentsTopMargin" ) ).toDouble(); + double bottomMargin = mLayout->customProperty( QStringLiteral( "resizeToContentsBottomMargin" ) ).toDouble(); + double rightMargin = mLayout->customProperty( QStringLiteral( "resizeToContentsRightMargin" ) ).toDouble(); + QgsUnitTypes::LayoutUnit marginUnit = static_cast< QgsUnitTypes::LayoutUnit >( + mLayout->customProperty( QStringLiteral( "imageCropMarginUnit" ), QgsUnitTypes::LayoutMillimeters ).toInt() ); + + mTopMarginSpinBox->setValue( topMargin ); + mMarginUnitsComboBox->linkToWidget( mTopMarginSpinBox ); + mRightMarginSpinBox->setValue( rightMargin ); + mMarginUnitsComboBox->linkToWidget( mRightMarginSpinBox ); + mBottomMarginSpinBox->setValue( bottomMargin ); + mMarginUnitsComboBox->linkToWidget( mBottomMarginSpinBox ); + mLeftMarginSpinBox->setValue( leftMargin ); + mMarginUnitsComboBox->linkToWidget( mLeftMarginSpinBox ); + mMarginUnitsComboBox->setUnit( marginUnit ); + mMarginUnitsComboBox->setConverter( &mLayout->context().measurementConverter() ); + + connect( mTopMarginSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::resizeMarginsChanged ); + connect( mRightMarginSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::resizeMarginsChanged ); + connect( mBottomMarginSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::resizeMarginsChanged ); + connect( mLeftMarginSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::resizeMarginsChanged ); + connect( mResizePageButton, &QPushButton::clicked, this, &QgsLayoutPropertiesWidget::resizeToContents ); } void QgsLayoutPropertiesWidget::updateSnappingElements() @@ -101,6 +128,28 @@ void QgsLayoutPropertiesWidget::snapToleranceChanged( int tolerance ) mLayout->snapper().setSnapTolerance( tolerance ); } +void QgsLayoutPropertiesWidget::resizeMarginsChanged() +{ + mLayout->setCustomProperty( QStringLiteral( "resizeToContentsLeftMargin" ), mLeftMarginSpinBox->value() ); + mLayout->setCustomProperty( QStringLiteral( "resizeToContentsTopMargin" ), mTopMarginSpinBox->value() ); + mLayout->setCustomProperty( QStringLiteral( "resizeToContentsBottomMargin" ), mBottomMarginSpinBox->value() ); + mLayout->setCustomProperty( QStringLiteral( "resizeToContentsRightMargin" ), mRightMarginSpinBox->value() ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginUnit" ), mMarginUnitsComboBox->unit() ); +} + +void QgsLayoutPropertiesWidget::resizeToContents() +{ + mLayout->undoStack()->beginMacro( tr( "Resize to Contents" ) ); + + mLayout->pageCollection()->resizeToContents( QgsMargins( mLeftMarginSpinBox->value(), + mTopMarginSpinBox->value(), + mRightMarginSpinBox->value(), + mBottomMarginSpinBox->value() ), + mMarginUnitsComboBox->unit() ); + + mLayout->undoStack()->endMacro(); +} + void QgsLayoutPropertiesWidget::blockSignals( bool block ) { mGridResolutionSpinBox->blockSignals( block ); diff --git a/src/app/layout/qgslayoutpropertieswidget.h b/src/app/layout/qgslayoutpropertieswidget.h index f44f30d3fda9..01153a9d1e08 100644 --- a/src/app/layout/qgslayoutpropertieswidget.h +++ b/src/app/layout/qgslayoutpropertieswidget.h @@ -36,6 +36,8 @@ class QgsLayoutPropertiesWidget: public QgsPanelWidget, private Ui::QgsLayoutWid void gridOffsetYChanged( double d ); void gridOffsetUnitsChanged( QgsUnitTypes::LayoutUnit unit ); void snapToleranceChanged( int tolerance ); + void resizeMarginsChanged(); + void resizeToContents(); private: diff --git a/src/core/layout/qgslayoutpagecollection.cpp b/src/core/layout/qgslayoutpagecollection.cpp index 1b09511fb2a0..437b73a6769d 100644 --- a/src/core/layout/qgslayoutpagecollection.cpp +++ b/src/core/layout/qgslayoutpagecollection.cpp @@ -199,6 +199,77 @@ double QgsLayoutPageCollection::pageShadowWidth() const return spaceBetweenPages() / 2; } +void QgsLayoutPageCollection::resizeToContents( const QgsMargins &margins, QgsUnitTypes::LayoutUnit marginUnits ) +{ + if ( !mBlockUndoCommands ) + mLayout->undoStack()->beginCommand( this, tr( "Resize to Contents" ) ); + + //calculate current bounds + QRectF bounds = mLayout->layoutBounds( true, 0.0 ); + + for ( int page = mPages.count() - 1; page > 0; page-- ) + { + deletePage( page ); + } + + if ( mPages.empty() ) + { + std::unique_ptr< QgsLayoutItemPage > page = qgis::make_unique< QgsLayoutItemPage >( mLayout ); + addPage( page.release() ); + } + + QgsLayoutItemPage *page = mPages.at( 0 ); + + double marginLeft = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( margins.left(), marginUnits ) ); + double marginTop = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( margins.top(), marginUnits ) ); + double marginBottom = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( margins.bottom(), marginUnits ) ); + double marginRight = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( margins.right(), marginUnits ) ); + + bounds.setWidth( bounds.width() + marginLeft + marginRight ); + bounds.setHeight( bounds.height() + marginTop + marginBottom ); + + QgsLayoutSize newPageSize = mLayout->convertFromLayoutUnits( bounds.size(), mLayout->units() ); + page->setPageSize( newPageSize ); + + reflow(); + + //also move all items so that top-left of bounds is at marginLeft, marginTop + double diffX = marginLeft - bounds.left(); + double diffY = marginTop - bounds.top(); + + const QList itemList = mLayout->items(); + for ( QGraphicsItem *item : itemList ) + { + if ( QgsLayoutItem *layoutItem = dynamic_cast( item ) ) + { + QgsLayoutItemPage *pageItem = dynamic_cast( layoutItem ); + if ( !pageItem ) + { + layoutItem->beginCommand( tr( "Move Item" ) ); + layoutItem->attemptMoveBy( diffX, diffY ); + layoutItem->endCommand(); + } + } + } + + //also move guides + mLayout->undoStack()->beginCommand( &mLayout->guides(), tr( "Move Guides" ) ); + const QList< QgsLayoutGuide * > verticalGuides = mLayout->guides().guides( Qt::Vertical ); + for ( QgsLayoutGuide *guide : verticalGuides ) + { + guide->setLayoutPosition( guide->layoutPosition() + diffX ); + } + const QList< QgsLayoutGuide * > horizontalGuides = mLayout->guides().guides( Qt::Horizontal ); + for ( QgsLayoutGuide *guide : horizontalGuides ) + { + guide->setLayoutPosition( guide->layoutPosition() + diffY ); + } + mLayout->undoStack()->endCommand(); + + if ( !mBlockUndoCommands ) + mLayout->undoStack()->endCommand(); +} + bool QgsLayoutPageCollection::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const { QDomElement element = document.createElement( QStringLiteral( "PageCollection" ) ); diff --git a/src/core/layout/qgslayoutpagecollection.h b/src/core/layout/qgslayoutpagecollection.h index 579da36eeed6..da2a2ae9fd8b 100644 --- a/src/core/layout/qgslayoutpagecollection.h +++ b/src/core/layout/qgslayoutpagecollection.h @@ -306,6 +306,16 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri */ double pageShadowWidth() const; + /** + * Resizes the layout to a single page which fits the current contents of the layout. + * + * Calling this method resets the number of pages to 1, with the size set to the + * minimum size required to fit all existing layout items. Items will also be + * repositioned so that the new top-left bounds of the layout is at the point + * (marginLeft, marginTop). An optional margin can be specified. + */ + void resizeToContents( const QgsMargins &margins, QgsUnitTypes::LayoutUnit marginUnits ); + /** * Stores the collection's state in a DOM element. The \a parentElement should refer to the parent layout's DOM element. * \see readXml() diff --git a/src/ui/layout/qgslayoutwidgetbase.ui b/src/ui/layout/qgslayoutwidgetbase.ui index b53dd76b7b8d..88a5d33be19d 100644 --- a/src/ui/layout/qgslayoutwidgetbase.ui +++ b/src/ui/layout/qgslayoutwidgetbase.ui @@ -175,6 +175,126 @@ + + + + Resize layout to content + + + + + + + + Margin units + + + + + + + + + + + + + + Top margin + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 0.100000000000000 + + + + + + + + + Left + + + + + + + 0.100000000000000 + + + + + + + Right + + + + + + + 0.100000000000000 + + + + + + + + + Bottom + + + + + + + 0.100000000000000 + + + + + + + Resize layout + + + + + + + + @@ -196,16 +316,6 @@ - - QgsDoubleSpinBox - QDoubleSpinBox -
qgsdoublespinbox.h
-
- - QgsLayoutUnitsComboBox - QComboBox -
qgslayoutunitscombobox.h
-
QgsScrollArea QScrollArea @@ -218,6 +328,16 @@
qgscollapsiblegroupbox.h
1
+ + QgsDoubleSpinBox + QDoubleSpinBox +
qgsdoublespinbox.h
+
+ + QgsLayoutUnitsComboBox + QComboBox +
qgslayoutunitscombobox.h
+
QgsSpinBox QSpinBox @@ -233,6 +353,12 @@ mOffsetYSpinBox mGridOffsetUnitsComboBox mSnapToleranceSpinBox + mMarginUnitsComboBox + mTopMarginSpinBox + mLeftMarginSpinBox + mRightMarginSpinBox + mBottomMarginSpinBox + mResizePageButton diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index 0b4c5945837b..b652bc1aa586 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -267,7 +267,12 @@ void TestQgsLayout::bounds() //add some items to a layout QgsProject p; QgsLayout l( &p ); - l.initializeDefaults(); + QgsLayoutItemPage *page = new QgsLayoutItemPage( &l ); + page->setPageSize( "A4", QgsLayoutItemPage::Landscape ); + l.pageCollection()->addPage( page ); + QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); + page2->setPageSize( "A4", QgsLayoutItemPage::Landscape ); + l.pageCollection()->addPage( page2 ); QgsLayoutItemShape *shape1 = new QgsLayoutItemShape( &l ); shape1->attemptResize( QgsLayoutSize( 90, 50 ) ); @@ -276,44 +281,41 @@ void TestQgsLayout::bounds() l.addLayoutItem( shape1 ); QgsLayoutItemShape *shape2 = new QgsLayoutItemShape( &l ); shape2->attemptResize( QgsLayoutSize( 110, 50 ) ); - shape2->attemptMove( QgsLayoutPoint( 100, 150 ) ); + shape2->attemptMove( QgsLayoutPoint( 100, 150 ), true, false, 0 ); l.addLayoutItem( shape2 ); - -#if 0 - QgsLayoutItemRectangularShape *shape3 = new QgsLayoutItemRectangularShape( &l ); - l.addLayoutItem( shape3 ); - shape3->setItemPosition( 210, 30, 50, 100, QgsComposerItem::UpperLeft, false, 2 ); - QgsLayoutItemRectangularShape *shape4 = new QgsLayoutItemRectangularShape( &l ); - l.addLayoutItem( shape4 ); - shape4->setItemPosition( 10, 120, 50, 30, QgsComposerItem::UpperLeft, false, 2 ); + QgsLayoutItemShape *shape3 = new QgsLayoutItemShape( &l ); +// l.addLayoutItem( shape3 ); + shape3->attemptResize( QgsLayoutSize( 50, 100 ) ); + shape3->attemptMove( QgsLayoutPoint( 210, 30 ), true, false, 1 ); + QgsLayoutItemShape *shape4 = new QgsLayoutItemShape( &l ); +// l.addLayoutItem( shape4 ); + shape4->attemptResize( QgsLayoutSize( 50, 30 ) ); + shape4->attemptMove( QgsLayoutPoint( 10, 120 ), true, false, 1 ); shape4->setVisibility( false ); -#endif //check bounds QRectF layoutBounds = l.layoutBounds( false ); -#if 0 // correct values when 2nd page items are added back in - QGSCOMPARENEAR( layoutBounds.height(), 372.15, 0.01 ); - QGSCOMPARENEAR( layoutBounds.width(), 301.00, 0.01 ); - QGSCOMPARENEAR( layoutBounds.left(), -2, 0.01 ); - QGSCOMPARENEAR( layoutBounds.top(), -2, 0.01 ); - - QRectF compositionBoundsNoPage = l.layoutBounds( true ); - QGSCOMPARENEAR( compositionBoundsNoPage.height(), 320.36, 0.01 ); - QGSCOMPARENEAR( compositionBoundsNoPage.width(), 250.30, 0.01 ); - QGSCOMPARENEAR( compositionBoundsNoPage.left(), 9.85, 0.01 ); - QGSCOMPARENEAR( compositionBoundsNoPage.top(), 49.79, 0.01 ); -#endif +// QGSCOMPARENEAR( layoutBounds.height(), 430, 0.01 ); +// QGSCOMPARENEAR( layoutBounds.width(), 297.00, 0.01 ); +// QGSCOMPARENEAR( layoutBounds.left(), 0.0, 0.01 ); +// QGSCOMPARENEAR( layoutBounds.top(), 0.0, 0.01 ); + + QRectF layoutBoundsNoPage = l.layoutBounds( true ); + QGSCOMPARENEAR( layoutBoundsNoPage.height(), 320.36, 0.01 ); + QGSCOMPARENEAR( layoutBoundsNoPage.width(), 250.30, 0.01 ); + QGSCOMPARENEAR( layoutBoundsNoPage.left(), 9.85, 0.01 ); + QGSCOMPARENEAR( layoutBoundsNoPage.top(), 49.79, 0.01 ); QGSCOMPARENEAR( layoutBounds.height(), 210.000000, 0.01 ); QGSCOMPARENEAR( layoutBounds.width(), 297.000000, 0.01 ); QGSCOMPARENEAR( layoutBounds.left(), 0.00000, 0.01 ); QGSCOMPARENEAR( layoutBounds.top(), 0.00000, 0.01 ); - QRectF compositionBoundsNoPage = l.layoutBounds( true ); - QGSCOMPARENEAR( compositionBoundsNoPage.height(), 174.859607, 0.01 ); - QGSCOMPARENEAR( compositionBoundsNoPage.width(), 124.859607, 0.01 ); - QGSCOMPARENEAR( compositionBoundsNoPage.left(), 85.290393, 0.01 ); - QGSCOMPARENEAR( compositionBoundsNoPage.top(), 25.290393, 0.01 ); + layoutBoundsNoPage = l.layoutBounds( true ); + QGSCOMPARENEAR( layoutBoundsNoPage.height(), 174.859607, 0.01 ); + QGSCOMPARENEAR( layoutBoundsNoPage.width(), 124.859607, 0.01 ); + QGSCOMPARENEAR( layoutBoundsNoPage.left(), 85.290393, 0.01 ); + QGSCOMPARENEAR( layoutBoundsNoPage.top(), 25.290393, 0.01 ); #if 0 QRectF page1Bounds = composition->pageItemBounds( 0, true ); From 082733abdaef7f16ca058f3baa4882a7635a1542 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 6 Dec 2017 08:21:30 +1000 Subject: [PATCH 09/56] Fix unit test --- tests/src/core/testqgslayout.cpp | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index b652bc1aa586..8379a6967874 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -277,28 +277,28 @@ void TestQgsLayout::bounds() QgsLayoutItemShape *shape1 = new QgsLayoutItemShape( &l ); shape1->attemptResize( QgsLayoutSize( 90, 50 ) ); shape1->attemptMove( QgsLayoutPoint( 90, 50 ) ); - shape1->setItemRotation( 45 ); + shape1->setItemRotation( 45, false ); l.addLayoutItem( shape1 ); QgsLayoutItemShape *shape2 = new QgsLayoutItemShape( &l ); shape2->attemptResize( QgsLayoutSize( 110, 50 ) ); shape2->attemptMove( QgsLayoutPoint( 100, 150 ), true, false, 0 ); l.addLayoutItem( shape2 ); QgsLayoutItemShape *shape3 = new QgsLayoutItemShape( &l ); -// l.addLayoutItem( shape3 ); + l.addLayoutItem( shape3 ); shape3->attemptResize( QgsLayoutSize( 50, 100 ) ); shape3->attemptMove( QgsLayoutPoint( 210, 30 ), true, false, 1 ); QgsLayoutItemShape *shape4 = new QgsLayoutItemShape( &l ); -// l.addLayoutItem( shape4 ); + l.addLayoutItem( shape4 ); shape4->attemptResize( QgsLayoutSize( 50, 30 ) ); shape4->attemptMove( QgsLayoutPoint( 10, 120 ), true, false, 1 ); shape4->setVisibility( false ); //check bounds QRectF layoutBounds = l.layoutBounds( false ); -// QGSCOMPARENEAR( layoutBounds.height(), 430, 0.01 ); -// QGSCOMPARENEAR( layoutBounds.width(), 297.00, 0.01 ); -// QGSCOMPARENEAR( layoutBounds.left(), 0.0, 0.01 ); -// QGSCOMPARENEAR( layoutBounds.top(), 0.0, 0.01 ); + QGSCOMPARENEAR( layoutBounds.height(), 430, 0.01 ); + QGSCOMPARENEAR( layoutBounds.width(), 297.00, 0.01 ); + QGSCOMPARENEAR( layoutBounds.left(), 0.0, 0.01 ); + QGSCOMPARENEAR( layoutBounds.top(), 0.0, 0.01 ); QRectF layoutBoundsNoPage = l.layoutBounds( true ); QGSCOMPARENEAR( layoutBoundsNoPage.height(), 320.36, 0.01 ); @@ -306,17 +306,6 @@ void TestQgsLayout::bounds() QGSCOMPARENEAR( layoutBoundsNoPage.left(), 9.85, 0.01 ); QGSCOMPARENEAR( layoutBoundsNoPage.top(), 49.79, 0.01 ); - QGSCOMPARENEAR( layoutBounds.height(), 210.000000, 0.01 ); - QGSCOMPARENEAR( layoutBounds.width(), 297.000000, 0.01 ); - QGSCOMPARENEAR( layoutBounds.left(), 0.00000, 0.01 ); - QGSCOMPARENEAR( layoutBounds.top(), 0.00000, 0.01 ); - - layoutBoundsNoPage = l.layoutBounds( true ); - QGSCOMPARENEAR( layoutBoundsNoPage.height(), 174.859607, 0.01 ); - QGSCOMPARENEAR( layoutBoundsNoPage.width(), 124.859607, 0.01 ); - QGSCOMPARENEAR( layoutBoundsNoPage.left(), 85.290393, 0.01 ); - QGSCOMPARENEAR( layoutBoundsNoPage.top(), 25.290393, 0.01 ); - #if 0 QRectF page1Bounds = composition->pageItemBounds( 0, true ); QGSCOMPARENEAR( page1Bounds.height(), 150.36, 0.01 ); From d7e179cb5acb57c54dae9b3f16ba2e7dca0cdf1c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 6 Dec 2017 08:37:49 +1000 Subject: [PATCH 10/56] Add unit test for resizing pages --- .../python/test_qgslayoutpagecollection.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/src/python/test_qgslayoutpagecollection.py b/tests/src/python/test_qgslayoutpagecollection.py index a819593ffec6..9cf5e546fdc2 100644 --- a/tests/src/python/test_qgslayoutpagecollection.py +++ b/tests/src/python/test_qgslayoutpagecollection.py @@ -22,9 +22,13 @@ QgsLayoutPoint, QgsLayoutObject, QgsProject, + QgsMargins, QgsProperty, + QgsLayoutGuide, + QgsLayoutMeasurement, QgsLayoutPageCollection, QgsSimpleFillSymbolLayer, + QgsLayoutItemShape, QgsFillSymbol, QgsReadWriteContext) from qgis.PyQt.QtCore import Qt, QCoreApplication, QEvent, QPointF, QRectF @@ -741,6 +745,68 @@ def testUndoRedo(self): self.assertEqual(collection.pageCount(), 1) self.assertEqual(collection.page(0).pageSize().width(), 148) + def testResizeToContents(self): + p = QgsProject() + l = QgsLayout(p) + + shape1 = QgsLayoutItemShape(l) + shape1.attemptResize(QgsLayoutSize(90, 50)) + shape1.attemptMove(QgsLayoutPoint(90, 50)) + shape1.setItemRotation(45, False) + l.addLayoutItem(shape1) + shape2 = QgsLayoutItemShape(l) + shape2.attemptResize(QgsLayoutSize(110, 50)) + shape2.attemptMove(QgsLayoutPoint(100, 150), True, False, 0) + l.addLayoutItem(shape2) + shape3 = QgsLayoutItemShape(l) + l.addLayoutItem(shape3) + shape3.attemptResize(QgsLayoutSize(50, 100)) + shape3.attemptMove(QgsLayoutPoint(210, 250), True, False, 0) + shape4 = QgsLayoutItemShape(l) + l.addLayoutItem(shape4) + shape4.attemptResize(QgsLayoutSize(50, 30)) + shape4.attemptMove(QgsLayoutPoint(10, 340), True, False, 0) + shape4.setVisibility(False) + + # resize with no existing pages + l.pageCollection().resizeToContents(QgsMargins(1, 2, 3, 4), QgsUnitTypes.LayoutCentimeters) + self.assertEqual(l.pageCollection().pageCount(), 1) + + self.assertAlmostEqual(l.pageCollection().page(0).sizeWithUnits().width(), 290.3, 2) + self.assertAlmostEqual(l.pageCollection().page(0).sizeWithUnits().height(), 380.36, 2) + self.assertAlmostEqual(l.pageCollection().page(0).sizeWithUnits().units(), QgsUnitTypes.LayoutMillimeters) + + self.assertAlmostEqual(shape1.positionWithUnits().x(), 90.15, 2) + self.assertAlmostEqual(shape1.positionWithUnits().y(), 20.21, 2) + self.assertAlmostEqual(shape2.positionWithUnits().x(), 100.15, 2) + self.assertAlmostEqual(shape2.positionWithUnits().y(), 120.21, 2) + self.assertAlmostEqual(shape3.positionWithUnits().x(), 210.15, 2) + self.assertAlmostEqual(shape3.positionWithUnits().y(), 220.21, 2) + self.assertAlmostEqual(shape4.positionWithUnits().x(), 10.15, 2) + self.assertAlmostEqual(shape4.positionWithUnits().y(), 310.21, 2) + + # add a second page + page2 = QgsLayoutItemPage(l) + page2.setPageSize("A4", QgsLayoutItemPage.Landscape) + l.pageCollection().addPage(page2) + + # add some guides + g1 = QgsLayoutGuide(Qt.Horizontal, QgsLayoutMeasurement(2.5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0)) + l.guides().addGuide(g1) + g2 = QgsLayoutGuide(Qt.Vertical, QgsLayoutMeasurement(4.5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0)) + l.guides().addGuide(g2) + + # second page should be removed + l.pageCollection().resizeToContents(QgsMargins(0, 0, 0, 0), QgsUnitTypes.LayoutCentimeters) + self.assertEqual(l.pageCollection().pageCount(), 1) + + self.assertAlmostEqual(l.pageCollection().page(0).sizeWithUnits().width(), 250.3, 2) + self.assertAlmostEqual(l.pageCollection().page(0).sizeWithUnits().height(), 320.36, 2) + self.assertAlmostEqual(l.pageCollection().page(0).sizeWithUnits().units(), QgsUnitTypes.LayoutMillimeters) + + self.assertAlmostEqual(g1.position().length(), 0.5, 2) + self.assertAlmostEqual(g2.position().length(), 3.5, 2) + if __name__ == '__main__': unittest.main() From 71dd3b933a0a5fab96c026c120e78eba9e2f2574 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 6 Dec 2017 18:06:51 +1000 Subject: [PATCH 11/56] Port pageItemBounds method from composer --- src/core/layout/qgslayout.cpp | 26 ++++++++++++++++++++++++++ src/core/layout/qgslayout.h | 14 ++++++++++++++ tests/src/core/testqgslayout.cpp | 8 +++----- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 4ca83b5a40ac..c78cce6ade8c 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -420,6 +420,32 @@ QRectF QgsLayout::layoutBounds( bool ignorePages, double margin ) const } +QRectF QgsLayout::pageItemBounds( int page, bool visibleOnly ) const +{ + //start with an empty rectangle + QRectF bounds; + + //add all QgsLayoutItems on page + const QList itemList = items(); + for ( QGraphicsItem *item : itemList ) + { + const QgsLayoutItem *layoutItem = dynamic_cast( item ); + if ( layoutItem && layoutItem->type() != QgsLayoutItemRegistry::LayoutPage && layoutItem->page() == page ) + { + if ( visibleOnly && !layoutItem->isVisible() ) + continue; + + //expand bounds with current item's bounds + if ( bounds.isValid() ) + bounds = bounds.united( item->sceneBoundingRect() ); + else + bounds = item->sceneBoundingRect(); + } + } + + return bounds; +} + void QgsLayout::addLayoutItem( QgsLayoutItem *item ) { addLayoutItemPrivate( item ); diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 43780a19de9e..df21a602cb11 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -427,9 +427,23 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * \param ignorePages set to true to ignore page items * \param margin optional marginal (in percent, e.g., 0.05 = 5% ) to add around items * \returns layout bounds, in layout units. + * + * \see pageItemBounds() */ QRectF layoutBounds( bool ignorePages = false, double margin = 0.0 ) const; + /** + * Returns the bounding box of the items contained on a specified \a page. + * A page number of 0 represents the first page in the layout. + * + * Set \a visibleOnly to true to only include visible items. + * + * The returned bounds are in layout units. + * + * \see layoutBounds() + */ + QRectF pageItemBounds( int page, bool visibleOnly = false ) const; + /** * Adds an \a item to the layout. This should be called instead of the base class addItem() * method. Ownership of the item is transferred to the layout. diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index 8379a6967874..1c9e90dd8e9e 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -306,25 +306,23 @@ void TestQgsLayout::bounds() QGSCOMPARENEAR( layoutBoundsNoPage.left(), 9.85, 0.01 ); QGSCOMPARENEAR( layoutBoundsNoPage.top(), 49.79, 0.01 ); -#if 0 - QRectF page1Bounds = composition->pageItemBounds( 0, true ); + QRectF page1Bounds = l.pageItemBounds( 0, true ); QGSCOMPARENEAR( page1Bounds.height(), 150.36, 0.01 ); QGSCOMPARENEAR( page1Bounds.width(), 155.72, 0.01 ); QGSCOMPARENEAR( page1Bounds.left(), 54.43, 0.01 ); QGSCOMPARENEAR( page1Bounds.top(), 49.79, 0.01 ); - QRectF page2Bounds = composition->pageItemBounds( 1, true ); + QRectF page2Bounds = l.pageItemBounds( 1, true ); QGSCOMPARENEAR( page2Bounds.height(), 100.30, 0.01 ); QGSCOMPARENEAR( page2Bounds.width(), 50.30, 0.01 ); QGSCOMPARENEAR( page2Bounds.left(), 209.85, 0.01 ); QGSCOMPARENEAR( page2Bounds.top(), 249.85, 0.01 ); - QRectF page2BoundsWithHidden = composition->pageItemBounds( 1, false ); + QRectF page2BoundsWithHidden = l.pageItemBounds( 1, false ); QGSCOMPARENEAR( page2BoundsWithHidden.height(), 120.30, 0.01 ); QGSCOMPARENEAR( page2BoundsWithHidden.width(), 250.30, 0.01 ); QGSCOMPARENEAR( page2BoundsWithHidden.left(), 9.85, 0.01 ); QGSCOMPARENEAR( page2BoundsWithHidden.top(), 249.85, 0.01 ); -#endif } void TestQgsLayout::addItem() From e8a42c92b49460bac20d0568d5261cfd42f5df45 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 6 Dec 2017 18:55:24 +1000 Subject: [PATCH 12/56] Add method to determine largest page size --- src/core/layout/qgslayoutpagecollection.cpp | 19 ++++++++++++++++++- src/core/layout/qgslayoutpagecollection.h | 10 ++++++++++ .../python/test_qgslayoutpagecollection.py | 10 ++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/core/layout/qgslayoutpagecollection.cpp b/src/core/layout/qgslayoutpagecollection.cpp index 437b73a6769d..04e9540ba13a 100644 --- a/src/core/layout/qgslayoutpagecollection.cpp +++ b/src/core/layout/qgslayoutpagecollection.cpp @@ -71,13 +71,30 @@ void QgsLayoutPageCollection::reflow() double QgsLayoutPageCollection::maximumPageWidth() const { double maxWidth = 0; - Q_FOREACH ( QgsLayoutItemPage *page, mPages ) + for ( QgsLayoutItemPage *page : mPages ) { maxWidth = std::max( maxWidth, mLayout->convertToLayoutUnits( page->pageSize() ).width() ); } return maxWidth; } +QSizeF QgsLayoutPageCollection::maximumPageSize() const +{ + double maxArea = 0; + QSizeF maxSize; + for ( QgsLayoutItemPage *page : mPages ) + { + QSizeF pageSize = mLayout->convertToLayoutUnits( page->pageSize() ); + double area = pageSize.width() * pageSize.height(); + if ( area > maxArea ) + { + maxArea = area; + maxSize = pageSize; + } + } + return maxSize; +} + int QgsLayoutPageCollection::pageNumberForPoint( QPointF point ) const { int pageNumber = 0; diff --git a/src/core/layout/qgslayoutpagecollection.h b/src/core/layout/qgslayoutpagecollection.h index da2a2ae9fd8b..f0eb4c6f1b32 100644 --- a/src/core/layout/qgslayoutpagecollection.h +++ b/src/core/layout/qgslayoutpagecollection.h @@ -229,9 +229,19 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri /** * Returns the maximum width of pages in the collection. The returned value is * in layout units. + * + * \see maximumPageSize() */ double maximumPageWidth() const; + /** + * Returns the maximum size of any page in the collection, by area. The returned value + * is in layout units. + * + * \see maximumPageWidth() + */ + QSizeF maximumPageSize() const; + /** * Returns the page number corresponding to a \a point in the layout (in layout units). * diff --git a/tests/src/python/test_qgslayoutpagecollection.py b/tests/src/python/test_qgslayoutpagecollection.py index 9cf5e546fdc2..0f17938fb9be 100644 --- a/tests/src/python/test_qgslayoutpagecollection.py +++ b/tests/src/python/test_qgslayoutpagecollection.py @@ -250,9 +250,9 @@ def testExtendByNewPage(self): self.assertEqual(collection.pageCount(), 3) self.assertEqual(new_page2.sizeWithUnits(), new_page.sizeWithUnits()) - def testMaxPageWidth(self): + def testMaxPageWidthAndSize(self): """ - Test calculating maximum page width + Test calculating maximum page width and size """ p = QgsProject() l = QgsLayout(p) @@ -263,18 +263,24 @@ def testMaxPageWidth(self): page.setPageSize('A4') collection.addPage(page) self.assertEqual(collection.maximumPageWidth(), 210.0) + self.assertEqual(collection.maximumPageSize().width(), 210.0) + self.assertEqual(collection.maximumPageSize().height(), 297.0) # add a second page page2 = QgsLayoutItemPage(l) page2.setPageSize('A3') collection.addPage(page2) self.assertEqual(collection.maximumPageWidth(), 297.0) + self.assertEqual(collection.maximumPageSize().width(), 297.0) + self.assertEqual(collection.maximumPageSize().height(), 420.0) # add a page with other units page3 = QgsLayoutItemPage(l) page3.setPageSize(QgsLayoutSize(100, 100, QgsUnitTypes.LayoutMeters)) collection.addPage(page3) self.assertEqual(collection.maximumPageWidth(), 100000.0) + self.assertEqual(collection.maximumPageSize().width(), 100000.0) + self.assertEqual(collection.maximumPageSize().height(), 100000.0) def testReflow(self): """ From a56c937012ce6d0197ecf93d3462856a5a75ff26 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 6 Dec 2017 18:55:43 +1000 Subject: [PATCH 13/56] Expand docs --- src/core/layout/qgslayout.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index df21a602cb11..97f42ea53870 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -35,6 +35,13 @@ class QgsLayoutUndoStack; * \ingroup core * \class QgsLayout * \brief Base class for layouts, which can contain items such as maps, labels, scalebars, etc. + * + * While the raw QGraphicsScene API can be used to render the contents of a QgsLayout + * to a QPainter, it is recommended to instead use a QgsLayoutExporter to handle rendering + * layouts instead. QgsLayoutExporter automatically takes care of the intracacies of + * preparing the layout and paint devices for correct exports, respecting various + * user settings such as the layout context DPI. + * * \since QGIS 3.0 */ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContextGenerator, public QgsLayoutUndoObjectInterface From aa7986f8fc66ce2b0b9169127555276658eb3bcd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 12:07:29 +1000 Subject: [PATCH 14/56] Implement methods for exporting layouts as raster, add tests --- python/core/layout/qgslayoutexporter.sip | 115 ++++++- src/core/layout/qgslayoutexporter.cpp | 228 +++++++++++++- src/core/layout/qgslayoutexporter.h | 124 +++++++- tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgslayoutexporter.py | 281 ++++++++++++++++++ ...outexporter_exporttoimagecropped_page1.png | Bin 0 -> 1971 bytes ...outexporter_exporttoimagecropped_page2.png | Bin 0 -> 1957 bytes ..._layoutexporter_exporttoimagedpi_page1.png | Bin 0 -> 3981 bytes ..._layoutexporter_exporttoimagedpi_page2.png | Bin 0 -> 2657 bytes ...layoutexporter_exporttoimagesize_page2.png | Bin 0 -> 4002 bytes .../expected_layoutexporter_renderpage.png | Bin 0 -> 4255 bytes .../expected_layoutexporter_renderregion.png | Bin 0 -> 2060 bytes ...ected_layoutexporter_rendertoimagepage.png | Bin 0 -> 4255 bytes ..._layoutexporter_rendertoimageregiondpi.png | Bin 0 -> 572 bytes ...xporter_rendertoimageregionoverridedpi.png | Bin 0 -> 1219 bytes ...layoutexporter_rendertoimageregionsize.png | Bin 0 -> 2060 bytes 16 files changed, 743 insertions(+), 6 deletions(-) create mode 100644 tests/src/python/test_qgslayoutexporter.py create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagecropped_page1/expected_layoutexporter_exporttoimagecropped_page1.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagecropped_page2/expected_layoutexporter_exporttoimagecropped_page2.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagedpi_page1/expected_layoutexporter_exporttoimagedpi_page1.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagedpi_page2/expected_layoutexporter_exporttoimagedpi_page2.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagesize_page2/expected_layoutexporter_exporttoimagesize_page2.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_renderpage/expected_layoutexporter_renderpage.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_renderregion/expected_layoutexporter_renderregion.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimagepage/expected_layoutexporter_rendertoimagepage.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimageregiondpi/expected_layoutexporter_rendertoimageregiondpi.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimageregionoverridedpi/expected_layoutexporter_rendertoimageregionoverridedpi.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimageregionsize/expected_layoutexporter_rendertoimageregionsize.png diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index 53964ff416c4..8921afb13b37 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -26,7 +26,7 @@ class QgsLayoutExporter Constructor for QgsLayoutExporter, for the specified ``layout``. %End - void renderPage( QPainter *painter, int page ); + void renderPage( QPainter *painter, int page ) const; %Docstring Renders a full page to a destination ``painter``. @@ -36,12 +36,123 @@ are 0 based, such that the first page in a layout is page 0. .. seealso:: :py:func:`renderRect()` %End - void renderRegion( QPainter *painter, const QRectF ®ion ); + QImage renderPageToImage( int page, QSize imageSize = QSize(), double dpi = 0 ) const; +%Docstring + Renders a full page to an image. + + The ``page`` argument specifies the page number to render. Page numbers + are 0 based, such that the first page in a layout is page 0. + + The optional ``imageSize`` parameter can specify the target image size, in pixels. + It is the caller's responsibility to ensure that the ratio of the target image size + matches the ratio of the corresponding layout page size. + + The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print + resolution. This parameter has no effect if ``imageSize`` is specified. + + Returns the rendered image, or a null QImage if the image does not fit into available memory. + +.. seealso:: :py:func:`renderPage()` +.. seealso:: :py:func:`renderRegionToImage()` + :rtype: QImage +%End + + void renderRegion( QPainter *painter, const QRectF ®ion ) const; %Docstring Renders a ``region`` from the layout to a ``painter``. This method can be used to render sections of pages rather than full pages. .. seealso:: :py:func:`renderPage()` +.. seealso:: :py:func:`renderRegionToImage()` +%End + + QImage renderRegionToImage( const QRectF ®ion, QSize imageSize = QSize(), double dpi = 0 ) const; +%Docstring + Renders a ``region`` of the layout to an image. This method can be used to render + sections of pages rather than full pages. + + The optional ``imageSize`` parameter can specify the target image size, in pixels. + It is the caller's responsibility to ensure that the ratio of the target image size + matches the ratio of the specified region of the layout. + + The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print + resolution. This parameter has no effect if ``imageSize`` is specified. + + Returns the rendered image, or a null QImage if the image does not fit into available memory. + +.. seealso:: :py:func:`renderRegion()` +.. seealso:: :py:func:`renderPageToImage()` + :rtype: QImage +%End + + + enum ExportResult + { + Success, + MemoryError, + FileError, + }; + + struct ImageExportSettings + { + double dpi; +%Docstring +Resolution to export layout at +%End + + QSize imageSize; +%Docstring + Manual size in pixels for output image. If imageSize is not + set then it will be automatically calculated based on the + output dpi and layout size. + + If cropToContents is true then imageSize has no effect. + + Be careful when specifying manual sizes if pages in the layout + have differing sizes! It's likely not going to give a reasonable + output in this case, and the automatic dpi-based image size should be + used instead. +%End + + bool cropToContents; +%Docstring + Set to true if image should be cropped so only parts of the layout + containing items are exported. +%End + + QgsMargins cropMargins; +%Docstring + Crop to content margins, in pixels. These margins will be added + to the bounds of the exported layout if cropToContents is true. +%End + + QList< int > pages; +%Docstring + List of specific pages to export, or an empty list to + export all pages. + + Page numbers are 0 index based, so the first page in the + layout corresponds to page 0. +%End + + bool generateWorldFile; +%Docstring + Set to true to generate an external world file alonside + exported images. +%End + + }; + + ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings ); +%Docstring + Exports the layout to the a ``filePath``, using the specified export ``settings``. + + If the layout is a multi-page layout, then filenames for each page will automatically + be generated by appending "_1", "_2", etc to the image file's base name. + + Returns a result code indicating whether the export was successful or an + error was encountered. + :rtype: ExportResult %End }; diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 475dc758e8c3..9a76ab75d182 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -18,6 +18,8 @@ #include "qgslayout.h" #include "qgslayoutitemmap.h" #include "qgslayoutpagecollection.h" +#include +#include QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout ) : mLayout( layout ) @@ -25,7 +27,7 @@ QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout ) } -void QgsLayoutExporter::renderPage( QPainter *painter, int page ) +void QgsLayoutExporter::renderPage( QPainter *painter, int page ) const { if ( !mLayout ) return; @@ -45,7 +47,27 @@ void QgsLayoutExporter::renderPage( QPainter *painter, int page ) renderRegion( painter, paperRect ); } -void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) +QImage QgsLayoutExporter::renderPageToImage( int page, QSize imageSize, double dpi ) const +{ + if ( !mLayout ) + return QImage(); + + if ( mLayout->pageCollection()->pageCount() <= page || page < 0 ) + { + return QImage(); + } + + QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( page ); + if ( !pageItem ) + { + return QImage(); + } + + QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() ); + return renderRegionToImage( paperRect, imageSize, dpi ); +} + +void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) const { QPaintDevice *paintDevice = painter->device(); if ( !paintDevice || !mLayout ) @@ -68,3 +90,205 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) mLayout->context().mIsPreviewRender = true; } +QImage QgsLayoutExporter::renderRegionToImage( const QRectF ®ion, QSize imageSize, double dpi ) const +{ + double resolution = mLayout->context().dpi(); + double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) ); + if ( imageSize.isValid() ) + { + //output size in pixels specified, calculate resolution using average of + //derived x/y dpi + resolution = ( imageSize.width() / region.width() + + imageSize.height() / region.height() ) / 2.0 * oneInchInLayoutUnits; + } + else if ( dpi > 0 ) + { + //dpi overridden by function parameters + resolution = dpi; + } + + int width = imageSize.isValid() ? imageSize.width() + : static_cast< int >( resolution * region.width() / oneInchInLayoutUnits ); + int height = imageSize.isValid() ? imageSize.height() + : static_cast< int >( resolution * region.height() / oneInchInLayoutUnits ); + + QImage image( QSize( width, height ), QImage::Format_ARGB32 ); + if ( !image.isNull() ) + { + image.setDotsPerMeterX( resolution / 25.4 * 1000 ); + image.setDotsPerMeterY( resolution / 25.4 * 1000 ); + image.fill( Qt::transparent ); + QPainter imagePainter( &image ); + renderRegion( &imagePainter, region ); + if ( !imagePainter.isActive() ) + return QImage(); + } + + return image; +} + +///@cond PRIVATE +class LayoutDpiRestorer +{ + public: + + LayoutDpiRestorer( QgsLayout *layout ) + : mLayout( layout ) + , mPreviousSetting( layout->context().dpi() ) + { + } + + ~LayoutDpiRestorer() + { + mLayout->context().setDpi( mPreviousSetting ); + } + + private: + QgsLayout *mLayout = nullptr; + double mPreviousSetting = 0; +}; +///@endcond PRIVATE +QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ) +{ + int worldFilePageNo = -1; + if ( mLayout->referenceMap() ) + { + worldFilePageNo = mLayout->referenceMap()->page(); + } + + QFileInfo fi( filePath ); + QString path = fi.path(); + QString baseName = fi.baseName(); + QString extension = fi.completeSuffix(); + + LayoutDpiRestorer dpiRestorer( mLayout ); + ( void )dpiRestorer; + mLayout->context().setDpi( settings.dpi ); + + QList< int > pages; + if ( settings.pages.empty() ) + { + for ( int page = 0; page < mLayout->pageCollection()->pageCount(); ++page ) + pages << page; + } + else + { + for ( int page : settings.pages ) + { + if ( page >= 0 && page < mLayout->pageCollection()->pageCount() ) + pages << page; + } + } + + for ( int page : qgis::as_const( pages ) ) + { + if ( !mLayout->pageCollection()->shouldExportPage( page ) ) + { + continue; + } + + bool skip = false; + QRectF bounds; + QImage image = createImage( settings, page, bounds, skip ); + + if ( skip ) + continue; // should skip this page, e.g. null size + + if ( image.isNull() ) + { + return MemoryError; + } + + QString outputFilePath = generateFileName( path, baseName, extension, page ); + if ( !saveImage( image, outputFilePath, extension ) ) + { + return FileError; + } + +#if 0 //TODO + if ( page == worldFilePageNo ) + { + mLayout->georeferenceOutput( outputFilePath, nullptr, bounds, imageDlg.resolution() ); + + if ( settings.generateWorldFile ) + { + // should generate world file for this page + double a, b, c, d, e, f; + if ( bounds.isValid() ) + mLayout->computeWorldFileParameters( bounds, a, b, c, d, e, f ); + else + mLayout->computeWorldFileParameters( a, b, c, d, e, f ); + + QFileInfo fi( outputFilePath ); + // build the world file name + QString outputSuffix = fi.suffix(); + QString worldFileName = fi.absolutePath() + '/' + fi.baseName() + '.' + + outputSuffix.at( 0 ) + outputSuffix.at( fi.suffix().size() - 1 ) + 'w'; + + writeWorldFile( worldFileName, a, b, c, d, e, f ); + } + } +#endif + } + return Success; +} + +QImage QgsLayoutExporter::createImage( const QgsLayoutExporter::ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const +{ + bounds = QRectF(); + skipPage = false; + + if ( settings.cropToContents ) + { + if ( mLayout->pageCollection()->pageCount() == 1 ) + { + // single page, so include everything + bounds = mLayout->layoutBounds( true ); + } + else + { + // multi page, so just clip to items on current page + bounds = mLayout->pageItemBounds( page, true ); + } + if ( bounds.width() <= 0 || bounds.height() <= 0 ) + { + //invalid size, skip page + skipPage = true; + return QImage(); + } + + double pixelToLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutPixels ) ); + bounds = bounds.adjusted( -settings.cropMargins.left() * pixelToLayoutUnits, + -settings.cropMargins.top() * pixelToLayoutUnits, + settings.cropMargins.right() * pixelToLayoutUnits, + settings.cropMargins.bottom() * pixelToLayoutUnits ); + return renderRegionToImage( bounds, QSize(), settings.dpi ); + } + else + { + return renderPageToImage( page, settings.imageSize, settings.dpi ); + } +} + +QString QgsLayoutExporter::generateFileName( const QString &path, const QString &baseName, const QString &suffix, int page ) const +{ + if ( page == 0 ) + { + return path + '/' + baseName + '.' + suffix; + } + else + { + return path + '/' + baseName + '_' + QString::number( page + 1 ) + '.' + suffix; + } +} + +bool QgsLayoutExporter::saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat ) +{ + QImageWriter w( imageFilename, imageFormat.toLocal8Bit().constData() ); + if ( imageFormat.compare( QLatin1String( "tiff" ), Qt::CaseInsensitive ) == 0 || imageFormat.compare( QLatin1String( "tif" ), Qt::CaseInsensitive ) == 0 ) + { + w.setCompression( 1 ); //use LZW compression + } + return w.write( image ); +} + diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 9f89820c27bc..2656837e2bb7 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -17,7 +17,9 @@ #define QGSLAYOUTEXPORTER_H #include "qgis_core.h" +#include "qgsmargins.h" #include +#include class QgsLayout; class QPainter; @@ -46,19 +48,137 @@ class CORE_EXPORT QgsLayoutExporter * * \see renderRect() */ - void renderPage( QPainter *painter, int page ); + void renderPage( QPainter *painter, int page ) const; + + /** + * Renders a full page to an image. + * + * The \a page argument specifies the page number to render. Page numbers + * are 0 based, such that the first page in a layout is page 0. + * + * The optional \a imageSize parameter can specify the target image size, in pixels. + * It is the caller's responsibility to ensure that the ratio of the target image size + * matches the ratio of the corresponding layout page size. + * + * The \a dpi parameter is an optional dpi override. Set to 0 to use the default layout print + * resolution. This parameter has no effect if \a imageSize is specified. + * + * Returns the rendered image, or a null QImage if the image does not fit into available memory. + * + * \see renderPage() + * \see renderRegionToImage() + */ + QImage renderPageToImage( int page, QSize imageSize = QSize(), double dpi = 0 ) const; /** * Renders a \a region from the layout to a \a painter. This method can be used * to render sections of pages rather than full pages. * * \see renderPage() + * \see renderRegionToImage() */ - void renderRegion( QPainter *painter, const QRectF ®ion ); + void renderRegion( QPainter *painter, const QRectF ®ion ) const; + + /** + * Renders a \a region of the layout to an image. This method can be used to render + * sections of pages rather than full pages. + * + * The optional \a imageSize parameter can specify the target image size, in pixels. + * It is the caller's responsibility to ensure that the ratio of the target image size + * matches the ratio of the specified region of the layout. + * + * The \a dpi parameter is an optional dpi override. Set to 0 to use the default layout print + * resolution. This parameter has no effect if \a imageSize is specified. + * + * Returns the rendered image, or a null QImage if the image does not fit into available memory. + * + * \see renderRegion() + * \see renderPageToImage() + */ + QImage renderRegionToImage( const QRectF ®ion, QSize imageSize = QSize(), double dpi = 0 ) const; + + + //! Result codes for exporting layouts + enum ExportResult + { + Success, //!< Export was successful + MemoryError, //!< Unable to allocate memory required to export + FileError, //!< Could not write to destination file, likely due to a lock held by another application + }; + + //! Contains settings relating to exporting layouts to raster images + struct ImageExportSettings + { + //! Resolution to export layout at + double dpi; + + /** + * Manual size in pixels for output image. If imageSize is not + * set then it will be automatically calculated based on the + * output dpi and layout size. + * + * If cropToContents is true then imageSize has no effect. + * + * Be careful when specifying manual sizes if pages in the layout + * have differing sizes! It's likely not going to give a reasonable + * output in this case, and the automatic dpi-based image size should be + * used instead. + */ + QSize imageSize; + + /** + * Set to true if image should be cropped so only parts of the layout + * containing items are exported. + */ + bool cropToContents = false; + + /** + * Crop to content margins, in pixels. These margins will be added + * to the bounds of the exported layout if cropToContents is true. + */ + QgsMargins cropMargins; + + /** + * List of specific pages to export, or an empty list to + * export all pages. + * + * Page numbers are 0 index based, so the first page in the + * layout corresponds to page 0. + */ + QList< int > pages; + + /** + * Set to true to generate an external world file alonside + * exported images. + */ + bool generateWorldFile = false; + + }; + + /** + * Exports the layout to the a \a filePath, using the specified export \a settings. + * + * If the layout is a multi-page layout, then filenames for each page will automatically + * be generated by appending "_1", "_2", etc to the image file's base name. + * + * Returns a result code indicating whether the export was successful or an + * error was encountered. + */ + ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings ); private: QPointer< QgsLayout > mLayout; + + QImage createImage( const ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const; + + QString generateFileName( const QString &path, const QString &baseName, const QString &suffix, int page ) const; + + /** + * Saves an image to a file, possibly using format specific options (e.g. LZW compression for tiff) + */ + static bool saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat ); + }; #endif //QGSLAYOUTEXPORTER_H diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 5482bae17c5d..0273ef0f2ff0 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -83,6 +83,7 @@ ADD_PYTHON_TEST(PyQgsLayerTreeMapCanvasBridge test_qgslayertreemapcanvasbridge.p ADD_PYTHON_TEST(PyQgsLayerTree test_qgslayertree.py) ADD_PYTHON_TEST(PyQgsLayout test_qgslayout.py) ADD_PYTHON_TEST(PyQgsLayoutAlign test_qgslayoutaligner.py) +ADD_PYTHON_TEST(PyQgsLayoutExporter test_qgslayoutexporter.py) ADD_PYTHON_TEST(PyQgsLayoutManager test_qgslayoutmanager.py) ADD_PYTHON_TEST(PyQgsLayoutPageCollection test_qgslayoutpagecollection.py) ADD_PYTHON_TEST(PyQgsLayoutView test_qgslayoutview.py) diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py new file mode 100644 index 000000000000..eb246770fc14 --- /dev/null +++ b/tests/src/python/test_qgslayoutexporter.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLayoutExporter + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = 'Nyall Dawson' +__date__ = '11/12/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import qgis # NOQA +import sip +import tempfile +import shutil +import os + +from qgis.core import (QgsMultiRenderChecker, + QgsLayoutExporter, + QgsLayout, + QgsProject, + QgsMargins, + QgsLayoutItemShape, + QgsLayoutItemPage, + QgsLayoutPoint, + QgsSimpleFillSymbolLayer, + QgsFillSymbol) +from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt +from qgis.PyQt.QtGui import QImage, QPainter + +from qgis.testing import start_app, unittest + +start_app() + + +class TestQgsLayoutExporter(unittest.TestCase): + + @classmethod + def setUpClass(cls): + """Run before all tests""" + cls.basetestpath = tempfile.mkdtemp() + cls.dots_per_meter = 96 / 25.4 * 1000 + + def setUp(self): + self.report = "

Python QgsLayoutExporter Tests

\n" + + def tearDown(self): + report_file_path = "%s/qgistest.html" % QDir.tempPath() + with open(report_file_path, 'a') as report_file: + report_file.write(self.report) + + def checkImage(self, name, reference_image, rendered_image): + checker = QgsMultiRenderChecker() + checker.setControlPathPrefix("layout_exporter") + checker.setControlName("expected_layoutexporter_" + reference_image) + checker.setRenderedImage(rendered_image) + checker.setColorTolerance(2) + result = checker.runTest(name, 20) + self.report += checker.report() + print((self.report)) + return result + + def testRenderPage(self): + l = QgsLayout(QgsProject.instance()) + l.initializeDefaults() + + # add some items + item1 = QgsLayoutItemShape(l) + item1.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.green) + fill.setStrokeStyle(Qt.NoPen) + item1.setSymbol(fill_symbol) + l.addItem(item1) + + # get width/height, create image and render the composition to it + size = QSize(1122, 794) + output_image = QImage(size, QImage.Format_RGB32) + + output_image.setDotsPerMeterX(self.dots_per_meter) + output_image.setDotsPerMeterY(self.dots_per_meter) + QgsMultiRenderChecker.drawBackground(output_image) + painter = QPainter(output_image) + exporter = QgsLayoutExporter(l) + + # valid page + exporter.renderPage(painter, 0) + painter.end() + + rendered_file_path = os.path.join(self.basetestpath, 'test_renderpage.png') + output_image.save(rendered_file_path, "PNG") + self.assertTrue(self.checkImage('renderpage', 'renderpage', rendered_file_path)) + + def testRenderPageToImage(self): + l = QgsLayout(QgsProject.instance()) + l.initializeDefaults() + + # add some items + item1 = QgsLayoutItemShape(l) + item1.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.green) + fill.setStrokeStyle(Qt.NoPen) + item1.setSymbol(fill_symbol) + l.addItem(item1) + + exporter = QgsLayoutExporter(l) + size = QSize(1122, 794) + + # bad page numbers + image = exporter.renderPageToImage(-1, size) + self.assertTrue(image.isNull()) + image = exporter.renderPageToImage(1, size) + self.assertTrue(image.isNull()) + + # good page + image = exporter.renderPageToImage(0, size) + self.assertFalse(image.isNull()) + + rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimagepage.png') + image.save(rendered_file_path, "PNG") + self.assertTrue(self.checkImage('rendertoimagepage', 'rendertoimagepage', rendered_file_path)) + + def testRenderRegion(self): + l = QgsLayout(QgsProject.instance()) + l.initializeDefaults() + + # add some items + item1 = QgsLayoutItemShape(l) + item1.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.green) + fill.setStrokeStyle(Qt.NoPen) + item1.setSymbol(fill_symbol) + l.addItem(item1) + + # get width/height, create image and render the composition to it + size = QSize(560, 509) + output_image = QImage(size, QImage.Format_RGB32) + + output_image.setDotsPerMeterX(self.dots_per_meter) + output_image.setDotsPerMeterY(self.dots_per_meter) + QgsMultiRenderChecker.drawBackground(output_image) + painter = QPainter(output_image) + exporter = QgsLayoutExporter(l) + + exporter.renderRegion(painter, QRectF(5, 10, 110, 100)) + painter.end() + + rendered_file_path = os.path.join(self.basetestpath, 'test_renderregion.png') + output_image.save(rendered_file_path, "PNG") + self.assertTrue(self.checkImage('renderregion', 'renderregion', rendered_file_path)) + + def testRenderRegionToImage(self): + l = QgsLayout(QgsProject.instance()) + l.initializeDefaults() + + # add some items + item1 = QgsLayoutItemShape(l) + item1.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.green) + fill.setStrokeStyle(Qt.NoPen) + item1.setSymbol(fill_symbol) + l.addItem(item1) + + exporter = QgsLayoutExporter(l) + size = QSize(560, 509) + + image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100), size) + self.assertFalse(image.isNull()) + + rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimageregionsize.png') + image.save(rendered_file_path, "PNG") + self.assertTrue(self.checkImage('rendertoimageregionsize', 'rendertoimageregionsize', rendered_file_path)) + + # using layout dpi + l.context().setDpi(40) + image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100)) + self.assertFalse(image.isNull()) + + rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimageregiondpi.png') + image.save(rendered_file_path, "PNG") + self.assertTrue(self.checkImage('rendertoimageregiondpi', 'rendertoimageregiondpi', rendered_file_path)) + + # overridding dpi + image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100), QSize(), 80) + self.assertFalse(image.isNull()) + + rendered_file_path = os.path.join(self.basetestpath, 'test_rendertoimageregionoverridedpi.png') + image.save(rendered_file_path, "PNG") + self.assertTrue(self.checkImage('rendertoimageregionoverridedpi', 'rendertoimageregionoverridedpi', rendered_file_path)) + + def testExportToImage(self): + l = QgsLayout(QgsProject.instance()) + l.initializeDefaults() + + # add a second page + page2 = QgsLayoutItemPage(l) + page2.setPageSize('A5') + l.pageCollection().addPage(page2) + + # add some items + item1 = QgsLayoutItemShape(l) + item1.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.green) + fill.setStrokeStyle(Qt.NoPen) + item1.setSymbol(fill_symbol) + l.addItem(item1) + + item2 = QgsLayoutItemShape(l) + item2.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + item2.attemptMove(QgsLayoutPoint(10, 20), page=1) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.cyan) + fill.setStrokeStyle(Qt.NoPen) + item2.setSymbol(fill_symbol) + l.addItem(item2) + + exporter = QgsLayoutExporter(l) + # setup settings + settings = QgsLayoutExporter.ImageExportSettings() + settings.dpi = 80 + + rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi.png') + self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success) + + self.assertTrue(self.checkImage('exporttoimagedpi_page1', 'exporttoimagedpi_page1', rendered_file_path)) + page2_path = os.path.join(self.basetestpath, 'test_exporttoimagedpi_2.png') + self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path)) + + # crop to contents + settings.cropToContents = True + settings.cropMargins = QgsMargins(10, 20, 30, 40) + + rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped.png') + self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success) + + self.assertTrue(self.checkImage('exporttoimagecropped_page1', 'exporttoimagecropped_page1', rendered_file_path)) + page2_path = os.path.join(self.basetestpath, 'test_exporttoimagecropped_2.png') + self.assertTrue(self.checkImage('exporttoimagecropped_page2', 'exporttoimagecropped_page2', page2_path)) + + # specific pages + settings.cropToContents = False + settings.pages = [1] + + rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagepages.png') + self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success) + + self.assertFalse(os.path.exists(rendered_file_path)) + page2_path = os.path.join(self.basetestpath, 'test_exporttoimagepages_2.png') + self.assertTrue(self.checkImage('exporttoimagedpi_page2', 'exporttoimagedpi_page2', page2_path)) + + # image size + settings.imageSize = QSize(600, 851) + + rendered_file_path = os.path.join(self.basetestpath, 'test_exporttoimagesize.png') + self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success) + self.assertFalse(os.path.exists(rendered_file_path)) + page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesize_2.png') + self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagecropped_page1/expected_layoutexporter_exporttoimagecropped_page1.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagecropped_page1/expected_layoutexporter_exporttoimagecropped_page1.png new file mode 100644 index 0000000000000000000000000000000000000000..f7d5421ade7c1e92f7bb393ed02f158142bbb7c7 GIT binary patch literal 1971 zcmeAS@N?(olHy`uVBq!ia0y~yU`%3QU=rbA1Bxh|SF;9EoCO|{#S9EQz97ta>D*dB z1_t(2PZ!6KiaBrZ8gel?3NReB6m;of`TyV2Ir(7D-$SZ#>|sZljRVkP`QK)^QuY`>!*Z$h3hxca7>IG6F{fYBZq8$oHekW;EH* zIN4agm)vzGb>8PcUv(K){=PGL@9n5;)!B?QB>jaMEJ?4asGDN<<=RMztcnX@2G+t1 Mp00i_>zopr04F;V(f|Me literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagecropped_page2/expected_layoutexporter_exporttoimagecropped_page2.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagecropped_page2/expected_layoutexporter_exporttoimagecropped_page2.png new file mode 100644 index 0000000000000000000000000000000000000000..b7812b816f89527e5c967012c761ee77e97a6afe GIT binary patch literal 1957 zcmeAS@N?(olHy`uVBq!ia0y~yU`%3QU=rbA1Bxh|SF;9EoCO|{#S9EQz97ta>D*dB z1_t(UPZ!6KiaBp@DCRLU@-R5Yi)bovt+?{Pex1RN1t)$Uns~YCT!Ob*)Pv;ma)uLk zUNJNLvsPs5;52mb>0nH9;t>!|YM3Fwe56G}LG1{GA%3a5=Xu_q`4d;e;qcbd*nPwI zkdFUp&q_ZkGHm^RriQ(GuNniXCX(llQC&nv*l0kF1{4|jd^FjNCL6jWn~lF`EL$v=2);YFes?|F)}cm=nMvm7L5v18Yat&{us~yd2YK6+ zUQ1_ScynvpwWzBU_Bdb~ zEpq^ICI|!jDGX1JXaY&gLSX;J!EaP}G)#s_g4k(4J4sP;g}|96VDe?~boFyt=akR{ E0DKF9mjD0& literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagedpi_page2/expected_layoutexporter_exporttoimagedpi_page2.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_exporttoimagedpi_page2/expected_layoutexporter_exporttoimagedpi_page2.png new file mode 100644 index 0000000000000000000000000000000000000000..37ead9bc4abeeb42bb0b99c0d340e07d2fc6f753 GIT binary patch literal 2657 zcmeAS@N?(olHy`uVBq!ia0y~yV7$b@z%-SE4JcB5HLD&-aTa()7BevL_<}IwrE_cj z7#O&OJzX3_D(1YsanO?`K!EjNr0BubA0=jJN=#SmWdCUJEMfES$Q51>&aO?=+r68a z;m_&AFPRx0%;VH#U~%GNXq>?0ps<8NfMeKkZRX1SpE>jXWOjx-2Tq@{nP=Cudgq&W z^Q7wM&+HF=%wYb>(l~#4|AVYOWsC{}K@1!%A`DE5tPKtxqukNZ7)=bLd116D7%d5h zO-a~v`rW&gAJ@0QLzIsW%5&|NmfTdU S=mu;vGI+ZBxvX@h#7DFDXf7Dd1*5rOG#8BKg3(+snhVIu1si|AV?4j-+?n6+ z1Q}!==6+|;d%W#h)NXeMhB-%EIYxUCqdlnM)s3uXIDJNVpDoLdrNB-%gQu&X%Q~lo FCIGtmN7n!V literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_renderpage/expected_layoutexporter_renderpage.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_renderpage/expected_layoutexporter_renderpage.png new file mode 100644 index 0000000000000000000000000000000000000000..a5a2e21ae49802bfa1c3aee603a651d571839a44 GIT binary patch literal 4255 zcmeAS@N?(olHy`uVBq!ia0y~yU`b+NV3y)w0*W}bm%jp1oCO|{#S9F5he4R}c>anM z1_psZPZ!6KiaBp@8TK|ginv`&+^nRrv4KfNFh!ArF@(e6KtOKcZC$n#^?i-+x0+Y( zwtNmW>B0H)yZM3i4;>>;1_p%`AqEDA6DkZ04HGcrpM9(!!HuG_tl*Q)Pv=A3)AtPCHfhNWMsy`%qlQ-#Ql1)nWWt1vvMx4AnS z4Wo%~G;fZUNTcQOXf-ujIgd6;MjK*OY>15(DI=^%dGl7=C!IHU{>D9vrUR#XQ>M8SM~`+-~qkCiRWdx~wKoCjv_b22WQ%mvv4FO#ohn Bo)rK9 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_renderregion/expected_layoutexporter_renderregion.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_renderregion/expected_layoutexporter_renderregion.png new file mode 100644 index 0000000000000000000000000000000000000000..6660fb73a7994a523e7deed386a055fb5b2466af GIT binary patch literal 2060 zcmeAS@N?(olHy`uVBq!ia0y~yU@~A}VEoI$1QhA!y?7o-aTa()7BevL9R^{>S?m5Pgu{pLuTkJ~q35>$P=jll$J+ zl&@tJC_h(uW@Gl*(|ipqj!X)U6Bs-MR2YO W@ayEZhWo(kox#)9&t;ucLK6V4IA=Ei literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimagepage/expected_layoutexporter_rendertoimagepage.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimagepage/expected_layoutexporter_rendertoimagepage.png new file mode 100644 index 0000000000000000000000000000000000000000..a5a2e21ae49802bfa1c3aee603a651d571839a44 GIT binary patch literal 4255 zcmeAS@N?(olHy`uVBq!ia0y~yU`b+NV3y)w0*W}bm%jp1oCO|{#S9F5he4R}c>anM z1_psZPZ!6KiaBp@8TK|ginv`&+^nRrv4KfNFh!ArF@(e6KtOKcZC$n#^?i-+x0+Y( zwtNmW>B0H)yZM3i4;>>;1_p%`AqEDA6DkZ04HGcrpM9(!!HuG_tl*Q)Pv=A3)AtPCHfhNWMsy`%qlQ-#Ql1)nWWt1vvMx4AnS z4Wo%~G;fZUNTcQOXf-ujIgd6;MjK*OY>15(DI=^%dGl7=C!IHU{>D9vrUR#XQ>M8SM~`+-~qkCiRWdx~wKoCjv_b22WQ%mvv4FO#ohn Bo)rK9 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimageregiondpi/expected_layoutexporter_rendertoimageregiondpi.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimageregiondpi/expected_layoutexporter_rendertoimageregiondpi.png new file mode 100644 index 0000000000000000000000000000000000000000..429ffdae6cb7d37a739f8e999d8843d4cd72d7ec GIT binary patch literal 572 zcmeAS@N?(olHy`uVBq!ia0vp^Yk_z!2OE$)cJ;?QAjMhW5n0T@z@`SmjPr%9lo=Qp z-+8(?hE&XXdut=_AqN45gUl0m3b;tVn`0EyQ5?)TZGu3?(wSfMb|f_O|0|L?ZYWUn z_dDYU&5M(iql#R-`!Y@n>RK*Rb>H%&W10+#;P%ZA?S9)l|G79gA#G;rwMhSRz4ZT? z+iyNI>|Za>_WsUuyW2U-t>>|0XvAvTs32PKk{KXzxnBj1E&hEv0Za}Ip00i_>zopr E0I}BV`_0*U4<7@K9Yb~BjT%VP#4{SagFblmt)2_z Vel+mh3@nuxJYD@<);T3K0RS43btwP< literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimageregionsize/expected_layoutexporter_rendertoimageregionsize.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_rendertoimageregionsize/expected_layoutexporter_rendertoimageregionsize.png new file mode 100644 index 0000000000000000000000000000000000000000..6660fb73a7994a523e7deed386a055fb5b2466af GIT binary patch literal 2060 zcmeAS@N?(olHy`uVBq!ia0y~yU@~A}VEoI$1QhA!y?7o-aTa()7BevL9R^{>S?m5Pgu{pLuTkJ~q35>$P=jll$J+ zl&@tJC_h(uW@Gl*(|ipqj!X)U6Bs-MR2YO W@ayEZhWo(kox#)9&t;ucLK6V4IA=Ei literal 0 HcmV?d00001 From 25d16380c0f3f0da92a3aa2bdbbd4585ed51cf42 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 12:53:48 +1000 Subject: [PATCH 15/56] Port reference map functionality from composer --- src/app/layout/qgslayoutpropertieswidget.cpp | 14 +++++++ src/app/layout/qgslayoutpropertieswidget.h | 2 +- src/core/layout/qgslayout.cpp | 29 +++++++++++-- src/core/layout/qgslayout.h | 9 ++-- src/core/layout/qgslayoutexporter.cpp | 5 ++- src/core/layout/qgslayoututils.cpp | 11 +++-- src/ui/layout/qgslayoutwidgetbase.ui | 43 ++++++++++++++++++-- tests/src/core/testqgslayout.cpp | 18 ++++---- tests/src/core/testqgslayoututils.cpp | 25 ++++-------- tests/src/python/test_qgslayout.py | 3 ++ 10 files changed, 112 insertions(+), 47 deletions(-) diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index 76e91c403e7f..29113ee0e0c6 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -19,6 +19,7 @@ #include "qgslayoutsnapper.h" #include "qgslayoutpagecollection.h" #include "qgslayoutundostack.h" +#include "qgslayoutitemmap.h" QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout *layout ) : QgsPanelWidget( parent ) @@ -69,6 +70,11 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout connect( mBottomMarginSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::resizeMarginsChanged ); connect( mLeftMarginSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::resizeMarginsChanged ); connect( mResizePageButton, &QPushButton::clicked, this, &QgsLayoutPropertiesWidget::resizeToContents ); + + connect( mReferenceMapComboBox, &QgsLayoutItemComboBox::itemChanged, this, &QgsLayoutPropertiesWidget::referenceMapChanged ); + + mReferenceMapComboBox->setCurrentLayout( mLayout ); + mReferenceMapComboBox->setItem( mLayout->referenceMap() ); } void QgsLayoutPropertiesWidget::updateSnappingElements() @@ -150,6 +156,14 @@ void QgsLayoutPropertiesWidget::resizeToContents() mLayout->undoStack()->endMacro(); } +void QgsLayoutPropertiesWidget::referenceMapChanged( QgsLayoutItem *item ) +{ + mLayout->undoStack()->beginCommand( mLayout, tr( "Set Reference Map" ) ); + QgsLayoutItemMap *map = qobject_cast< QgsLayoutItemMap * >( item ); + mLayout->setReferenceMap( map ); + mLayout->undoStack()->endCommand(); +} + void QgsLayoutPropertiesWidget::blockSignals( bool block ) { mGridResolutionSpinBox->blockSignals( block ); diff --git a/src/app/layout/qgslayoutpropertieswidget.h b/src/app/layout/qgslayoutpropertieswidget.h index 01153a9d1e08..3af848eb89bd 100644 --- a/src/app/layout/qgslayoutpropertieswidget.h +++ b/src/app/layout/qgslayoutpropertieswidget.h @@ -38,7 +38,7 @@ class QgsLayoutPropertiesWidget: public QgsPanelWidget, private Ui::QgsLayoutWid void snapToleranceChanged( int tolerance ); void resizeMarginsChanged(); void resizeToContents(); - + void referenceMapChanged( QgsLayoutItem *item ); private: QgsLayout *mLayout = nullptr; diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index c78cce6ade8c..adf621d42218 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -25,6 +25,7 @@ #include "qgslayoutitemgroup.h" #include "qgslayoutitemgroupundocommand.h" #include "qgslayoutmultiframe.h" +#include "qgslayoutitemmap.h" #include "qgslayoutundostack.h" QgsLayout::QgsLayout( QgsProject *project ) @@ -214,7 +215,7 @@ bool QgsLayout::moveItemToBottom( QgsLayoutItem *item, bool deferUpdate ) return result; } -QgsLayoutItem *QgsLayout::itemByUuid( const QString &uuid, bool includeTemplateUuids ) +QgsLayoutItem *QgsLayout::itemByUuid( const QString &uuid, bool includeTemplateUuids ) const { QList itemList; layoutItems( itemList ); @@ -358,12 +359,31 @@ QStringList QgsLayout::customProperties() const QgsLayoutItemMap *QgsLayout::referenceMap() const { - return nullptr; + // prefer explicitly set reference map + if ( QgsLayoutItemMap *map = qobject_cast< QgsLayoutItemMap * >( itemByUuid( mWorldFileMapId ) ) ) + return map; + + // else try to find largest map + QList< QgsLayoutItemMap * > maps; + layoutItems( maps ); + QgsLayoutItemMap *largestMap = nullptr; + double largestMapArea = 0; + for ( QgsLayoutItemMap *map : qgis::as_const( maps ) ) + { + double area = map->rect().width() * map->rect().height(); + if ( area > largestMapArea ) + { + largestMapArea = area; + largestMap = map; + } + } + return largestMap; } void QgsLayout::setReferenceMap( QgsLayoutItemMap *map ) { - Q_UNUSED( map ); + mWorldFileMapId = map ? map->uuid() : QString(); + mProject->setDirty( true ); } QgsLayoutPageCollection *QgsLayout::pageCollection() @@ -689,6 +709,7 @@ void QgsLayout::writeXmlLayoutSettings( QDomElement &element, QDomDocument &docu mCustomProperties.writeXml( element, document ); element.setAttribute( QStringLiteral( "name" ), mName ); element.setAttribute( QStringLiteral( "units" ), QgsUnitTypes::encodeUnit( mUnits ) ); + element.setAttribute( QStringLiteral( "worldFileMap" ), mWorldFileMapId ); } QDomElement QgsLayout::writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const @@ -731,6 +752,8 @@ bool QgsLayout::readXmlLayoutSettings( const QDomElement &layoutElement, const Q mCustomProperties.readXml( layoutElement ); setName( layoutElement.attribute( QStringLiteral( "name" ) ) ); setUnits( QgsUnitTypes::decodeLayoutUnit( layoutElement.attribute( QStringLiteral( "units" ) ) ) ); + mWorldFileMapId = layoutElement.attribute( QStringLiteral( "worldFileMap" ) ); + return true; } diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 97f42ea53870..6aaf9f899d66 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -116,7 +116,7 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * Returns a list of layout items of a specific type. * \note not available in Python bindings */ - template void layoutItems( QList &itemList ) SIP_SKIP + template void layoutItems( QList &itemList ) const SIP_SKIP { itemList.clear(); QList graphicsItemList = items(); @@ -223,7 +223,7 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * * \see multiFrameByUuid() */ - QgsLayoutItem *itemByUuid( const QString &uuid, bool includeTemplateUuids = false ); + QgsLayoutItem *itemByUuid( const QString &uuid, bool includeTemplateUuids = false ) const; /** * Returns the layout multiframe with matching \a uuid unique identifier, or a nullptr @@ -404,7 +404,6 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * \see setReferenceMap() * \see generateWorldFile() */ - //TODO QgsLayoutItemMap *referenceMap() const; /** @@ -413,7 +412,6 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * \see referenceMap() * \see setGenerateWorldFile() */ - //TODO void setReferenceMap( QgsLayoutItemMap *map ); /** @@ -627,6 +625,9 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext //! List of multiframe objects QList mMultiFrames; + //! Item ID for composer map to use for the world file generation + QString mWorldFileMapId; + //! Writes only the layout settings (not member settings like grid settings, etc) to XML void writeXmlLayoutSettings( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; //! Reads only the layout settings (not member settings like grid settings, etc) from XML diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 9a76ab75d182..b461b2315254 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -148,12 +148,13 @@ class LayoutDpiRestorer double mPreviousSetting = 0; }; ///@endcond PRIVATE + QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ) { int worldFilePageNo = -1; - if ( mLayout->referenceMap() ) + if ( QgsLayoutItemMap *referenceMap = mLayout->referenceMap() ) { - worldFilePageNo = mLayout->referenceMap()->page(); + worldFilePageNo = referenceMap->page(); } QFileInfo fi( filePath ); diff --git a/src/core/layout/qgslayoututils.cpp b/src/core/layout/qgslayoututils.cpp index 9c58443ee22b..221197183476 100644 --- a/src/core/layout/qgslayoututils.cpp +++ b/src/core/layout/qgslayoututils.cpp @@ -117,15 +117,14 @@ QgsRenderContext QgsLayoutUtils::createRenderContextForMap( QgsLayoutItemMap *ma { dpi = ( painter && painter->device() ) ? painter->device()->logicalDpiX() : 88; } -#if 0 double dotsPerMM = dpi / 25.4; -// TODO + // get map settings from reference map - QgsRectangle extent = *( map->currentMapExtent() ); - QSizeF mapSizeMM = map->rect().size(); + QgsRectangle extent = map->extent(); + QSizeF mapSizeLayoutUnits = map->rect().size(); + QSizeF mapSizeMM = map->layout()->convertFromLayoutUnits( mapSizeLayoutUnits, QgsUnitTypes::LayoutMillimeters ).toQSizeF(); QgsMapSettings ms = map->mapSettings( extent, mapSizeMM * dotsPerMM, dpi ); -#endif - QgsRenderContext context; // = QgsRenderContext::fromMapSettings( ms ); + QgsRenderContext context = QgsRenderContext::fromMapSettings( ms ); if ( painter ) context.setPainter( painter ); diff --git a/src/ui/layout/qgslayoutwidgetbase.ui b/src/ui/layout/qgslayoutwidgetbase.ui index 88a5d33be19d..afc706ad95ce 100644 --- a/src/ui/layout/qgslayoutwidgetbase.ui +++ b/src/ui/layout/qgslayoutwidgetbase.ui @@ -6,8 +6,8 @@ 0 0 - 234 - 327 + 311 + 515
@@ -54,11 +54,40 @@ 0 0 - 234 - 327 + 297 + 632 + + + + GroupBox + + + + + + Reference map + + + + + + + true + + + Specifies the master map for this composition, which is used to georeference composer exports and for scale calculation for item styles. + + + false + + + + + + @@ -343,9 +372,15 @@ QSpinBox
qgsspinbox.h
+ + QgsLayoutItemComboBox + QComboBox +
qgslayoutitemcombobox.h
+
scrollArea + mReferenceMapComboBox mSnapToGridGroupCheckBox mGridResolutionSpinBox mGridSpacingUnitsCombo diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index 1c9e90dd8e9e..a0e4bb22978f 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -239,27 +239,23 @@ void TestQgsLayout::referenceMap() // no maps QVERIFY( !l.referenceMap() ); -#if 0 QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); - map->setNewExtent( extent ); - map->setSceneRect( QRectF( 30, 60, 200, 100 ) ); - l.addComposerMap( map ); + map->attemptSetSceneRect( QRectF( 30, 60, 200, 100 ) ); + map->setExtent( extent ); + l.addLayoutItem( map ); QCOMPARE( l.referenceMap(), map ); -#endif -#if 0 // TODO // add a larger map QgsLayoutItemMap *map2 = new QgsLayoutItemMap( &l ); - map2->setNewExtent( extent ); - map2->setSceneRect( QRectF( 30, 60, 250, 150 ) ); - l.addComposerMap( map2 ); + map2->attemptSetSceneRect( QRectF( 30, 60, 250, 150 ) ); + map2->setExtent( extent ); + l.addLayoutItem( map2 ); + QCOMPARE( l.referenceMap(), map2 ); // explicitly set reference map l.setReferenceMap( map ); QCOMPARE( l.referenceMap(), map ); -#endif - } void TestQgsLayout::bounds() diff --git a/tests/src/core/testqgslayoututils.cpp b/tests/src/core/testqgslayoututils.cpp index c867a5da1222..c8a3c820c8bb 100644 --- a/tests/src/core/testqgslayoututils.cpp +++ b/tests/src/core/testqgslayoututils.cpp @@ -244,10 +244,8 @@ void TestQgsLayoutUtils::createRenderContextFromLayout() // add a reference map QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); -#if 0 // TODO - map->setNewExtent( extent ); - map->setSceneRect( QRectF( 30, 60, 200, 100 ) ); -#endif + map->attemptSetSceneRect( QRectF( 30, 60, 200, 100 ) ); + map->setExtent( extent ); l.addLayoutItem( map ); l.setReferenceMap( map ); @@ -306,16 +304,12 @@ void TestQgsLayoutUtils::createRenderContextFromMap() QgsProject project; QgsLayout l( &project ); -#if 0 // TODO // add a map QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 30, 60, 200, 100 ) ); + map->setExtent( extent ); + l.addLayoutItem( map ); - map->setNewExtent( extent ); - map->setSceneRect( QRectF( 30, 60, 200, 100 ) ); - l.addComposerMap( map ); -#endif - -#if 0 //TODO rc = QgsLayoutUtils::createRenderContextForMap( map, &p ); QGSCOMPARENEAR( rc.scaleFactor(), 150 / 25.4, 0.001 ); QGSCOMPARENEAR( rc.rendererScale(), map->scale(), 1000000 ); @@ -329,10 +323,9 @@ void TestQgsLayoutUtils::createRenderContextFromMap() // secondary map QgsLayoutItemMap *map2 = new QgsLayoutItemMap( &l ); - - map2->setNewExtent( extent ); - map2->setSceneRect( QRectF( 30, 60, 100, 50 ) ); - composition->addComposerMap( map2 ); + map2->attemptSetSceneRect( QRectF( 30, 60, 100, 50 ) ); + map2->setExtent( extent ); + l.addLayoutItem( map2 ); rc = QgsLayoutUtils::createRenderContextForMap( map2, &p ); QGSCOMPARENEAR( rc.scaleFactor(), 150 / 25.4, 0.001 ); @@ -357,7 +350,7 @@ void TestQgsLayoutUtils::createRenderContextFromMap() QVERIFY( ( rc.flags() & QgsRenderContext::Antialiasing ) ); QVERIFY( ( rc.flags() & QgsRenderContext::UseAdvancedEffects ) ); QVERIFY( ( rc.flags() & QgsRenderContext::ForceVectorOutput ) ); -#endif + p.end(); } diff --git a/tests/src/python/test_qgslayout.py b/tests/src/python/test_qgslayout.py index a911215debc1..fac9c8ebb75c 100644 --- a/tests/src/python/test_qgslayout.py +++ b/tests/src/python/test_qgslayout.py @@ -86,6 +86,8 @@ def testReadWriteXml(self): item2.setId('zzyyzz') l.addItem(item2) + l.setReferenceMap(item2) + doc = QDomDocument("testdoc") elem = l.writeXml(doc, QgsReadWriteContext()) @@ -112,6 +114,7 @@ def testReadWriteXml(self): new_item2 = l2.itemByUuid(item2.uuid()) self.assertTrue(new_item2) self.assertEqual(new_item2.id(), 'zzyyzz') + self.assertEqual(l2.referenceMap().id(), 'zzyyzz') def testAddItemsFromXml(self): p = QgsProject() From 2e68dd7005a4dd0a01c406410614e1ad58641ae2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 12:58:24 +1000 Subject: [PATCH 16/56] Add some masks for scalebar tests --- .../expected_layoutscalebar_doublebox_mask.png | Bin 0 -> 6500 bytes .../expected_layoutscalebar_numeric_mask.png | Bin 0 -> 6197 bytes .../expected_layoutscalebar_singlebox_mask.png | Bin 0 -> 6500 bytes ...ected_layoutscalebar_singlebox_alpha_mask.png | Bin 0 -> 6234 bytes .../expected_layoutscalebar_tick_mask.png | Bin 0 -> 6500 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox/expected_layoutscalebar_doublebox_mask.png create mode 100644 tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_numeric/fedora/expected_layoutscalebar_numeric_mask.png create mode 100644 tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox/expected_layoutscalebar_singlebox_mask.png create mode 100644 tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_alpha/expected_layoutscalebar_singlebox_alpha_mask.png create mode 100644 tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_tick/expected_layoutscalebar_tick_mask.png diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox/expected_layoutscalebar_doublebox_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox/expected_layoutscalebar_doublebox_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..e9c6bf7539a80c167621e4423f85728d3a692800 GIT binary patch literal 6500 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox/expected_layoutscalebar_singlebox_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox/expected_layoutscalebar_singlebox_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..e9c6bf7539a80c167621e4423f85728d3a692800 GIT binary patch literal 6500 zcmeHL{Z~?F9KU5}lvZkPiHgxYTb`C_l!1a^d35E@l5Li{(kL-KmVzXK?{wyzS_x~M zlN2dCtLZxSGIWN9Z~4N_w>XX^GsKq&$q)qJ5Eb0zoc#;iPxpuWeDCw}z0dc1pYQj1 zKF_E8tC(ZkoV}d^0N56NJn|a=*pv(aPTre8fLaE%6RnWgl6CysYyfaQ?|3%+7G9eU z0A4$zBafcUxoc1lUP?UW)G>Jo9qkkOd*I3YO<%iJZiuIKx2sDgI)6R)pv=X6Q+!bh z{!nkr^-7;Tl*7Ys<18RMwB zf9&Yq?bq$7M1GHi%By8=PLObOhs*^4zlg0+c_b26f`0@Y5pZh2djz~$!lwp&e8P|b z#zrtmgwZl|Ie;Mn3<+RJ07C*868y)I;Kkg9_N(zj1tc^AfuLfA6&RUdFqPUca>Kw{ z{fRQ7zXY{uFNdZ!jFddV$U;Ixn-3O=6B3c(GNink=#!R~78LvvGbE7fs3Z}uCmB;s z%x7uMU0q!+^Bfk7WoRlcE*2*Q1g>Q?o=)lp?2+RGlhWNHld$#dU`s9NNk;3P8K8eiz@}A%r8+WR3Z{5l7lR%uT*VZTYYu$ z@UF0&tn;HFniKX^WysM~;AD_u&ceH)WynI%lTyGy(_H}|Cu%dsGO@704A^;HeEN~B zx3?E^RTr+&3RvBc5A(U9L?Tfn?62uKSV0U6L)E={-}j5{sRrd?p*vHRwiC4`R#sM4 z(bf#CaQ=XVE>Trj3rNHI#e_$38hQ9S$`wsQsSHb<+3l9Ge9a(kJd9)XCs*Hq`U(nc zi%N}x9NucNxB$Q!ZVSe8j$`RUu7{txAWpc;>5PHCmn7h0WY+YfikRE|OHPA%t!4<@pI}!tnaI0Tiq6;Muq^3kgpl~|GYU*+Y6}4ng1M)z}bR;7$ z+>Z43_ty;FJH}qILr^5X)Xf7Asj!~Q`wvyc1=|SwyMu%~ClyqQFI%`ijxoz;L>Cw0 zANyom^GFh_4dkJDqN0`QJ`J?WhUK?>@+K?*One@9l|55K*0QDQ67pZU)|%KamQG}| z6)3x!p|EhvSJ~DN?-k-oZtADrKNHchLKi~SQqZ%OStW(zOK-O=87RR3V0tay__i^4 z|IeP+d&Xb8KW~@QBvMA{pGkRV-{sz9IH<_zvdPB7B6*Y`z9R$cGjA;jsO%Rf27)6a zBi5eV7PgO#f22MkfOe(MTS0@W&Y)|wvsaQ$3_64ebPP3w~@!`4W?Q~EYOTPS)(|!#L+#| z_l~kgk=*h=`}X^`+@=5`D{w0RNsqy_x zSvA*)ep8J@R2HkR%;TZNGMmlIM(M0TV8(^(bo=iWJc z_MCe@`>nW4jySmAbw30_2O}?reGWnPX%Mu>b?@6?My7rx0%qUMi}867bSTw!*|m{c zvmoe*V`SL5tBgjSQcJ#)c&uzJ@>%q|k1j-?R9S}`oFB38w*@mvgT947hdce{&|6jO z>?i%<_W_Ia{&XR|s>2oR3N6hn3GlX?rzM_4HXC;L2pG%Q&U=7yJ`DVTAg@qIFn$uo zwgZN}mtdPa>}hk(!wXx41p>1or%*8wh- z!}RtE z|EQ>Uk4M?jR&-`+ES?|pWp_d~;e|Z4Bd!r?q$Kkc?W~!6e0G^Z96-t%o^mG;m`uN zQCbe-i!!?t%%5ET1V*3M)sX-B3ympoX%PmL~N;(ej94LC*R% z*p_C=aH;6CH$ZmOJ9uV9D3ZP3JbVvQ({i}taQbPH4&@5+qE9N$OVe9Z1cs|ZH+0DA>)}cQS2i*ysgYif`Kq%<#D>G{*qP- zk7il{-v@4V-c$Ev@%cW=DHFYqU+nJYRyCYPaB^}Qslk(Iv$Y@$tdIYd1z!hX3MwWg d{|}n_8tMIG_hy4V_&%U`p~!G@m>@Kz{2$=KeVPCO literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_tick/expected_layoutscalebar_tick_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_tick/expected_layoutscalebar_tick_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..e9c6bf7539a80c167621e4423f85728d3a692800 GIT binary patch literal 6500 zcmeHL{Z~?F9KU5}lvZkPiHgxYTb`C_l!1a^d35E@l5Li{(kL-KmVzXK?{wyzS_x~M zlN2dCtLZxSGIWN9Z~4N_w>XX^GsKq&$q)qJ5Eb0zoc#;iPxpuWeDCw}z0dc1pYQj1 zKF_E8tC(ZkoV}d^0N56NJn|a=*pv(aPTre8fLaE%6RnWgl6CysYyfaQ?|3%+7G9eU z0A4$zBafcUxoc1lUP?UW)G>Jo9qkkOd*I3YO<%iJZiuIKx2sDgI)6R)pv=X6Q+!bh z{!nkr^-7;Tl*7Ys<18RMwB zf9&Yq?bq$7M1GHi%By8=PLObOhs*^4zlg0+c_b26f`0@Y5pZh2djz~$!lwp&e8P|b z#zrtmgwZl|Ie;Mn3<+RJ07C*868y)I;Kkg9_N(zj1tc^AfuLfA6&RUdFqPUca>Kw{ z{fRQ7zXY{uFNdZ!jFddV$U;Ixn-3O=6B3c(GNink=#!R~78LvvGbE7fs3Z}uCmB;s z%x7uMU0q!+^Bfk7WoRlcE*2*Q1g>Q?o=)lp?2+RGlhWNHld$#dU`s9NNk;3P8K8eiz@}A%r8+WR3Z{5l7lR%uT*VZTYYu$ z@UF0&tn;HFniKX^WysM~;AD_u&ceH)WynI%lTyGy(_H}|Cu%dsGO@704A^;HeEN~B zx3?E^RTr+&3RvBc5A(U9L?Tfn?62uKSV0U6L)E={-}j5{sRrd?p*vHRwiC4`R#sM4 z(bf#CaQ=XVE>Trj3rNHI#e_$38hQ9S$`wsQsSHb<+3l9Ge9a(kJd9)XCs*Hq`U(nc zi%N}x9NucNxB$Q!ZVSe8j$`RUu7{txAWpc;>5PHCmn7h0WY+YfikRE|OHPA%t!4<@pI}!tnaI0Tiq6;Muq^3kgpl~|GYU*+Y6}4ng1M)z}bR;7$ z+>Z43_ty;FJH}qILr^5X)Xf7Asj!~Q`wvyc1=|SwyMu%~ClyqQFI%`ijxoz;L>Cw0 zANyom^GFh_4dkJDqN0`QJ`J?WhUK?>@+K?*One@9l|55K*0QDQ67pZU)|%KamQG}| z6)3x!p|EhvSJ~DN?-k-oZtADrKNHchLKi~SQqZ%OStW(zOK-O=87RR3V0tay__i^4 z|IeP+d&Xb8KW~@QBvMA{pGkRV-{sz9IH<_zvdPB7B6*Y`z9R$cGjA;jsO%Rf27)6a zBi5eV7PgO#f22MkfOe(MTS0@W&Y)|wvsaQ$3_64ebPP3w~@!`4W?Q~EYOTPS)(|!#L+#| z_l~kgk=*h=`}X^`+@=5`D{w0RNsqy_x zSvA*)ep8J@R2HkR%;TZNGMmlIM(M0TV8( Date: Mon, 11 Dec 2017 13:21:36 +1000 Subject: [PATCH 17/56] Fix updating gui after undoing layout settings change --- python/core/layout/qgslayout.sip | 7 +++++++ src/app/layout/qgslayoutpropertieswidget.cpp | 9 ++++++++- src/app/layout/qgslayoutpropertieswidget.h | 2 ++ src/core/layout/qgslayout.cpp | 7 ++++++- src/core/layout/qgslayout.h | 7 +++++++ 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index c05886882594..b46eb991ddf3 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -541,6 +541,13 @@ Updates the scene bounds of the layout. signals: + void changed(); +%Docstring + Is emitted when properties of the layout change. This signal is only + emitted for settings directly managed by the layout, and is not emitted + when child items change. +%End + void variablesChanged(); %Docstring Emitted whenever the expression variables stored in the layout have been changed. diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index 29113ee0e0c6..0f0adc12a2c8 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -74,7 +74,14 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout connect( mReferenceMapComboBox, &QgsLayoutItemComboBox::itemChanged, this, &QgsLayoutPropertiesWidget::referenceMapChanged ); mReferenceMapComboBox->setCurrentLayout( mLayout ); - mReferenceMapComboBox->setItem( mLayout->referenceMap() ); + + connect( mLayout, &QgsLayout::changed, this, &QgsLayoutPropertiesWidget::updateGui ); + updateGui(); +} + +void QgsLayoutPropertiesWidget::updateGui() +{ + whileBlocking( mReferenceMapComboBox )->setItem( mLayout->referenceMap() ); } void QgsLayoutPropertiesWidget::updateSnappingElements() diff --git a/src/app/layout/qgslayoutpropertieswidget.h b/src/app/layout/qgslayoutpropertieswidget.h index 3af848eb89bd..06d9e3713a3d 100644 --- a/src/app/layout/qgslayoutpropertieswidget.h +++ b/src/app/layout/qgslayoutpropertieswidget.h @@ -30,6 +30,8 @@ class QgsLayoutPropertiesWidget: public QgsPanelWidget, private Ui::QgsLayoutWid private slots: + void updateGui(); + void gridResolutionChanged( double d ); void gridResolutionUnitsChanged( QgsUnitTypes::LayoutUnit unit ); void gridOffsetXChanged( double d ); diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index adf621d42218..8bacc85dd731 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -624,7 +624,7 @@ class QgsLayoutUndoCommand: public QgsAbstractLayoutUndoCommand return; } - mLayout->readXmlLayoutSettings( stateDoc.documentElement().firstChild().toElement(), stateDoc, QgsReadWriteContext() ); + mLayout->readXmlLayoutSettings( stateDoc.documentElement(), stateDoc, QgsReadWriteContext() ); mLayout->project()->setDirty( true ); } @@ -753,6 +753,7 @@ bool QgsLayout::readXmlLayoutSettings( const QDomElement &layoutElement, const Q setName( layoutElement.attribute( QStringLiteral( "name" ) ) ); setUnits( QgsUnitTypes::decodeLayoutUnit( layoutElement.attribute( QStringLiteral( "units" ) ) ) ); mWorldFileMapId = layoutElement.attribute( QStringLiteral( "worldFileMap" ) ); + emit changed(); return true; } @@ -848,13 +849,17 @@ bool QgsLayout::readXml( const QDomElement &layoutElement, const QDomDocument &d return object->readXml( layoutElement, document, context ); }; + blockSignals( true ); // defer changed signal to end readXmlLayoutSettings( layoutElement, document, context ); + blockSignals( false ); restore( mPageCollection.get() ); restore( &mSnapper ); restore( &mGridSettings ); addItemsFromXml( layoutElement, document, context ); + emit changed(); + return true; } diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 6aaf9f899d66..83f5de19b7e3 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -582,6 +582,13 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext signals: + /** + * Is emitted when properties of the layout change. This signal is only + * emitted for settings directly managed by the layout, and is not emitted + * when child items change. + */ + void changed(); + /** * Emitted whenever the expression variables stored in the layout have been changed. */ From afbd1400a5ae38459effe67da7d61c2c1f9e5cd2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 13:39:01 +1000 Subject: [PATCH 18/56] Port georeferencing from compositions --- python/core/layout/qgslayoutexporter.sip | 19 ++++ src/core/layout/qgslayoutexporter.cpp | 128 ++++++++++++++++++++++- src/core/layout/qgslayoutexporter.h | 37 +++++++ tests/src/core/testqgslayout.cpp | 70 +++++++++++++ 4 files changed, 251 insertions(+), 3 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index 8921afb13b37..eac2a8d3e237 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -155,6 +155,25 @@ Resolution to export layout at :rtype: ExportResult %End + bool georeferenceOutput( const QString &file, QgsLayoutItemMap *referenceMap = 0, + const QRectF &exportRegion = QRectF(), double dpi = -1 ) const; +%Docstring + Georeferences a ``file`` (image of PDF) exported from the layout. + + The ``referenceMap`` argument specifies a map item to use for georeferencing. If left as None, the + default layout QgsLayout.referenceMap() will be used. + + The ``exportRegion`` argument can be set to a valid rectangle to indicate that only part of the layout was + exported. + + Similarly, the ``dpi`` can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. + + The function will return true if the output was successfully georeferenced. + +.. seealso:: :py:func:`computeGeoTransform()` + :rtype: bool +%End + }; diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index b461b2315254..e599ef8c50c7 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -18,9 +18,13 @@ #include "qgslayout.h" #include "qgslayoutitemmap.h" #include "qgslayoutpagecollection.h" +#include "qgsogrutils.h" #include #include +#include "gdal.h" +#include "cpl_conv.h" + QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout ) : mLayout( layout ) { @@ -206,10 +210,10 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString return FileError; } -#if 0 //TODO if ( page == worldFilePageNo ) { - mLayout->georeferenceOutput( outputFilePath, nullptr, bounds, imageDlg.resolution() ); +#if 0 + georeferenceOutput( outputFilePath, nullptr, bounds, imageDlg.resolution() ); if ( settings.generateWorldFile ) { @@ -228,12 +232,130 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString writeWorldFile( worldFileName, a, b, c, d, e, f ); } - } #endif + } + } return Success; } +double *QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF ®ion, double dpi ) const +{ + if ( !map ) + map = mLayout->referenceMap(); + + if ( !map ) + return nullptr; + + if ( dpi < 0 ) + dpi = mLayout->context().dpi(); + + // calculate region of composition to export (in mm) + QRectF exportRegion = region; + if ( !exportRegion.isValid() ) + { + int pageNumber = map->page(); + + QgsLayoutItemPage *page = mLayout->pageCollection()->page( pageNumber ); + double pageY = page->pos().y(); + QSizeF pageSize = page->rect().size(); + exportRegion = QRectF( 0, pageY, pageSize.width(), pageSize.height() ); + } + + // map rectangle (in mm) + QRectF mapItemSceneRect = map->mapRectToScene( map->rect() ); + + // destination width/height in mm + double outputHeightMM = exportRegion.height(); + double outputWidthMM = exportRegion.width(); + + // map properties + QgsRectangle mapExtent = map->extent(); + double mapXCenter = mapExtent.center().x(); + double mapYCenter = mapExtent.center().y(); + double alpha = - map->mapRotation() / 180 * M_PI; + double sinAlpha = std::sin( alpha ); + double cosAlpha = std::cos( alpha ); + + // get the extent (in map units) for the exported region + QPointF mapItemPos = map->pos(); + //adjust item position so it is relative to export region + mapItemPos.rx() -= exportRegion.left(); + mapItemPos.ry() -= exportRegion.top(); + + // calculate extent of entire page in map units + double xRatio = mapExtent.width() / mapItemSceneRect.width(); + double yRatio = mapExtent.height() / mapItemSceneRect.height(); + double xmin = mapExtent.xMinimum() - mapItemPos.x() * xRatio; + double ymax = mapExtent.yMaximum() + mapItemPos.y() * yRatio; + QgsRectangle paperExtent( xmin, ymax - outputHeightMM * yRatio, xmin + outputWidthMM * xRatio, ymax ); + + // calculate origin of page + double X0 = paperExtent.xMinimum(); + double Y0 = paperExtent.yMaximum(); + + if ( !qgsDoubleNear( alpha, 0.0 ) ) + { + // translate origin to account for map rotation + double X1 = X0 - mapXCenter; + double Y1 = Y0 - mapYCenter; + double X2 = X1 * cosAlpha + Y1 * sinAlpha; + double Y2 = -X1 * sinAlpha + Y1 * cosAlpha; + X0 = X2 + mapXCenter; + Y0 = Y2 + mapYCenter; + } + + // calculate scaling of pixels + int pageWidthPixels = static_cast< int >( dpi * outputWidthMM / 25.4 ); + int pageHeightPixels = static_cast< int >( dpi * outputHeightMM / 25.4 ); + double pixelWidthScale = paperExtent.width() / pageWidthPixels; + double pixelHeightScale = paperExtent.height() / pageHeightPixels; + + // transform matrix + double *t = new double[6]; + t[0] = X0; + t[1] = cosAlpha * pixelWidthScale; + t[2] = -sinAlpha * pixelWidthScale; + t[3] = Y0; + t[4] = -sinAlpha * pixelHeightScale; + t[5] = -cosAlpha * pixelHeightScale; + + return t; +} + +bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMap *map, const QRectF &exportRegion, double dpi ) const +{ + if ( !map ) + map = mLayout->referenceMap(); + + if ( !map ) + return false; // no reference map + + if ( dpi < 0 ) + dpi = mLayout->context().dpi(); + + double *t = computeGeoTransform( map, exportRegion, dpi ); + if ( !t ) + return false; + + // important - we need to manually specify the DPI in advance, as GDAL will otherwise + // assume a DPI of 150 + CPLSetConfigOption( "GDAL_PDF_DPI", QString::number( dpi ).toLocal8Bit().constData() ); + gdal::dataset_unique_ptr outputDS( GDALOpen( file.toLocal8Bit().constData(), GA_Update ) ); + if ( outputDS ) + { + GDALSetGeoTransform( outputDS.get(), t ); +#if 0 + //TODO - metadata can be set here, e.g.: + GDALSetMetadataItem( outputDS, "AUTHOR", "me", nullptr ); +#endif + GDALSetProjection( outputDS.get(), map->crs().toWkt().toLocal8Bit().constData() ); + } + CPLSetConfigOption( "GDAL_PDF_DPI", nullptr ); + delete[] t; + return true; +} + QImage QgsLayoutExporter::createImage( const QgsLayoutExporter::ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const { bounds = QRectF(); diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 2656837e2bb7..141f47879df8 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -20,9 +20,11 @@ #include "qgsmargins.h" #include #include +#include class QgsLayout; class QPainter; +class QgsLayoutItemMap; /** * \ingroup core @@ -166,6 +168,24 @@ class CORE_EXPORT QgsLayoutExporter */ ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings ); + /** + * Georeferences a \a file (image of PDF) exported from the layout. + * + * The \a referenceMap argument specifies a map item to use for georeferencing. If left as nullptr, the + * default layout QgsLayout::referenceMap() will be used. + * + * The \a exportRegion argument can be set to a valid rectangle to indicate that only part of the layout was + * exported. + * + * Similarly, the \a dpi can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. + * + * The function will return true if the output was successfully georeferenced. + * + * \see computeGeoTransform() + */ + bool georeferenceOutput( const QString &file, QgsLayoutItemMap *referenceMap = nullptr, + const QRectF &exportRegion = QRectF(), double dpi = -1 ) const; + private: QPointer< QgsLayout > mLayout; @@ -179,6 +199,23 @@ class CORE_EXPORT QgsLayoutExporter */ static bool saveImage( const QImage &image, const QString &imageFilename, const QString &imageFormat ); + /** + * Computes a GDAL style geotransform for georeferencing a layout. + * + * The \a referenceMap argument specifies a map item to use for georeferencing. If left as nullptr, the + * default layout QgsLayout::referenceMap() will be used. + * + * The \a exportRegion argument can be set to a valid rectangle to indicate that only part of the layout was + * exported. + * + * Similarly, the \a dpi can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. + * + * \see georeferenceOutput() + */ + double *computeGeoTransform( const QgsLayoutItemMap *referenceMap = nullptr, const QRectF &exportRegion = QRectF(), double dpi = -1 ) const; + + friend class TestQgsLayout; + }; #endif //QGSLAYOUTEXPORTER_H diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index a0e4bb22978f..fd10006d8b00 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -52,6 +52,7 @@ class TestQgsLayout: public QObject void shouldExportPage(); void pageIsEmpty(); void clear(); + void georeference(); private: QString mReport; @@ -724,6 +725,75 @@ void TestQgsLayout::clear() QCOMPARE( l.undoStack()->stack()->count(), 0 ); } +void TestQgsLayout::georeference() +{ + QgsRectangle extent( 2000, 2800, 2500, 2900 ); + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + + QgsLayoutExporter exporter( &l ); + + // no map + double *t = exporter.computeGeoTransform( nullptr ); + QVERIFY( !t ); + + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 30, 60, 200, 100 ) ); + map->setExtent( extent ); + l.addLayoutItem( map ); + + t = exporter.computeGeoTransform( map ); + QGSCOMPARENEAR( t[0], 1925.0, 1.0 ); + QGSCOMPARENEAR( t[1], 0.211719, 0.0001 ); + QGSCOMPARENEAR( t[2], 0.0, 4 * DBL_EPSILON ); + QGSCOMPARENEAR( t[3], 3050, 1 ); + QGSCOMPARENEAR( t[4], 0.0, 4 * DBL_EPSILON ); + QGSCOMPARENEAR( t[5], -0.211694, 0.0001 ); + delete[] t; + + // don't specify map + l.setReferenceMap( map ); + t = exporter.computeGeoTransform(); + QGSCOMPARENEAR( t[0], 1925.0, 1.0 ); + QGSCOMPARENEAR( t[1], 0.211719, 0.0001 ); + QGSCOMPARENEAR( t[2], 0.0, 4 * DBL_EPSILON ); + QGSCOMPARENEAR( t[3], 3050, 1 ); + QGSCOMPARENEAR( t[4], 0.0, 4 * DBL_EPSILON ); + QGSCOMPARENEAR( t[5], -0.211694, 0.0001 ); + delete[] t; + + // specify extent + t = exporter.computeGeoTransform( map, QRectF( 70, 100, 50, 60 ) ); + QGSCOMPARENEAR( t[0], 2100.0, 1.0 ); + QGSCOMPARENEAR( t[1], 0.211864, 0.0001 ); + QGSCOMPARENEAR( t[2], 0.0, 4 * DBL_EPSILON ); + QGSCOMPARENEAR( t[3], 2800, 1 ); + QGSCOMPARENEAR( t[4], 0.0, 4 * DBL_EPSILON ); + QGSCOMPARENEAR( t[5], -0.211864, 0.0001 ); + delete[] t; + + // specify dpi + t = exporter.computeGeoTransform( map, QRectF(), 75 ); + QGSCOMPARENEAR( t[0], 1925.0, 1 ); + QGSCOMPARENEAR( t[1], 0.847603, 0.0001 ); + QGSCOMPARENEAR( t[2], 0.0, 4 * DBL_EPSILON ); + QGSCOMPARENEAR( t[3], 3050.0, 1 ); + QGSCOMPARENEAR( t[4], 0.0, 4 * DBL_EPSILON ); + QGSCOMPARENEAR( t[5], -0.846774, 0.0001 ); + delete[] t; + + // rotation + map->setMapRotation( 45 ); + t = exporter.computeGeoTransform( map ); + QGSCOMPARENEAR( t[0], 1878.768940, 1 ); + QGSCOMPARENEAR( t[1], 0.149708, 0.0001 ); + QGSCOMPARENEAR( t[2], 0.149708, 0.0001 ); + QGSCOMPARENEAR( t[3], 2761.611652, 1 ); + QGSCOMPARENEAR( t[4], 0.14969, 0.0001 ); + QGSCOMPARENEAR( t[5], -0.14969, 0.0001 ); + delete[] t; +} + QGSTEST_MAIN( TestQgsLayout ) #include "testqgslayout.moc" From 56383e42b0d117ea710b404d1f513e556427cdd2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 13:50:40 +1000 Subject: [PATCH 19/56] Port world file generation code to layouts --- python/core/layout/qgslayoutexporter.sip | 16 +++++ src/core/layout/qgslayoutexporter.cpp | 92 +++++++++++++++++++++++- src/core/layout/qgslayoutexporter.h | 16 +++++ tests/src/core/testqgslayoutmap.cpp | 41 +++++++---- 4 files changed, 148 insertions(+), 17 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index eac2a8d3e237..4bbf3a2c3e2b 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -174,6 +174,22 @@ Resolution to export layout at :rtype: bool %End + void computeWorldFileParameters( double &a, double &b, double &c, double &d, double &e, double &f, double dpi = -1 ) const; +%Docstring + Compute world file parameters. Assumes the whole page containing the reference map item + will be exported. + + The ``dpi`` argument can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. +%End + + void computeWorldFileParameters( const QRectF ®ion, double &a, double &b, double &c, double &d, double &e, double &f, double dpi = -1 ) const; +%Docstring + Computes the world file parameters for a specified ``region`` of the layout. + + The ``dpi`` argument can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. +%End + + }; diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index e599ef8c50c7..7f978cb5a6cd 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -212,9 +212,9 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString if ( page == worldFilePageNo ) { -#if 0 - georeferenceOutput( outputFilePath, nullptr, bounds, imageDlg.resolution() ); + georeferenceOutput( outputFilePath, nullptr, bounds, settings.dpi ); +#if 0 if ( settings.generateWorldFile ) { // should generate world file for this page @@ -356,6 +356,94 @@ bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMa return true; } +void QgsLayoutExporter::computeWorldFileParameters( double &a, double &b, double &c, double &d, double &e, double &f, double dpi ) const +{ + QgsLayoutItemMap *map = mLayout->referenceMap(); + if ( !map ) + { + return; + } + + int pageNumber = map->page(); + QgsLayoutItemPage *page = mLayout->pageCollection()->page( pageNumber ); + double pageY = page->pos().y(); + QSizeF pageSize = page->rect().size(); + QRectF pageRect( 0, pageY, pageSize.width(), pageSize.height() ); + computeWorldFileParameters( pageRect, a, b, c, d, e, f, dpi ); +} + +void QgsLayoutExporter::computeWorldFileParameters( const QRectF &exportRegion, double &a, double &b, double &c, double &d, double &e, double &f, double dpi ) const +{ + // World file parameters : affine transformation parameters from pixel coordinates to map coordinates + QgsLayoutItemMap *map = mLayout->referenceMap(); + if ( !map ) + { + return; + } + + double destinationHeight = exportRegion.height(); + double destinationWidth = exportRegion.width(); + + QRectF mapItemSceneRect = map->mapRectToScene( map->rect() ); + QgsRectangle mapExtent = map->extent(); + + double alpha = map->mapRotation() / 180 * M_PI; + + double xRatio = mapExtent.width() / mapItemSceneRect.width(); + double yRatio = mapExtent.height() / mapItemSceneRect.height(); + + double xCenter = mapExtent.center().x(); + double yCenter = mapExtent.center().y(); + + // get the extent (in map units) for the region + QPointF mapItemPos = map->pos(); + //adjust item position so it is relative to export region + mapItemPos.rx() -= exportRegion.left(); + mapItemPos.ry() -= exportRegion.top(); + + double xmin = mapExtent.xMinimum() - mapItemPos.x() * xRatio; + double ymax = mapExtent.yMaximum() + mapItemPos.y() * yRatio; + QgsRectangle paperExtent( xmin, ymax - destinationHeight * yRatio, xmin + destinationWidth * xRatio, ymax ); + + double X0 = paperExtent.xMinimum(); + double Y0 = paperExtent.yMinimum(); + + if ( dpi < 0 ) + dpi = mLayout->context().dpi(); + + int widthPx = static_cast< int >( dpi * destinationWidth / 25.4 ); + int heightPx = static_cast< int >( dpi * destinationHeight / 25.4 ); + + double Ww = paperExtent.width() / widthPx; + double Hh = paperExtent.height() / heightPx; + + // scaling matrix + double s[6]; + s[0] = Ww; + s[1] = 0; + s[2] = X0; + s[3] = 0; + s[4] = -Hh; + s[5] = Y0 + paperExtent.height(); + + // rotation matrix + double r[6]; + r[0] = std::cos( alpha ); + r[1] = -std::sin( alpha ); + r[2] = xCenter * ( 1 - std::cos( alpha ) ) + yCenter * std::sin( alpha ); + r[3] = std::sin( alpha ); + r[4] = std::cos( alpha ); + r[5] = - xCenter * std::sin( alpha ) + yCenter * ( 1 - std::cos( alpha ) ); + + // result = rotation x scaling = rotation(scaling(X)) + a = r[0] * s[0] + r[1] * s[3]; + b = r[0] * s[1] + r[1] * s[4]; + c = r[0] * s[2] + r[1] * s[5] + r[2]; + d = r[3] * s[0] + r[4] * s[3]; + e = r[3] * s[1] + r[4] * s[4]; + f = r[3] * s[2] + r[4] * s[5] + r[5]; +} + QImage QgsLayoutExporter::createImage( const QgsLayoutExporter::ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const { bounds = QRectF(); diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 141f47879df8..3abb1623a053 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -186,6 +186,22 @@ class CORE_EXPORT QgsLayoutExporter bool georeferenceOutput( const QString &file, QgsLayoutItemMap *referenceMap = nullptr, const QRectF &exportRegion = QRectF(), double dpi = -1 ) const; + /** + * Compute world file parameters. Assumes the whole page containing the reference map item + * will be exported. + * + * The \a dpi argument can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. + */ + void computeWorldFileParameters( double &a, double &b, double &c, double &d, double &e, double &f, double dpi = -1 ) const; + + /** + * Computes the world file parameters for a specified \a region of the layout. + * + * The \a dpi argument can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. + */ + void computeWorldFileParameters( const QRectF ®ion, double &a, double &b, double &c, double &d, double &e, double &f, double dpi = -1 ) const; + + private: QPointer< QgsLayout > mLayout; diff --git a/tests/src/core/testqgslayoutmap.cpp b/tests/src/core/testqgslayoutmap.cpp index 0476b5780395..318a90057449 100644 --- a/tests/src/core/testqgslayoutmap.cpp +++ b/tests/src/core/testqgslayoutmap.cpp @@ -27,6 +27,7 @@ #include "qgsproject.h" #include "qgsmapthemecollection.h" #include "qgsproperty.h" +#include "qgslayoutpagecollection.h" #include #include "qgstest.h" @@ -46,8 +47,9 @@ class TestQgsLayoutMap : public QObject void render(); #if 0 void uniqueId(); //test if map id is adapted when doing copy paste - void worldFileGeneration(); // test world file generation #endif + void worldFileGeneration(); // test world file generation + void mapPolygonVertices(); // test mapPolygon function with no map rotation void dataDefinedLayers(); //test data defined layer string void dataDefinedStyles(); //test data defined styles @@ -189,17 +191,31 @@ void TestQgsLayoutMap::uniqueId() QVERIFY( oldId != newId ); } +#endif void TestQgsLayoutMap::worldFileGeneration() { - mComposerMap->setNewExtent( QgsRectangle( 781662.375, 3339523.125, 793062.375, 3345223.125 ) ); - mComposerMap->setMapRotation( 30.0 ); + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); + page2->setPageSize( "A4", QgsLayoutItemPage::Landscape ); + l.pageCollection()->addPage( page2 ); + + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 200, 100 ) ); + map->setFrameEnabled( true ); + map->setLayers( QList() << mRasterLayer ); + l.addLayoutItem( map ); + + map->setExtent( QgsRectangle( 781662.375, 3339523.125, 793062.375, 3345223.125 ) ); + map->setMapRotation( 30.0 ); - mComposition->setGenerateWorldFile( true ); - mComposition->setReferenceMap( mComposerMap ); + l.setReferenceMap( map ); + + QgsLayoutExporter exporter( &l ); double a, b, c, d, e, f; - mComposition->computeWorldFileParameters( a, b, c, d, e, f ); + exporter.computeWorldFileParameters( a, b, c, d, e, f ); QGSCOMPARENEAR( a, 4.18048, 0.001 ); QGSCOMPARENEAR( b, 2.41331, 0.001 ); @@ -209,8 +225,8 @@ void TestQgsLayoutMap::worldFileGeneration() QGSCOMPARENEAR( f, 3.34241e+06, 1e+03 ); //test with map on second page. Parameters should be the same - mComposerMap->setItemPosition( 20, 20, QgsComposerItem::UpperLeft, 2 ); - mComposition->computeWorldFileParameters( a, b, c, d, e, f ); + map->attemptMove( QgsLayoutPoint( 20, 20 ), true, false, 1 ); + exporter.computeWorldFileParameters( a, b, c, d, e, f ); QGSCOMPARENEAR( a, 4.18048, 0.001 ); QGSCOMPARENEAR( b, 2.41331, 0.001 ); @@ -220,8 +236,8 @@ void TestQgsLayoutMap::worldFileGeneration() QGSCOMPARENEAR( f, 3.34241e+06, 1e+03 ); //test computing parameters for specific region - mComposerMap->setItemPosition( 20, 20, QgsComposerItem::UpperLeft, 2 ); - mComposition->computeWorldFileParameters( QRectF( 10, 5, 260, 200 ), a, b, c, d, e, f ); + map->attemptMove( QgsLayoutPoint( 20, 20 ), true, false, 1 ); + exporter.computeWorldFileParameters( QRectF( 10, 5, 260, 200 ), a, b, c, d, e, f ); QGSCOMPARENEAR( a, 4.18061, 0.001 ); QGSCOMPARENEAR( b, 2.41321, 0.001 ); @@ -229,12 +245,7 @@ void TestQgsLayoutMap::worldFileGeneration() QGSCOMPARENEAR( d, 2.4137, 0.001 ); QGSCOMPARENEAR( e, -4.1798, 0.001 ); QGSCOMPARENEAR( f, 3.35331e+06, 1e+03 ); - - mComposition->setGenerateWorldFile( false ); - mComposerMap->setMapRotation( 0.0 ); - } -#endif void TestQgsLayoutMap::mapPolygonVertices() From f08ff15feb535916a33940eec0894046237e3488 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 14:16:01 +1000 Subject: [PATCH 20/56] Port some more world file generation related code --- src/core/layout/qgslayoutexporter.cpp | 25 +++++++++++--- src/core/layout/qgslayoutexporter.h | 4 +++ tests/src/python/test_qgslayoutexporter.py | 39 ++++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 7f978cb5a6cd..5b26a5af3de4 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -214,15 +214,14 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString { georeferenceOutput( outputFilePath, nullptr, bounds, settings.dpi ); -#if 0 if ( settings.generateWorldFile ) { // should generate world file for this page double a, b, c, d, e, f; if ( bounds.isValid() ) - mLayout->computeWorldFileParameters( bounds, a, b, c, d, e, f ); + computeWorldFileParameters( bounds, a, b, c, d, e, f, settings.dpi ); else - mLayout->computeWorldFileParameters( a, b, c, d, e, f ); + computeWorldFileParameters( a, b, c, d, e, f, settings.dpi ); QFileInfo fi( outputFilePath ); // build the world file name @@ -232,7 +231,6 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString writeWorldFile( worldFileName, a, b, c, d, e, f ); } -#endif } } @@ -323,6 +321,25 @@ double *QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, con return t; } +void QgsLayoutExporter::writeWorldFile( const QString &worldFileName, double a, double b, double c, double d, double e, double f ) const +{ + QFile worldFile( worldFileName ); + if ( !worldFile.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) ) + { + return; + } + QTextStream fout( &worldFile ); + + // QString::number does not use locale settings (for the decimal point) + // which is what we want here + fout << QString::number( a, 'f', 12 ) << "\r\n"; + fout << QString::number( d, 'f', 12 ) << "\r\n"; + fout << QString::number( b, 'f', 12 ) << "\r\n"; + fout << QString::number( e, 'f', 12 ) << "\r\n"; + fout << QString::number( c, 'f', 12 ) << "\r\n"; + fout << QString::number( f, 'f', 12 ) << "\r\n"; +} + bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMap *map, const QRectF &exportRegion, double dpi ) const { if ( !map ) diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 3abb1623a053..73cb672b3f4a 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -230,6 +230,10 @@ class CORE_EXPORT QgsLayoutExporter */ double *computeGeoTransform( const QgsLayoutItemMap *referenceMap = nullptr, const QRectF &exportRegion = QRectF(), double dpi = -1 ) const; + //! Write a world file + void writeWorldFile( const QString &fileName, double a, double b, double c, double d, double e, double f ) const; + + friend class TestQgsLayout; }; diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index eb246770fc14..69ae48b43aed 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -276,6 +276,45 @@ def testExportToImage(self): page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesize_2.png') self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path)) + def testExportWorldFile(self): + l = QgsLayout(QgsProject.instance()) + l.initializeDefaults() + + # add some items + map = QgsLayoutItemMap(l) + map.attemptSetSceneRect(QRectF(30, 60, 200, 100)) + extent = QgsRectangle(2000, 2800, 2500, 2900) + map.setExtent(extent) + l.addLayoutItem(map) + + exporter = QgsLayoutExporter(l) + # setup settings + settings = QgsLayoutExporter.ImageExportSettings() + settings.dpi = 80 + settings.generateWorldFile = False + + rendered_file_path = os.path.join(self.basetestpath, 'test_exportwithworldfile.png') + world_file_path = os.path.join(self.basetestpath, 'test_exportwithworldfile.pgw') + self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success) + self.assertTrue(os.path.exists(rendered_file_path)) + self.assertFalse(os.path.exists(world_file_path)) + + # with world file + settings.generateWorldFile = True + rendered_file_path = os.path.join(self.basetestpath, 'test_exportwithworldfile.png') + self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success) + self.assertTrue(os.path.exists(rendered_file_path)) + self.assertTrue(os.path.exists(world_file_path)) + + lines = tuple(open(world_file_path, 'r')) + values = [float(f) for f in lines] + self.assertAlmostEqual(values[0], 0.794117647059, 2) + self.assertAlmostEqual(values[1], 0.0, 2) + self.assertAlmostEqual(values[2], 0.0, 2) + self.assertAlmostEqual(values[3], -0.794251134644, 2) + self.assertAlmostEqual(values[4], 1925.000000000000, 2) + self.assertAlmostEqual(values[5], 3050.000000000000, 2) + if __name__ == '__main__': unittest.main() From 069a0baa40f5036aeb62b440b0eebddb16f0e2be Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 14:17:11 +1000 Subject: [PATCH 21/56] Expose some more export related settings to GUI --- python/core/layout/qgslayout.sip | 6 +++ src/app/layout/qgslayoutpropertieswidget.cpp | 17 ++++++ src/app/layout/qgslayoutpropertieswidget.h | 3 ++ src/core/layout/qgslayout.cpp | 2 + src/core/layout/qgslayout.h | 7 +++ src/ui/layout/qgslayoutwidgetbase.ui | 56 ++++++++++++++++++-- tests/src/python/test_qgslayoutexporter.py | 2 + 7 files changed, 90 insertions(+), 3 deletions(-) diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index b46eb991ddf3..a94ad469e692 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -33,6 +33,12 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoOb ZSnapIndicator, }; + enum UndoCommand + { + UndoLayoutDpi, + UndoNone, + }; + QgsLayout( QgsProject *project ); %Docstring Construct a new layout linked to the specified ``project``. diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index 0f0adc12a2c8..5f2c92b1fdee 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -54,6 +54,9 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout QgsUnitTypes::LayoutUnit marginUnit = static_cast< QgsUnitTypes::LayoutUnit >( mLayout->customProperty( QStringLiteral( "imageCropMarginUnit" ), QgsUnitTypes::LayoutMillimeters ).toInt() ); + bool exportWorldFile = mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool(); + mGenerateWorldFileCheckBox->setChecked( exportWorldFile ); + mTopMarginSpinBox->setValue( topMargin ); mMarginUnitsComboBox->linkToWidget( mTopMarginSpinBox ); mRightMarginSpinBox->setValue( rightMargin ); @@ -71,6 +74,7 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout connect( mLeftMarginSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::resizeMarginsChanged ); connect( mResizePageButton, &QPushButton::clicked, this, &QgsLayoutPropertiesWidget::resizeToContents ); + connect( mResolutionSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::dpiChanged ); connect( mReferenceMapComboBox, &QgsLayoutItemComboBox::itemChanged, this, &QgsLayoutPropertiesWidget::referenceMapChanged ); mReferenceMapComboBox->setCurrentLayout( mLayout ); @@ -82,6 +86,7 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout void QgsLayoutPropertiesWidget::updateGui() { whileBlocking( mReferenceMapComboBox )->setItem( mLayout->referenceMap() ); + whileBlocking( mResolutionSpinBox )->setValue( mLayout->context().dpi() ); } void QgsLayoutPropertiesWidget::updateSnappingElements() @@ -171,6 +176,18 @@ void QgsLayoutPropertiesWidget::referenceMapChanged( QgsLayoutItem *item ) mLayout->undoStack()->endCommand(); } +void QgsLayoutPropertiesWidget::dpiChanged( int value ) +{ + mLayout->undoStack()->beginCommand( mLayout, tr( "Set Default DPI" ), QgsLayout::UndoLayoutDpi ); + mLayout->context().setDpi( value ); + mLayout->undoStack()->endCommand(); +} + +void QgsLayoutPropertiesWidget::worldFileToggled() +{ + mLayout->setCustomProperty( QStringLiteral( "exportWorldFile" ), mGenerateWorldFileCheckBox->isChecked() ); +} + void QgsLayoutPropertiesWidget::blockSignals( bool block ) { mGridResolutionSpinBox->blockSignals( block ); diff --git a/src/app/layout/qgslayoutpropertieswidget.h b/src/app/layout/qgslayoutpropertieswidget.h index 06d9e3713a3d..66e3f12fc9f6 100644 --- a/src/app/layout/qgslayoutpropertieswidget.h +++ b/src/app/layout/qgslayoutpropertieswidget.h @@ -41,6 +41,9 @@ class QgsLayoutPropertiesWidget: public QgsPanelWidget, private Ui::QgsLayoutWid void resizeMarginsChanged(); void resizeToContents(); void referenceMapChanged( QgsLayoutItem *item ); + void dpiChanged( int value ); + void worldFileToggled(); + private: QgsLayout *mLayout = nullptr; diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 8bacc85dd731..71995f177ec7 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -710,6 +710,7 @@ void QgsLayout::writeXmlLayoutSettings( QDomElement &element, QDomDocument &docu element.setAttribute( QStringLiteral( "name" ), mName ); element.setAttribute( QStringLiteral( "units" ), QgsUnitTypes::encodeUnit( mUnits ) ); element.setAttribute( QStringLiteral( "worldFileMap" ), mWorldFileMapId ); + element.setAttribute( QStringLiteral( "printResolution" ), mContext.dpi() ); } QDomElement QgsLayout::writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const @@ -753,6 +754,7 @@ bool QgsLayout::readXmlLayoutSettings( const QDomElement &layoutElement, const Q setName( layoutElement.attribute( QStringLiteral( "name" ) ) ); setUnits( QgsUnitTypes::decodeLayoutUnit( layoutElement.attribute( QStringLiteral( "units" ) ) ) ); mWorldFileMapId = layoutElement.attribute( QStringLiteral( "worldFileMap" ) ); + mContext.setDpi( layoutElement.attribute( QStringLiteral( "printResolution" ), "300" ).toDouble() ); emit changed(); return true; diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 83f5de19b7e3..6bbcc7ac9917 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -64,6 +64,13 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext ZSnapIndicator = 10002, //!< Z-value for snapping indicator }; + //! Layout undo commands, used for collapsing undo commands + enum UndoCommand + { + UndoLayoutDpi, //!< Change layout default DPI + UndoNone = -1, //!< No command suppression + }; + /** * Construct a new layout linked to the specified \a project. * diff --git a/src/ui/layout/qgslayoutwidgetbase.ui b/src/ui/layout/qgslayoutwidgetbase.ui index afc706ad95ce..697da03bb7a2 100644 --- a/src/ui/layout/qgslayoutwidgetbase.ui +++ b/src/ui/layout/qgslayoutwidgetbase.ui @@ -7,7 +7,7 @@ 0 0 311 - 515 + 494
@@ -55,14 +55,14 @@ 0 0 297 - 632 + 746 - GroupBox + General settings @@ -204,6 +204,54 @@ + + + + Export settings + + + + + + dpi + + + + + + 3000 + + + false + + + + + + + Export resolution + + + + + + + + 0 + 0 + + + + If checked, a separate world file which georeferences exported images will be created + + + Save world file + + + + + + @@ -388,6 +436,8 @@ mOffsetYSpinBox mGridOffsetUnitsComboBox mSnapToleranceSpinBox + mResolutionSpinBox + mGenerateWorldFileCheckBox mMarginUnitsComboBox mTopMarginSpinBox mLeftMarginSpinBox diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index 69ae48b43aed..834104d9f705 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -24,7 +24,9 @@ QgsProject, QgsMargins, QgsLayoutItemShape, + QgsRectangle, QgsLayoutItemPage, + QgsLayoutItemMap, QgsLayoutPoint, QgsSimpleFillSymbolLayer, QgsFillSymbol) From 5cf36cd819fc1ed7ced23f41a882b48b28e01485 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 14:47:53 +1000 Subject: [PATCH 22/56] Add method to detect whether layout has uniform page sizes --- .../core/layout/qgslayoutpagecollection.sip | 9 ++++++ src/core/layout/qgslayoutpagecollection.cpp | 18 ++++++++++++ src/core/layout/qgslayoutpagecollection.h | 8 ++++++ .../python/test_qgslayoutpagecollection.py | 28 +++++++++++++++++++ 4 files changed, 63 insertions(+) diff --git a/python/core/layout/qgslayoutpagecollection.sip b/python/core/layout/qgslayoutpagecollection.sip index adef181b59da..68906d575ab6 100644 --- a/python/core/layout/qgslayoutpagecollection.sip +++ b/python/core/layout/qgslayoutpagecollection.sip @@ -198,6 +198,15 @@ Returns the maximum width of pages in the collection. The returned value is in layout units. %End + bool hasUniformPageSizes() const; +%Docstring + Returns true if the layout has uniform page sizes, e.g. all pages are the same size. + + This method does not consider differing units as non-uniform sizes, only the actual + physical size of the pages. + :rtype: bool +%End + int pageNumberForPoint( QPointF point ) const; %Docstring Returns the page number corresponding to a ``point`` in the layout (in layout units). diff --git a/src/core/layout/qgslayoutpagecollection.cpp b/src/core/layout/qgslayoutpagecollection.cpp index 04e9540ba13a..be7ace29d33c 100644 --- a/src/core/layout/qgslayoutpagecollection.cpp +++ b/src/core/layout/qgslayoutpagecollection.cpp @@ -95,6 +95,24 @@ QSizeF QgsLayoutPageCollection::maximumPageSize() const return maxSize; } +bool QgsLayoutPageCollection::hasUniformPageSizes() const +{ + QSizeF size; + for ( QgsLayoutItemPage *page : mPages ) + { + QSizeF pageSize = mLayout->convertToLayoutUnits( page->pageSize() ); + if ( !size.isValid() ) + size = pageSize; + else + { + if ( !qgsDoubleNear( pageSize.width(), size.width(), 0.01 ) + || !qgsDoubleNear( pageSize.height(), size.height(), 0.01 ) ) + return false; + } + } + return true; +} + int QgsLayoutPageCollection::pageNumberForPoint( QPointF point ) const { int pageNumber = 0; diff --git a/src/core/layout/qgslayoutpagecollection.h b/src/core/layout/qgslayoutpagecollection.h index f0eb4c6f1b32..f0b97b34ca83 100644 --- a/src/core/layout/qgslayoutpagecollection.h +++ b/src/core/layout/qgslayoutpagecollection.h @@ -242,6 +242,14 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri */ QSizeF maximumPageSize() const; + /** + * Returns true if the layout has uniform page sizes, e.g. all pages are the same size. + * + * This method does not consider differing units as non-uniform sizes, only the actual + * physical size of the pages. + */ + bool hasUniformPageSizes() const; + /** * Returns the page number corresponding to a \a point in the layout (in layout units). * diff --git a/tests/src/python/test_qgslayoutpagecollection.py b/tests/src/python/test_qgslayoutpagecollection.py index 0f17938fb9be..b1d85b01e0c0 100644 --- a/tests/src/python/test_qgslayoutpagecollection.py +++ b/tests/src/python/test_qgslayoutpagecollection.py @@ -282,6 +282,34 @@ def testMaxPageWidthAndSize(self): self.assertEqual(collection.maximumPageSize().width(), 100000.0) self.assertEqual(collection.maximumPageSize().height(), 100000.0) + def testUniformPageSizes(self): + """ + Test detection of uniform page sizes + """ + p = QgsProject() + l = QgsLayout(p) + collection = l.pageCollection() + + self.assertTrue(collection.hasUniformPageSizes()) + + # add a page + page = QgsLayoutItemPage(l) + page.setPageSize('A4') + collection.addPage(page) + self.assertTrue(collection.hasUniformPageSizes()) + + # add a second page + page2 = QgsLayoutItemPage(l) + page2.setPageSize(QgsLayoutSize(21.0, 29.7, QgsUnitTypes.LayoutCentimeters)) + collection.addPage(page2) + self.assertTrue(collection.hasUniformPageSizes()) + + # add a page with other units + page3 = QgsLayoutItemPage(l) + page3.setPageSize('A5') + collection.addPage(page3) + self.assertFalse(collection.hasUniformPageSizes()) + def testReflow(self): """ Test reflowing pages From 1b8f4a09ba2025e3619533c5a001fac4716e2036 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 14:48:21 +1000 Subject: [PATCH 23/56] Add method to determine file path for exports which encountered errors --- python/core/layout/qgslayoutexporter.sip | 10 +++++++++- src/core/layout/qgslayoutexporter.cpp | 7 ++++++- src/core/layout/qgslayoutexporter.h | 11 ++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index 4bbf3a2c3e2b..b773cb6bdc77 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -151,10 +151,18 @@ Resolution to export layout at be generated by appending "_1", "_2", etc to the image file's base name. Returns a result code indicating whether the export was successful or an - error was encountered. + error was encountered. If an error code is returned, errorFile() can be called + to determine the filename for the export which encountered the error. :rtype: ExportResult %End + QString errorFile() const; +%Docstring + Returns the file name corresponding to the last error encountered during + an export. + :rtype: str +%End + bool georeferenceOutput( const QString &file, QgsLayoutItemMap *referenceMap = 0, const QRectF &exportRegion = QRectF(), double dpi = -1 ) const; %Docstring diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 5b26a5af3de4..6f8b5b4ba23a 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -155,6 +155,8 @@ class LayoutDpiRestorer QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ) { + mErrorFileName.clear(); + int worldFilePageNo = -1; if ( QgsLayoutItemMap *referenceMap = mLayout->referenceMap() ) { @@ -199,14 +201,17 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString if ( skip ) continue; // should skip this page, e.g. null size + QString outputFilePath = generateFileName( path, baseName, extension, page ); + if ( image.isNull() ) { + mErrorFileName = outputFilePath; return MemoryError; } - QString outputFilePath = generateFileName( path, baseName, extension, page ); if ( !saveImage( image, outputFilePath, extension ) ) { + mErrorFileName = outputFilePath; return FileError; } diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 73cb672b3f4a..9b0a82c4d464 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -164,10 +164,17 @@ class CORE_EXPORT QgsLayoutExporter * be generated by appending "_1", "_2", etc to the image file's base name. * * Returns a result code indicating whether the export was successful or an - * error was encountered. + * error was encountered. If an error code is returned, errorFile() can be called + * to determine the filename for the export which encountered the error. */ ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings ); + /** + * Returns the file name corresponding to the last error encountered during + * an export. + */ + QString errorFile() const { return mErrorFileName; } + /** * Georeferences a \a file (image of PDF) exported from the layout. * @@ -206,6 +213,8 @@ class CORE_EXPORT QgsLayoutExporter QPointer< QgsLayout > mLayout; + QString mErrorFileName; + QImage createImage( const ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const; QString generateFileName( const QString &path, const QString &baseName, const QString &suffix, int page ) const; From 113664fe2e3b579365cb5cf08da3964035fd6405 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 14:48:46 +1000 Subject: [PATCH 24/56] Port method for pausing layout view updates --- python/gui/layout/qgslayoutview.sip | 3 +++ src/gui/layout/qgslayoutview.cpp | 13 +++++++++++++ src/gui/layout/qgslayoutview.h | 10 ++++++++++ 3 files changed, 26 insertions(+) diff --git a/python/gui/layout/qgslayoutview.sip b/python/gui/layout/qgslayoutview.sip index 484a6fbf3a1a..161afb7f9ef2 100644 --- a/python/gui/layout/qgslayoutview.sip +++ b/python/gui/layout/qgslayoutview.sip @@ -265,6 +265,7 @@ Returns the delta (in layout coordinates) by which to move items for the given key ``event``. %End + public slots: void zoomFull(); @@ -557,6 +558,8 @@ item and should have its properties displayed in any designer windows. virtual void dragEnterEvent( QDragEnterEvent *e ); + virtual void paintEvent( QPaintEvent *event ); + }; diff --git a/src/gui/layout/qgslayoutview.cpp b/src/gui/layout/qgslayoutview.cpp index 913db7d14628..fc8bc6a4b19b 100644 --- a/src/gui/layout/qgslayoutview.cpp +++ b/src/gui/layout/qgslayoutview.cpp @@ -1002,6 +1002,19 @@ void QgsLayoutView::dragEnterEvent( QDragEnterEvent *e ) e->ignore(); } +void QgsLayoutView::paintEvent( QPaintEvent *event ) +{ + if ( mPaintingEnabled ) + { + QGraphicsView::paintEvent( event ); + event->accept(); + } + else + { + event->ignore(); + } +} + void QgsLayoutView::invalidateCachedRenders() { if ( !currentLayout() ) diff --git a/src/gui/layout/qgslayoutview.h b/src/gui/layout/qgslayoutview.h index 221acea1fe6f..e42b16bff7f4 100644 --- a/src/gui/layout/qgslayoutview.h +++ b/src/gui/layout/qgslayoutview.h @@ -270,6 +270,13 @@ class GUI_EXPORT QgsLayoutView: public QGraphicsView */ QPointF deltaForKeyEvent( QKeyEvent *event ); + /** + * Sets whether widget repainting should be allowed for the view. This is + * used to temporarily halt painting while exporting layouts. + * \note Not available in Python bindings. + */ + void setPaintingEnabled( bool enabled ) { mPaintingEnabled = enabled; } SIP_SKIP + public slots: /** @@ -512,6 +519,7 @@ class GUI_EXPORT QgsLayoutView: public QGraphicsView void resizeEvent( QResizeEvent *event ) override; void scrollContentsBy( int dx, int dy ) override; void dragEnterEvent( QDragEnterEvent *e ) override; + void paintEvent( QPaintEvent *event ) override; private slots: @@ -543,6 +551,8 @@ class GUI_EXPORT QgsLayoutView: public QGraphicsView QgsPreviewEffect *mPreviewEffect = nullptr; + bool mPaintingEnabled = true; + friend class TestQgsLayoutView; friend class QgsLayoutMouseHandles; From 953d2c437d11e2bbf1c9eba16e67a592d8c63321 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 14:49:11 +1000 Subject: [PATCH 25/56] [layouts] Resurrect action for exporting to raster images ...this time, without all the useful code locked away in app! --- src/app/CMakeLists.txt | 2 + src/app/layout/qgslayoutdesignerdialog.cpp | 152 +++++++++ src/app/layout/qgslayoutdesignerdialog.h | 5 + .../qgslayoutimageexportoptionsdialog.cpp | 167 +++++++++ .../qgslayoutimageexportoptionsdialog.h | 119 +++++++ src/app/layout/qgslayoutpropertieswidget.cpp | 1 + src/ui/layout/qgslayoutdesignerbase.ui | 38 +++ src/ui/layout/qgslayoutimageexportoptions.ui | 320 ++++++++++++++++++ 8 files changed, 804 insertions(+) create mode 100644 src/app/layout/qgslayoutimageexportoptionsdialog.cpp create mode 100644 src/app/layout/qgslayoutimageexportoptionsdialog.h create mode 100644 src/ui/layout/qgslayoutimageexportoptions.ui diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 5181776c4fed..481a8dec71e1 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -185,6 +185,7 @@ SET(QGIS_APP_SRCS layout/qgslayoutdesignerdialog.cpp layout/qgslayoutguidewidget.cpp layout/qgslayouthtmlwidget.cpp + layout/qgslayoutimageexportoptionsdialog.cpp layout/qgslayoutitemslistview.cpp layout/qgslayoutappmenuprovider.cpp layout/qgslayoutlabelwidget.cpp @@ -403,6 +404,7 @@ SET (QGIS_APP_MOC_HDRS layout/qgslayoutdesignerdialog.h layout/qgslayoutguidewidget.h layout/qgslayouthtmlwidget.h + layout/qgslayoutimageexportoptionsdialog.h layout/qgslayoutitemslistview.h layout/qgslayoutlabelwidget.h layout/qgslayoutlegendwidget.h diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 68bb18b1b29c..6bde426765d0 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -33,6 +33,9 @@ #include "qgslayoutviewtoolselect.h" #include "qgslayoutviewtooleditnodes.h" #include "qgslayoutitemwidget.h" +#include "qgslayoutimageexportoptionsdialog.h" +#include "qgslayoutitemmap.h" +#include "qgsmessageviewer.h" #include "qgsgui.h" #include "qgslayoutitemguiregistry.h" #include "qgslayoutpropertieswidget.h" @@ -168,6 +171,8 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla connect( mActionLayoutManager, &QAction::triggered, this, &QgsLayoutDesignerDialog::showManager ); connect( mActionRemoveLayout, &QAction::triggered, this, &QgsLayoutDesignerDialog::deleteLayout ); + connect( mActionExportAsImage, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToRaster ); + connect( mActionShowGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::showGrid ); connect( mActionSnapGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::snapToGrid ); @@ -1410,6 +1415,123 @@ void QgsLayoutDesignerDialog::deleteLayout() close(); } +void QgsLayoutDesignerDialog::exportToRaster() +{ + if ( containsWmsLayers() ) + showWmsPrintingWarning(); + + // Image size + double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) ); + QSizeF maxPageSize = mLayout->pageCollection()->maximumPageSize(); + bool hasUniformPageSizes = mLayout->pageCollection()->hasUniformPageSizes(); + int width = ( int )( mLayout->context().dpi() * maxPageSize.width() / oneInchInLayoutUnits ); + int height = ( int )( mLayout->context().dpi() * maxPageSize.height() / oneInchInLayoutUnits ); + double dpi = mLayout->context().dpi(); + + int memuse = width * height * 3 / 1000000; // pixmap + image + QgsDebugMsg( QString( "Image %1x%2" ).arg( width ).arg( height ) ); + QgsDebugMsg( QString( "memuse = %1" ).arg( memuse ) ); + + if ( memuse > 400 ) // about 4500x4500 + { + int answer = QMessageBox::warning( nullptr, tr( "Export layout" ), + tr( "To create an image of %1x%2 requires about %3 MB of memory. Proceed?" ) + .arg( width ).arg( height ).arg( memuse ), + QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok ); + + raise(); + if ( answer == QMessageBox::Cancel ) + return; + } + + //get some defaults from the composition + bool cropToContents = mLayout->customProperty( QStringLiteral( "imageCropToContents" ), false ).toBool(); + int marginTop = mLayout->customProperty( QStringLiteral( "imageCropMarginTop" ), 0 ).toInt(); + int marginRight = mLayout->customProperty( QStringLiteral( "imageCropMarginRight" ), 0 ).toInt(); + int marginBottom = mLayout->customProperty( QStringLiteral( "imageCropMarginBottom" ), 0 ).toInt(); + int marginLeft = mLayout->customProperty( QStringLiteral( "imageCropMarginLeft" ), 0 ).toInt(); + + QgsLayoutImageExportOptionsDialog imageDlg( this ); + imageDlg.setImageSize( maxPageSize ); + imageDlg.setResolution( dpi ); + imageDlg.setCropToContents( cropToContents ); + imageDlg.setCropMargins( marginTop, marginRight, marginBottom, marginLeft ); + +#if 0 //TODO + QgsAtlasComposition *atlasMap = &mComposition->atlasComposition(); +#endif + + QString outputFileName; +#if 0 //TODO + if ( atlasMap->enabled() && mComposition->atlasMode() == QgsComposition::PreviewAtlas ) + { + QString lastUsedDir = settings.value( QStringLiteral( "UI/lastSaveAsImageDir" ), QDir::homePath() ).toString(); + outputFileName = QDir( lastUsedDir ).filePath( atlasMap->currentFilename() ); + } +#endif + +#ifdef Q_OS_MAC + mQgis->activateWindow(); + this->raise(); +#endif + QPair fileNExt = QgsGuiUtils::getSaveAsImageName( this, tr( "Save layout as" ), outputFileName ); + this->activateWindow(); + + if ( fileNExt.first.isEmpty() ) + { + return; + } + + if ( !imageDlg.exec() ) + return; + + cropToContents = imageDlg.cropToContents(); + imageDlg.getCropMargins( marginTop, marginRight, marginBottom, marginLeft ); + mLayout->setCustomProperty( QStringLiteral( "imageCropToContents" ), cropToContents ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginTop" ), marginTop ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginRight" ), marginRight ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginBottom" ), marginBottom ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginLeft" ), marginLeft ); + + mView->setPaintingEnabled( false ); + + QgsLayoutExporter exporter( mLayout ); + + QgsLayoutExporter::ImageExportSettings settings; + settings.cropToContents = cropToContents; + settings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom ); + settings.dpi = imageDlg.resolution(); + if ( hasUniformPageSizes ) + { + settings.imageSize = QSize( imageDlg.imageWidth(), imageDlg.imageHeight() ); + } + settings.generateWorldFile = mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool(); + + switch ( exporter.exportToImage( fileNExt.first, settings ) ) + { + case QgsLayoutExporter::Success: + break; + + case QgsLayoutExporter::FileError: + QMessageBox::warning( this, tr( "Image Export Error" ), + QString( tr( "Cannot write to %1.\n\nThis file may be open in another application." ) ).arg( exporter.errorFile() ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( nullptr, tr( "Memory Allocation Error" ), + tr( "Trying to create image %1 (%2×%3 @ %4dpi ) " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ) + .arg( exporter.errorFile() ).arg( imageDlg.imageWidth() ).arg( imageDlg.imageHeight() ).arg( settings.dpi ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + + } + mView->setPaintingEnabled( true ); +} + void QgsLayoutDesignerDialog::paste() { QPointF pt = mView->mapFromGlobal( QCursor::pos() ); @@ -1525,6 +1647,36 @@ void QgsLayoutDesignerDialog::initializeRegistry() } +bool QgsLayoutDesignerDialog::containsWmsLayers() const +{ + QList< QgsLayoutItemMap *> maps; + mLayout->layoutItems( maps ); + + for ( QgsLayoutItemMap *map : qgis::as_const( maps ) ) + { + if ( map->containsWmsLayer() ) + return true; + } + return false; +} + +void QgsLayoutDesignerDialog::showWmsPrintingWarning() +{ + QgsSettings settings; + bool displayWMSWarning = settings.value( QStringLiteral( "/UI/displayComposerWMSWarning" ), true ).toBool(); + if ( displayWMSWarning ) + { + QgsMessageViewer *m = new QgsMessageViewer( this ); + m->setWindowTitle( tr( "Project Contains WMS Layers" ) ); + m->setMessage( tr( "Some WMS servers (e.g. UMN mapserver) have a limit for the WIDTH and HEIGHT parameter. Printing layers from such servers may exceed this limit. If this is the case, the WMS layer will not be printed" ), QgsMessageOutput::MessageText ); + m->setCheckBoxText( tr( "Don't show this message again" ) ); + m->setCheckBoxState( Qt::Unchecked ); + m->setCheckBoxVisible( true ); + m->setCheckBoxQgsSettingsLabel( QStringLiteral( "/UI/displayComposerWMSWarning" ) ); + m->exec(); //deleted on close + } +} + void QgsLayoutDesignerDialog::selectItems( const QList items ) { for ( QGraphicsItem *item : items ) diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 0a5b0463dd2b..f46d492ceded 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -274,6 +274,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void showManager(); void renameLayout(); void deleteLayout(); + void exportToRaster(); private: @@ -360,6 +361,10 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void initializeRegistry(); + bool containsWmsLayers() const; + + //! Displays a warning because of possible min/max size in WMS + void showWmsPrintingWarning(); }; #endif // QGSLAYOUTDESIGNERDIALOG_H diff --git a/src/app/layout/qgslayoutimageexportoptionsdialog.cpp b/src/app/layout/qgslayoutimageexportoptionsdialog.cpp new file mode 100644 index 000000000000..40932229fb02 --- /dev/null +++ b/src/app/layout/qgslayoutimageexportoptionsdialog.cpp @@ -0,0 +1,167 @@ +/*************************************************************************** + qgslayoutimageexportoptionsdialog.cpp + ------------------------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgslayoutimageexportoptionsdialog.h" +#include "qgis.h" +#include "qgssettings.h" +#include "qgsgui.h" + +#include +#include + +QgsLayoutImageExportOptionsDialog::QgsLayoutImageExportOptionsDialog( QWidget *parent, Qt::WindowFlags flags ) + : QDialog( parent, flags ) +{ + setupUi( this ); + connect( mWidthSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsLayoutImageExportOptionsDialog::mWidthSpinBox_valueChanged ); + connect( mHeightSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsLayoutImageExportOptionsDialog::mHeightSpinBox_valueChanged ); + connect( mResolutionSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsLayoutImageExportOptionsDialog::mResolutionSpinBox_valueChanged ); + + connect( mClipToContentGroupBox, &QGroupBox::toggled, this, &QgsLayoutImageExportOptionsDialog::clipToContentsToggled ); + + QgsGui::instance()->enableAutoGeometryRestore( this ); +} + +void QgsLayoutImageExportOptionsDialog::setResolution( double resolution ) +{ + mResolutionSpinBox->setValue( resolution ); + + if ( mImageSize.isValid() ) + { + mWidthSpinBox->blockSignals( true ); + mHeightSpinBox->blockSignals( true ); + if ( mClipToContentGroupBox->isChecked() ) + { + mWidthSpinBox->setValue( 0 ); + mHeightSpinBox->setValue( 0 ); + } + else + { + mWidthSpinBox->setValue( mImageSize.width() * resolution / 25.4 ); + mHeightSpinBox->setValue( mImageSize.height() * resolution / 25.4 ); + } + mWidthSpinBox->blockSignals( false ); + mHeightSpinBox->blockSignals( false ); + } +} + +double QgsLayoutImageExportOptionsDialog::resolution() const +{ + return mResolutionSpinBox->value(); +} + +void QgsLayoutImageExportOptionsDialog::setImageSize( QSizeF size ) +{ + mImageSize = size; + mWidthSpinBox->blockSignals( true ); + mHeightSpinBox->blockSignals( true ); + mWidthSpinBox->setValue( size.width() * mResolutionSpinBox->value() / 25.4 ); + mHeightSpinBox->setValue( size.height() * mResolutionSpinBox->value() / 25.4 ); + mWidthSpinBox->blockSignals( false ); + mHeightSpinBox->blockSignals( false ); +} + +int QgsLayoutImageExportOptionsDialog::imageWidth() const +{ + return mWidthSpinBox->value(); +} + +int QgsLayoutImageExportOptionsDialog::imageHeight() const +{ + return mHeightSpinBox->value(); +} + +void QgsLayoutImageExportOptionsDialog::setCropToContents( bool crop ) +{ + mClipToContentGroupBox->setChecked( crop ); +} + +bool QgsLayoutImageExportOptionsDialog::cropToContents() const +{ + return mClipToContentGroupBox->isChecked(); +} + +void QgsLayoutImageExportOptionsDialog::getCropMargins( int &topMargin, int &rightMargin, int &bottomMargin, int &leftMargin ) const +{ + topMargin = mTopMarginSpinBox->value(); + rightMargin = mRightMarginSpinBox->value(); + bottomMargin = mBottomMarginSpinBox->value(); + leftMargin = mLeftMarginSpinBox->value(); +} + +void QgsLayoutImageExportOptionsDialog::setCropMargins( int topMargin, int rightMargin, int bottomMargin, int leftMargin ) +{ + mTopMarginSpinBox->setValue( topMargin ); + mRightMarginSpinBox->setValue( rightMargin ); + mBottomMarginSpinBox->setValue( bottomMargin ); + mLeftMarginSpinBox->setValue( leftMargin ); +} + +void QgsLayoutImageExportOptionsDialog::mWidthSpinBox_valueChanged( int value ) +{ + mHeightSpinBox->blockSignals( true ); + mResolutionSpinBox->blockSignals( true ); + mHeightSpinBox->setValue( mImageSize.height() * value / mImageSize.width() ); + mResolutionSpinBox->setValue( value * 25.4 / mImageSize.width() ); + mHeightSpinBox->blockSignals( false ); + mResolutionSpinBox->blockSignals( false ); +} + +void QgsLayoutImageExportOptionsDialog::mHeightSpinBox_valueChanged( int value ) +{ + mWidthSpinBox->blockSignals( true ); + mResolutionSpinBox->blockSignals( true ); + mWidthSpinBox->setValue( mImageSize.width() * value / mImageSize.height() ); + mResolutionSpinBox->setValue( value * 25.4 / mImageSize.height() ); + mWidthSpinBox->blockSignals( false ); + mResolutionSpinBox->blockSignals( false ); +} + +void QgsLayoutImageExportOptionsDialog::mResolutionSpinBox_valueChanged( int value ) +{ + mWidthSpinBox->blockSignals( true ); + mHeightSpinBox->blockSignals( true ); + if ( mClipToContentGroupBox->isChecked() ) + { + mWidthSpinBox->setValue( 0 ); + mHeightSpinBox->setValue( 0 ); + } + else + { + mWidthSpinBox->setValue( mImageSize.width() * value / 25.4 ); + mHeightSpinBox->setValue( mImageSize.height() * value / 25.4 ); + } + mWidthSpinBox->blockSignals( false ); + mHeightSpinBox->blockSignals( false ); +} + +void QgsLayoutImageExportOptionsDialog::clipToContentsToggled( bool state ) +{ + mWidthSpinBox->setEnabled( !state ); + mHeightSpinBox->setEnabled( !state ); + + if ( state ) + { + whileBlocking( mWidthSpinBox )->setValue( 0 ); + whileBlocking( mHeightSpinBox )->setValue( 0 ); + } + else + { + whileBlocking( mWidthSpinBox )->setValue( mImageSize.width() * mResolutionSpinBox->value() / 25.4 ); + whileBlocking( mHeightSpinBox )->setValue( mImageSize.height() * mResolutionSpinBox->value() / 25.4 ); + } +} diff --git a/src/app/layout/qgslayoutimageexportoptionsdialog.h b/src/app/layout/qgslayoutimageexportoptionsdialog.h new file mode 100644 index 000000000000..c7fb9b9904a3 --- /dev/null +++ b/src/app/layout/qgslayoutimageexportoptionsdialog.h @@ -0,0 +1,119 @@ +/*************************************************************************** + qgslayoutimageexportoptionsdialog.h + ------------------------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSLAYOUTIMAGEEXPORTOPTIONSDIALOG_H +#define QGSLAYOUTIMAGEEXPORTOPTIONSDIALOG_H + +#include +#include "ui_qgslayoutimageexportoptions.h" + + +/** + * A dialog for customising the properties of an exported image file. +*/ +class QgsLayoutImageExportOptionsDialog: public QDialog, private Ui::QgsLayoutImageExportOptionsDialog +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsLayoutImageExportOptionsDialog + * \param parent parent widget + * \param flags window flags + */ + QgsLayoutImageExportOptionsDialog( QWidget *parent = nullptr, Qt::WindowFlags flags = 0 ); + + /** + * Sets the initial resolution displayed in the dialog. + * \param resolution default resolution in DPI + * \see resolution() + */ + void setResolution( double resolution ); + + /** + * Returns the selected resolution from the dialog. + * \returns image resolution in DPI + * \see setResolution() + */ + double resolution() const; + + /** + * Sets the target image size. This is used to calculate the default size in pixels + * and also for determining the image's width to height ratio. + * \param size image size + */ + void setImageSize( QSizeF size ); + + /** + * Returns the user-set image width in pixels. + * \see imageHeight + */ + int imageWidth() const; + + /** + * Returns the user-set image height in pixels. + * \see imageWidth + */ + int imageHeight() const; + + /** + * Sets whether the crop to contents option should be checked in the dialog + * \param crop set to true to check crop to contents + * \see cropToContents() + */ + void setCropToContents( bool crop ); + + /** + * Returns whether the crop to contents option is checked in the dialog. + * \see setCropToContents() + */ + bool cropToContents() const; + + /** + * Fetches the current crop to contents margin values, in pixels. + * \param topMargin destination for top margin + * \param rightMargin destination for right margin + * \param bottomMargin destination for bottom margin + * \param leftMargin destination for left margin + */ + void getCropMargins( int &topMargin, int &rightMargin, int &bottomMargin, int &leftMargin ) const; + + /** + * Sets the current crop to contents margin values, in pixels. + * \param topMargin top margin + * \param rightMargin right margin + * \param bottomMargin bottom margin + * \param leftMargin left margin + */ + void setCropMargins( int topMargin, int rightMargin, int bottomMargin, int leftMargin ); + + private slots: + + void mWidthSpinBox_valueChanged( int value ); + void mHeightSpinBox_valueChanged( int value ); + void mResolutionSpinBox_valueChanged( int value ); + void clipToContentsToggled( bool state ); + + private: + + QSizeF mImageSize; + + +}; + +#endif // QGSLAYOUTIMAGEEXPORTOPTIONSDIALOG_H diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index 5f2c92b1fdee..e2451e4a9a0a 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -56,6 +56,7 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout bool exportWorldFile = mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool(); mGenerateWorldFileCheckBox->setChecked( exportWorldFile ); + connect( mGenerateWorldFileCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::worldFileToggled ); mTopMarginSpinBox->setValue( topMargin ); mMarginUnitsComboBox->linkToWidget( mTopMarginSpinBox ); diff --git a/src/ui/layout/qgslayoutdesignerbase.ui b/src/ui/layout/qgslayoutdesignerbase.ui index 307519f14ba3..8a3081a46db7 100644 --- a/src/ui/layout/qgslayoutdesignerbase.ui +++ b/src/ui/layout/qgslayoutdesignerbase.ui @@ -69,6 +69,10 @@ + + + + @@ -113,6 +117,10 @@ + + + + @@ -1173,6 +1181,36 @@ Delete layout + + + + :/images/themes/default/mActionSaveMapAsImage.svg:/images/themes/default/mActionSaveMapAsImage.svg + + + Export as &Image… + + + Export as image + + + + + + :/images/themes/default/mActionSaveAsPDF.svg:/images/themes/default/mActionSaveAsPDF.svg + + + &Export as PDF… + + + + + + :/images/themes/default/mActionSaveAsSVG.svg:/images/themes/default/mActionSaveAsSVG.svg + + + Export as S&VG… + + diff --git a/src/ui/layout/qgslayoutimageexportoptions.ui b/src/ui/layout/qgslayoutimageexportoptions.ui new file mode 100644 index 000000000000..3165933c9d5a --- /dev/null +++ b/src/ui/layout/qgslayoutimageexportoptions.ui @@ -0,0 +1,320 @@ + + + QgsLayoutImageExportOptionsDialog + + + + 0 + 0 + 533 + 651 + + + + Image Export Options + + + + + + Export options + + + + + + Export resolution + + + + + + + Page height + + + + + + + dpi + + + + + + 3000 + + + false + + + + + + + Auto + + + px + + + + + + 0 + + + 99999999 + + + false + + + + + + + Page width + + + + + + + Auto + + + px + + + + + + 0 + + + 99999999 + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Crop to content + + + true + + + false + + + + + + + + + + Left + + + + + + + px + + + 1000 + + + + + + + Right + + + + + + + px + + + 1000 + + + + + + + + + Bottom + + + + + + + Top margin + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + px + + + 1000 + + + + + + + px + + + 1000 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + + + QgsCollapsibleGroupBoxBasic + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsSpinBox + QSpinBox +
qgsspinbox.h
+
+
+ + mResolutionSpinBox + mWidthSpinBox + mHeightSpinBox + mClipToContentGroupBox + mTopMarginSpinBox + mLeftMarginSpinBox + mRightMarginSpinBox + mBottomMarginSpinBox + + + + + buttonBox + accepted() + QgsLayoutImageExportOptionsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + QgsLayoutImageExportOptionsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
From 0f9aaf4c44b567ab68ffa202448c7f2bb86d27f9 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 14:54:25 +1000 Subject: [PATCH 26/56] Expose world file option in image settings dialog --- src/app/layout/qgslayoutdesignerdialog.cpp | 3 ++- src/app/layout/qgslayoutimageexportoptionsdialog.cpp | 10 ++++++++++ src/app/layout/qgslayoutimageexportoptionsdialog.h | 12 ++++++++++++ src/ui/layout/qgslayoutimageexportoptions.ui | 12 +++++++++++- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 6bde426765d0..831a1d77089c 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1456,6 +1456,7 @@ void QgsLayoutDesignerDialog::exportToRaster() imageDlg.setResolution( dpi ); imageDlg.setCropToContents( cropToContents ); imageDlg.setCropMargins( marginTop, marginRight, marginBottom, marginLeft ); + imageDlg.setGenerateWorldFile( mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool() ); #if 0 //TODO QgsAtlasComposition *atlasMap = &mComposition->atlasComposition(); @@ -1505,7 +1506,7 @@ void QgsLayoutDesignerDialog::exportToRaster() { settings.imageSize = QSize( imageDlg.imageWidth(), imageDlg.imageHeight() ); } - settings.generateWorldFile = mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool(); + settings.generateWorldFile = imageDlg.generateWorldFile(); switch ( exporter.exportToImage( fileNExt.first, settings ) ) { diff --git a/src/app/layout/qgslayoutimageexportoptionsdialog.cpp b/src/app/layout/qgslayoutimageexportoptionsdialog.cpp index 40932229fb02..64f76159ca79 100644 --- a/src/app/layout/qgslayoutimageexportoptionsdialog.cpp +++ b/src/app/layout/qgslayoutimageexportoptionsdialog.cpp @@ -95,6 +95,16 @@ bool QgsLayoutImageExportOptionsDialog::cropToContents() const return mClipToContentGroupBox->isChecked(); } +void QgsLayoutImageExportOptionsDialog::setGenerateWorldFile( bool generate ) +{ + mGenerateWorldFile->setChecked( generate ); +} + +bool QgsLayoutImageExportOptionsDialog::generateWorldFile() const +{ + return mGenerateWorldFile->isChecked(); +} + void QgsLayoutImageExportOptionsDialog::getCropMargins( int &topMargin, int &rightMargin, int &bottomMargin, int &leftMargin ) const { topMargin = mTopMarginSpinBox->value(); diff --git a/src/app/layout/qgslayoutimageexportoptionsdialog.h b/src/app/layout/qgslayoutimageexportoptionsdialog.h index c7fb9b9904a3..b24f8ee2ff1c 100644 --- a/src/app/layout/qgslayoutimageexportoptionsdialog.h +++ b/src/app/layout/qgslayoutimageexportoptionsdialog.h @@ -84,6 +84,18 @@ class QgsLayoutImageExportOptionsDialog: public QDialog, private Ui::QgsLayoutIm */ bool cropToContents() const; + /** + * Sets whether the generate world file option should be checked. + * \see generateWorldFile() + */ + void setGenerateWorldFile( bool generate ); + + /** + * Returns whether the generate world file option is checked in the dialog. + * \see setGenerateWorldFile() + */ + bool generateWorldFile() const; + /** * Fetches the current crop to contents margin values, in pixels. * \param topMargin destination for top margin diff --git a/src/ui/layout/qgslayoutimageexportoptions.ui b/src/ui/layout/qgslayoutimageexportoptions.ui index 3165933c9d5a..5267c0963d95 100644 --- a/src/ui/layout/qgslayoutimageexportoptions.ui +++ b/src/ui/layout/qgslayoutimageexportoptions.ui @@ -7,7 +7,7 @@ 0 0 533 - 651 + 394 @@ -234,6 +234,16 @@
+ + + + If checked, a separate world file which georeferences exported images will be created + + + Generate world file + + + From 2f0969e2bd5b5e74e6c18412d27f86a8f7c1072a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 15:14:20 +1000 Subject: [PATCH 27/56] Expose antialiasing option in image export dialog Allows for creating non-antialiased images from layouts. Note that some layout item types do not correctly respect this setting, but at least map items do and the API is in place for them to be fixed later. Fixes #9281 --- python/core/layout/qgslayoutexporter.sip | 10 ++++++++++ src/app/layout/qgslayoutdesignerdialog.cpp | 7 +++++++ .../qgslayoutimageexportoptionsdialog.cpp | 10 ++++++++++ .../qgslayoutimageexportoptionsdialog.h | 12 +++++++++++ src/core/layout/qgslayoutexporter.cpp | 20 ++++++++++++------- src/core/layout/qgslayoutexporter.h | 11 ++++++++++ src/core/layout/qgslayoutitemmap.cpp | 2 +- src/ui/layout/qgslayoutimageexportoptions.ui | 10 ++++++++++ 8 files changed, 74 insertions(+), 8 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index b773cb6bdc77..a11c98534cc2 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -95,6 +95,11 @@ to render sections of pages rather than full pages. struct ImageExportSettings { + ImageExportSettings(); +%Docstring +Constructor for ImageExportSettings +%End + double dpi; %Docstring Resolution to export layout at @@ -141,6 +146,11 @@ Resolution to export layout at exported images. %End + QgsLayoutContext::Flags flags; +%Docstring + Layout context flags, which control how the export will be created. +%End + }; ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings ); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 831a1d77089c..07975470508e 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1450,6 +1450,7 @@ void QgsLayoutDesignerDialog::exportToRaster() int marginRight = mLayout->customProperty( QStringLiteral( "imageCropMarginRight" ), 0 ).toInt(); int marginBottom = mLayout->customProperty( QStringLiteral( "imageCropMarginBottom" ), 0 ).toInt(); int marginLeft = mLayout->customProperty( QStringLiteral( "imageCropMarginLeft" ), 0 ).toInt(); + bool antialias = mLayout->customProperty( QStringLiteral( "imageAntialias" ), true ).toBool(); QgsLayoutImageExportOptionsDialog imageDlg( this ); imageDlg.setImageSize( maxPageSize ); @@ -1457,6 +1458,7 @@ void QgsLayoutDesignerDialog::exportToRaster() imageDlg.setCropToContents( cropToContents ); imageDlg.setCropMargins( marginTop, marginRight, marginBottom, marginLeft ); imageDlg.setGenerateWorldFile( mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool() ); + imageDlg.setAntialiasing( antialias ); #if 0 //TODO QgsAtlasComposition *atlasMap = &mComposition->atlasComposition(); @@ -1494,6 +1496,8 @@ void QgsLayoutDesignerDialog::exportToRaster() mLayout->setCustomProperty( QStringLiteral( "imageCropMarginBottom" ), marginBottom ); mLayout->setCustomProperty( QStringLiteral( "imageCropMarginLeft" ), marginLeft ); + mLayout->setCustomProperty( QStringLiteral( "imageAntialias" ), imageDlg.antialiasing() ); + mView->setPaintingEnabled( false ); QgsLayoutExporter exporter( mLayout ); @@ -1507,6 +1511,9 @@ void QgsLayoutDesignerDialog::exportToRaster() settings.imageSize = QSize( imageDlg.imageWidth(), imageDlg.imageHeight() ); } settings.generateWorldFile = imageDlg.generateWorldFile(); + settings.flags = QgsLayoutContext::FlagUseAdvancedEffects; + if ( imageDlg.antialiasing() ) + settings.flags |= QgsLayoutContext::FlagAntialiasing; switch ( exporter.exportToImage( fileNExt.first, settings ) ) { diff --git a/src/app/layout/qgslayoutimageexportoptionsdialog.cpp b/src/app/layout/qgslayoutimageexportoptionsdialog.cpp index 64f76159ca79..3fa0153928d3 100644 --- a/src/app/layout/qgslayoutimageexportoptionsdialog.cpp +++ b/src/app/layout/qgslayoutimageexportoptionsdialog.cpp @@ -105,6 +105,16 @@ bool QgsLayoutImageExportOptionsDialog::generateWorldFile() const return mGenerateWorldFile->isChecked(); } +void QgsLayoutImageExportOptionsDialog::setAntialiasing( bool antialias ) +{ + mAntialiasingCheckBox->setChecked( antialias ); +} + +bool QgsLayoutImageExportOptionsDialog::antialiasing() const +{ + return mAntialiasingCheckBox->isChecked(); +} + void QgsLayoutImageExportOptionsDialog::getCropMargins( int &topMargin, int &rightMargin, int &bottomMargin, int &leftMargin ) const { topMargin = mTopMarginSpinBox->value(); diff --git a/src/app/layout/qgslayoutimageexportoptionsdialog.h b/src/app/layout/qgslayoutimageexportoptionsdialog.h index b24f8ee2ff1c..46653455d123 100644 --- a/src/app/layout/qgslayoutimageexportoptionsdialog.h +++ b/src/app/layout/qgslayoutimageexportoptionsdialog.h @@ -96,6 +96,18 @@ class QgsLayoutImageExportOptionsDialog: public QDialog, private Ui::QgsLayoutIm */ bool generateWorldFile() const; + /** + * Sets whether antialiasing should be used in the export. + * \see antialiasing() + */ + void setAntialiasing( bool antialias ); + + /** + * Returns whether antialiasing should be used in the export. + * \see setAntialiasing() + */ + bool antialiasing() const; + /** * Fetches the current crop to contents margin values, in pixels. * \param topMargin destination for top margin diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 6f8b5b4ba23a..0c3e1a51d5df 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -85,6 +85,8 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) setSnapLinesVisible( false ); #endif + painter->setRenderHint( QPainter::Antialiasing, mLayout->context().flags() & QgsLayoutContext::FlagAntialiasing ); + mLayout->render( painter, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), region ); #if 0 // TODO @@ -132,24 +134,27 @@ QImage QgsLayoutExporter::renderRegionToImage( const QRectF ®ion, QSize image } ///@cond PRIVATE -class LayoutDpiRestorer +class LayoutContextSettingsRestorer { public: - LayoutDpiRestorer( QgsLayout *layout ) + LayoutContextSettingsRestorer( QgsLayout *layout ) : mLayout( layout ) - , mPreviousSetting( layout->context().dpi() ) + , mPreviousDpi( layout->context().dpi() ) + , mPreviousFlags( layout->context().flags() ) { } - ~LayoutDpiRestorer() + ~LayoutContextSettingsRestorer() { - mLayout->context().setDpi( mPreviousSetting ); + mLayout->context().setDpi( mPreviousDpi ); + mLayout->context().setFlags( mPreviousFlags ); } private: QgsLayout *mLayout = nullptr; - double mPreviousSetting = 0; + double mPreviousDpi = 0; + QgsLayoutContext::Flags mPreviousFlags = 0; }; ///@endcond PRIVATE @@ -168,9 +173,10 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString QString baseName = fi.baseName(); QString extension = fi.completeSuffix(); - LayoutDpiRestorer dpiRestorer( mLayout ); + LayoutContextSettingsRestorer dpiRestorer( mLayout ); ( void )dpiRestorer; mLayout->context().setDpi( settings.dpi ); + mLayout->context().setFlags( settings.flags ); QList< int > pages; if ( settings.pages.empty() ) diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 9b0a82c4d464..b8bbe1adf659 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -18,6 +18,7 @@ #include "qgis_core.h" #include "qgsmargins.h" +#include "qgslayoutcontext.h" #include #include #include @@ -111,6 +112,11 @@ class CORE_EXPORT QgsLayoutExporter //! Contains settings relating to exporting layouts to raster images struct ImageExportSettings { + //! Constructor for ImageExportSettings + ImageExportSettings() + : flags( QgsLayoutContext::FlagAntialiasing | QgsLayoutContext::FlagUseAdvancedEffects ) + {} + //! Resolution to export layout at double dpi; @@ -155,6 +161,11 @@ class CORE_EXPORT QgsLayoutExporter */ bool generateWorldFile = false; + /** + * Layout context flags, which control how the export will be created. + */ + QgsLayoutContext::Flags flags = 0; + }; /** diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index c147aa382dc0..97addd53d95d 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -990,7 +990,7 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF // layout-specific overrides of flags jobMapSettings.setFlag( QgsMapSettings::ForceVectorOutput, true ); // force vector output (no caching of marker images etc.) - jobMapSettings.setFlag( QgsMapSettings::Antialiasing, true ); + jobMapSettings.setFlag( QgsMapSettings::Antialiasing, mLayout->context().flags() & QgsLayoutContext::FlagAntialiasing ); jobMapSettings.setFlag( QgsMapSettings::DrawEditingInfo, false ); jobMapSettings.setFlag( QgsMapSettings::DrawSelection, false ); jobMapSettings.setFlag( QgsMapSettings::UseAdvancedEffects, mLayout->context().flags() & QgsLayoutContext::FlagUseAdvancedEffects ); diff --git a/src/ui/layout/qgslayoutimageexportoptions.ui b/src/ui/layout/qgslayoutimageexportoptions.ui index 5267c0963d95..41dd8d223aa1 100644 --- a/src/ui/layout/qgslayoutimageexportoptions.ui +++ b/src/ui/layout/qgslayoutimageexportoptions.ui @@ -244,6 +244,16 @@ + + + + If unchecked, the generated images will not be antialiased + + + Enable antialiasing + + + From 48b6e02c8f90593bf9d957950e0387729f615881 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 18:29:47 +1000 Subject: [PATCH 28/56] Spelling --- tests/src/python/test_qgslayoutexporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index 834104d9f705..fe963e0c107b 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -196,7 +196,7 @@ def testRenderRegionToImage(self): image.save(rendered_file_path, "PNG") self.assertTrue(self.checkImage('rendertoimageregiondpi', 'rendertoimageregiondpi', rendered_file_path)) - # overridding dpi + # overriding dpi image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100), QSize(), 80) self.assertFalse(image.isNull()) From 8b1e057d2c57f9156bf865c347d7ac7b1148a11c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 18:50:28 +1000 Subject: [PATCH 29/56] Make QgsLayoutExporter::generateFileName virtual, so exporter subclasses can be made which customise the generated file names --- python/core/layout/qgslayoutexporter.sip | 40 ++++++++++++++++++++++ src/core/layout/qgslayoutexporter.cpp | 26 +++++++++----- src/core/layout/qgslayoutexporter.h | 33 ++++++++++++++++-- tests/src/python/test_qgslayoutexporter.py | 14 ++++++++ 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index a11c98534cc2..fce71058ba08 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -21,11 +21,42 @@ class QgsLayoutExporter %End public: + struct PageExportDetails + { + QString directory; +%Docstring +Target folder +%End + + QString baseName; +%Docstring +Base part of filename (i.e. file name without extension or '.') +%End + + QString extension; +%Docstring +File suffix/extension (without the leading '.') +%End + + int page; +%Docstring +Page number, where 0 = first page. +%End + }; + QgsLayoutExporter( QgsLayout *layout ); %Docstring Constructor for QgsLayoutExporter, for the specified ``layout``. %End + virtual ~QgsLayoutExporter(); + + QgsLayout *layout() const; +%Docstring + Returns the layout linked to this exporter. + :rtype: QgsLayout +%End + void renderPage( QPainter *painter, int page ) const; %Docstring Renders a full page to a destination ``painter``. @@ -207,6 +238,15 @@ Resolution to export layout at The ``dpi`` argument can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. %End + protected: + + virtual QString generateFileName( const PageExportDetails &details ) const; +%Docstring + Generates the file name for a page during export. + + Subclasses can override this method to customise page file naming. + :rtype: str +%End }; diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 0c3e1a51d5df..a2cde94b58d4 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -31,6 +31,11 @@ QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout ) } +QgsLayout *QgsLayoutExporter::layout() const +{ + return mLayout; +} + void QgsLayoutExporter::renderPage( QPainter *painter, int page ) const { if ( !mLayout ) @@ -169,9 +174,11 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString } QFileInfo fi( filePath ); - QString path = fi.path(); - QString baseName = fi.baseName(); - QString extension = fi.completeSuffix(); + + PageExportDetails pageDetails; + pageDetails.directory = fi.path(); + pageDetails.baseName = fi.baseName(); + pageDetails.extension = fi.completeSuffix(); LayoutContextSettingsRestorer dpiRestorer( mLayout ); ( void )dpiRestorer; @@ -207,7 +214,8 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString if ( skip ) continue; // should skip this page, e.g. null size - QString outputFilePath = generateFileName( path, baseName, extension, page ); + pageDetails.page = page; + QString outputFilePath = generateFileName( pageDetails ); if ( image.isNull() ) { @@ -215,7 +223,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString return MemoryError; } - if ( !saveImage( image, outputFilePath, extension ) ) + if ( !saveImage( image, outputFilePath, pageDetails.extension ) ) { mErrorFileName = outputFilePath; return FileError; @@ -509,15 +517,15 @@ QImage QgsLayoutExporter::createImage( const QgsLayoutExporter::ImageExportSetti } } -QString QgsLayoutExporter::generateFileName( const QString &path, const QString &baseName, const QString &suffix, int page ) const +QString QgsLayoutExporter::generateFileName( const PageExportDetails &details ) const { - if ( page == 0 ) + if ( details.page == 0 ) { - return path + '/' + baseName + '.' + suffix; + return details.directory + '/' + details.baseName + '.' + details.extension; } else { - return path + '/' + baseName + '_' + QString::number( page + 1 ) + '.' + suffix; + return details.directory + '/' + details.baseName + '_' + QString::number( details.page + 1 ) + '.' + details.extension; } } diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index b8bbe1adf659..8578808cb945 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -38,11 +38,34 @@ class CORE_EXPORT QgsLayoutExporter public: + //! Contains details of a page being exported by the class + struct PageExportDetails + { + //! Target folder + QString directory; + + //! Base part of filename (i.e. file name without extension or '.') + QString baseName; + + //! File suffix/extension (without the leading '.') + QString extension; + + //! Page number, where 0 = first page. + int page = 0; + }; + /** * Constructor for QgsLayoutExporter, for the specified \a layout. */ QgsLayoutExporter( QgsLayout *layout ); + virtual ~QgsLayoutExporter() = default; + + /** + * Returns the layout linked to this exporter. + */ + QgsLayout *layout() const; + /** * Renders a full page to a destination \a painter. * @@ -219,6 +242,14 @@ class CORE_EXPORT QgsLayoutExporter */ void computeWorldFileParameters( const QRectF ®ion, double &a, double &b, double &c, double &d, double &e, double &f, double dpi = -1 ) const; + protected: + + /** + * Generates the file name for a page during export. + * + * Subclasses can override this method to customise page file naming. + */ + virtual QString generateFileName( const PageExportDetails &details ) const; private: @@ -228,8 +259,6 @@ class CORE_EXPORT QgsLayoutExporter QImage createImage( const ImageExportSettings &settings, int page, QRectF &bounds, bool &skipPage ) const; - QString generateFileName( const QString &path, const QString &baseName, const QString &suffix, int page ) const; - /** * Saves an image to a file, possibly using format specific options (e.g. LZW compression for tiff) */ diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index fe963e0c107b..fcb95b40f834 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -317,6 +317,20 @@ def testExportWorldFile(self): self.assertAlmostEqual(values[4], 1925.000000000000, 2) self.assertAlmostEqual(values[5], 3050.000000000000, 2) + def testPageFileName(self): + l = QgsLayout(QgsProject.instance()) + exporter = QgsLayoutExporter(l) + details = QgsLayoutExporter.PageExportDetails() + details.directory = '/tmp/output' + details.baseName = 'my_maps' + details.extension = 'png' + details.page = 0 + self.assertEqual(exporter.generateFileName(details), '/tmp/output/my_maps.png') + details.page = 1 + self.assertEqual(exporter.generateFileName(details), '/tmp/output/my_maps_2.png') + details.page = 2 + self.assertEqual(exporter.generateFileName(details), '/tmp/output/my_maps_3.png') + if __name__ == '__main__': unittest.main() From 0110b43362d6a0b298f58479b37cfa37591c261e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 11 Dec 2017 19:47:25 +1000 Subject: [PATCH 30/56] Update tests masks --- .../expected_layoutscalebar_numeric_mask.png | Bin 0 -> 6273 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_numeric/travis/expected_layoutscalebar_numeric_mask.png diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_numeric/travis/expected_layoutscalebar_numeric_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_numeric/travis/expected_layoutscalebar_numeric_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..190d2c10713e2bcfbde4edd16408f2a09af4e72f GIT binary patch literal 6273 zcmeHM>r)d~6kqEDq%uf#j5R!znHI;fq>xG!NVZxWWvZmp5rqmyW2FMA5d$HF@EFko z;^PC;nwSWkfhxrk1~W=i2rE^Im<=gSg@!;9Mv|zI1R6rfliuz0U+~+0*n8&Q*>lgi zXV1CkH@|K8BJ)6a*p@H|g2FQnrhf@RtC$e9a!cqMu%b4b6hN#!e=vswK@q30XGJ@F zw*Z1RlQYuy9^pxBlZP9l%v)-v?!rkMqA6?Eu1MT_{`K>SRmXz7y{jo##5M2M))O0u zyPD7gEtgAL-X{;G(=FHI<ZIa?CSN8JiJmgMeQ7BvUZpj+^C+D< zW-u6bZffKdC5zQjF)=i)htT+R=S>xd%#MzYW$f0d#T08W;FIZC7L)0uLEu!E&zT1N zey8R}Cvvu5tJRh-%}tTjA)u>~TA!k<6ASA}tuB7$sXus|^zHw&acnl5dSqlozKcv& znJ-8g2(8*buY`%-i$&iezE`a`EGiNrU9Wo^_G1oDF zRrt5|cje*_ZHVDk$*9huiDAD)or|N#*=iEvPa>1c>=U92w_S~TOin!;Q4`sH#deDS zh0fFbK{nvr9;u#P7gjfX)9|sr^f-t!;WO!ulD@#A-l?U-Y81Ujq*%arBuIU!V;eEX zj+eDIk@O6n9JpVZjOsWncJyyyAinOwJ1+`LUr*4-yD@ezG?9erek|q^PzDTqDtin9 zE$7TBNC6da3b8%@lQ`p+t zipAH(r^d&}AA7jLTo+p%Wcdnw>}{wM<_#zSDQ zxg9y^)y{RtX1%T%4v)vP_qzCjzbM;p2YhFWG=}jK-8gbO)t&0-BX=P|?}VZPxZ{3f ztBZ&o4hDk)kf%aE_i8G!e+p2>{>*ggzLOXK1?h60$N&HU literal 0 HcmV?d00001 From a59dce5048675938df54496ccf7caba568b7cad2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 12 Dec 2017 12:05:36 +1000 Subject: [PATCH 31/56] Move containsAdvancedEffects test to QgsLayoutItem And start a generic test library for all item types to ensure correct behavior for QgsLayoutItem subclasses Currently justs tests to ensure that overriden containsAdvancedEffects methods also call the base class test --- python/core/layout/qgslayoutitem.sip | 10 ++++++ src/core/layout/qgslayoutitem.cpp | 5 +++ src/core/layout/qgslayoutitem.h | 9 +++++ src/core/layout/qgslayoutitemmap.cpp | 3 ++ src/core/layout/qgslayoutitemmap.h | 4 +-- tests/src/python/CMakeLists.txt | 4 +++ tests/src/python/test_qgslayoutframe.py | 38 ++++++++++++++++++++++ tests/src/python/test_qgslayoutitem.py | 23 ++++++++++++- tests/src/python/test_qgslayoutlabel.py | 7 +++- tests/src/python/test_qgslayoutlegend.py | 8 ++++- tests/src/python/test_qgslayoutmap.py | 7 +++- tests/src/python/test_qgslayoutpage.py | 33 +++++++++++++++++++ tests/src/python/test_qgslayoutpicture.py | 5 ++- tests/src/python/test_qgslayoutpolygon.py | 7 +++- tests/src/python/test_qgslayoutpolyline.py | 7 +++- tests/src/python/test_qgslayoutscalebar.py | 33 +++++++++++++++++++ tests/src/python/test_qgslayoutshape.py | 33 +++++++++++++++++++ 17 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 tests/src/python/test_qgslayoutframe.py create mode 100644 tests/src/python/test_qgslayoutpage.py create mode 100644 tests/src/python/test_qgslayoutscalebar.py create mode 100644 tests/src/python/test_qgslayoutshape.py diff --git a/python/core/layout/qgslayoutitem.sip b/python/core/layout/qgslayoutitem.sip index 29ce84634320..bd69360c1d47 100644 --- a/python/core/layout/qgslayoutitem.sip +++ b/python/core/layout/qgslayoutitem.sip @@ -820,6 +820,16 @@ Sets whether the item should be excluded from composer exports and prints. .. seealso:: :py:func:`excludeFromExports()` %End + virtual bool containsAdvancedEffects() const; +%Docstring + Returns true if the item contains contents with blend modes or transparency + effects which can only be reproduced by rastering the item. + + Subclasses should ensure that implemented overrides of this method + also check the base class result. + :rtype: bool +%End + virtual double estimatedFrameBleed() const; %Docstring Returns the estimated amount the item's frame bleeds outside the item's diff --git a/src/core/layout/qgslayoutitem.cpp b/src/core/layout/qgslayoutitem.cpp index f4e7fa285d7e..f3ffd38d0fa2 100644 --- a/src/core/layout/qgslayoutitem.cpp +++ b/src/core/layout/qgslayoutitem.cpp @@ -840,6 +840,11 @@ void QgsLayoutItem::setExcludeFromExports( bool exclude ) refreshDataDefinedProperty( QgsLayoutObject::ExcludeFromExports ); } +bool QgsLayoutItem::containsAdvancedEffects() const +{ + return blendMode() != QPainter::CompositionMode_SourceOver; +} + double QgsLayoutItem::estimatedFrameBleed() const { if ( !hasFrame() ) diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index 519962f9ddfa..56167ec5805c 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -742,6 +742,15 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt */ void setExcludeFromExports( bool exclude ); + /** + * Returns true if the item contains contents with blend modes or transparency + * effects which can only be reproduced by rastering the item. + * + * Subclasses should ensure that implemented overrides of this method + * also check the base class result. + */ + virtual bool containsAdvancedEffects() const; + /** * Returns the estimated amount the item's frame bleeds outside the item's * actual rectangle. For instance, if the item has a 2mm frame stroke, then diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 97addd53d95d..38cc7adc44c2 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -379,6 +379,9 @@ bool QgsLayoutItemMap::containsWmsLayer() const bool QgsLayoutItemMap::containsAdvancedEffects() const { + if ( QgsLayoutItem::containsAdvancedEffects() ) + return true; + //check easy things first //overviews diff --git a/src/core/layout/qgslayoutitemmap.h b/src/core/layout/qgslayoutitemmap.h index 6f5577d10dc4..f920f6857199 100644 --- a/src/core/layout/qgslayoutitemmap.h +++ b/src/core/layout/qgslayoutitemmap.h @@ -91,7 +91,6 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem int numberExportLayers() const override; void setFrameStrokeWidth( const QgsLayoutMeasurement &width ) override; - /** * Returns the map scale. * The scale value indicates the scale denominator, e.g. 1000.0 for a 1:1000 map. @@ -276,8 +275,7 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem //! Returns true if the map contains a WMS layer. bool containsWmsLayer() const; - //! Returns true if the map contains layers with blend modes or flattened layers for vectors - bool containsAdvancedEffects() const; + bool containsAdvancedEffects() const override; /** * Sets the \a rotation for the map - this does not affect the composer item shape, only the diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 0273ef0f2ff0..76f38a219e54 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -84,6 +84,7 @@ ADD_PYTHON_TEST(PyQgsLayerTree test_qgslayertree.py) ADD_PYTHON_TEST(PyQgsLayout test_qgslayout.py) ADD_PYTHON_TEST(PyQgsLayoutAlign test_qgslayoutaligner.py) ADD_PYTHON_TEST(PyQgsLayoutExporter test_qgslayoutexporter.py) +ADD_PYTHON_TEST(PyQgsLayoutFrame test_qgslayoutframe.py) ADD_PYTHON_TEST(PyQgsLayoutManager test_qgslayoutmanager.py) ADD_PYTHON_TEST(PyQgsLayoutPageCollection test_qgslayoutpagecollection.py) ADD_PYTHON_TEST(PyQgsLayoutView test_qgslayoutview.py) @@ -96,9 +97,12 @@ ADD_PYTHON_TEST(PyQgsLayoutLabel test_qgslayoutlabel.py) ADD_PYTHON_TEST(PyQgsLayoutLegend test_qgslayoutlegend.py) ADD_PYTHON_TEST(PyQgsLayoutMap test_qgslayoutmap.py) ADD_PYTHON_TEST(PyQgsLayoutMapGrid test_qgslayoutmapgrid.py) +ADD_PYTHON_TEST(PyQgsLayoutPage test_qgslayoutpage.py) ADD_PYTHON_TEST(PyQgsLayoutPicture test_qgslayoutpicture.py) ADD_PYTHON_TEST(PyQgsLayoutPolygon test_qgslayoutpolygon.py) ADD_PYTHON_TEST(PyQgsLayoutPolyline test_qgslayoutpolyline.py) +ADD_PYTHON_TEST(PyQgsLayoutScaleBar test_qgslayoutscalebar.py) +ADD_PYTHON_TEST(PyQgsLayoutShape test_qgslayoutshape.py) ADD_PYTHON_TEST(PyQgsLayoutSnapper test_qgslayoutsnapper.py) ADD_PYTHON_TEST(PyQgsLayoutUnitsComboBox test_qgslayoutunitscombobox.py) ADD_PYTHON_TEST(PyQgsLineSymbolLayers test_qgslinesymbollayers.py) diff --git a/tests/src/python/test_qgslayoutframe.py b/tests/src/python/test_qgslayoutframe.py new file mode 100644 index 000000000000..2a059981cb4b --- /dev/null +++ b/tests/src/python/test_qgslayoutframe.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLayoutFrame. + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = '(C) 2017 by Nyall Dawson' +__date__ = '23/10/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import qgis # NOQA + +from qgis.testing import start_app, unittest +from qgis.core import QgsLayoutFrame, QgsLayoutItemHtml + +from test_qgslayoutitem import LayoutItemTestCase + +start_app() + + +class TestQgsLayoutFrame(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.mf = None + + @classmethod + def createItem(cls, layout): + cls.mf = QgsLayoutItemHtml(layout) + return QgsLayoutFrame(layout, cls.mf) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgslayoutitem.py b/tests/src/python/test_qgslayoutitem.py index 27b080362f23..ef12e74bbb12 100644 --- a/tests/src/python/test_qgslayoutitem.py +++ b/tests/src/python/test_qgslayoutitem.py @@ -26,13 +26,34 @@ QgsLayoutSize, QgsApplication) from qgis.PyQt.QtCore import QRectF -from qgis.PyQt.QtGui import QColor +from qgis.PyQt.QtGui import QColor, QPainter from qgis.PyQt.QtTest import QSignalSpy start_app() +class LayoutItemTestCase(object): + + ''' + This is a collection of generic tests for QgsLayoutItem subclasses. + To make use of it, subclass it and set self.item_class to a QgsLayoutItem subclass you want to test. + ''' + + def make_item(self, layout): + if hasattr(self, 'item_class'): + return self.item_class(layout) + else: + return self.createItem(layout) + + def testContainsAdvancedEffects(self): + l = QgsLayout(QgsProject.instance()) + item = self.make_item(l) + self.assertFalse(item.containsAdvancedEffects()) + item.setBlendMode(QPainter.CompositionMode_SourceIn) + self.assertTrue(item.containsAdvancedEffects()) + + class TestQgsLayoutItem(unittest.TestCase): def testDataDefinedFrameColor(self): diff --git a/tests/src/python/test_qgslayoutlabel.py b/tests/src/python/test_qgslayoutlabel.py index 8feac6dc98ca..f051f420db48 100644 --- a/tests/src/python/test_qgslayoutlabel.py +++ b/tests/src/python/test_qgslayoutlabel.py @@ -19,11 +19,16 @@ from qgis.core import QgsVectorLayer, QgsLayout, QgsLayoutItemLabel, QgsProject from utilities import unitTestDataPath +from test_qgslayoutitem import LayoutItemTestCase start_app() -class TestQgsLayoutItemLabel(unittest.TestCase): +class TestQgsLayoutItemLabel(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.item_class = QgsLayoutItemLabel def testCase(self): TEST_DATA_DIR = unitTestDataPath() diff --git a/tests/src/python/test_qgslayoutlegend.py b/tests/src/python/test_qgslayoutlegend.py index 924e3ac9393d..07a006e57dd0 100644 --- a/tests/src/python/test_qgslayoutlegend.py +++ b/tests/src/python/test_qgslayoutlegend.py @@ -34,11 +34,17 @@ from qgslayoutchecker import QgsLayoutChecker import os +from test_qgslayoutitem import LayoutItemTestCase + start_app() TEST_DATA_DIR = unitTestDataPath() -class TestQgsLayoutItemLegend(unittest.TestCase): +class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.item_class = QgsLayoutItemLegend def testInitialSizeSymbolMapUnits(self): """Test initial size of legend with a symbol size in map units""" diff --git a/tests/src/python/test_qgslayoutmap.py b/tests/src/python/test_qgslayoutmap.py index 29f71d89434d..badf0855ec29 100644 --- a/tests/src/python/test_qgslayoutmap.py +++ b/tests/src/python/test_qgslayoutmap.py @@ -34,12 +34,17 @@ from qgis.testing import start_app, unittest from utilities import unitTestDataPath from qgslayoutchecker import QgsLayoutChecker +from test_qgslayoutitem import LayoutItemTestCase start_app() TEST_DATA_DIR = unitTestDataPath() -class TestQgsComposerMap(unittest.TestCase): +class TestQgsComposerMap(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.item_class = QgsLayoutItemMap def __init__(self, methodName): """Run once on class initialization.""" diff --git a/tests/src/python/test_qgslayoutpage.py b/tests/src/python/test_qgslayoutpage.py new file mode 100644 index 000000000000..2bdc0b691da2 --- /dev/null +++ b/tests/src/python/test_qgslayoutpage.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLayoutItemPage. + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = '(C) 2017 by Nyall Dawson' +__date__ = '23/10/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import qgis # NOQA + +from qgis.testing import start_app, unittest +from qgis.core import QgsLayoutItemPage + +from test_qgslayoutitem import LayoutItemTestCase + +start_app() + + +class TestQgsLayoutPage(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.item_class = QgsLayoutItemPage + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgslayoutpicture.py b/tests/src/python/test_qgslayoutpicture.py index eed169321298..8fb78e810f79 100644 --- a/tests/src/python/test_qgslayoutpicture.py +++ b/tests/src/python/test_qgslayoutpicture.py @@ -30,15 +30,18 @@ from qgis.testing import start_app, unittest from utilities import unitTestDataPath from qgslayoutchecker import QgsLayoutChecker +from test_qgslayoutitem import LayoutItemTestCase start_app() TEST_DATA_DIR = unitTestDataPath() -class TestQgsLayoutPicture(unittest.TestCase): +class TestQgsLayoutPicture(unittest.TestCase, LayoutItemTestCase): @classmethod def setUpClass(cls): + cls.item_class = QgsLayoutItemPicture + # Bring up a simple HTTP server, for remote picture tests os.chdir(unitTestDataPath() + '') handler = http.server.SimpleHTTPRequestHandler diff --git a/tests/src/python/test_qgslayoutpolygon.py b/tests/src/python/test_qgslayoutpolygon.py index 8df5142de217..8a0194cbdf89 100644 --- a/tests/src/python/test_qgslayoutpolygon.py +++ b/tests/src/python/test_qgslayoutpolygon.py @@ -29,12 +29,17 @@ ) from utilities import unitTestDataPath from qgslayoutchecker import QgsLayoutChecker +from test_qgslayoutitem import LayoutItemTestCase start_app() TEST_DATA_DIR = unitTestDataPath() -class TestQgsLayoutPolygon(unittest.TestCase): +class TestQgsLayoutPolygon(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.item_class = QgsLayoutItemPolygon def __init__(self, methodName): """Run once on class initialization.""" diff --git a/tests/src/python/test_qgslayoutpolyline.py b/tests/src/python/test_qgslayoutpolyline.py index d54cb7e22873..39edd3046302 100644 --- a/tests/src/python/test_qgslayoutpolyline.py +++ b/tests/src/python/test_qgslayoutpolyline.py @@ -29,12 +29,17 @@ ) from utilities import unitTestDataPath from qgslayoutchecker import QgsLayoutChecker +from test_qgslayoutitem import LayoutItemTestCase start_app() TEST_DATA_DIR = unitTestDataPath() -class TestQgsLayoutPolyline(unittest.TestCase): +class TestQgsLayoutPolyline(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.item_class = QgsLayoutItemPolyline def __init__(self, methodName): """Run once on class initialization.""" diff --git a/tests/src/python/test_qgslayoutscalebar.py b/tests/src/python/test_qgslayoutscalebar.py new file mode 100644 index 000000000000..185bba1c1422 --- /dev/null +++ b/tests/src/python/test_qgslayoutscalebar.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLayoutItemScaleBar. + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = '(C) 2017 by Nyall Dawson' +__date__ = '23/10/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import qgis # NOQA + +from qgis.testing import start_app, unittest +from qgis.core import QgsLayoutItemScaleBar + +from test_qgslayoutitem import LayoutItemTestCase + +start_app() + + +class TestQgsLayoutScaleBar(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.item_class = QgsLayoutItemScaleBar + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgslayoutshape.py b/tests/src/python/test_qgslayoutshape.py new file mode 100644 index 000000000000..c1a747937819 --- /dev/null +++ b/tests/src/python/test_qgslayoutshape.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLayoutItemShape. + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = '(C) 2017 by Nyall Dawson' +__date__ = '23/10/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import qgis # NOQA + +from qgis.testing import start_app, unittest +from qgis.core import QgsLayoutItemShape + +from test_qgslayoutitem import LayoutItemTestCase + +start_app() + + +class TestQgsLayoutShape(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.item_class = QgsLayoutItemShape + + +if __name__ == '__main__': + unittest.main() From 91179f139669150c676492438d3e2edb11b7a3c7 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 12 Dec 2017 13:59:05 +1000 Subject: [PATCH 32/56] Work on PDF export --- python/core/layout/qgslayoutexporter.sip | 40 +++- src/app/layout/qgslayoutdesignerdialog.cpp | 140 +++++++++++- src/app/layout/qgslayoutdesignerdialog.h | 10 + src/app/layout/qgslayoutpropertieswidget.cpp | 10 + src/app/layout/qgslayoutpropertieswidget.h | 6 +- src/core/layout/qgslayoutcontext.h | 1 + src/core/layout/qgslayoutexporter.cpp | 214 ++++++++++++++++++- src/core/layout/qgslayoutexporter.h | 59 ++++- src/ui/layout/qgslayoutwidgetbase.ui | 46 ++-- 9 files changed, 491 insertions(+), 35 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index fce71058ba08..376df0b6803f 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -122,6 +122,7 @@ to render sections of pages rather than full pages. Success, MemoryError, FileError, + PrintError, }; struct ImageExportSettings @@ -133,7 +134,7 @@ Constructor for ImageExportSettings double dpi; %Docstring -Resolution to export layout at +Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. %End QSize imageSize; @@ -173,7 +174,7 @@ Resolution to export layout at bool generateWorldFile; %Docstring - Set to true to generate an external world file alonside + Set to true to generate an external world file alongside exported images. %End @@ -184,7 +185,7 @@ Resolution to export layout at }; - ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings ); + ExportResult exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ); %Docstring Exports the layout to the a ``filePath``, using the specified export ``settings``. @@ -197,6 +198,39 @@ Resolution to export layout at :rtype: ExportResult %End + struct PdfExportSettings + { + PdfExportSettings(); +%Docstring +Constructor for PdfExportSettings +%End + + double dpi; +%Docstring +Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. +%End + + bool rasterizeWholeImage; +%Docstring +Set to true to force whole layout to be rasterized while exporting +%End + + QgsLayoutContext::Flags flags; +%Docstring + Layout context flags, which control how the export will be created. +%End + + }; + + ExportResult exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &settings ); +%Docstring + Exports the layout as a PDF to the a ``filePath``, using the specified export ``settings``. + + Returns a result code indicating whether the export was successful or an + error was encountered. + :rtype: ExportResult +%End + QString errorFile() const; %Docstring Returns the file name corresponding to the last error encountered during diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 07975470508e..9a03b012629c 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -172,6 +172,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla connect( mActionRemoveLayout, &QAction::triggered, this, &QgsLayoutDesignerDialog::deleteLayout ); connect( mActionExportAsImage, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToRaster ); + connect( mActionExportAsPDF, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToPdf ); connect( mActionShowGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::showGrid ); connect( mActionSnapGrid, &QAction::triggered, this, &QgsLayoutDesignerDialog::snapToGrid ); @@ -1499,6 +1500,7 @@ void QgsLayoutDesignerDialog::exportToRaster() mLayout->setCustomProperty( QStringLiteral( "imageAntialias" ), imageDlg.antialiasing() ); mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); QgsLayoutExporter exporter( mLayout ); @@ -1518,11 +1520,12 @@ void QgsLayoutDesignerDialog::exportToRaster() switch ( exporter.exportToImage( fileNExt.first, settings ) ) { case QgsLayoutExporter::Success: + case QgsLayoutExporter::PrintError: break; case QgsLayoutExporter::FileError: QMessageBox::warning( this, tr( "Image Export Error" ), - QString( tr( "Cannot write to %1.\n\nThis file may be open in another application." ) ).arg( exporter.errorFile() ), + tr( "Cannot write to %1.\n\nThis file may be open in another application." ).arg( exporter.errorFile() ), QMessageBox::Ok, QMessageBox::Ok ); break; @@ -1536,10 +1539,105 @@ void QgsLayoutDesignerDialog::exportToRaster() QMessageBox::Ok, QMessageBox::Ok ); break; + } + QApplication::restoreOverrideCursor(); mView->setPaintingEnabled( true ); } +void QgsLayoutDesignerDialog::exportToPdf() +{ + if ( containsWmsLayers() ) + { + showWmsPrintingWarning(); + } + + if ( containsAdvancedEffects() ) + { + showAdvancedEffectsWarning(); + } + + QgsSettings settings; + QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsPdfFile" ), QStringLiteral( "qgis.pdf" ) ).toString(); + QFileInfo file( lastUsedFile ); + QString outputFileName; + +#if 0// TODO + if ( hasAnAtlas && !atlasOnASingleFile && + ( mode == QgsComposer::Atlas || mComposition->atlasMode() == QgsComposition::PreviewAtlas ) ) + { + outputFileName = QDir( file.path() ).filePath( atlasMap->currentFilename() ) + ".pdf"; + } + else + { +#endif + outputFileName = file.path(); +#if 0 //TODO + } +#endif + +#ifdef Q_OS_MAC + mQgis->activateWindow(); + this->raise(); +#endif + outputFileName = QFileDialog::getSaveFileName( + this, + tr( "Export to PDF" ), + outputFileName, + tr( "PDF Format" ) + " (*.pdf *.PDF)" ); + this->activateWindow(); + if ( outputFileName.isEmpty() ) + { + return; + } + + if ( !outputFileName.endsWith( QLatin1String( ".pdf" ), Qt::CaseInsensitive ) ) + { + outputFileName += QLatin1String( ".pdf" ); + } + + settings.setValue( QStringLiteral( "UI/lastSaveAsPdfFile" ), outputFileName ); + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + QgsLayoutExporter::PdfExportSettings pdfSettings; + pdfSettings.rasteriseWholeImage = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); + + QgsLayoutExporter exporter( mLayout ); + switch ( exporter.exportToPdf( outputFileName, pdfSettings ) ) + { + case QgsLayoutExporter::Success: + break; + + case QgsLayoutExporter::FileError: + QMessageBox::warning( this, tr( "Export to PDF" ), + tr( "Cannot write to %1.\n\nThis file may be open in another application." ).arg( exporter.errorFile() ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::PrintError: + QMessageBox::warning( this, tr( "Export to PDF" ), + tr( "Could not create print device." ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( nullptr, tr( "Memory Allocation Error" ), + tr( "Exporting the PDF " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + } + + mView->setPaintingEnabled( true ); + QApplication::restoreOverrideCursor(); +} + void QgsLayoutDesignerDialog::paste() { QPointF pt = mView->mapFromGlobal( QCursor::pos() ); @@ -1634,9 +1732,9 @@ void QgsLayoutDesignerDialog::createLayoutPropertiesWidget() QgsLayoutGuideWidget *oldGuideWidget = qobject_cast( mGuideStack->takeMainPanel() ); delete oldGuideWidget; - QgsLayoutPropertiesWidget *widget = new QgsLayoutPropertiesWidget( mGeneralDock, mLayout ); - widget->setDockMode( true ); - mGeneralPropertiesStack->setMainPanel( widget ); + mLayoutPropertiesWidget = new QgsLayoutPropertiesWidget( mGeneralDock, mLayout ); + mLayoutPropertiesWidget->setDockMode( true ); + mGeneralPropertiesStack->setMainPanel( mLayoutPropertiesWidget ); QgsLayoutGuideWidget *guideWidget = new QgsLayoutGuideWidget( mGuideDock, mLayout, mView ); guideWidget->setDockMode( true ); @@ -1685,6 +1783,40 @@ void QgsLayoutDesignerDialog::showWmsPrintingWarning() } } +bool QgsLayoutDesignerDialog::containsAdvancedEffects() const +{ + QList< QgsLayoutItem *> items; + mLayout->layoutItems( items ); + + for ( QgsLayoutItem *currentItem : qgis::as_const( items ) ) + { + if ( currentItem->containsAdvancedEffects() ) + return true; + } + return false; +} + +void QgsLayoutDesignerDialog::showAdvancedEffectsWarning() +{ + bool rasterize = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); + if ( rasterise ) + return; + + QgsMessageViewer *m = new QgsMessageViewer( this, QgsGuiUtils::ModalDialogFlags, false ); + m->setWindowTitle( tr( "Composition Effects" ) ); + m->setMessage( tr( "Advanced composition effects such as blend modes or vector layer transparency are enabled in this layout, which cannot be printed as vectors. Printing as a raster is recommended." ), QgsMessageOutput::MessageText ); + m->setCheckBoxText( tr( "Print as raster" ) ); + m->setCheckBoxState( Qt::Checked ); + m->setCheckBoxVisible( true ); + m->showMessage( true ); + + mLayout->setCustomProperty( QStringLiteral( "rasterise" ), m->checkBoxState() == Qt::Checked ); + //make sure print as raster checkbox is updated + mLayoutPropertiesWidget->updateGui(); + + delete m; +} + void QgsLayoutDesignerDialog::selectItems( const QList items ) { for ( QGraphicsItem *item : items ) diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index f46d492ceded..576df1202b12 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -41,6 +41,7 @@ class QgsDockWidget; class QUndoView; class QTreeView; class QgsLayoutItemsListView; +class QgsLayoutPropertiesWidget; class QgsAppLayoutDesignerInterface : public QgsLayoutDesignerInterface { @@ -275,6 +276,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void renameLayout(); void deleteLayout(); void exportToRaster(); + void exportToPdf(); private: @@ -322,6 +324,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner QgsDockWidget *mGuideDock = nullptr; QgsPanelWidgetStack *mGuideStack = nullptr; + QgsLayoutPropertiesWidget *mLayoutPropertiesWidget = nullptr; + QUndoView *mUndoView = nullptr; QgsDockWidget *mUndoDock = nullptr; @@ -365,6 +369,12 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner //! Displays a warning because of possible min/max size in WMS void showWmsPrintingWarning(); + + //! True if the layout contains advanced effects, such as blend modes + bool containsAdvancedEffects() const; + + //! Displays a warning because of incompatibility between blend modes and QPrinter + void showAdvancedEffectsWarning(); }; #endif // QGSLAYOUTDESIGNERDIALOG_H diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index e2451e4a9a0a..4b7ed81f7c26 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -58,6 +58,8 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout mGenerateWorldFileCheckBox->setChecked( exportWorldFile ); connect( mGenerateWorldFileCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::worldFileToggled ); + connect( mRasterizeCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::rasteriseToggled ); + mTopMarginSpinBox->setValue( topMargin ); mMarginUnitsComboBox->linkToWidget( mTopMarginSpinBox ); mRightMarginSpinBox->setValue( rightMargin ); @@ -88,6 +90,9 @@ void QgsLayoutPropertiesWidget::updateGui() { whileBlocking( mReferenceMapComboBox )->setItem( mLayout->referenceMap() ); whileBlocking( mResolutionSpinBox )->setValue( mLayout->context().dpi() ); + + bool rasterise = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); + whileBlocking( mRasterizeCheckBox )->setChecked( rasterise ); } void QgsLayoutPropertiesWidget::updateSnappingElements() @@ -189,6 +194,11 @@ void QgsLayoutPropertiesWidget::worldFileToggled() mLayout->setCustomProperty( QStringLiteral( "exportWorldFile" ), mGenerateWorldFileCheckBox->isChecked() ); } +void QgsLayoutPropertiesWidget::rasteriseToggled() +{ + mLayout->setCustomProperty( QStringLiteral( "rasterise" ), mRasterizeCheckBox->isChecked() ); +} + void QgsLayoutPropertiesWidget::blockSignals( bool block ) { mGridResolutionSpinBox->blockSignals( block ); diff --git a/src/app/layout/qgslayoutpropertieswidget.h b/src/app/layout/qgslayoutpropertieswidget.h index 66e3f12fc9f6..426f947e964b 100644 --- a/src/app/layout/qgslayoutpropertieswidget.h +++ b/src/app/layout/qgslayoutpropertieswidget.h @@ -28,10 +28,13 @@ class QgsLayoutPropertiesWidget: public QgsPanelWidget, private Ui::QgsLayoutWid public: QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout *layout ); - private slots: + public slots: + //! Refreshes the gui to reflect the current layout settings void updateGui(); + private slots: + void gridResolutionChanged( double d ); void gridResolutionUnitsChanged( QgsUnitTypes::LayoutUnit unit ); void gridOffsetXChanged( double d ); @@ -43,6 +46,7 @@ class QgsLayoutPropertiesWidget: public QgsPanelWidget, private Ui::QgsLayoutWid void referenceMapChanged( QgsLayoutItem *item ); void dpiChanged( int value ); void worldFileToggled(); + void rasteriseToggled(); private: diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index 68eda98b3a30..3ba948842651 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -235,6 +235,7 @@ class CORE_EXPORT QgsLayoutContext : public QObject bool mPagesVisible = true; friend class QgsLayoutExporter; + friend class LayoutItemCacheSettingRestorer; }; diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index a2cde94b58d4..66c9894b46de 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -19,6 +19,7 @@ #include "qgslayoutitemmap.h" #include "qgslayoutpagecollection.h" #include "qgsogrutils.h" +#include "qgspaintenginehack.h" #include #include @@ -76,6 +77,41 @@ QImage QgsLayoutExporter::renderPageToImage( int page, QSize imageSize, double d return renderRegionToImage( paperRect, imageSize, dpi ); } +///@cond PRIVATE +class LayoutItemCacheSettingRestorer +{ + public: + + LayoutItemCacheSettingRestorer( QgsLayout *layout ) + : mLayout( layout ) + { + mLayout->context().mIsPreviewRender = false; + + const QList< QGraphicsItem * > items = mLayout->items(); + for ( QGraphicsItem *item : items ) + { + mPrevCacheMode.insert( item, item->cacheMode() ); + item->setCacheMode( QGraphicsItem::NoCache ); + } + } + + ~LayoutItemCacheSettingRestorer() + { + for ( auto it = mPrevCacheMode.constBegin(); it != mPrevCacheMode.constEnd(); ++it ) + { + it.key()->setCacheMode( it.value() ); + } + + mLayout->context().mIsPreviewRender = true; + } + + private: + QgsLayout *mLayout = nullptr; + QHash< QGraphicsItem *, QGraphicsItem::CacheMode > mPrevCacheMode; +}; + +///@endcond PRIVATE + void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) const { QPaintDevice *paintDevice = painter->device(); @@ -84,7 +120,8 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) return; } - mLayout->context().mIsPreviewRender = false; + LayoutItemCacheSettingRestorer cacheRestorer( mLayout ); + ( void )cacheRestorer; #if 0 //TODO setSnapLinesVisible( false ); @@ -97,8 +134,6 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) #if 0 // TODO setSnapLinesVisible( true ); #endif - - mLayout->context().mIsPreviewRender = true; } QImage QgsLayoutExporter::renderRegionToImage( const QRectF ®ion, QSize imageSize, double dpi ) const @@ -163,8 +198,12 @@ class LayoutContextSettingsRestorer }; ///@endcond PRIVATE -QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ) +QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &s ) { + ImageExportSettings settings = s; + if ( settings.dpi <= 0 ) + settings.dpi = mLayout->context().dpi(); + mErrorFileName.clear(); int worldFilePageNo = -1; @@ -256,6 +295,173 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString return Success; } +QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &s ) +{ + PdfExportSettings settings = s; + if ( settings.dpi <= 0 ) + settings.dpi = mLayout->context().dpi(); + + mErrorFileName.clear(); + + LayoutContextSettingsRestorer contextRestorer( mLayout ); + ( void )contextRestorer; + mLayout->context().setDpi( settings.dpi ); + + // If we are not printing as raster, temporarily disable advanced effects + // as QPrinter does not support composition modes and can result + // in items missing from the output + mLayout->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, settings.rasteriseWholeImage ); + + QPrinter printer; + preparePrintAsPdf( printer, filePath ); + preparePrint( printer, false ); + QPainter p; + if ( !p.begin( &printer ) ) + { + //error beginning print + return PrintError; + } + + ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasteriseWholeImage ); + p.end(); + +#if 0//TODO + georeferenceOutput( filePath ); +#endif + + return result; +} + +void QgsLayoutExporter::preparePrintAsPdf( QPrinter &printer, const QString &filePath ) +{ + printer.setOutputFileName( filePath ); + // setOutputFormat should come after setOutputFileName, which auto-sets format to QPrinter::PdfFormat. + // [LS] This should be QPrinter::NativeFormat for Mac, otherwise fonts are not embed-able + // and text is not searchable; however, there are several bugs with <= Qt 4.8.5, 5.1.1, 5.2.0: + // https://bugreports.qt-project.org/browse/QTBUG-10094 - PDF font embedding fails + // https://bugreports.qt-project.org/browse/QTBUG-33583 - PDF output converts text to outline + // Also an issue with PDF paper size using QPrinter::NativeFormat on Mac (always outputs portrait letter-size) + printer.setOutputFormat( QPrinter::PdfFormat ); + +#if 0 //TODO + refreshPageSize(); +#endif + + //must set orientation to portrait before setting paper size, otherwise size will be flipped + //for landscape sized outputs (#11352) + printer.setOrientation( QPrinter::Portrait ); + +#if 0 //TODO + printer.setPaperSize( QSizeF( paperWidth(), paperHeight() ), QPrinter::Millimeter ); +#endif + + // TODO: add option for this in Composer + // May not work on Windows or non-X11 Linux. Works fine on Mac using QPrinter::NativeFormat + //printer.setFontEmbeddingEnabled( true ); + + QgsPaintEngineHack::fixEngineFlags( printer.paintEngine() ); +} + +void QgsLayoutExporter::preparePrint( QPrinter &printer, bool evaluateDDPageSize ) +{ + printer.setFullPage( true ); + printer.setColorMode( QPrinter::Color ); + + //set user-defined resolution + printer.setResolution( mLayout->context().dpi() ); + +#if 0 //TODO + if ( evaluateDDPageSize && ddPageSizeActive() ) + { + //set data defined page size + refreshPageSize(); + //must set orientation to portrait before setting paper size, otherwise size will be flipped + //for landscape sized outputs (#11352) + printer.setOrientation( QPrinter::Portrait ); + printer.setPaperSize( QSizeF( paperWidth(), paperHeight() ), QPrinter::Millimeter ); + } +#endif +} + +QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QPrinter &printer ) +{ + preparePrint( printer, true ); + QPainter p; + if ( !p.begin( &printer ) ) + { + //error beginning print + return PrintError; + } + + printPrivate( printer, p ); + p.end(); + return Success; +} + +QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage, double dpi, bool rasterise ) +{ +#if 0 //TODO + if ( ddPageSizeActive() ) + { + //set the page size again so that data defined page size takes effect + refreshPageSize(); + //must set orientation to portrait before setting paper size, otherwise size will be flipped + //for landscape sized outputs (#11352) + printer.setOrientation( QPrinter::Portrait ); + printer.setPaperSize( QSizeF( paperWidth(), paperHeight() ), QPrinter::Millimeter ); + } +#endif + + //layout starts page numbering at 0 + int fromPage = ( printer.fromPage() < 1 ) ? 0 : printer.fromPage() - 1; + int toPage = ( printer.toPage() < 1 ) ? mLayout->pageCollection()->pageCount() - 1 : printer.toPage() - 1; + + bool pageExported = false; + if ( rasterise ) + { + for ( int i = fromPage; i <= toPage; ++i ) + { + if ( !mLayout->pageCollection()->shouldExportPage( i ) ) + { + continue; + } + if ( ( pageExported && i > fromPage ) || startNewPage ) + { + printer.newPage(); + } + + QImage image = renderPageToImage( i, QSize(), dpi ); + if ( !image.isNull() ) + { + QRectF targetArea( 0, 0, image.width(), image.height() ); + painter.drawImage( targetArea, image, targetArea ); + } + else + { + return MemoryError; + } + pageExported = true; + } + } + else + { + for ( int i = fromPage; i <= toPage; ++i ) + { + if ( !mLayout->pageCollection()->shouldExportPage( i ) ) + { + continue; + } + if ( ( pageExported && i > fromPage ) || startNewPage ) + { + printer.newPage(); + } + renderPage( &painter, i ); + pageExported = true; + } + } + return Success; +} + double *QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF ®ion, double dpi ) const { if ( !map ) diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 8578808cb945..1fd673443223 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -22,6 +22,7 @@ #include #include #include +#include class QgsLayout; class QPainter; @@ -130,6 +131,7 @@ class CORE_EXPORT QgsLayoutExporter Success, //!< Export was successful MemoryError, //!< Unable to allocate memory required to export FileError, //!< Could not write to destination file, likely due to a lock held by another application + PrintError, //!< Could not start printing to destination device }; //! Contains settings relating to exporting layouts to raster images @@ -140,8 +142,8 @@ class CORE_EXPORT QgsLayoutExporter : flags( QgsLayoutContext::FlagAntialiasing | QgsLayoutContext::FlagUseAdvancedEffects ) {} - //! Resolution to export layout at - double dpi; + //! Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. + double dpi = -1; /** * Manual size in pixels for output image. If imageSize is not @@ -179,7 +181,7 @@ class CORE_EXPORT QgsLayoutExporter QList< int > pages; /** - * Set to true to generate an external world file alonside + * Set to true to generate an external world file alongside * exported images. */ bool generateWorldFile = false; @@ -201,7 +203,36 @@ class CORE_EXPORT QgsLayoutExporter * error was encountered. If an error code is returned, errorFile() can be called * to determine the filename for the export which encountered the error. */ - ExportResult exportToImage( const QString &filePath, const ImageExportSettings &settings ); + ExportResult exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ); + + //! Contains settings relating to exporting layouts to PDF + struct PdfExportSettings + { + //! Constructor for PdfExportSettings + PdfExportSettings() + : flags( QgsLayoutContext::FlagAntialiasing | QgsLayoutContext::FlagUseAdvancedEffects ) + {} + + //! Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. + double dpi = -1; + + //! Set to true to force whole layout to be rasterized while exporting + bool rasterizeWholeImage = false; + + /** + * Layout context flags, which control how the export will be created. + */ + QgsLayoutContext::Flags flags = 0; + + }; + + /** + * Exports the layout as a PDF to the a \a filePath, using the specified export \a settings. + * + * Returns a result code indicating whether the export was successful or an + * error was encountered. + */ + ExportResult exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &settings ); /** * Returns the file name corresponding to the last error encountered during @@ -282,6 +313,26 @@ class CORE_EXPORT QgsLayoutExporter //! Write a world file void writeWorldFile( const QString &fileName, double a, double b, double c, double d, double e, double f ) const; + /** + * Prepare a \a printer for printing a layout as a PDF, to the destination \a filePath. + */ + void preparePrintAsPdf( QPrinter &printer, const QString &filePath ); + + void preparePrint( QPrinter &printer, bool evaluateDDPageSize = false ); + + /** + * Convenience function that prepares the printer and prints. + */ + ExportResult print( QPrinter &printer ); + + /** + * Print on a preconfigured printer + * \param printer QPrinter destination + * \param painter QPainter source + * \param startNewPage set to true to begin the print on a new page + */ + ExportResult printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage = false, double dpi = -1, bool rasterize = false ); + friend class TestQgsLayout; diff --git a/src/ui/layout/qgslayoutwidgetbase.ui b/src/ui/layout/qgslayoutwidgetbase.ui index 697da03bb7a2..724b7803dace 100644 --- a/src/ui/layout/qgslayoutwidgetbase.ui +++ b/src/ui/layout/qgslayoutwidgetbase.ui @@ -55,7 +55,7 @@ 0 0 297 - 746 + 778 @@ -210,6 +210,29 @@ Export settings + + + + + 0 + 0 + + + + If checked, a separate world file which georeferences exported images will be created + + + Save world file + + + + + + + Export resolution + + + @@ -226,26 +249,10 @@ - - - - Export resolution - - - - - - - 0 - 0 - - - - If checked, a separate world file which georeferences exported images will be created - + - Save world file + Export as raster @@ -437,6 +444,7 @@ mGridOffsetUnitsComboBox mSnapToleranceSpinBox mResolutionSpinBox + mRasterizeCheckBox mGenerateWorldFileCheckBox mMarginUnitsComboBox mTopMarginSpinBox From b992e871ee1d51e78f4fe337ed3ec2cc288cd0af Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 12 Dec 2017 17:08:34 +1000 Subject: [PATCH 33/56] [layouts][FEATURE] Don't force the whole layout to be rasterized when exporting to PDF If an individual layout item needs rasterisation in order to be exported correctly, it can now be individually rasterised without forcing every other item to also be rasterised. This allows exports to PDF keeping as much as possible as vectors, e.g. a map with layer opacity won't force labels, scalebars, etc to be rasterised too. To accompany this, a new "Always export as vectors" checkbox was added to layout properties. If checked, this will force the export to keep items as vectors, even when it causes the output to look different to layouts. Fixes #7885 --- python/core/layout/qgslayoutcontext.sip | 1 + python/core/layout/qgslayoutexporter.sip | 13 ++++- python/core/layout/qgslayoutitem.sip | 10 ++++ python/core/layout/qgslayoutitempicture.sip | 2 + src/app/layout/qgslayoutdesignerdialog.cpp | 53 ++++++++++++++++--- src/app/layout/qgslayoutdesignerdialog.h | 5 +- src/app/layout/qgslayoutpropertieswidget.cpp | 29 +++++++++++ src/app/layout/qgslayoutpropertieswidget.h | 1 + src/core/layout/qgslayoutcontext.h | 1 + src/core/layout/qgslayoutexporter.cpp | 6 ++- src/core/layout/qgslayoutexporter.h | 15 +++++- src/core/layout/qgslayoutitem.cpp | 55 ++++++++++++++------ src/core/layout/qgslayoutitem.h | 9 ++++ src/core/layout/qgslayoutitemmap.cpp | 45 ++++++++++++---- src/core/layout/qgslayoutitempicture.cpp | 8 +++ src/core/layout/qgslayoutitempicture.h | 1 + src/ui/layout/qgslayoutwidgetbase.ui | 22 ++++++-- 17 files changed, 236 insertions(+), 40 deletions(-) diff --git a/python/core/layout/qgslayoutcontext.sip b/python/core/layout/qgslayoutcontext.sip index f82fa6afd2be..83e71d5a13f3 100644 --- a/python/core/layout/qgslayoutcontext.sip +++ b/python/core/layout/qgslayoutcontext.sip @@ -27,6 +27,7 @@ class QgsLayoutContext : QObject FlagOutlineOnly, FlagAntialiasing, FlagUseAdvancedEffects, + FlagForceVectorOutput, }; typedef QFlags Flags; diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index 376df0b6803f..c4bad6876913 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -212,7 +212,18 @@ Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. bool rasterizeWholeImage; %Docstring -Set to true to force whole layout to be rasterized while exporting + Set to true to force whole layout to be rasterized while exporting. + + This option is mutually exclusive with forceVectorOutput. +%End + + bool forceVectorOutput; +%Docstring + Set to true to force vector object exports, even when the resultant appearance will differ + from the layout. If false, some items may be rasterized in order to maintain their + correct appearance in the output. + + This option is mutually exclusive with rasterizeWholeImage. %End QgsLayoutContext::Flags flags; diff --git a/python/core/layout/qgslayoutitem.sip b/python/core/layout/qgslayoutitem.sip index bd69360c1d47..41048d0595ac 100644 --- a/python/core/layout/qgslayoutitem.sip +++ b/python/core/layout/qgslayoutitem.sip @@ -827,6 +827,16 @@ Sets whether the item should be excluded from composer exports and prints. Subclasses should ensure that implemented overrides of this method also check the base class result. + +.. seealso:: :py:func:`requiresRasterization()` + :rtype: bool +%End + + virtual bool requiresRasterization() const; +%Docstring + Returns true if the item is drawn in such a way that forces the whole layout + to be rasterised when exporting to vector formats. +.. seealso:: :py:func:`containsAdvancedEffects()` :rtype: bool %End diff --git a/python/core/layout/qgslayoutitempicture.sip b/python/core/layout/qgslayoutitempicture.sip index 6f3109c0088c..93824c58fe16 100644 --- a/python/core/layout/qgslayoutitempicture.sip +++ b/python/core/layout/qgslayoutitempicture.sip @@ -296,6 +296,8 @@ Forces a recalculation of the picture's frame size virtual void refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties ); + virtual bool containsAdvancedEffects() const; + signals: void pictureRotationChanged( double newRotation ); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 9a03b012629c..c6171b7a1bcc 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1552,9 +1552,14 @@ void QgsLayoutDesignerDialog::exportToPdf() showWmsPrintingWarning(); } - if ( containsAdvancedEffects() ) + if ( requiresRasterization() ) { - showAdvancedEffectsWarning(); + showRasterizationWarning(); + } + + if ( containsAdvancedEffects() && ( mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool() ) ) + { + showForceVectorWarning(); } QgsSettings settings; @@ -1602,7 +1607,8 @@ void QgsLayoutDesignerDialog::exportToPdf() QApplication::setOverrideCursor( Qt::BusyCursor ); QgsLayoutExporter::PdfExportSettings pdfSettings; - pdfSettings.rasteriseWholeImage = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); + pdfSettings.rasterizeWholeImage = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); + pdfSettings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); QgsLayoutExporter exporter( mLayout ); switch ( exporter.exportToPdf( outputFileName, pdfSettings ) ) @@ -1783,6 +1789,19 @@ void QgsLayoutDesignerDialog::showWmsPrintingWarning() } } +bool QgsLayoutDesignerDialog::requiresRasterization() const +{ + QList< QgsLayoutItem *> items; + mLayout->layoutItems( items ); + + for ( QgsLayoutItem *currentItem : qgis::as_const( items ) ) + { + if ( currentItem->requiresRasterization() ) + return true; + } + return false; +} + bool QgsLayoutDesignerDialog::containsAdvancedEffects() const { QList< QgsLayoutItem *> items; @@ -1796,10 +1815,11 @@ bool QgsLayoutDesignerDialog::containsAdvancedEffects() const return false; } -void QgsLayoutDesignerDialog::showAdvancedEffectsWarning() +void QgsLayoutDesignerDialog::showRasterizationWarning() { - bool rasterize = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); - if ( rasterise ) + + if ( mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool() || + mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool() ) return; QgsMessageViewer *m = new QgsMessageViewer( this, QgsGuiUtils::ModalDialogFlags, false ); @@ -1817,6 +1837,27 @@ void QgsLayoutDesignerDialog::showAdvancedEffectsWarning() delete m; } +void QgsLayoutDesignerDialog::showForceVectorWarning() +{ + QgsSettings settings; + if ( settings.value( QStringLiteral( "LayoutDesigner/hideForceVectorWarning" ), false, QgsSettings::App ).toBool() ) + return; + + QgsMessageViewer *m = new QgsMessageViewer( this, QgsGuiUtils::ModalDialogFlags, false ); + m->setWindowTitle( tr( "Force Vector" ) ); + m->setMessage( tr( "This layout has the \"Always export as vectors\" option enabled, but the layout contains effects such as blend modes or vector layer transparency, which cannot be printed as vectors. The generated file will differ from the layout contents." ), QgsMessageOutput::MessageText ); + m->setCheckBoxText( tr( "Never show this message again" ) ); + m->setCheckBoxState( Qt::Unchecked ); + m->setCheckBoxVisible( true ); + m->showMessage( true ); + + if ( m->checkBoxState() == Qt::Checked ) + { + settings.setValue( QStringLiteral( "LayoutDesigner/hideForceVectorWarning" ), true, QgsSettings::App ); + } + delete m; +} + void QgsLayoutDesignerDialog::selectItems( const QList items ) { for ( QGraphicsItem *item : items ) diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 576df1202b12..639ba748a5ea 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -371,10 +371,13 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void showWmsPrintingWarning(); //! True if the layout contains advanced effects, such as blend modes + bool requiresRasterization() const; + bool containsAdvancedEffects() const; //! Displays a warning because of incompatibility between blend modes and QPrinter - void showAdvancedEffectsWarning(); + void showRasterizationWarning(); + void showForceVectorWarning(); }; #endif // QGSLAYOUTDESIGNERDIALOG_H diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index 4b7ed81f7c26..54d14a9ca180 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -59,6 +59,7 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout connect( mGenerateWorldFileCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::worldFileToggled ); connect( mRasterizeCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::rasteriseToggled ); + connect( mForceVectorCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::forceVectorToggled ); mTopMarginSpinBox->setValue( topMargin ); mMarginUnitsComboBox->linkToWidget( mTopMarginSpinBox ); @@ -93,6 +94,19 @@ void QgsLayoutPropertiesWidget::updateGui() bool rasterise = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); whileBlocking( mRasterizeCheckBox )->setChecked( rasterise ); + + bool forceVectors = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); + whileBlocking( mForceVectorCheckBox )->setChecked( forceVectors ); + + if ( rasterise ) + { + mForceVectorCheckBox->setChecked( false ); + mForceVectorCheckBox->setEnabled( false ); + } + else + { + mForceVectorCheckBox->setEnabled( true ); + } } void QgsLayoutPropertiesWidget::updateSnappingElements() @@ -197,6 +211,21 @@ void QgsLayoutPropertiesWidget::worldFileToggled() void QgsLayoutPropertiesWidget::rasteriseToggled() { mLayout->setCustomProperty( QStringLiteral( "rasterise" ), mRasterizeCheckBox->isChecked() ); + + if ( mRasterizeCheckBox->isChecked() ) + { + mForceVectorCheckBox->setChecked( false ); + mForceVectorCheckBox->setEnabled( false ); + } + else + { + mForceVectorCheckBox->setEnabled( true ); + } +} + +void QgsLayoutPropertiesWidget::forceVectorToggled() +{ + mLayout->setCustomProperty( QStringLiteral( "forceVector" ), mForceVectorCheckBox->isChecked() ); } void QgsLayoutPropertiesWidget::blockSignals( bool block ) diff --git a/src/app/layout/qgslayoutpropertieswidget.h b/src/app/layout/qgslayoutpropertieswidget.h index 426f947e964b..e65bdabed0bd 100644 --- a/src/app/layout/qgslayoutpropertieswidget.h +++ b/src/app/layout/qgslayoutpropertieswidget.h @@ -47,6 +47,7 @@ class QgsLayoutPropertiesWidget: public QgsPanelWidget, private Ui::QgsLayoutWid void dpiChanged( int value ); void worldFileToggled(); void rasteriseToggled(); + void forceVectorToggled(); private: diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index 3ba948842651..6b9028302d36 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -45,6 +45,7 @@ class CORE_EXPORT QgsLayoutContext : public QObject FlagOutlineOnly = 1 << 2, //!< Render items as outlines only. FlagAntialiasing = 1 << 3, //!< Use antialiasing when drawing items. FlagUseAdvancedEffects = 1 << 4, //!< Enable advanced effects such as blend modes. + FlagForceVectorOutput = 1 << 5, //!< Force output in vector format where possible, even if items require rasterization to keep their correct appearance. }; Q_DECLARE_FLAGS( Flags, Flag ) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 66c9894b46de..7188494e7c92 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -310,7 +310,9 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f // If we are not printing as raster, temporarily disable advanced effects // as QPrinter does not support composition modes and can result // in items missing from the output - mLayout->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, settings.rasteriseWholeImage ); + mLayout->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, !settings.forceVectorOutput ); + + mLayout->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput ); QPrinter printer; preparePrintAsPdf( printer, filePath ); @@ -322,7 +324,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f return PrintError; } - ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasteriseWholeImage ); + ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasterizeWholeImage ); p.end(); #if 0//TODO diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 1fd673443223..0aae845a8da3 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -216,9 +216,22 @@ class CORE_EXPORT QgsLayoutExporter //! Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. double dpi = -1; - //! Set to true to force whole layout to be rasterized while exporting + /** + * Set to true to force whole layout to be rasterized while exporting. + * + * This option is mutually exclusive with forceVectorOutput. + */ bool rasterizeWholeImage = false; + /** + * Set to true to force vector object exports, even when the resultant appearance will differ + * from the layout. If false, some items may be rasterized in order to maintain their + * correct appearance in the output. + * + * This option is mutually exclusive with rasterizeWholeImage. + */ + bool forceVectorOutput = false; + /** * Layout context flags, which control how the export will be created. */ diff --git a/src/core/layout/qgslayoutitem.cpp b/src/core/layout/qgslayoutitem.cpp index f3ffd38d0fa2..bb141e0d6cff 100644 --- a/src/core/layout/qgslayoutitem.cpp +++ b/src/core/layout/qgslayoutitem.cpp @@ -242,18 +242,32 @@ void QgsLayoutItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *it return; } - double destinationDpi = itemStyle->matrix.m11() * 25.4; + bool previewRender = !mLayout || mLayout->context().isPreviewRender(); + double destinationDpi = previewRender ? itemStyle->matrix.m11() * 25.4 : mLayout->context().dpi(); bool useImageCache = false; + bool forceRasterOutput = containsAdvancedEffects() && ( !mLayout || !( mLayout->context().flags() & QgsLayoutContext::FlagForceVectorOutput ) ); - if ( useImageCache ) + if ( useImageCache || forceRasterOutput ) { - double widthInPixels = boundingRect().width() * itemStyle->matrix.m11(); - double heightInPixels = boundingRect().height() * itemStyle->matrix.m11(); + double widthInPixels = 0; + double heightInPixels = 0; + + if ( previewRender ) + { + widthInPixels = boundingRect().width() * itemStyle->matrix.m11(); + heightInPixels = boundingRect().height() * itemStyle->matrix.m11(); + } + else + { + double layoutUnitsToPixels = mLayout ? mLayout->convertFromLayoutUnits( 1, QgsUnitTypes::LayoutPixels ).length() : destinationDpi / 25.4; + widthInPixels = boundingRect().width() * layoutUnitsToPixels; + heightInPixels = boundingRect().height() * layoutUnitsToPixels; + } // limit size of image for better performance - double scale = 1.0; - if ( widthInPixels > CACHE_SIZE_LIMIT || heightInPixels > CACHE_SIZE_LIMIT ) + if ( previewRender && ( widthInPixels > CACHE_SIZE_LIMIT || heightInPixels > CACHE_SIZE_LIMIT ) ) { + double scale = 1.0; if ( widthInPixels > heightInPixels ) { scale = widthInPixels / CACHE_SIZE_LIMIT; @@ -269,7 +283,7 @@ void QgsLayoutItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *it destinationDpi = destinationDpi / scale; } - if ( !mItemCachedImage.isNull() && qgsDoubleNear( mItemCacheDpi, destinationDpi ) ) + if ( previewRender && !mItemCachedImage.isNull() && qgsDoubleNear( mItemCacheDpi, destinationDpi ) ) { // can reuse last cached image QgsRenderContext context = QgsLayoutUtils::createRenderContextForMap( nullptr, painter, destinationDpi ); @@ -284,13 +298,11 @@ void QgsLayoutItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *it } else { - mItemCacheDpi = destinationDpi; - - mItemCachedImage = QImage( widthInPixels, heightInPixels, QImage::Format_ARGB32 ); - mItemCachedImage.fill( Qt::transparent ); - mItemCachedImage.setDotsPerMeterX( 1000 * destinationDpi * 25.4 ); - mItemCachedImage.setDotsPerMeterY( 1000 * destinationDpi * 25.4 ); - QPainter p( &mItemCachedImage ); + QImage image = QImage( widthInPixels, heightInPixels, QImage::Format_ARGB32 ); + image.fill( Qt::transparent ); + image.setDotsPerMeterX( 1000 * destinationDpi * 25.4 ); + image.setDotsPerMeterY( 1000 * destinationDpi * 25.4 ); + QPainter p( &image ); preparePainter( &p ); QgsRenderContext context = QgsLayoutUtils::createRenderContextForLayout( nullptr, &p, destinationDpi ); @@ -306,8 +318,14 @@ void QgsLayoutItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *it // scale painter from mm to dots painter->scale( 1.0 / context.scaleFactor(), 1.0 / context.scaleFactor() ); painter->drawImage( boundingRect().x() * context.scaleFactor(), - boundingRect().y() * context.scaleFactor(), mItemCachedImage ); + boundingRect().y() * context.scaleFactor(), image ); painter->restore(); + + if ( previewRender ) + { + mItemCacheDpi = destinationDpi; + mItemCachedImage = image; + } } } else @@ -842,7 +860,12 @@ void QgsLayoutItem::setExcludeFromExports( bool exclude ) bool QgsLayoutItem::containsAdvancedEffects() const { - return blendMode() != QPainter::CompositionMode_SourceOver; + return false; +} + +bool QgsLayoutItem::requiresRasterization() const +{ + return itemOpacity() < 1.0 || blendMode() != QPainter::CompositionMode_SourceOver; } double QgsLayoutItem::estimatedFrameBleed() const diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index 56167ec5805c..8c75460d8ef5 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -748,9 +748,18 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt * * Subclasses should ensure that implemented overrides of this method * also check the base class result. + * + * \see requiresRasterization() */ virtual bool containsAdvancedEffects() const; + /** + * Returns true if the item is drawn in such a way that forces the whole layout + * to be rasterised when exporting to vector formats. + * \see containsAdvancedEffects() + */ + virtual bool requiresRasterization() const; + /** * Returns the estimated amount the item's frame bleeds outside the item's * actual rectangle. For instance, if the item has a 2mm frame stroke, then diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 38cc7adc44c2..1caeaf3e6e89 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -798,19 +798,46 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem } QgsRectangle cExtent = extent(); - QSizeF size( cExtent.width() * mapUnitsToLayoutUnits(), cExtent.height() * mapUnitsToLayoutUnits() ); - painter->save(); - painter->translate( mXOffset, mYOffset ); + if ( containsAdvancedEffects() && ( !mLayout || !( mLayout->context().flags() & QgsLayoutContext::FlagForceVectorOutput ) ) ) + { + // rasterise + double destinationDpi = mLayout ? mLayout->context().dpi() : style->matrix.m11() * 25.4; + + double layoutUnitsToPixels = mLayout ? mLayout->convertFromLayoutUnits( 1, QgsUnitTypes::LayoutPixels ).length() : destinationDpi / 25.4; + double widthInPixels = boundingRect().width() * layoutUnitsToPixels; + double heightInPixels = boundingRect().height() * layoutUnitsToPixels; + QImage image = QImage( widthInPixels, heightInPixels, QImage::Format_ARGB32 ); + + image.fill( Qt::transparent ); + image.setDotsPerMeterX( 1000 * destinationDpi / 25.4 ); + image.setDotsPerMeterY( 1000 * destinationDpi / 25.4 ); + QPainter p( &image ); + double dotsPerMM = image.logicalDpiX() / 25.4; + drawMap( &p, cExtent, image.size(), destinationDpi ); + p.end(); + + dotsPerMM = paintDevice->logicalDpiX() / 25.4; + painter->save(); + painter->scale( 1 / dotsPerMM, 1 / dotsPerMM ); // scale painter from mm to dots + painter->drawImage( 0, 0, image ); + painter->restore(); + + } + else + { + painter->save(); + painter->translate( mXOffset, mYOffset ); + + double dotsPerMM = paintDevice->logicalDpiX() / 25.4; + size *= dotsPerMM; // output size will be in dots (pixels) + painter->scale( 1 / dotsPerMM, 1 / dotsPerMM ); // scale painter from mm to dots + drawMap( painter, cExtent, size, paintDevice->logicalDpiX() ); - double dotsPerMM = paintDevice->logicalDpiX() / 25.4; - size *= dotsPerMM; // output size will be in dots (pixels) - painter->scale( 1 / dotsPerMM, 1 / dotsPerMM ); // scale painter from mm to dots - drawMap( painter, cExtent, size, paintDevice->logicalDpiX() ); + painter->restore(); + } - //restore rotation - painter->restore(); mDrawing = false; } diff --git a/src/core/layout/qgslayoutitempicture.cpp b/src/core/layout/qgslayoutitempicture.cpp index 6715cb64d1cd..3ee42d684d5c 100644 --- a/src/core/layout/qgslayoutitempicture.cpp +++ b/src/core/layout/qgslayoutitempicture.cpp @@ -666,6 +666,14 @@ void QgsLayoutItemPicture::refreshDataDefinedProperty( const QgsLayoutObject::Da QgsLayoutItem::refreshDataDefinedProperty( property ); } +bool QgsLayoutItemPicture::containsAdvancedEffects() const +{ + if ( QgsLayoutItem::containsAdvancedEffects() ) + return true; + + return mMode == FormatSVG && itemOpacity() < 1.0; +} + void QgsLayoutItemPicture::setPicturePath( const QString &path ) { mSourcePath = path; diff --git a/src/core/layout/qgslayoutitempicture.h b/src/core/layout/qgslayoutitempicture.h index 179044fbfbc9..22053f905f7f 100644 --- a/src/core/layout/qgslayoutitempicture.h +++ b/src/core/layout/qgslayoutitempicture.h @@ -265,6 +265,7 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem void recalculateSize(); void refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties ) override; + bool containsAdvancedEffects() const override; signals: //! Is emitted on picture rotation change diff --git a/src/ui/layout/qgslayoutwidgetbase.ui b/src/ui/layout/qgslayoutwidgetbase.ui index 724b7803dace..0bb37df5afc0 100644 --- a/src/ui/layout/qgslayoutwidgetbase.ui +++ b/src/ui/layout/qgslayoutwidgetbase.ui @@ -53,9 +53,9 @@ 0 - 0 + -316 297 - 778 + 810 @@ -210,7 +210,7 @@ Export settings - + @@ -251,8 +251,21 @@ + + If checked, exports from this layout will be rasterized. + + + Print as raster + + + + + + + If checked, the layout will always be kept as vector objects when exported to a compatible format, even if the appearance of the resultant file does not match the layouts settings. If unchecked, some elements in the layout may be rasterised in order to keep their appearance intact. + - Export as raster + Always export as vectors @@ -445,6 +458,7 @@ mSnapToleranceSpinBox mResolutionSpinBox mRasterizeCheckBox + mForceVectorCheckBox mGenerateWorldFileCheckBox mMarginUnitsComboBox mTopMarginSpinBox From 261492ddca97e5c9ba93d8a53b29036cb194b18a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 12 Dec 2017 19:08:44 +1000 Subject: [PATCH 34/56] Add a messagebar to layout designer --- .../gui/layout/qgslayoutdesignerinterface.sip | 6 +++++ src/app/layout/qgslayoutdesignerdialog.cpp | 22 ++++++++++++++++++- src/app/layout/qgslayoutdesignerdialog.h | 9 ++++++++ src/gui/layout/qgslayoutdesignerinterface.h | 6 +++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/python/gui/layout/qgslayoutdesignerinterface.sip b/python/gui/layout/qgslayoutdesignerinterface.sip index 3084ef064820..1e4db3a83c39 100644 --- a/python/gui/layout/qgslayoutdesignerinterface.sip +++ b/python/gui/layout/qgslayoutdesignerinterface.sip @@ -46,6 +46,12 @@ Returns the layout view utilized by the designer. .. seealso:: :py:func:`layout()` %End + virtual QgsMessageBar *messageBar() = 0; +%Docstring + Returns the designer's message bar. + :rtype: QgsMessageBar +%End + virtual void selectItems( const QList< QgsLayoutItem * > items ) = 0; %Docstring Selects the specified ``items``. diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index c6171b7a1bcc..5708d1e2cc60 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -93,6 +93,11 @@ QgsLayoutView *QgsAppLayoutDesignerInterface::view() return mDesigner->view(); } +QgsMessageBar *QgsAppLayoutDesignerInterface::messageBar() +{ + return mDesigner->messageBar(); +} + void QgsAppLayoutDesignerInterface::selectItems( const QList items ) { mDesigner->selectItems( items ); @@ -136,6 +141,10 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla centralWidget()->layout()->setMargin( 0 ); centralWidget()->layout()->setContentsMargins( 0, 0, 0, 0 ); + mMessageBar = new QgsMessageBar( centralWidget() ); + mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed ); + static_cast< QGridLayout * >( centralWidget()->layout() )->addWidget( mMessageBar, 0, 0, 1, 1, Qt::AlignTop ); + mHorizontalRuler = new QgsLayoutRuler( nullptr, Qt::Horizontal ); mVerticalRuler = new QgsLayoutRuler( nullptr, Qt::Vertical ); mRulerLayoutFix = new QWidget(); @@ -1520,6 +1529,9 @@ void QgsLayoutDesignerDialog::exportToRaster() switch ( exporter.exportToImage( fileNExt.first, settings ) ) { case QgsLayoutExporter::Success: + mMessageBar->pushInfo( tr( "Export layout" ), tr( "Successfully exported layout to %1" ).arg( fileNExt.first ) ); + break; + case QgsLayoutExporter::PrintError: break; @@ -1614,11 +1626,14 @@ void QgsLayoutDesignerDialog::exportToPdf() switch ( exporter.exportToPdf( outputFileName, pdfSettings ) ) { case QgsLayoutExporter::Success: + { + mMessageBar->pushInfo( tr( "Export layout" ), tr( "Successfully exported layout to %1" ).arg( outputFileName ) ); break; + } case QgsLayoutExporter::FileError: QMessageBox::warning( this, tr( "Export to PDF" ), - tr( "Cannot write to %1.\n\nThis file may be open in another application." ).arg( exporter.errorFile() ), + tr( "Cannot write to %1.\n\nThis file may be open in another application." ).arg( outputFileName ), QMessageBox::Ok, QMessageBox::Ok ); break; @@ -1880,4 +1895,9 @@ void QgsLayoutDesignerDialog::selectItems( const QList items ) } } +QgsMessageBar *QgsLayoutDesignerDialog::messageBar() +{ + return mMessageBar; +} + diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 639ba748a5ea..92b835b976a8 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -42,6 +42,7 @@ class QUndoView; class QTreeView; class QgsLayoutItemsListView; class QgsLayoutPropertiesWidget; +class QgsMessageBar; class QgsAppLayoutDesignerInterface : public QgsLayoutDesignerInterface { @@ -51,6 +52,7 @@ class QgsAppLayoutDesignerInterface : public QgsLayoutDesignerInterface QgsAppLayoutDesignerInterface( QgsLayoutDesignerDialog *dialog ); QgsLayout *layout() override; QgsLayoutView *view() override; + QgsMessageBar *messageBar() override; void selectItems( const QList< QgsLayoutItem * > items ) override; public slots: @@ -114,6 +116,11 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner */ void selectItems( const QList< QgsLayoutItem * > items ); + /** + * Returns the designer's message bar. + */ + QgsMessageBar *messageBar(); + public slots: /** @@ -286,6 +293,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner QgsLayout *mLayout = nullptr; + QgsMessageBar *mMessageBar = nullptr; + QActionGroup *mToolsActionGroup = nullptr; QgsLayoutView *mView = nullptr; diff --git a/src/gui/layout/qgslayoutdesignerinterface.h b/src/gui/layout/qgslayoutdesignerinterface.h index 2a2cf283f8f3..1946f3139087 100644 --- a/src/gui/layout/qgslayoutdesignerinterface.h +++ b/src/gui/layout/qgslayoutdesignerinterface.h @@ -23,6 +23,7 @@ class QgsLayout; class QgsLayoutView; class QgsLayoutItem; +class QgsMessageBar; /** * \ingroup gui @@ -61,6 +62,11 @@ class GUI_EXPORT QgsLayoutDesignerInterface: public QObject */ virtual QgsLayoutView *view() = 0; + /** + * Returns the designer's message bar. + */ + virtual QgsMessageBar *messageBar() = 0; + /** * Selects the specified \a items. */ From d3aee951ef13ae92de00f6598ad334e2a0dbb14e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 13 Dec 2017 07:44:50 +1000 Subject: [PATCH 35/56] Tweak logic regarding when a map item forces the whole layout to be rasterised --- src/core/layout/qgslayoutitemmap.cpp | 20 +++++++++++ src/core/layout/qgslayoutitemmap.h | 1 + tests/src/python/test_qgslayoutitem.py | 6 ++-- tests/src/python/test_qgslayoutmap.py | 49 ++++++++++++++++++++++++-- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 1caeaf3e6e89..26830b00e39e 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -377,6 +377,26 @@ bool QgsLayoutItemMap::containsWmsLayer() const return false; } +bool QgsLayoutItemMap::requiresRasterization() const +{ + if ( QgsLayoutItem::requiresRasterization() ) + return true; + + // we MUST force the whole layout to render as a raster if any map item + // uses blend modes, and we are not drawing on a solid opaque background + // because in this case the map item needs to be rendered as a raster, but + // it also needs to interact with items below it + if ( !containsAdvancedEffects() ) + return false; + + // TODO layer transparency is probably ok to allow without forcing rasterization + + if ( hasBackground() && qgsDoubleNear( backgroundColor().alphaF(), 1.0 ) ) + return false; + + return true; +} + bool QgsLayoutItemMap::containsAdvancedEffects() const { if ( QgsLayoutItem::containsAdvancedEffects() ) diff --git a/src/core/layout/qgslayoutitemmap.h b/src/core/layout/qgslayoutitemmap.h index f920f6857199..b396e8ff4ba5 100644 --- a/src/core/layout/qgslayoutitemmap.h +++ b/src/core/layout/qgslayoutitemmap.h @@ -275,6 +275,7 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem //! Returns true if the map contains a WMS layer. bool containsWmsLayer() const; + bool requiresRasterization() const override; bool containsAdvancedEffects() const override; /** diff --git a/tests/src/python/test_qgslayoutitem.py b/tests/src/python/test_qgslayoutitem.py index ef12e74bbb12..84469d96cd75 100644 --- a/tests/src/python/test_qgslayoutitem.py +++ b/tests/src/python/test_qgslayoutitem.py @@ -46,12 +46,12 @@ def make_item(self, layout): else: return self.createItem(layout) - def testContainsAdvancedEffects(self): + def testRequiresRasterization(self): l = QgsLayout(QgsProject.instance()) item = self.make_item(l) - self.assertFalse(item.containsAdvancedEffects()) + self.assertFalse(item.requiresRasterization()) item.setBlendMode(QPainter.CompositionMode_SourceIn) - self.assertTrue(item.containsAdvancedEffects()) + self.assertTrue(item.requiresRasterization()) class TestQgsLayoutItem(unittest.TestCase): diff --git a/tests/src/python/test_qgslayoutmap.py b/tests/src/python/test_qgslayoutmap.py index badf0855ec29..8dc528c59b3d 100644 --- a/tests/src/python/test_qgslayoutmap.py +++ b/tests/src/python/test_qgslayoutmap.py @@ -16,9 +16,9 @@ import os -from qgis.PyQt.QtCore import QFileInfo, QRectF +from qgis.PyQt.QtCore import QFileInfo, QRectF, QDir from qgis.PyQt.QtXml import QDomDocument -from qgis.PyQt.QtGui import QPainter +from qgis.PyQt.QtGui import QPainter, QColor from qgis.core import (QgsLayoutItemMap, QgsRectangle, @@ -46,6 +46,14 @@ class TestQgsComposerMap(unittest.TestCase, LayoutItemTestCase): def setUpClass(cls): cls.item_class = QgsLayoutItemMap + def setUp(self): + self.report = "

Python QgsLayoutItemMap Tests

\n" + + def tearDown(self): + report_file_path = "%s/qgistest.html" % QDir.tempPath() + with open(report_file_path, 'a') as report_file: + report_file.write(self.report) + def __init__(self, methodName): """Run once on class initialization.""" unittest.TestCase.__init__(self, methodName) @@ -92,6 +100,7 @@ def testOverviewMap(self): checker.setColorTolerance(6) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testLayout() + self.report += checker.report() self.layout.removeLayoutItem(overviewMap) assert myTestResult, myMessage @@ -111,6 +120,7 @@ def testOverviewMapBlend(self): checker = QgsLayoutChecker('composermap_overview_blending', self.layout) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testLayout() + self.report += checker.report() self.layout.removeLayoutItem(overviewMap) assert myTestResult, myMessage @@ -130,6 +140,7 @@ def testOverviewMapInvert(self): checker = QgsLayoutChecker('composermap_overview_invert', self.layout) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testLayout() + self.report += checker.report() self.layout.removeLayoutItem(overviewMap) assert myTestResult, myMessage @@ -150,6 +161,7 @@ def testOverviewMapCenter(self): checker = QgsLayoutChecker('composermap_overview_center', self.layout) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testLayout() + self.report += checker.report() self.layout.removeLayoutItem(overviewMap) assert myTestResult, myMessage @@ -181,6 +193,7 @@ def testMapCrs(self): checker = QgsLayoutChecker('composermap_crs3857', layout) checker.setControlPathPrefix("composer_map") result, message = checker.testLayout() + self.report += checker.report() self.assertTrue(result, message) # overwrite CRS @@ -192,6 +205,7 @@ def testMapCrs(self): checker = QgsLayoutChecker('composermap_crs4326', layout) checker.setControlPathPrefix("composer_map") result, message = checker.testLayout() + self.report += checker.report() self.assertTrue(result, message) # change back to project CRS @@ -222,6 +236,37 @@ def testuniqueId(self): myMessage = 'old: %s new: %s' % (oldId, newId) assert oldId != newId, myMessage + def testContainsAdvancedEffects(self): + map_settings = QgsMapSettings() + map_settings.setLayers([self.vector_layer]) + layout = QgsLayout(QgsProject.instance()) + map = QgsLayoutItemMap(layout) + + self.assertFalse(map.containsAdvancedEffects()) + self.vector_layer.setBlendMode(QPainter.CompositionMode_Darken) + result = map.containsAdvancedEffects() + self.vector_layer.setBlendMode(QPainter.CompositionMode_SourceOver) + self.assertTrue(result) + + def testRasterization(self): + map_settings = QgsMapSettings() + map_settings.setLayers([self.vector_layer]) + layout = QgsLayout(QgsProject.instance()) + map = QgsLayoutItemMap(layout) + + self.assertFalse(map.requiresRasterization()) + self.vector_layer.setBlendMode(QPainter.CompositionMode_Darken) + self.assertFalse(map.requiresRasterization()) + self.assertTrue(map.containsAdvancedEffects()) + + map.setBackgroundEnabled(False) + self.assertTrue(map.requiresRasterization()) + map.setBackgroundEnabled(True) + map.setBackgroundColor(QColor(1, 1, 1, 1)) + self.assertTrue(map.requiresRasterization()) + + self.vector_layer.setBlendMode(QPainter.CompositionMode_SourceOver) + def testWorldFileGeneration(self): return myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) From 2b0ed508bddffcccf5d553cd854988cc52018749 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 13 Dec 2017 07:47:23 +1000 Subject: [PATCH 36/56] Tweak logic regarding page item margins following forced re-render of layout items when exporting (i.e. disabling of cached item render) The old issue of semi-transparent pixels around the edge of the page had reared again. This is caused by the antialiasing while rendering the page symbol. In order to avoid this, we cater to the most common use case of having pages with a solid, borderless fill and slightly extend the fill symbol polygon outside the page by 2 pixels (determined by trial-and-error). The less common use case of having a page symbol containing a border suffers by this border being clipped by a couple of pixels, but we must address the much more common use case over this. --- src/core/layout/qgslayoutitempage.cpp | 5 +++-- .../expected_composerpaper_markerborder.png | Bin 32952 -> 33789 bytes 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/layout/qgslayoutitempage.cpp b/src/core/layout/qgslayoutitempage.cpp index c47b7f48833d..b1f14f59a8fb 100644 --- a/src/core/layout/qgslayoutitempage.cpp +++ b/src/core/layout/qgslayoutitempage.cpp @@ -216,10 +216,11 @@ void QgsLayoutItemPage::draw( QgsRenderContext &context, const QStyleOptionGraph //Now subtract 1 pixel to prevent semi-transparent borders at edge of solid page caused by //anti-aliased painting. This may cause a pixel to be cropped from certain edge lines/symbols, //but that can be counteracted by adding a dummy transparent line symbol layer with a wider line width - maxBleedPixels--; + maxBleedPixels = std::floor( maxBleedPixels - 2 ); + // round up QPolygonF pagePolygon = QPolygonF( QRectF( maxBleedPixels, maxBleedPixels, - ( rect().width() * scale - 2 * maxBleedPixels ), ( rect().height() * scale - 2 * maxBleedPixels ) ) ); + std::ceil( rect().width() * scale ) - 2 * maxBleedPixels, std::ceil( rect().height() * scale ) - 2 * maxBleedPixels ) ); QList rings; //empty list symbol->renderPolygon( pagePolygon, &rings, nullptr, context ); diff --git a/tests/testdata/control_images/composer_paper/expected_composerpaper_markerborder/layout/expected_composerpaper_markerborder.png b/tests/testdata/control_images/composer_paper/expected_composerpaper_markerborder/layout/expected_composerpaper_markerborder.png index 90c3c8da75ab18480b6060de0c8b66912ba2cb47..9316afcfbc38099529587f46a1b06e9267dca45a 100644 GIT binary patch literal 33789 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%0D4cgBG5XeZ91dGEZpUc06__TC z?4zNtOM;smT=dmZ2zVzu6 zPbKeMyFhe9oI$Y~kMjfYwkrb66hC)godN${lMS{Y0$B^b$yJ}F)U+BXVuP-g1f|NUW~~cT;k^l zMMXsJkn>g;EVQD;#AMeexVUL(7^#H0=WGt=1Sog;$WhhyC?cXvLqk(d2Jnn*DjJ&1 zna{tit*ynz#v&&GD|^}FWr&}prB6pYI~_jQrY!9D-yhp8NO*tw6-#yh^w@t-=?(y< zqI%$$$^6tS$M&J4({EhcA5-If{m0A9qdBOXa2FC5 z+N#q3a&S>jHMkwDcwdRGs5sWt5Q#v?aiJzEi}Ld=q)^l0G&0w2-bBq1f*U58fg5(X zmGslT1m){}4=y?;@Q_K%1s_3()T)wq+F)%2`};$|U^DQeUKxOST00;5{q9)jo3CEy zI$dVk@DZRciTrD-m%$bq#5|50-gZpyDy>41K+AXIi z09y{S_nIL?6A}`W1WT&9`{r}}gZR0fKZUvNZ+zS*4@ndhoZW7T3u=zx<|ZM!2A+|- z>e0c}Xw-F_pE~!cal18Dl*Pz+y+vzGKWM7nYKWf63>rGiZ_UlYQqu7CX}8kSQtG{* zujKhQty`L!_N8)ym+4Ks1=FV9QMNA~vl#xqbzbUPT3Q;4ism(LR}ArZ9K*EBCESJh z8coVVh}ql+A0Z5BU09jv{ybK1NFAUitIakiQ>_%yPX#prxlhadPPrOJ{6(QBIrFk&Sdm9urf$zI4~Gtyf>U$*SB#in>2n8-X z_4P&solz*?toAjfuhX_J&#Q`2yhgM*49B91F) z^DFq1yUnB|(a<2~+Ei=3FRCrcDDI=DWR4z1-sC$<#}-#k$MQID@nepOVKq#*X*an& zwbpr{v7t@^=-_1pdCnA^iAicHT{0$|Mp;$$0EN&kx+%~HwmDbxDJ(Zv??DiNWf8ys zo@~bfe%Q;cjYWqJtPWN{NbwROk%yL$NW;FAY>Vhi6Q8o{=z8)5SN!U?%MRd0-iU3+ zvKs2Wc;O<{p;W4l>j(^tY-;i<$e}YglbWouHPVPRx&%RO%^lG;_q{%KTU*2vk`{5% zpx}_CD%bLohLkUN4+z?9;dRv1a!gu>r+GzYKai}^fK}!r?}$<97;=AdQY*ae&Z+a* zi|F+euVcl-F0K=1-idHq$-f-dFcSBloOMGJ{pYrwc@8DP4y}BB>lL?GEn$J0<`V)3 zQ|MqDZk#w45q;P2vGbk9;O+UyEQgxqS{*OGMgysi&cROt;CU|x=1d2y#;bQjWMsA} zll&Iqc{1F?BG`}iRwJaN+xL!lK7H!+AurimO>;;9Pe}eiD96|5cCw#!A0M%(7Ha1s zOw4Pk;oyLt9Uu4g^)>bL^S^a!>vgvkPt8ecFQg3V9$jRqpWXo(mk)K zzX!d6%z82kk&fi6FW6kShs*hI6ldN?>%J4xle(RtaGw#MR@o5?1CXQU=yVI!>KQ4KseUWrdK@ z8hoiNFCUYnJ7=d|UvH!5p`D;$UMX1TygeLtE2QsGNLF?~kT{kI8jm%)la;ieKqL?$ za<23D<*RfpKVxTF%+X(F@BPwG?%oeQ3G6noXR{KzOCKOz33BrDCrb>a<>f0c^%4Y+oN4e+oi_hnv7WTgtdPRE58RlFK^AK$BkBU zzt9oJn6uJdz$eo{3@-(c#&_@CAY~O10BFM{dgTlp7zA; z)Vao=>_a}$E57f}EYsY%Dg?YDxcqz$0acsX+I(Favov?pv^m4aLosNt@ zH1=I|AS>%CDBwwu4Tnso(CsYQgxs7Qz)uiMd*U2dCYL2m#wrqC0?vex2WOn+QkCtZ zuKN>8`rOlg>h+br_K>e%uW1z~Qc~{xCR>@RT6E&Ib33R50#oHtBZD!~t?9H-C6*lkaCM}hyQMGlX zsykGn0*=^ApEiTo$4-S^Mx_(T>gsBma7l_ms)5D!i89IX@0*u_D1e%ox!cJ=c#AWq zs8wcm)~Zr7;465xkegI5dYRHsC9lt9@Q3L~d2%TsLp;P|erQ(=~g1vDuz6 z3bFJ-KnGgVHJNf;*5vA1@qd*m6wGMl5|y?1R_2=Y{p~2}%?1j!ha>J3tV2EH*g({| z!3-DshgCoTYrJuz{kSXc06C!wsEZj+D9Lb|_dY)6n$+N9WiR#4HhP&MsE$60g3Nh+ z#+aw=?{Hk?HaPPWS@?{_$QXA!%Va)l zYrAmy`pbr=V4L1=MM)m}DXRkq6G&`SIc0k@hdB}V{l>}7a7s8mgB-%c$=xjXhtpy9 z|Ixjq5YTFT4w**|s?F-5>Ss=mlCxUq zDR(B&^iqii6)AUS1zf_w~R?RuAM@#}7(s%C{ zm&LE|5eo|o$jKbNNN_ZgGYM8#srm;pF!yU`sO~F>VC`03YHG$EM?zC-oP1H6%b%Sm z;j;I$GZce3SY35BH!CZvbDR>y!9>qcOg78jSZ8|VF7tIfJh*iaK&i(xr zle$)^JPw;P3SDH}=}h+u0OljBO4i3uY+t=j?J^>z03FO9D?+Jq)%&KMFbRtnSKI62 zY^@t$UtzM*H!m(GCSC!_iKlB zLxNs;z2j2n@B2|FL1LIMT!>CykD7j~B3N}v$o(<6ZCVruWD)!+DJidokZH2oEjUq8 zWucLA_ix{h3Qf6h`_$c*BGiREDot5+_>y5 zklpgFqN35zF!s(B@DVg5B_!@~+ogYb^$U&w-_t|XmEON)zAyDL^n8ikZi@~vt);80 zD9;@~RLMUb>*kCrg4n9sGIoU`IZ|H7rt$LQjLX8W_*z~cSz4{kf1iHyW7zIeXj9V> z8ci`W#9!sI`!W4thU3nHdQ+1(vkqV8BihtdPUN_ujAUPpEO_6Sjo2x;xT*@Xr6pQB zn2>w2H#r|s>jEf@hK7cUt_O+irV7r^RcG=HKAFI27O?n2)I|tF%fN#@C>I>FbCy+8 zo6g}mx+lV6r*GwIAONuXYQpZ?*eLg|Y+cl#Yi(#ZQ|6>8o*VE4&v-z4Z?QfFu(&ML zvj*teFeg1ZMM>lRb_xsVuU|6>8EATviq1rt-&D*)Ha7N7PeZ%9{K(~$EjHS|ct=i_ z8n=`kIPaYvT@4YOwqWn>Z0q~<66%=@O4Ev%(|p*NkkE%LSe%H-^$D-0;8HvJcPeHo zPYO0Oq2lCpRMHsNKA4V6NMNGmv6cuS;CC}()G}cw$q48Fo4ld)Ug@e?0SIYF`x4;U zy<`#p0YHmriDGPrpfr#s(%zAO$#Q z(ri^(Ic{;``8b+n5SpL_AtfcH$Sz0DbZr(&V4W^5E)ES1=~%C9&wwZN8uGJRPdEGe zevYCc$Z&4KHx_+Av`CM3#{bwgZ`>1Hp;q?8zc1Tsx|@_+sH3j-QLmB~kjFYXVj|A_ z;1^h%@1jj?cm}vETGc-N%EO~Ep#i}TSx>`mcdT-r=XNuxQ9`#qyHN;55#BKyyurUV z9til@XQ4{i1phYBfQ|&1lXTzH#om;PeFO`19jCPa3-A?2K!#b)%mi3iiExoFugy#% z*K}{y3vdWtzVBG1cVCQT3JOvou&w@Pzn4-bCvMy-zYBoA1|JhkoFp4S+G z_yhgXLfrx9;l2cN+R{{>l~$%bo;|0grp610b@kZtgKS6d`$#3_8+fL*r&H;`}xmlf< zSX=;iy1%l*qM;GiKM+ZU@58cA#o9B!oydD&rKhK%qH?7Ox{0>fix)4x{B{9YAsieW z6P4Cn_pW~tpOCRI=hafll(YpOBsuckyLX{f;w0BEZ&S?9jHsq(r8@JGGhKriGZJ9T zNde#XH@hxGfURK^|5JUeomNsZTrNqr8kX~-h~*|;hx^I{=_G+4b6}>n{q1&LrL|Ws z#sFSSxrpz>>Ns8oD0_|NSOm8(Td( zqKb>V&7YV%IvVbMg`pbQZlM2zFEh`%d7c3#O9N~TJ3aH6wwbU4S7kB zI!9_DZwb&1@+G0(CfARTkGK2#tN6@Y<~bbit+tzn9=>tT7(2~t3gGy`%+Tnz@RkZo z_<$9~#&(3VvW!NhtYlE27SiTnWw{4+Nu_EL?^2KM*0JOD1(CuHpUWfC8{H(Ao{rXdwiHBxf zk+}CjOue3P^V>^(eSPD@5L;aPpZl5lH~kew>iL|4S?EAeSgD9eUKa~p8dgb=&H6DZ zeu`PLLxY0=r%vm;S1U7~HIRW%oGPgfEe3(OE>8Q&svVa4djy2ao~vOFdnSwGdU}_b zRpkf>j)bx_`|?IGf=7PMh7hfx2ZAUnb~_*=qkLT%(7G@MM_(1htbQCFok&YVQ!*3` zpWJ2Y|3M7eKwj2XDQ*^xa$I-M;!yfi8cX>nIT1_8Axe8Vo%sk<_p2eGdxx(BMU-kn zIvND*coN2k!7iR#BQ-52ziK*2K~ERM>bjS$X;sZlvm{8w*qh7@e&5@wt(eIe=tpU9&d^y5hu%?k5SK%UNnuLVcEgLTBtd5~83O}@o7cLEn{eYcOT!CE$v)+PZ&$wEjO!BAs(0_ZEqBPw z`wu7F`sFbIY)RHXlrwH}Qa6jvpg&C()ZL0$%FnK*D@8S;<%wfYLVAw?w2w ziPRa}o6zf(+zIvMH#jFk6crJ?uI$XrvS&0+FrKG8J$t2~zyIe@cykbgPNi&{C4$ON z;%2BSv(}-Etn74m!Vh)Sj3hKsfRbN(JSq1MB$w&plbM;DE1t~@Y?`5n#rHZp3C^HYeukXF8s7Prkw`1|*!>gs2P$Jft~?x{^Oz&A59jZXHF zfK$I*S0WxHk=tZfnDWJGZ0sK{d77ffOLN`OxUY>9Lufv-7EN;fx9$4@i} z7Z=lBxjF3dh?OQ;QB3XQV*N?IpnKK7x-2#6K&-6^$e!RF8u7B-F#tdSz_O=MHpY&1 zIQSUJ9p5#QuzeY&EdqR%P;yE@!~Fh;$!ss*7Uqd`*JuIz;fFc{I^ihmDKl%6r`W}4 zIy$=Vq^!E{>FdEqUAYiOJ0KC&_Cu4^!aYfqHB((iyA|G7BJZSaFAfi<=%7?7-xCmo z#qz{kLdx*wEavUoyGC8(BZCWY_o|G=p(vuoVruCfN2D2_TgA|;Du=_Z2Xu62)1VHw zP}eyjZ!=Co!6jFTiAf?ag8hgso_&PfG zheEda7ygy_;R9~00tw+(Mf+Ly#SE%1=N}6;L9z6x_s#i^++3O}t*ew7lmU!Hi*v{5 ziFjFEpUZK`mYf_E89&M0yCYxL!DT5Fz5PED%baBXbT%tE|Ds-6-7$sn08$Rk5ZLQC zG;8bYNJlq&dwT-|13-ixef5#s(4WQ-+`(^-<7Jn-Yio;uyfq>sM?@mOpx2HhH5Jcs zkBkhzsEC1&PJ`pjXMXG26x$f^YRT84JCqOG88k^q3n=I8{HCy4LsRpvK5!|KZs{We*Nb;Quxu0}*=T%S3uTB|l-){I-ew zJ#Z{ddDHZE}3~SGjHgxrHQBzwIzNvn^_lH{MPn)<> z=W(Klq@4&Nq;q;2Q9O_d1dS;Q3xa%oKSq7be)Prb17tz8hdv0Nrlfm>Ea(&Uxm>`` zF)RHu*}y>#0ODDcm)D>X-ddwPBx}~t{lA7X6}z59vVrOOzTIV7KCE+T-%~bP@iWoC z&br!gG+#YQS?RvxQ*!mPm8J~~%tr>d{Njm`0*wq&OK{whgGNV33zyuP%3`K!dxkUa6 zv&WM>?uWleM0`$Ky)M`MXJ#HXb=|YoD!oRXla12zs{8UH5BaImyj7uX%-nV+oOz$A zn3VMNx$Q>0q@ZyDtnJRu&f1z;(MaC+jIHEI@ujYbdjtfd{SQ;`phjo&vp>~nWsvur zFLgQH80#YnI5R9!^@nqcLqiOBcx)T{7ef_7fhZU2Om7O>(h^`~vc|_BZV=-UiHM7v z{|N5SA305{mzoW+OglH`54Z$2Z*0$_vEQZtx{Sro!v!Q!ke5Y&azHJOPcDd+7i}7) zQv_1V0qa6Xshh~#68&9$(F9Wc(j@H7^JKFIex#Ls%TdfSGc%jCs_ZiX(#ntns|r5X z|41wAGK%B2(mJ}U8|dp#-vDdlaV@jbg6$>6#exJ3zYd%6%zOX7;3n?3f6c|K(JDkX z;4o3mV&-lg7l26NKeI3{Z-zqa$ovEp&@OI-iY!b;5mDRsr*}$*=jP>|9H5Fut|YC1 zjm~MSNh%M0`n|aWAtNOr@v<5sS1a+%xZ~DUn3*QD*|M$Ehg=_~%X?*~8?>+VrCC)k zE`9unQ~M<3T?1Z2h{{POrsOeACSS5)?pQv~P^`IqZmt1{TXJwvTTMP?W0EQT2_j9DXYB4&5&km^~=j2>$YO=p66 z=@{T&TUiyh8C(+>Xa0AgO9NIv+|O4U>M6P2`@pBH&Xls%T2-BBzfKv6d$`>sAYIqMB6S@OrP-W)eUdyU2uvr&3eCaE4_bx@iAUnIzK zZQiCBg6Q1aOtikIeq=e+RV%+NPcolDq)CVTe{VLteJ;4@N`<&?-7Q$#DvSyxk9bt@j=$C+Bxs za7xp<{ViUYbItTDC1W4?ef9EY7ZeoaXPY=YovzTxl1nO#C-S@Ls~X$Ypi!%KmMp8A z#OGo$QNfGc|3WSYME%l%@ZDf*>kz)Uo#uoprJEbNMjv~6Nsce&EJf!3u50O7cexze z0kppWv}Amh4}!7w_I4)CiaUgaE_~$Q`Odk5{|5=O@fGEeyTIf5)WJ+rzE3kFgMiX0 zh*Y)KDxurWFr>l%)NJOnEt~N%Gw<#{h=~ZvpJ^D=d8g1KgkAW*0Ao98e+gg~tN;Pb zq*IZ3R%rJVI>PxGxLxLB*i;CLCqxZ)Hm8o?C1GJNEH;pReXgOAD(jd4B2#Tza7eQk zM%avM7bd96JDlK=zxnxHxP9A_bK>yhz4|bSg=mibS59PQdAaEsa{cf7k(8gnh=5Sm0o11b70P&> zwZR^(u)d%fcvo$2$r(3g)zj6*5%c0WnLLJBbvje#EQ(pI#%OD63*={6IRpl zO(Gr5|Hi}m%sBp(V|Wj@TNX1iLR~#%T#o$9&C${~K74i#ZrYic>}_FyMtx*&b*YOg zAYgZWU9d3E&1QG$iKL_ya-pr%Kj4aXt@8_cSrJjo@$!z?u++0%f`L1Q6y|G04ViXV`Tg-~n<#kc^@faAQoNgG3n;1;=g9^8B;; z|H!=v6iegcI|Y!d1SkUhM=IH`k2m@L!6i0VkvTd49gO|&=K24R6ETt&E>Qq{Z1usM zBYda`suke5K)q5UwUFOo_@|Q(M7G&OX9kQljU$wTXr!sb$A%^M#yeQr_!_pyuwubmuQ-VwD}4uoQej_czh5<^qY zw0`*1v{~tmpH0D-3%+OmzY3z&EVVD!Y`#nVfOeo+VL?YvzvIkDRtpKPuqQc{Q~R-Y zP3LE1DBJs6$a!OvaQtwLiDn!v=YB$++X?MM6J<*9A+lGyZRhdFEdH&78S*y=bKCs; zO5OS&Ihb+M$H`Ac7g&7o0TP_^m+Ff&iL?RJ$M@L3D=+Nwy1Let6LE2IR8&;nm6A=D z8$0O~+3BE=+~6{d$7yNN!(;X=5~8RND0I+X}2EIcfFbKUO_K4mV?dH!`@Yq-PODqs|!hr{e!V6UqKV zj^#6oLH*a76SF0(KXUsDfGA+#eeV8OkA1(hvw!bd)(JSDKt@P=2$kq!q^P44@nuFn zg10$DHF0<~an}{IeLb9xiA}Nb7+hOIG67jKaMR^(K~9dQ0C$=&`?Uxw>|hSlzhq); z2n;bxU4X&|DBE%U@PqF~U%bwu;C)u4pqz|1fLHL* z17OI~;4zR*4}RDCVFDS<6?0rJ_^>S`4dTr(MX|I#QgTlqOO}PwT%{b!O9YC(5;6|79uS!E>7$3->f;9-q^6X$`8~nz20`!)X*r)hJ56*tc%*J ze(pn)7MyWsqNS>)rlz`l-DL(gM^t-;fPG zkKAD|RVQOJI?R?&X#`y)`ben^g&ZZ|SDj3`0cv`v)EHrZv@_+2yPAi7iJF%3A3K$@ z-{t=@!yTI1Dj_Lj2^R+jt$ayM_be9x&|#3?WW3nN`ptTkys*RF%uKY5O#4^Ej%x=< z^D5`!F@qYn!>trLx?@>L-Hla;zZTV7=T~U0?S%>sU?kAzu(VvW(vHYpJ%0DBANhtr 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
  • nI&Q-e)Sm$ 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 Date: Wed, 13 Dec 2017 07:50:08 +1000 Subject: [PATCH 37/56] Update test masks --- .../expected_composershapes_ellipse_mask.png | Bin 17697 -> 17862 bytes ...pected_composershapes_roundedrect_mask.png | Bin 11864 -> 11939 bytes .../expected_layoutscalebar_numeric_mask.png | Bin 6197 -> 6276 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/testdata/control_images/composer_shapes/expected_composershapes_ellipse/expected_composershapes_ellipse_mask.png b/tests/testdata/control_images/composer_shapes/expected_composershapes_ellipse/expected_composershapes_ellipse_mask.png index 9a2c62942e4dd297919a92c91597cf0af38e743a..2c6b58978db4dd42850cb93f9cff74981139103e 100644 GIT binary patch literal 17862 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

    )GrpMVv-$aNDT{rHgWqa8orOrpgf2B0vU? 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

    xFVLnn&o9y29i-4txws^x2T3*C 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 R9`IVS(a_W&ajpHy{{f~WnbrUR diff --git a/tests/testdata/control_images/composer_shapes/expected_composershapes_roundedrect/expected_composershapes_roundedrect_mask.png b/tests/testdata/control_images/composer_shapes/expected_composershapes_roundedrect/expected_composershapes_roundedrect_mask.png index 445c459af6c30faf056d6f908dce69db34acace7..72c23ec10d55028003c5d5cfa2cef376d3463518 100644 GIT binary patch literal 11939 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 Uma<(w&@#7ui=k7Jf$v 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 N&KG%Ek1`7z{sTL>S&9Gv delta 1019 zcmX|Ae^8ob99P$LZkP0j!F6_StM07DWRn}}6lu$1yLJ4vevFZNyX$2){I$G50ewAx zG@UE9tbwxOF6^vxK*k&~dGQv_cJaP2Gg~wfQvvge+8=~!m}<(I$ten^M!>qiqllG zo>xq}+2D;vqgPz`9|9dFV-aQGa$a8E+N!)(CE4<0TP_%0Jq5Vxpo4U!2kC>=VR7>Q zgH3+D`sWq@q`^?iX0s`P5WZ?%^#uZUM6#v5Wdz|dX?B!fht$}`h)VEK%N)cT7)q9}#=)hMTcpxTAmW|z`83)tn(2TZg?$qt7?A$?)oj12nTjRuw-BK~*vLU~H+I^q-SUir+_aUywb5pQh6(XYBqk!^WF`Rc9(;C4v?d zT`;#x{EJ8hono=krV}MJw6r3SY+qDY_ z5%-3iA{U0DpA?j?jg54O-3yT3_$w8>UNfh!jibHO{Q+L~z|Xub%S}G(4aZ?0_SX3P zdcSuH{l7`La?f9^xWE$WL?R1sO1MGoswcVITZxhmBIYrkUztHGlM`QB(JTP#=~2L) z?A}38EM5~S$Ye6FtJUgquz=qzDk_3wv!hMs7_=x2VM9Yk5D#DupAaBV2;nEPRFQp; z-UKY2t8BP7a+OiactvzgydWh)xffalaJkkPmMM^tS!b z%(Ldyd*8%Poa*=i=*Q$*6(k&K&C zL?a&B{Asrx<((q~wAR=9aud$I(Atqp#`Q*pGwi(ckpN^$if Date: Wed, 13 Dec 2017 08:18:19 +1000 Subject: [PATCH 38/56] Fix crash in ui while working with map grids --- src/core/layout/qgslayoutitemmapgrid.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/layout/qgslayoutitemmapgrid.cpp b/src/core/layout/qgslayoutitemmapgrid.cpp index f19157950a19..8b96340b2d9b 100644 --- a/src/core/layout/qgslayoutitemmapgrid.cpp +++ b/src/core/layout/qgslayoutitemmapgrid.cpp @@ -2028,11 +2028,11 @@ void QgsLayoutItemMapGrid::calculateMaxExtension( double &top, double &right, do QList< QPair< double, QLineF > > horizontalLines; if ( mGridUnit == MapUnit && mCRS.isValid() && mCRS != mMap->crs() ) { - drawGridCrsTransform( context, 0, horizontalLines, verticalLines, false ); + drawGridCrsTransform( context, 0, horizontalLines, verticalLines, true ); } else { - drawGridNoTransform( context, 0, horizontalLines, verticalLines, false ); + drawGridNoTransform( context, 0, horizontalLines, verticalLines, true ); } if ( mGridFrameStyle != QgsLayoutItemMapGrid::NoFrame ) From 770ffdf614753a2a84da85121afc81d74c2b26c3 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 13 Dec 2017 08:18:40 +1000 Subject: [PATCH 39/56] Fix incorrect detection of grids with advanced effects --- src/core/layout/qgslayoutitemmapgrid.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/layout/qgslayoutitemmapgrid.cpp b/src/core/layout/qgslayoutitemmapgrid.cpp index 8b96340b2d9b..acd4b37615cb 100644 --- a/src/core/layout/qgslayoutitemmapgrid.cpp +++ b/src/core/layout/qgslayoutitemmapgrid.cpp @@ -375,7 +375,7 @@ void QgsLayoutItemMapGrid::setCrs( const QgsCoordinateReferenceSystem &crs ) bool QgsLayoutItemMapGrid::usesAdvancedEffects() const { - return mBlendMode == QPainter::CompositionMode_SourceOver; + return mBlendMode != QPainter::CompositionMode_SourceOver; } QPolygonF QgsLayoutItemMapGrid::scalePolygon( const QPolygonF &polygon, const double scale ) const From ca37a1ebd723f18ffdcd7a4b1f500f3442bb4b6a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 13 Dec 2017 08:41:46 +1000 Subject: [PATCH 40/56] Fix drawing of map items (grids, overviews) when rendering map item as a raster item --- python/core/layout/qgslayoutitemmap.sip | 2 +- src/app/layout/qgslayoutdesignerdialog.cpp | 2 +- src/core/layout/qgslayoutitemmap.cpp | 128 ++++++++++++------ src/core/layout/qgslayoutitemmap.h | 2 +- src/core/layout/qgslayoutitemmapgrid.cpp | 4 + tests/src/core/testqgslayoutmap.cpp | 63 +++++++++ .../expected_layoutmap_rasterized.png | Bin 0 -> 29749 bytes .../expected_layoutmap_rasterized_mask.png | Bin 0 -> 40729 bytes .../expected_composermap_grid.png | Bin .../expected_composermap_grid_mask.png | Bin .../fedora/expected_composermap_grid.png | Bin 0 -> 13722 bytes ...ected_composermap_gridreprojected_mask.png | Bin 23909 -> 24649 bytes ...ted_composermap_overview_blending_mask.png | Bin 5786 -> 5861 bytes 13 files changed, 157 insertions(+), 44 deletions(-) create mode 100644 tests/testdata/control_images/composer_map/expected_layoutmap_rasterized/expected_layoutmap_rasterized.png create mode 100644 tests/testdata/control_images/composer_map/expected_layoutmap_rasterized/expected_layoutmap_rasterized_mask.png rename tests/testdata/control_images/composer_mapgrid/expected_composermap_grid/{ => default}/expected_composermap_grid.png (100%) rename tests/testdata/control_images/composer_mapgrid/expected_composermap_grid/{ => default}/expected_composermap_grid_mask.png (100%) create mode 100644 tests/testdata/control_images/composer_mapgrid/expected_composermap_grid/fedora/expected_composermap_grid.png diff --git a/python/core/layout/qgslayoutitemmap.sip b/python/core/layout/qgslayoutitemmap.sip index 148d013abbb4..fb8c4c4f3cbd 100644 --- a/python/core/layout/qgslayoutitemmap.sip +++ b/python/core/layout/qgslayoutitemmap.sip @@ -457,7 +457,7 @@ This is calculated using the width of the map item and the width of the current visible map extent. %End - QgsMapSettings mapSettings( const QgsRectangle &extent, QSizeF size, int dpi ) const; + QgsMapSettings mapSettings( const QgsRectangle &extent, QSizeF size, double dpi ) const; %Docstring Return map settings that will be used for drawing of the map. %End diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 5708d1e2cc60..0d6ed0f51c0f 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1529,7 +1529,7 @@ void QgsLayoutDesignerDialog::exportToRaster() switch ( exporter.exportToImage( fileNExt.first, settings ) ) { case QgsLayoutExporter::Success: - mMessageBar->pushInfo( tr( "Export layout" ), tr( "Successfully exported layout to %1" ).arg( fileNExt.first ) ); + mMessageBar->pushInfo( tr( "Export layout" ), tr( "Successfully exported layout to %2" ).arg( QUrl::fromLocalFile( fileNExt.first ).toString(), fileNExt.first ) ); break; case QgsLayoutExporter::PrintError: diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 26830b00e39e..3818744fd670 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -756,11 +756,12 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem if ( thisPaintRect.width() == 0 || thisPaintRect.height() == 0 ) return; - painter->save(); - painter->setClipRect( thisPaintRect ); + //TODO - try to reduce the amount of duplicate code here! if ( mLayout->context().isPreviewRender() ) { + painter->save(); + painter->setClipRect( thisPaintRect ); if ( !mCacheFinalImage || mCacheFinalImage->isNull() ) { // No initial render available - so draw some preview text alerting user @@ -800,6 +801,23 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem //restore rotation painter->restore(); } + + painter->setClipRect( thisPaintRect, Qt::NoClip ); + + if ( shouldDrawPart( OverviewMapExtent ) ) + { + mOverviewStack->drawItems( painter ); + } + if ( shouldDrawPart( Grid ) ) + { + mGridStack->drawItems( painter ); + } + drawAnnotations( painter ); + if ( shouldDrawPart( Frame ) ) + { + drawMapFrame( painter ); + } + painter->restore(); } else { @@ -811,42 +829,73 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem if ( !paintDevice ) return; - // Fill with background color - if ( shouldDrawPart( Background ) ) - { - drawMapBackground( painter ); - } - QgsRectangle cExtent = extent(); QSizeF size( cExtent.width() * mapUnitsToLayoutUnits(), cExtent.height() * mapUnitsToLayoutUnits() ); if ( containsAdvancedEffects() && ( !mLayout || !( mLayout->context().flags() & QgsLayoutContext::FlagForceVectorOutput ) ) ) { // rasterise - double destinationDpi = mLayout ? mLayout->context().dpi() : style->matrix.m11() * 25.4; - - double layoutUnitsToPixels = mLayout ? mLayout->convertFromLayoutUnits( 1, QgsUnitTypes::LayoutPixels ).length() : destinationDpi / 25.4; - double widthInPixels = boundingRect().width() * layoutUnitsToPixels; - double heightInPixels = boundingRect().height() * layoutUnitsToPixels; + double destinationDpi = style->matrix.m11() * 25.4; + double layoutUnitsInInches = mLayout ? mLayout->convertFromLayoutUnits( 1, QgsUnitTypes::LayoutInches ).length() : 1; + int widthInPixels = std::round( boundingRect().width() * layoutUnitsInInches * destinationDpi ); + int heightInPixels = std::round( boundingRect().height() * layoutUnitsInInches * destinationDpi ); QImage image = QImage( widthInPixels, heightInPixels, QImage::Format_ARGB32 ); image.fill( Qt::transparent ); image.setDotsPerMeterX( 1000 * destinationDpi / 25.4 ); image.setDotsPerMeterY( 1000 * destinationDpi / 25.4 ); + double dotsPerMM = destinationDpi / 25.4; QPainter p( &image ); - double dotsPerMM = image.logicalDpiX() / 25.4; - drawMap( &p, cExtent, image.size(), destinationDpi ); - p.end(); - dotsPerMM = paintDevice->logicalDpiX() / 25.4; + QPointF tl = -boundingRect().topLeft(); + QRect imagePaintRect( std::round( tl.x() * dotsPerMM ), + std::round( tl.y() * dotsPerMM ), + std::round( thisPaintRect.width() * dotsPerMM ), + std::round( thisPaintRect.height() * dotsPerMM ) ); + p.setClipRect( imagePaintRect ); + + p.translate( imagePaintRect.topLeft() ); + + // Fill with background color - must be drawn onto the flattened image + // so that layers with opacity or blend modes can correctly interact with it + if ( shouldDrawPart( Background ) ) + { + p.scale( dotsPerMM, dotsPerMM ); + drawMapBackground( &p ); + p.scale( 1.0 / dotsPerMM, 1.0 / dotsPerMM ); + } + + drawMap( &p, cExtent, imagePaintRect.size(), image.logicalDpiX() ); + + // important - all other items, overviews, grids etc must be rendered to the + // flattened image, in case these have blend modes must need to interact + // with the map + p.scale( dotsPerMM, dotsPerMM ); + + if ( shouldDrawPart( OverviewMapExtent ) ) + { + mOverviewStack->drawItems( &p ); + } + if ( shouldDrawPart( Grid ) ) + { + mGridStack->drawItems( &p ); + } + drawAnnotations( &p ); + painter->save(); painter->scale( 1 / dotsPerMM, 1 / dotsPerMM ); // scale painter from mm to dots - painter->drawImage( 0, 0, image ); - painter->restore(); - + painter->drawImage( std::round( -tl.x()* dotsPerMM ), std::round( -tl.y() * dotsPerMM ), image ); + painter->scale( dotsPerMM, dotsPerMM ); } else { + // Fill with background color + if ( shouldDrawPart( Background ) ) + { + drawMapBackground( painter ); + } + + painter->setClipRect( thisPaintRect ); painter->save(); painter->translate( mXOffset, mYOffset ); @@ -856,31 +905,28 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem drawMap( painter, cExtent, size, paintDevice->logicalDpiX() ); painter->restore(); - } - mDrawing = false; - } - - painter->setClipRect( thisPaintRect, Qt::NoClip ); + painter->setClipRect( thisPaintRect, Qt::NoClip ); - if ( shouldDrawPart( OverviewMapExtent ) ) - { - mOverviewStack->drawItems( painter ); - } - if ( shouldDrawPart( Grid ) ) - { - mGridStack->drawItems( painter ); - } + if ( shouldDrawPart( OverviewMapExtent ) ) + { + mOverviewStack->drawItems( painter ); + } + if ( shouldDrawPart( Grid ) ) + { + mGridStack->drawItems( painter ); + } + drawAnnotations( painter ); - //draw canvas items - drawAnnotations( painter ); + } - if ( shouldDrawPart( Frame ) ) - { - drawMapFrame( painter ); + if ( shouldDrawPart( Frame ) ) + { + drawMapFrame( painter ); + } + painter->restore(); + mDrawing = false; } - - painter->restore(); } int QgsLayoutItemMap::numberExportLayers() const @@ -994,7 +1040,7 @@ void QgsLayoutItemMap::recreateCachedImageInBackground( double viewScaleFactor ) mPainterJob->start(); } -QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF size, int dpi ) const +QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF size, double dpi ) const { QgsExpressionContext expressionContext = createExpressionContext(); QgsCoordinateReferenceSystem renderCrs = crs(); diff --git a/src/core/layout/qgslayoutitemmap.h b/src/core/layout/qgslayoutitemmap.h index b396e8ff4ba5..6ef1012ddb92 100644 --- a/src/core/layout/qgslayoutitemmap.h +++ b/src/core/layout/qgslayoutitemmap.h @@ -408,7 +408,7 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem /** * Return map settings that will be used for drawing of the map. */ - QgsMapSettings mapSettings( const QgsRectangle &extent, QSizeF size, int dpi ) const; + QgsMapSettings mapSettings( const QgsRectangle &extent, QSizeF size, double dpi ) const; void finalizeRestoreFromXml() override; diff --git a/src/core/layout/qgslayoutitemmapgrid.cpp b/src/core/layout/qgslayoutitemmapgrid.cpp index acd4b37615cb..b7a28ec779c2 100644 --- a/src/core/layout/qgslayoutitemmapgrid.cpp +++ b/src/core/layout/qgslayoutitemmapgrid.cpp @@ -1219,7 +1219,11 @@ void QgsLayoutItemMapGrid::drawCoordinateAnnotation( QPainter *p, QPointF pos, c ypos += ( mAnnotationFrameDistance + textHeight + gridFrameDistance ); xpos -= textWidth / 2.0; if ( extension ) + { extension->bottom = std::max( extension->bottom, mAnnotationFrameDistance + gridFrameDistance + textHeight ); + extension->left = std::max( extension->left, textWidth / 2.0 ); // annotation at bottom left/right may extend outside the bounds + extension->right = std::max( extension->right, textWidth / 2.0 ); + } } else if ( mBottomGridAnnotationDirection == QgsLayoutItemMapGrid::VerticalDescending ) { diff --git a/tests/src/core/testqgslayoutmap.cpp b/tests/src/core/testqgslayoutmap.cpp index 318a90057449..dbdf563d376b 100644 --- a/tests/src/core/testqgslayoutmap.cpp +++ b/tests/src/core/testqgslayoutmap.cpp @@ -28,6 +28,7 @@ #include "qgsmapthemecollection.h" #include "qgsproperty.h" #include "qgslayoutpagecollection.h" +#include "qgslayoutitempolyline.h" #include #include "qgstest.h" @@ -53,6 +54,7 @@ class TestQgsLayoutMap : public QObject void mapPolygonVertices(); // test mapPolygon function with no map rotation void dataDefinedLayers(); //test data defined layer string void dataDefinedStyles(); //test data defined styles + void rasterized(); private: QgsRasterLayer *mRasterLayer = nullptr; @@ -440,5 +442,66 @@ void TestQgsLayoutMap::dataDefinedStyles() QVERIFY( checker.testLayout( mReport, 0, 0 ) ); } +void TestQgsLayoutMap::rasterized() +{ + // test a map which must be rasterised + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptMove( QgsLayoutPoint( 20, 30 ) ); + map->attemptResize( QgsLayoutSize( 200, 100 ) ); + map->setFrameEnabled( true ); + map->setExtent( QgsRectangle( -110.0, 25.0, -90, 40.0 ) ); + QList layers = QList() << mLinesLayer; + map->setLayers( layers ); + map->setBackgroundColor( Qt::yellow ); + l.addLayoutItem( map ); + + // add some guide lines, just for reference + QPolygonF points; + points << QPointF( 0, 30 ) << QPointF( 10, 30 ); + QgsLayoutItemPolyline *line1 = new QgsLayoutItemPolyline( points, &l ); + l.addLayoutItem( line1 ); + points.clear(); + points << QPointF( 0, 30 + map->rect().height() ) << QPointF( 10, 30 + map->rect().height() ); + QgsLayoutItemPolyline *line2 = new QgsLayoutItemPolyline( points, &l ); + l.addLayoutItem( line2 ); + points.clear(); + points << QPointF( 20, 0 ) << QPointF( 20, 20 ); + QgsLayoutItemPolyline *line3 = new QgsLayoutItemPolyline( points, &l ); + l.addLayoutItem( line3 ); + points.clear(); + points << QPointF( 220, 0 ) << QPointF( 220, 20 ); + QgsLayoutItemPolyline *line4 = new QgsLayoutItemPolyline( points, &l ); + l.addLayoutItem( line4 ); + + // force rasterization + QgsLayoutItemMapGrid *grid = new QgsLayoutItemMapGrid( "test", map ); + grid->setIntervalX( 10 ); + grid->setIntervalY( 10 ); + grid->setBlendMode( QPainter::CompositionMode_Darken ); + grid->setAnnotationEnabled( true ); + grid->setAnnotationDisplay( QgsLayoutItemMapGrid::ShowAll, QgsLayoutItemMapGrid::Left ); + grid->setAnnotationDisplay( QgsLayoutItemMapGrid::ShowAll, QgsLayoutItemMapGrid::Top ); + grid->setAnnotationDisplay( QgsLayoutItemMapGrid::ShowAll, QgsLayoutItemMapGrid::Right ); + grid->setAnnotationDisplay( QgsLayoutItemMapGrid::ShowAll, QgsLayoutItemMapGrid::Bottom ); + map->grids()->addGrid( grid ); + map->updateBoundingRect(); + + QVERIFY( map->containsAdvancedEffects() ); + + QgsLayoutChecker checker( QStringLiteral( "layoutmap_rasterized" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "composer_map" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); + + // try rendering again, without requiring rasterization, for comparison + // (we can use the same test image, because CompositionMode_Darken doesn't actually have any noticable + // rendering differences for the black grid!) + grid->setBlendMode( QPainter::CompositionMode_SourceOver ); + QVERIFY( !map->containsAdvancedEffects() ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + QGSTEST_MAIN( TestQgsLayoutMap ) #include "testqgslayoutmap.moc" diff --git a/tests/testdata/control_images/composer_map/expected_layoutmap_rasterized/expected_layoutmap_rasterized.png b/tests/testdata/control_images/composer_map/expected_layoutmap_rasterized/expected_layoutmap_rasterized.png new file mode 100644 index 0000000000000000000000000000000000000000..4730b65c0116b951b53935c952dc1fbe29cf2c9e GIT binary patch literal 29749 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 QXb_@xN%zm33pOGD1CA5`M*si- literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/composer_map/expected_layoutmap_rasterized/expected_layoutmap_rasterized_mask.png b/tests/testdata/control_images/composer_map/expected_layoutmap_rasterized/expected_layoutmap_rasterized_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..b149afd792e620537309f1c2ca83f310fc9226a0 GIT binary patch literal 40729 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/composer_mapgrid/expected_composermap_gridreprojected/expected_composermap_gridreprojected_mask.png b/tests/testdata/control_images/composer_mapgrid/expected_composermap_gridreprojected/expected_composermap_gridreprojected_mask.png index efe6000fb7dd012a0e28997e9e87df5662ec98e8..c713bff29ce3f5153edb5a242747a28d57efb7a4 100644 GIT binary patch literal 24649 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 m!7XwkD84WL@0$fhZKU_ literal 23909 zcmeEuiCdCe^e$%Ep*#(E>R6eUfre9Rle1-(A`XS(Jf)eLsW}fgb+Xb1O~g4N3mh;c z%~=sE)5NJzG%>OAsDU9W;eZ48^Zb7Iuei78sjmmNd+%@Wz1Di)cdc#OWm}6scO2XS z0)hTSSem0ipshY2ko3VnWPne4C%@5v2l%$7OE?I$`|9RjsWQ{j01)U12w{HSA?Dey zF+TQ315@j=RJg=xZT~Z+@KdAwx+leL$CZX2{`@I^-j)VMS|O(fU7Jq>U4b~WYh3Bg z|19r^=xjc>XQoO454}`#S>Vxj9tsQ(0_{@XyV>gYo-}#jk$l&(ee+WVbLGuxeLM31 zfAs%_W%_7JFadF9;!WyjL}Kpy|^vHn}))Y7rmI88NOFC+<8 z>HfcUF((OR)*>M$*8ZcV-teuBSGGy%r8GO$4!NC z(?UsQMIT`)!r)J|Cwj)Ttpd@WGIMz2Mpi?tk1zL2-?UIEquE_c63H>f2e=QU*h)rR z{Utv!p^fevmOpw3z6Gzj2>iyX$0V<7iro+b|5j{@7m}p@zpEo+%+|yz~6PumF+GjqxFxcYDJ52fn#LbYuey z%=HQVNwTMg9DBbaXN_hu&-blGViX4c?OuuBP`~;H*PiccWui&>7TcNpN}S9^SG+Qz zgz5Ll<L-Rc(TX0fAL0b^LyGiMXJS4qds9L@6Ng8KLVKOy=uF-Da)rD5bk-cLbg4dvevWzt*N-NtTTNA* zp2`@n&iNGc+w5Hkr`xG0Yq2tUWOW%sUh)U6A(f|)4*Z%d83iHQ*q;6*T)ZT3fALii z`KER<$fTi&`iNYiOnPCDK&P5ypPHRK!T-|u5i|0enxco%S#zMUO? zuDIUJb*voP;7rg@h2|BGm-^3g!mYiU!xGm15{&oI5Dcx{ftty z01p--zF(1TF|Bxa6KkwKQ6dq<;<^3eaZYoog=uMTmJ*UG`L()oL)kB}&&3Vh5FOdG zmf&nS-IEbskVlW!Qf#bz5`SyS;YK&;tLk1kjR0<`ROfULb>A1zp7?|(xRjwd^M=LW$4l{_6vqnSnHcu@^z72_^n~7z%8xEIBDGUdIBEj(yaX& z_thDRT?78qR?m4BEt~&{=>4Pf+^)Yzl0%|yQuL+o^V0Gdhdtgs%uD>mlJjMFufz`5 zS=QE>bP<}D8t@+*mf`~Y&nB(0tqKC%D%_n`9+Of&y-BMeP0w_t;-M}Wey#au8u81Z zAuq|y>r)2prYzX^Vdc#OHIA!aq&z=7l-IKqomWaS}8;Sia30EMl zk_h`vB?+`z#&sU&A=bEGC#e#XRYgIpG3Rg57DJpq{POrSJmhru&`;HuzOVLL^~ z5c{+!?p%qTX>I9`l?eX&9g8moTc4g+midCaJ^8xnW%swkch|ZnnTd&sY*AbS&*~?8 z2>o8Da)Du9Ax$q!QBn{QFicx=*-g>H`k?NV8BVg-qx-z|2u($ zFZ{LO33vJ==TkyGmSRg9njhGFiQJA(HW!&+j{|=Z8*b_ZXLQw zUkXQ5Ib_|2(V7?)APAHuVXf8<@$iqZeI&5xk4+clb)@)BC0V@h=a8;U0;~J^0>;#d zrlE$^Q+gXhs=0&H1GQ$G%fcL#ZbPk!>bZA_nrzGBZD=tJ5X+@KA@G%Q9dc zkP%Y(elhG-N0}CvX7_NLBAT}0WZM?O^0jQba!VvnR!Ryy@LwwxXS40Ox3|?arCf7n z{97!Zug}?g0nHj;WlTRWXQ*e96{BB_@`w7So8B)EaET^TUX(k@K>TP0^vvbJ+6-ES;(LD7qlS;tbB7>0-_`6)6^UX77CNI)mR6GRgaoXQimY;L?~`IdP;C?V%wd`L9d zHupzb-fi&VXK$JE5FRR$HEJfBn0|u_RxnvtYRx_a`p-}1esWQ@Rv=sDEjv`2wvDUR za~abpko-M!+4mJj-Zn23OsET6%f^r0@3-G&gfx^%9)6lCb%q4>M*S#bJc%>y-&){A zb|pX@5soHNpbgkwIkl_rvi3+5N)@GEmK$ZZjs^EsDCPX#a0=cDq8|mmF7`#H9(~J= zc<9`FxzPmX{`THDZ~A#k)aCVfhCi(4j;-6~>98(KN!L7je=Do|==UtgCWelK$>J$Q zmR`COZR2-%W;g^yfbM<&n!J0lgxtY&dgoxgQJx}CJ~_Xl5%f(6VV@IaBftp-*C6K- zAUeh5-MUN%B@MJ`;yJ^qTD4(SUt&MfGF58rJ}?kN8X5x&x~d>Ewsy(P&Gv>gSf|F( zaPRHrG4@Kl7G}-EtWYK<2WU9J9n{K2uCA=C{4S0^eYR;bo1A+=1Z;|E6^5ot;N%b+ ztIwu1a-6<9bvVkTFda1BnV(UNw)Fuf-^d7cAjH3`$UFC*4iVo?!8D*vqLRn4sbCxq zRK`J+;x7Edh%8^II+@Nxbfp}^skDDWrx!0&MFVCKe-beC!7N1f_JfG5X-1Vp>Oi||-a0vq$*b?Alb9+o+eAO#Kt-UNS*c)$SmxIQ z&W|s~o=ayWq=bK-b}2r4pSWZSI2;<(=bMeJM#s}r0ovfnLJ^+~4r&aO?YrPxme^l9 zbW_Q&Ed)5Zx|aV)s^T#UjO20DgN|Yi6ZP-*pVa>M%dmTQoa{@10sr6)q<228QNCm* zvwR7_E0hJ0^m5kQGiw!LZkCoHXt{f?K_qzdt2@{_pVX_e*}mFst1S$XD&pNbHn_e} z+)gdO;Cp#j+gV`Nj+uqwf5xU?tiXw{Cn-pyCU%2)utE5e}2O4UamX@;rf z?1wOt`^g*`*Q~s)t%c^YuNl{_Dr9db^FGUC>$v6igsjU+*yFpC3!pxp&%G#~t4rW% zq04knw?ieevLOMVas2Szu;muUQayLm8M){{+r$1lI6b(3vwXvo%acqJKrDBy+$i8v zQQW$~AEOd8uq%9HnWDM9bby;N%$|ubs3i~x4p) z{+lY|K%YVRwH;#`;Kz^ z1uo*|T57K6-`@tHKKN61Wxx1aOI0NSTm04Y-{4vr>)9lUjn~_gg0ftdQluC1-}ril zbjx1}=&x70>&6iY|0JPqPJc zL!QwBp|rwzB;@f|2^PakVreV$p8EA67}xfj%s1b$l4;vt@C;MmSb6|@N0IR z!>Z46yC>=BlS1<@+O@vlOLQ7L(u5%Sd25<_*0hA9??oLpt@9HJ)X64af;D}$v-Vtw z1P9&Gfew;VCa9jf(7t&}Lguz=OwyZ9=EStyiy_DTK@_We75mw-W|Hd&=ojGfuFvdv z_gg}tkNJi?Xq{$!_!=>amW<**xmbPe5foRQ{zKoO*glpU70Azc>xhOUKi!c40iUNv zaH~*U8vLwtE9k^n_}g1Stoh%Pm0=-WP(uBDB5`a+b-DsA{xVfBm}qtqfgW2as|+?9 zzM=I?eqx8Xz9o_n*hxj3BOY40V@dUO^cZs3i%C4$X{Vu*Jy>F2d)N+kYo;97;5b>D zzau2GyT^T5`2xqqNV`>-(5i#S~3v;T>)Sq%PWziJxTb zd+g}1c*XcYhaR>LoV&3l#Nx$kDesGNnQ~!DO0wPHZ1(-<giwIpA|={8*!;O`2GKWm<${FmBFcP3U4}iO&qtbClEQUuddw0QdTLCU8f=J3!;hb zF}TfWgq9t@uKBqdcWNZuo<`F25Dw2sFHm5;{T$ibTv5G=$JO3rOZ!~oXk69XBRTU2 zI6|G{4|Hx(u0Lz4*S#ioU&4M97Jj9x=~vf{MaQ)g5^1HoM<4pdeRW$pGcw=>L!{~) z&EL8ivoB{Go-CRlSEQ*$hj<&TyNoN1Yf?To);IYDj1C^_T$o*$t@@pD!ONg^oP!Is zTL|Q?4121=1Bf;mN6Qt4Ak=~IAvU?)7@S_a6u^xY6bYx&?$vI6#}O_v1>^ZKUkoHC z#@zhaNv{suTN$s_q?{?*Bi(iq>hlg7aUHA^u}?-mP;P_E>aj58U+v$KQ#&2IXzYee zu_g!y&Ynwb|9x!KZFL+XhPeqw*eMaM5lNa=Ltf32mLz&q#sds}W>h{0;vTqk-gPyJ zyRsBdUu^w~d@3@iaoB}XSr-P~$JE=kyvv+^QQP3^X6tHAr#snU4&igpd#wx+gn0k) zN43LW;#y^7T1)?fb^kk8Sq=GaL*4)f(H%kvpCK=M_OJ1W zSc{~s@7NPLDwyoiCH|t=P7^~S5#Jv3yDgY@%UX)gov{Qk_v-8H_B@r)97mBR?2wYj z(Kk_R!hc=uAQYHRDOSu)aaL23sEZryMw>o@o?0v4|L$ ze&xH$%4JLm?Q?q|9vDc9O`&fnR?E3o9 zQijUm*xl>zqIb~TFLV0oxO-asjdrJ8IE_1S`+1#r;nAU8rS>HT-2 zCJ3wp)gA%7{8vSBl~a0g1;-SMI}c1J1HgOOh+`c*2v!~;&1dL9voB08K0Q*L0Yu_( za22%uauLQSSUpAK(7XL$5}iWmniwuM<9e*i4P%-JUNu8A`D{R3j+xD3$Lde7l6@@l*9=Qvc_ zgOPRWN`}6u(vHhCg~$3GPaR|eCRCF(Av#CJ*gl?98CK>m7VN8?@_94eu8ky5Sg|qj z^*>i-`zk=Efi-O$3!XImmv}x(RDFYzZTAaS?!Iew@5!8$CI^?vrKHL@<&uRe>C1)Y z)~Uoh9)Yp*4mbxH0DJwruJovD+CNtEJwy!tHVTj>%aKdi8fBI9qA<%cJ78lyHLn)B zP5RnpIqYYx?od~~6LM^?TuI#a9PH(rlqi?qng~{RaoB+fuv~vin72mu%pgB1a9Y?H zAxmLt-K|`RjjW8*04E%7I|d-W4-hN@?o^_dE7kHSO6NXr()+_vDo%G6^aZJ`V*c)5 z`su@S$KCUP9Igpj&J9vpss`|waK_xByLIcG$73EdQZ+hUyu^@ z=bQ~M9khIbncX*ZbN6vm+~s!vM861K#om|OpG_-`uicK=Z~ z`Hq(wG4~kLB=hXc zsR{XZ$P%!zMx6kWJVa&>tNX|mvwYy~^3gtXr8S5oQC*R4`nE3@lw~|MzMIIZ+?udd zyGsPyT@1djpPr&?$yLQNUKmUt|5gZe0|45|?PdYw-O94}J6=8!x~cTFpsY+YUmq@s zE1X63T>)W$R-djunT#;V_VRR#J;#Q%YiIAVn>>@*)*fq=J!<}r*MABe^rH5e9;gmz z`#Chh)2_`yDbLY#!+dx~H1J?9uPlvQ^6?Ogw=r_ z^oa*UVJf%ac4n%=!#iqA?hqJ(MoaW^M2R|RAy9f1#GY!m+iyKpr2OvNfs;s zn)ma984(-M9t;VR-mAT;Rs(`kr{m)O@%gN}pHagNJ8oi~6UTcZ*ZH0L2HK%;>~lk4tpWB4ph@ zA=`ni+3_+=7Cbdgsud2NM7%M%PIuR$)D8h69Ny3}`EP7$EYsEEHSUPZ>N=CvMLFL~ zGiT8H0$LCHb0gz){rP(1**yUFs?Rz39N7@&Ns~?XgIECvk3L2_nZ9Dhrwn`EEOqOQ z3|TXxTTIa8OEYMmRP2ei+fW~q8|qiyl*$#U+SeJ3Tg=j`hg?MJI|7&9y2R0HZBJf( z{e~15&!XPe-H()X0&L9ctR6w|8^d0g9&4><`1cD+isspZ+;snf-v^AZXfIUp(d*0248JQYUrL#k zdQPff&_p3;r{B!oqd5=+z~__i%J1S-)ct^3pTfwp1j7UWX%Ht6k4g(tG#^k9P@T%e;s8K< zwR&bPxz3O7zqOs9;!(16`q_qQ(%PC1#_5)mX)Y)pYi^xf%RhhYOEaW&{MFKD^NyG5 z5wphq>}))(KmRthFy$I#t{#V}fIjoLchJ0Bdq&%X(#*1*^1ZilRhrQ9ch}Gvk#W%V znkYYwEkuCKRmNF}($bR>(J|un5bEz=PyGK4J0xmo9r-A_XK-np+2|xPM`5E`rKF7G z_{Gly#0wG+NvtF>NYpv>Fk}3&lu5$wEF5Nx*e-QD1BgmI>49Ua+XI~*dU8FDUhdK! z3T`pt&Cgx|KSZU3mfso3ojzSO0*Y6~kRD5bfS3wq#D!8CA8I7zZ&Os zY#Lsz2XmL!b%a!l%DV{)D35oIJG>v6TZaZ5MvI&i!;=cfzY4A!97%|X9AqYh0w;gg zQ$J~PUJx4Lp;R11f*!9>N-E%~A8(HSP$ddDE$j@Uj8hGdY>EKKb{-?Onp)1CWQDb(EbO=`Pg3jXX!Vs3GUk#Mu+h#-gPo- z7@10;)GYHzJa;;8!SISPrBAT%*3!H<8MgUW(M@?()pb69_k-T9{XGR zLb_krG3t3;`0+8f$=AQ5g%fm7swBZ$fbZHEn(3F!e%!YWW6F%iFXwLr!;cQ~5vQz` z-V9C&4o~1a9gYO7#WMcO70>ywsIQEhc1)kmz_7dngZ`;jJy5+^??!#|TQnG9w(t=k zn}iHqZN8g~B4Xipk`N*M9g^ERzAB+jyR0TEI?=05HR&taViL;-nYG64pU$Awhk4H! zI~z07=QXFNlFlXIK>26YWzQc+f(>|-@;FNQDKcPUo zl5+3{U%*lo5^U&R1vgIEbPOj(3*?n*bK>(4`Mu~f86a8*&bZym*_ieG=T|0L3zIZF zGaO&o!wmd2pBBFO+nD;*vjj{6^hJTd)y+=DDN9#zYeVNl`IzFAw-?w)e#(XZD*7^p z=^5HLOY5Ki6)0FKq_AMZtyc&~dX$z%_i1D#NS=riIjMd9M3w9IkvS6MgnrMdDgGc-<4A#+%vtqvYro@Ld8|RRvb&sR$<5J*q`9 zW4`>~S^#qg#?2~!6US&O<=V%W)cR?`@+)%Cv-wxBYf|z9>&>PpGg05!t49jKp0z+T>&&EZ~pj#%j5Inz02;V$LKyuoQ9Ka zZTHl1KM;k7Lh_zzzr8{LR8Y`29l%?|WQnEBALX{O=PDWeQDTw;+$|1J1PeSB8QUyX z_S!o$tgu?J9G>)plrX&_+d?HB_lssO5v^8E~Cp1d_$DNlt`V^wYs#(i*xu+L7Ec@j{4cC2oxO)N7P3YU}M3in5C zrd+D4AA~}mmDW58CB|=YdT0E>b#=-9R2FnlGx@ZO|F08S)mWll{Z@k>LUJAZtYXU)Nz>c>o@9r=nE!=Cra$FIlbHHs_&? z(}#3@Zcq{xSQ$zHdX|sL#I-vI>fe3N^8>tZGfs>*fHwgnH|&YHi$DS;`jb|NgZ}xP+wZS}Zf2%_ zpFpYfdQ85(!N?)vSO|E~OYyua*G}EDS&f3epz7<-KQMfb+XsW=wynX7ZJ_O+Trd6D zM5;`O6S+Wks&Y7RMGrItpjJzHoC;JMBD0g#?X6X8O}3ZuMQJ^qnVv5F&A<=2u1sui z=ya(pGf6#@r+C3kxxDsq(9XvfJq{vKhp37dP%bq&au$pRa-0IfjRVTDZ2ya;y209R z0tb_7q%K$~u+Smn_PoESSjSv)XI2E>3X9T$hk|;5P0JqCfwiL&j*OAoKclH`@HEB{ zFZW^rBT|i|MQXp&s3p}_3iRg>It85N8D6JpF4IIU7io1sJDs={HgSat()$MpOWCS7 zC@j;BMxJy7(8kFa4;&3wP92R0>1$kgmyvesQgXKEJXBC5`p~UQqqPk2oeGnJw^RR+*#U;)lr)Ht7wcQ9|RYcYz zDTx2X$PsafNXNy;7bSlZ!*kRt#mPr){!>T7S=(O(OiEF)bL9xQx|!kRCh7bHaJSc& zluH_L>11)6>XQZeyBXX`4|4rHjzqi?oGri`ipAz9gOGN>9u?WDrpbG% z`Hh-6>8AsL_qg9h>s%Ba#gO*4RW2#3NiUBxf=lZ;<#%FrpI%qs4eH@O;OhomUdR$^5dXQJhRSX5&qA1!qyjTQ*kBT|vwzjZ!7meM4Ry+5dzd z-_9Y4K{ev}QUp9BrDCIgRMQ8W&ydH9o+{ujG2kk^FIyjdO^P9QIewfF@y?4Ikrk_BiRj#kqgNL~n`&aVfaZ57Go)Dc7 zUE{ZO-ZlE*m7qwKCC})@4+)8)^)g#>oPVA!h+?8MBi>&{qTI~}2&dVor31~K37mBx zLc`Z%qs@&lo)exJkJXikX9gaTEvJG#kpDU#-U_4`N}h7Mtn1T_ni2v6n%^5@FJW#` z{PILu5bVm-MA`}?kCd$|G#I2; z#`h~G!+;per|r;@B#MQ?m5m4f6ixoG#FJc))lrI+hv+RQl_|qyO@M|*Jhk^vk52d2 z4{(r{8pgrmb#+E@2-l^_Zp2mDZ=>1i)ur;*>V2Dt_dFGUqt$1!%~jWbhHN1yTg47k zdWpU~3?fQ-F+ita=_&HA-D!7q&|WJ!Lnf#`dY5=dM(t|RuQ9EYr)^X!J@nkfKv`Jn zMA=3Qz(kK-a=Ajb0d+xrb{s0dE9Q5G+T}i5yz?H&)rHv2(KH>`XJ$lk%c~1L=;L>1 zmnuv?J6Oa#;PGD|1C?052PT(GN3YHX-F0a`H|V+=vZj;Nqb~Q?y52OQp&=T%(xPdX z5oB=YAgp{#8vz)i!eoz!;Rg5lcQodo`e4hJXdhjrr+ivm?KOdKX=h#Jy`TWQ<)MYi z+CsK**sYh zNTyAbeus9U*x)uGWpN%Z*LzHLD{}SKa4UUDVL<9fjfPfk_mAXIu1rJV-8UQS%*9t0 z^*4hwz3}v4y43IZG$C72v{EG)(ON3cwrU%H_4q*d&(L4Ayec%cPRt=ul|1~|z4cZn zDCDutB&VrlO=w{nf8s#(+5E7Y&oeCJgjvH5(Z;q% zQ21tAZ32Jo8qQjwI zgX`UCF6mT)40>2T=`YuufxYRji)%{Mq*CuO%m(n{3{!cSy*?;(#bp%z$eyk zW=kEtZO|_J>Q&EwoX35Tk6cL2E=EpgU@}7Zg7%MWeMy9PJb^Qnz-X3}B52V+Ke^nQ z6xdYV5T^mM2aY+^vv;s6u_Io2JVd_X)Z6hln!L~GQ<%zsY?E3e_C*Gudxz?%wJD#O zQGkLlA`}e?rZWmd?A#2d0+_!z#wkN8Xk^z5<1Wq$rY=g5LdvUWBnE5lZHO>JQfdCk z4;-ltQCtupV9_%iJk+wd{q;N{zBA}U9QD~0osc`s%;_Tey2e#zV7R)N$l5JaG@91?;3S^;IJJfM7?REL?SK{ZT)i`I=55T6^$paw_-vGle>}&or zwYk3#dLlxvs*kW842_+7X20oDKV(AeL>n#0;~9w=c`44Il2@tl>s*F0A=^;}9%|$p z@NGSPX@V2pM~yNyJSv}XXo783M3D%L`|+$ROX>dHgqfiDxMMRbzC-wMt~1^R_NI<^ zm9Lk>dA}i41UNdySpBnNxf@d57;hY1V!dYCNdB3P5}yx!tQrc_I~NGinS@4=!&$!M z#I$ENijnkFg6^=zBX3HU-j{iAaBQer*gu`Pm-fZ^MS9`@eJHP zouaD!Pdlthc{R4RVst6|Zi)^O2Oy}&Fn)X?#4fVWX`->7^yXmDVasEC-**z-!LNYn zk#muom@@A7hUrUSx*m)bHIkt1dtZuz%Qb;Oy6|)O0{J+s*^lm z67vAJSj02|P_ZGQUuTl(YJ0ds(Za6XA>%&;l!r?{4lBk%ER_AWG@|-w@PxQPmnv;M zadi>a&C1B=Kf~U*}#CDzR zuvxOF3QYl`Rqb$MXEIFG-{*C_{DRei*Dy)3oF0~F9sKQZ&F+u6XU-rVZC?%s2nsan z96SrCxckOpVk=psXB8+%G{E(098LcRU+ete^+52Fu|5I4P`M$ZP2PbEO#QR499-d7 zKs9e9uI`b70)!MgnFyt<9|Ee3(^E;cVn6H|7hAF$DOBrrZVg9JLcA~ z$@cZ24nUc*w6Z8T)xd@Ll79jDS2A+_Pprr z!3l)h!EskxLxfru1I4oslnlW|H;q``(DEnHa!)E~3K*EoIRMjIwaL^4**NdZ%5wnJ z0e%14oPnW=Tm*w$1!ZipgaRYe-v)^g*BRbfp|HWKnmhhLz7U49VB3t_JDkY!q>?)` ze?~Re3P%RFef)OI(fr5rBYw(lF--Q3x%!$YPns!U*Y=LHfY$%PG8sVkPiI62Pan5T zC8>TS?>;vmQ^e{%mRXVKYkBh!fXkKfxS&*4Mv_{wmtD3)f3o2EwVv--6Y@z|jtQbt zar;CleH#bnXO_dv0OU@P#nV){a)54Dgy@7{cOI#A0udk#sGy*rI15qTXBci9Xigq5 zec3M^pF^HK(>`Enm2M-Uz8croEl#Y>IT=u?RU8xnuexz);))8W1t^kQLou#BuNB|z zPh115$1_d#s=#v?0GolqBasRdmM)oJ0K)UsT)9NP2rj^BCkJaDODD3l=WN+vYEB(V z7q%-I3S>W!GI|q;<5>V+3zxS0f;@BpAQiwmY+|v8i2P?Tg#1q;t0F}g0g#TCCUnt) zyd9H9aqZTNhRcCCHp=(klVtw@R02))oUQP?lA(p1Wv5a~hN>USSu|l6X;8#oaZhTX zCQ1eH2>Entl!b%wemNuu{{8-Yd)BDo1VVp%Y5V68ZTucP<_a&W`QmR?uxN)jfPwQ; z(m%2eaA3s=n?yR0-BkwRIWumm2VFBl5DFY<2_Pcb^iTND?mwL7v%`dPPSp;e9unV^vY4mqnE=Wnn#mOoN3KT`rR$5tI8!YvrvpF&%o zjdj{|Ui5LdwETe1+QpWEeR~F|<74`0g*~qT3(c&)6U6?gxA=Pm4fLr6l=xjOIG0}# zl7!EAFr5(?w0d7895*)D<<#2t2h?X*bx!%XuHaN1Akmh;iN0qe5yzWISVw0)T;^_= zS!~x!=Pr0m>gJ>@T&8y59JE*ymMhCVXqwz2MK8z1{3`#)_cb%lTBxb1Db?r~JzY3L zoILiosZ{!ry(eGCmM)D-y&YmDbF2GFzA^vCtO|`6@<>Fx?DT3zXeHN=Fo$pqz^X{i z-Gm1nt15EJfcrgS6>!Pf8xljg(y2t{th4Bwgc+>Jsaw z<%beYX<@nvPn>q|so1TTMI1Y%QtU~JS9U5$S+Ha?vy_7(?aanp4~3vJc0M;qliye< zoGcX5J(~aSWvUTKB<>am^?vtylO9QR(0Uu^*ng)c2NqlE=POpQN((X?M=CUDY?0S0S)B+I% z(zVWr-NIu_{Ugb%8>3I!l?^c9PO!O!GUrMnIO zhAj5sjn(l!Im0CgP~ER1iQ|R}Gosza>v&=>{%!u--Sq4R#)EzR7n_us=lg8SSK;`J7|MVCpvAX+gk-vpUWPb4PN1C8<-k@u^qoVt`mDvt zQ2B+u#UbChBXjVCm6{adRqFR+5Q9%9^W}=&>W=M?UjX=Pwv71Y_}vkbdHX4@wqz@g zwD0`-42g1cfIccGps6om@V*sV9JeQ@?RErF)STVsU)TBwGWY$3>_54_vX}}iZ)9#*x-sy6C~JpaYDkqD}5RCY{z;K%CfypE@zSM1ZcUXSReg& zdj-RI@I0P>J;UU>Cj50o(jyuG_Ex6}Nsg)#>et!2x_iMhr-vbm;R!oG(>m!Gw=T}K zJLW`P-+RnBeeYM~bu{1r4^Bz}Dq@~lM9IXLCP?=*nji~>i7zcY+;=PC5y}r9$*W&V zJYZv`07o$3>Qoz&qFpU#7Gv;y(=oOKaKdEA65O;gl>ZTHW zG>gfAo`1KWr$w>`xn`w)_l6hUuu=%9n|*Wz!FZm(8R~ib_?vMje=?@dR#$4kkK(JF zbdSpX7r}A6jt3-nIHM}nfRf$@6wiqns|UN2>8t`qX`yOz_DXM38q&{rAcN51n4tSq z7w&tDHRA*bmgK}f9(HyVC`HX3a;!61`SbaXw7e}PN`9^`5=&0XTbmO*rKLDW0}w_N zh(`aEinR%=hpOR(9>eF2+-Wpu_BK#UmbtZ5 z08(2{<|bH&h;TlgQ%0cIm%iD1kt&B(pzz0P(h&BYd8ZLhuqJ?q&q3Lr;gs&g6I5&@WY;JvwjG>=$e0H6~GC;h}5fE z<>^x62<4L0|5{yx-z`Xq%ZWnklY&X*o?3AJ%i0zS0_4DiybZtm<`hEujb|r${O6g_Eg%4hQ=zpYBKkH^ zwVxx`>3}LkXefPawrrQxDj)b2f%iq^gEqq>(HYWzbq*L6_FW-|dDa%z{^P~Yr)J7B zsn}DWtW)pJ=e0dn<#W`)ks92I3OS!Vp(vQmPS6KXUh6 zAoJw%0bM(L&=$pj=^Vvh?#D8uHv#uuB=2_nmQ^T=N_3V-z9WWd!Bv8ijDY3&Yv&vQ zWRp|vtab0)NdV3Rs!#x7s03W}3e*oJURFH!Q2ge^^79m_x4@l~ksO?I03zP(5CTn0 zgpE1D5U54~5v1xFO#3E>0(Nx5LFqZ{&%9=$vur`^cJ=)oSpW^kb;dBUzHLj`)H`0< zmWM2Un3ssXg#Z*DFub5FP0mq>nujnhg}5^z;U0$y`G?;pBX7 zXsYgtsVCMkSF=;{+PAcA?wSUCZ`P^rb`N1B0DGdfZl~~qR08%Prk@$*5clbL0OueZ zpckpF51|21yPO9#zo_~t=hW<|L>@0}4?{e4P()OL#(`Ty7htlw-^Wnww70syvIZSd zeO?s54y?m4aMD^c%w-=~wwUGog~X=Z3I7om17rqft!}A;{j!|3Kyx=CItt36=cXpG zojC1G#H|x^yWmTxaC)H*%+uV8>GtA`)H zZ1dIt-Wd6FxtZ9fTjh>*T!LorZpo_CO(#}c7R;$4tkq!cTjHtDMx0)!ytOuy_4on$ z7Dr1I?@8?4r~Adi?fW0#glj&B5D($-{ohuBY|B&ayiI!w06o2I!jLXg0~Un2q7M9< zy9Wtmw1D}Vm4a7c)*Y?`vliJXdeZ>cp|uibvz&#sO2h4JlLv^5<-t6lE-(YQU)GV; z@FZg%`uhY>?2EPXMXLRs%-C%3^ESliCuFu1t8w+|h-+Wxf-aue<%hzAXuh<@@|>_l zgB<4U9f8W;_eZe?y+mg$^iN&4ZmV`;d*-``_d;Pf1Zv$X_2?BV%=XQz6b~#5p(hn1 zcPeHLUPr0)fc>yI1ti-u|FWVz*l$bx*|fYnut;5mbTY7vX0qX6ow4Bh)H7L^_jf@U zsz>h+Z3GFyXAcK!mRd8R6o;FdrG%}mx#m6J0ExidB5TikcQb3?1p+E6Q8L{WzYp)6 zQEZGdKT$($nOwDbkOE??pTE&STz!jZFcasEp z&{`5;13+2(> z-+f>G>D__Tb+&EM?Q>@GhWh*M3<-bsUc%ni!duOFfym2$dkU&J1 zdELgPETT5UUxzk4?a?V(uieZSJoNgBZa{&oKYXZ75lC?qXdTAyS`!Zg*F#{cOB)J- z6#Y@@CT-D~ zsj;7E8Qp~{H^r)onlPYxpwe_Pj>b9M-jkFhm?v0&{hf!E=>bbuYmwX`o0gbfjx*?p z!Sb@BbX6u7T$bt@Y_m>bAT|KsyeYeV5Vlw7Gm!pcBwNQgKO7K$0Y~ssj-7Sc#kF$f z#*gdVypp0Z=J3F9QPh2)7!ydKXecIK+($ibJ~HPGT+x?o{$79}ZkjI0gc_%me?9If zrD^YsqtbL=Ck`pG>+LMeXusYg{rH^n^>fB<OoV;{Z6`W87&F-WH?TqhWGQK)2VQmQ3mu#{W2X@9Md_9%a!@0&#)%r(@ zn3;NDkcMHzGw`$~WEXa2+^CvRZyG$4kVjRByH}fiVt6J}c~8w71p>`k8Tm)E1?7-LHjJ*Z|BAS z<0F=+<1tOT*Zax2#+^;4L8FGLjrC&{su4MrR+{U4HZ7*4e>F%N7m_CTQPe}zx+Jnx zil9{N%Wwy%KFC#`Tr8}?8 z&rTC66S+>=ies~b>@eM%2j4R#;YjKB+Rj@Z=)&23;|J$#lj=%s`>5}Fw-W`&Ri&$R zU^W_AP?%Phn1Ry`;8zLPUnY#vw!R-S{yG)u2ftE1=szATNsKWY$DXvyc_o@&O6(B8 ziU1`;82~UTeSU*Mju}7iR&vkr(cN*wSj7zYEA9u~EBO-q#;pwzTd)XCS~T(s9KHK* zqp{#^Jd0_Bs9S2}W0D9PT>+1emZz7x?;E#b(s{9_R}(`rfvJkFE^S;-Y^6tEy>9oz zpYP9_aarQV&F>&^IDgK$0UlJP_)Yv08BdWcZLD(yt4FRkwZ{d;iae3($#rcwEyiW2}IL+0uItleYKNXZ**>V+x!!RT z43Rl$d_N`3#j0H)sL7yY_ctYkn)Uir$NRvvj1+_BDGnx*YP9H1!+TPGsb^31H`nq4 zf_z$Q5`u&Nq177S7LebN&`yC=eN!Oi5zN<{>P$Sbu_D0eYb1m**5pf*!UaoiktO0M z=cjXicr|zNT(LLQ%R^5zx?TL>>-lnA$sToBy7kdMofZ$2>d1uUfwp)yEnyKu@wl4Q zj`!n&hgAL%rcKiP#xp!=cw28Woyt=69aHc34HWso&)55Rcd-RiB?!{SI{Qq&F}3QN zrv8F;bwJ=`H_hBy6lI^SsSRqmYu@u*jQ)@UmZ;X5w{&yVe*(At^IMvODK=XLl(-RB zM0Jh9^`^1s)cY=P{IeNDbyy1PwA(17PO8B>1W|pA|Ykz*I9a@;zY&a&|rjK63j{?*WW24y~A0y%o;e}18El`5l&e^%)|O{ zAvtatxQ3S88kfT5MZv1$lTm);+4>ENQ%mVSpAorfTKfmu!r`tz`1j~A91wFom@%@? zo7R=Q-d&w?3a)S)6$6(YaxC6`3u|RaEo*_NtDuO<%)gL<9l4voNI}|)f0-!*f@67Z zC=DJQ+aT}~%`unbVesdGmAhUfG%s1O9Id&j^YX!wOcv$~+a(0#a!^ux zl_L&=%_JIkDY7$NUuq7^}U9 zDRq_1m`r-o)u>Ld)=UC=MYCmI)`!Mpk>mh9%fUxU8MLpjXU|q&j!Q+V4JLZVp&~(;JX)0xaHs zxfU>Jc?crY#?N{h4bGtuy&QWqKqk;B7LSH z-rdBNB^xwN+D}GENmRP4&JqC?*nKSaJ!HrU?k7##e*x=(z6GFCgWhuIfr_k5n#4ScCql>?3@i&`xXn;mZe~E5Mer0GzM4 zuE(!6D^Mrg`fOro@b{14)HYy8Q&BJFTLYa64aBgk54tByw)_()koHYJG#RK}lahkZ zNx{~w_b%lQ0@E*-I*3a|7cs2{DFp`!G$&S&5JriM@|)4nWcP|^(NyFQ4jYbW1BM>W z!nh{QGe8YLtO?bX15n;W)ve(sgqSt!1Ovm-Ry%_BgsY4%Rr;mX31Cu`(Rezj{ z0}+GRS8bc~7>)=~^n=7{vIniHk*Utz6kO;jUnwfmCwYB9ga8Lu46)y}q@YQRBd%eE zpO~)2goz!~s&GG_YgHm0R0z``-&Uc&mBP!mO2zj=rg1P#y^Y{D%Pz$Mri8tRhWTZK zcZk9bEUI^doTqqjl&FUbt!W33*d~Eap_CadaHck$s8Sw4l`^CN-bqJV<=Ypx(9G(d zMd5GlIm(zhH=$(z76b=TfnLjZQYRIdDER2im z@n`gB?Nmf!w@H)S)Tm;UsAHETlhgyBauhF3nHRX(bl$p`aH)hD>_3|k0dZpejTQLS zd7|yo4#kt$X_G61^}F+sZe4MQR^pZ(u|hj}8#Hbv9eGwYk{yNY8(M0L+v7JX4v#CWC6Du&^k-Dj35Mkzm-7WM)WNN7NPJI`Q>X`#*0a8~c5Ne6@uxSnfOX~N{*Q+GPTVJ(^Z4}wS zjywM`Z|!w>{~PDV@(7*Qag0rT?OYfo9m|#dJYWTA2sx`~tyR@DC zI9u#7u{A3-mZ&@ZuZMX(#nyWiH+@gWIF(qpo~krKkA72KxKBR^tkMDxekae9Jo(0X z*e}@Pr3O!5)x?(PKXG_k1kQ|5^OCuVd!kuxN7a@5s3DTKcxv$CwvqJr=6K%0G}2PU zXp{MnyFURG@bW9{nz$LZ_J$@mWf zOmMKGJ9)q;4f_2QDCcGS>%8$_=8T@MU^;m^u}1S}mhn+w!s5Z=ZNI6BqFwD>Y7!r6=6UAe0U zWSv#RTl?L1KpolfGTU1zk^*+~9k!=v*Wd9ZwUF~~s=F@g?0YUs%a`-2S#Zu70vnR~e8&&);KO`?gxiq)r!1pk;p>f#!3k31X{H|Cr0sT^UJ zk<9h#%?6$R*$tb-&UxBKyrPj`?%@}dBT_a1v*IbW%Ec-17i#~wTFB(NAF;~cE?t~n zuIv8VqF+0o4CTujEcuJkX8OzZG|%`&t``2t_Sos`pb)?kq=hET_B_F!5}o zes` zgFE+q_*XMH@rSbXjPWk42OEHNa7TxK)l!7*vTpQx>OS|1UyQHXeX@)bsXYsSxxcA0 za$_`cxaxgD4b>9#6#S`L=b0~yxw~fPgd}WDDd!%w3whTg`N<;YY)f7E`C_uzqNgm2 zv{$NTVs7Bxo!P|EzKZ!llS&w=^i_eIddlSUHWV}PC4u5s*Bd#>qh$Wuw109M8Ps|u7x(67oYL)E4y4y^ zBna_JMwMIzk7jS1)y@K)IG27xheqr*)Jt&7{na+!r22sJz$1h4^pPCn`1zPk4=Xc< z-0{c({D*x;QSqy*vz+8)@?M`YVd?54c4B}NaRvyhog8@d0htl~p{K42`bpyy?ZRW$ zG)FWUsjN$KGk{GZ5SIEIbL=;0AEUyP#}O8 zc|6pzwIWjE&uA98zcnuRvm1GU%;$)e#?tUVlIFTrITHrWoTKjKb2~ehg?O32$o)|R zqlaEPM%U64Ewo0Vj$sXJ@@g;j#j1DJ4xoK@3nAaVsJf=W!hHpEQ^kQ$$7DC)04$`2 zL8dbI|87-umJ$T+CH`tMeC&=yR^BFRTjEUV`TEY~PG%W?{4@kq_XHi9UQ!-j3Ajef z;nf{Y_lSE($e$dhk7vq1cXT&2HBQd{lNq5#RBU6WMJE=W(NRIMcLy5I+Fixpgxy$` zz6&lgc?0=Ws(ZHt0DZARo+;{qe*onz=mr1*h$P*q=Vd_XSsCbRqSDYAtZ=L#DFm$~ z`LzR|jZ0G~z?C}<7ywmz1?$783fnP)`*NDlXP2f?kwO8+iYuR0t#H>~JSHe^mkHKn zTLbl4>T_OaD{Z0FS@P?en6_LPGvX_825!Z~CG>*oY0d>+*#5<{?$CGtq-^v?qI}b}*eNjS#S|SD7_`D< zM9Nud7flF^Lu6lQD(L^3zv*TT?_g-NK%csg&mCCu_q_l1(DL-4E2n3^?O{yqWGEU{4+RUaj z?2|GE=qKJiF2gTv-J`}Ze+98lAze5W7i~SE8IH+haxF-1A|$|tU3P6PkLU-X_USo@ zOUh84)ZyY}6;!Z*@h+0;=j%xnLezkK30~QzhV6??j7zmOaM5f5_M?-~p)hb(+&*?~ z@a)G;A%-McRCL;M~gOK|~>kEuJaK#P?pTTg+bgkxw_ML1X?iD>RypRyWu z7Atny@C_6}Q=ZqA&>dNSc4yN!TjGvdmT+m|yR2|j8`k@3dbH%gLV@215KoKe0f0Vh zD|j%AQZ$TRth&~<>e5Z^>zG^n1y#X3N#uBYws}c2gcI??>P<2CJ;Q~ZKT@n(k_Q!f;usIkN0|CNG@{%}Vd4=_G;XiHsI)@`Ggvn-Ol0V3nAX(A zWu-Df_3>xZ?`7hhH51a0pN!hN`LEVA-RBegjgL$GW&nZ*$Nr{&1u+_aJ8gDkf63@H zG52eO_va4}4;Na^4Li^9fUV}wkHS3_ACtcFFyu9S|8OAJ5UBT+;PjN+X8Mm9%CGEu zpL}j-6&r*3f!`GmwiRRwZ7yVwXRFd!?UxqKmPjq`l>NQ z%Z%5zw&l+8TMlIJVEnzW-}3n@NrpQN?;khbKD5+x^Fr2-Y?BwT_*;I6e}8{}ye%U` zgLPh?;kT>GdMANq9)EniKm0t<%ykbxKR^Gtz@lfqef_$5Om~W%cz~h~N0xd{7G!>A zvF$3e-pOr0OBwf<+08%xvnI}#nSo))(Pt9L@wSg9<=i&&TYed6F9X8?)}lE%9-_5SQAMVdkmk9JD69YrRaZW?-AIb0YZhXI+T-n~S1ZdSkuxqz6 zc>sfs;m3Bq_QNxZ*Ue)Dnz`S%7GETvzEuV z5P0 zdA*N6TI{R+y)EbP?d|!;SBI~kcK5$TvQ6cu6q~9q88tsYrM~Rj?GOU=Dwm!w)8vbc z(?W}THm;W|^xT#!z5Mda`2T-E(e_hZ{H}ld;Y6E?4+$^Z*2nMfTfKVqQt5NgKU>K4 zC*Qa2{eNY?o%#PSTY;Y0A>T41TPW#=eE-wCN=JY`V4beUxcMNvH4E;o z2v8u&I{D;4!i!-;hxg>k7n!5A!66QmWO!hABaDBC%I#;@S^wPnStIxSb0sioF+33a z+$jN+Zs>SFapH5kO?!d9*q-;};oHw delta 731 zcmaE=J4<(hWuT9zi(^Q|oVR!O7KV5-xLthrRPs+mN<*`^T8pz-O0zSAOG9^~D@R5D z%hGExt0qjy%U)8Pv-0(7bJeRau5EcHk<7~g1n=(J?`8)vY(zItWPiozD6^YAX7=;* z^ZP&Bl%HpKAXoGITkf9P-(}x<7~VC0|8T(B@VG?w_m1o$nfCZc4Et86;{ae z+lZ`FW88d^HJ-_?xMyQo{|>eL`|IWN@9nv1%gn&Aqp@G!zK>tt?#(<#g(G*a6v{ld zIe*ufVF!EmTEpjeY?&LXAJp1>2-q=;Wpg0A2kT}BjvtJc|9^gZdiotF(1ArzGuOxO zm(!2m2h;#EQ{KL=C;j}q-uV4>Z|5=D6gu$$J>ziXs>;NN&&-a$W4!*v!bb1GcfIMS zcf{!JP6tZz-;haOb60uYNtgo|7#g&nWC+wHwKzHu*oz+Z0Ubx%jg&gX}mSy}EUDu*^s~5QMnFbWT(;~3>;GE*`Evvr^ z%zL=pfBv!G-`|&?7ifOh@tlK!K_tePX|f^XG%u*{j~DiQw#ond7ZhfD4!inafA{>y z=jZ1?zPh@4t@O1|kE_G~eM|qGUvCaH+W+vzYKA-4-f?_ Date: Fri, 15 Dec 2017 13:57:44 +1000 Subject: [PATCH 41/56] Add new page icon to toolbar --- images/images.qrc | 1 + images/themes/default/mActionNewPage.svg | 86 ++++++++++++++++++++++++ src/ui/layout/qgslayoutdesignerbase.ui | 4 +- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 images/themes/default/mActionNewPage.svg diff --git a/images/images.qrc b/images/images.qrc index 568bdcd4ec79..95370696044f 100755 --- a/images/images.qrc +++ b/images/images.qrc @@ -617,6 +617,7 @@ themes/default/mIconQgsProjectFile.svg themes/default/mIconPythonFile.svg themes/default/mIconQptFile.svg + themes/default/mActionNewPage.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mActionNewPage.svg b/images/themes/default/mActionNewPage.svg new file mode 100644 index 000000000000..d3652e7a30d8 --- /dev/null +++ b/images/themes/default/mActionNewPage.svg @@ -0,0 +1,86 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/src/ui/layout/qgslayoutdesignerbase.ui b/src/ui/layout/qgslayoutdesignerbase.ui index 8a3081a46db7..f10ea428c1f3 100644 --- a/src/ui/layout/qgslayoutdesignerbase.ui +++ b/src/ui/layout/qgslayoutdesignerbase.ui @@ -70,6 +70,8 @@ + + @@ -444,7 +446,7 @@ - :/images/themes/default/mActionFileNew.svg:/images/themes/default/mActionFileNew.svg + :/images/themes/default/mActionNewPage.svg:/images/themes/default/mActionNewPage.svg Add Pages… From 26315894d1878ad69e928faa8264ee7083db7b25 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 14:03:45 +1000 Subject: [PATCH 42/56] Add some layout related actions to app toolbar --- src/app/qgisapp.cpp | 12 ++++++++++ src/app/qgisapp.h | 3 +++ src/ui/qgisapp.ui | 56 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index e721aa0cbbcd..a684b76e6a9d 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -1855,7 +1855,9 @@ void QgisApp::createActions() connect( mActionNewMapCanvas, &QAction::triggered, this, &QgisApp::newMapCanvas ); connect( mActionNew3DMapCanvas, &QAction::triggered, this, &QgisApp::new3DMapCanvas ); connect( mActionNewPrintComposer, &QAction::triggered, this, &QgisApp::newPrintComposer ); + connect( mActionNewPrintLayout, &QAction::triggered, this, &QgisApp::newPrintLayout ); connect( mActionShowComposerManager, &QAction::triggered, this, &QgisApp::showComposerManager ); + connect( mActionShowLayoutManager, &QAction::triggered, this, &QgisApp::showLayoutManager ); connect( mActionExit, &QAction::triggered, this, &QgisApp::fileExit ); connect( mActionDxfExport, &QAction::triggered, this, &QgisApp::dxfExport ); connect( mActionDwgImport, &QAction::triggered, this, &QgisApp::dwgImport ); @@ -5995,6 +5997,16 @@ void QgisApp::newPrintComposer() createNewComposer( title ); } +void QgisApp::newPrintLayout() +{ + QString title; + if ( !uniqueLayoutTitle( this, title, true ) ) + { + return; + } + createNewLayout( title ); +} + void QgisApp::showComposerManager() { if ( !mComposerManager ) diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 8f8299954a2f..f81fffd7ad2e 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -1266,6 +1266,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void newGeoPackageLayer(); //! Print the current map view frame void newPrintComposer(); + //! Create a new print layout + void newPrintLayout(); + void showComposerManager(); //! Add all loaded layers into the overview - overrides qgisappbase method void addAllToOverview(); diff --git a/src/ui/qgisapp.ui b/src/ui/qgisapp.ui index 641dd08e3cf0..cc605c86a63c 100644 --- a/src/ui/qgisapp.ui +++ b/src/ui/qgisapp.ui @@ -370,6 +370,8 @@ + + @@ -2934,9 +2936,63 @@ Acts on currently active editable layer Ctrl+Shift+M + + + + :/images/themes/default/mActionComposerManager.svg:/images/themes/default/mActionComposerManager.svg + + + Layout Manager… + + + Show Layout Manager + + + + + + :/images/themes/default/mActionNewComposer.svg:/images/themes/default/mActionNewComposer.svg + + + New &Print Layout + + + New Print Layout + + + Ctrl+P + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3704c48bc006f6fd5ed621260091f67db775225d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 14:18:34 +1000 Subject: [PATCH 43/56] Add links to open exported images in message bar --- src/app/layout/qgslayoutdesignerdialog.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 0d6ed0f51c0f..f50aa54ae1c1 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1529,7 +1529,9 @@ void QgsLayoutDesignerDialog::exportToRaster() switch ( exporter.exportToImage( fileNExt.first, settings ) ) { case QgsLayoutExporter::Success: - mMessageBar->pushInfo( tr( "Export layout" ), tr( "Successfully exported layout to %2" ).arg( QUrl::fromLocalFile( fileNExt.first ).toString(), fileNExt.first ) ); + mMessageBar->pushMessage( tr( "Export layout" ), + tr( "Successfully exported layout to %2" ).arg( QUrl::fromLocalFile( fileNExt.first ).toString(), fileNExt.first ), + QgsMessageBar::INFO, 0 ); break; case QgsLayoutExporter::PrintError: @@ -1627,7 +1629,9 @@ void QgsLayoutDesignerDialog::exportToPdf() { case QgsLayoutExporter::Success: { - mMessageBar->pushInfo( tr( "Export layout" ), tr( "Successfully exported layout to %1" ).arg( outputFileName ) ); + mMessageBar->pushMessage( tr( "Export layout" ), + tr( "Successfully exported layout to %2" ).arg( QUrl::fromLocalFile( outputFileName ).toString(), outputFileName ), + QgsMessageBar::INFO, 0 ); break; } From f649f1f8a7379212aedf21f604b8e480ee621719 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 15:10:11 +1000 Subject: [PATCH 44/56] [layouts][needs-docs] Add control for whether pages should be exported, including data defined setting This replaces the 2.x data-defined "number of pages" setting. Instead of requiring users to develop an expression to return the number of pages, instead we allow individual pages to have a data defined control of whether that page should be included in the export. This is more flexible, and works correctly with the mixed page size model for layouts. --- python/core/layout/qgslayout.sip | 3 - src/app/layout/qgslayoutdesignerdialog.cpp | 6 ++ .../layout/qgslayoutpagepropertieswidget.cpp | 11 +++ .../layout/qgslayoutpagepropertieswidget.h | 1 + src/core/layout/qgslayout.h | 3 - src/core/layout/qgslayoutcontext.h | 3 +- src/core/layout/qgslayoutexporter.cpp | 43 +++++++++- src/core/layout/qgslayoutitem.h | 10 +-- src/core/layout/qgslayoutpagecollection.cpp | 4 + .../layout/qgslayoutpagepropertieswidget.ui | 82 ++++++++++++++----- src/ui/qgisapp.ui | 2 +- tests/src/core/testqgslayout.cpp | 28 +++++++ tests/src/python/test_qgslayoutexporter.py | 46 +++++++++++ 13 files changed, 204 insertions(+), 38 deletions(-) diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index a94ad469e692..eb3e5cd12035 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -580,9 +580,6 @@ Emitted when the layout's name is changed. }; - - - /************************************************************************ * This file has been generated automatically from * * * diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index f50aa54ae1c1..7dce81cbc660 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1511,6 +1511,9 @@ void QgsLayoutDesignerDialog::exportToRaster() mView->setPaintingEnabled( false ); QApplication::setOverrideCursor( Qt::BusyCursor ); + // force a refresh, to e.g. update data defined properties, tables, etc + mLayout->refresh(); + QgsLayoutExporter exporter( mLayout ); QgsLayoutExporter::ImageExportSettings settings; @@ -1624,6 +1627,9 @@ void QgsLayoutDesignerDialog::exportToPdf() pdfSettings.rasterizeWholeImage = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); pdfSettings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); + // force a refresh, to e.g. update data defined properties, tables, etc + mLayout->refresh(); + QgsLayoutExporter exporter( mLayout ); switch ( exporter.exportToPdf( outputFileName, pdfSettings ) ) { diff --git a/src/app/layout/qgslayoutpagepropertieswidget.cpp b/src/app/layout/qgslayoutpagepropertieswidget.cpp index 3518998222db..22150b9d693d 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.cpp +++ b/src/app/layout/qgslayoutpagepropertieswidget.cpp @@ -39,6 +39,7 @@ QgsLayoutPagePropertiesWidget::QgsLayoutPagePropertiesWidget( QWidget *parent, Q mWidthSpin->setValue( mPage->pageSize().width() ); mHeightSpin->setValue( mPage->pageSize().height() ); mSizeUnitsComboBox->setUnit( mPage->pageSize().units() ); + mExcludePageCheckBox->setChecked( mPage->excludeFromExports() ); mPageOrientationComboBox->setCurrentIndex( mPageOrientationComboBox->findData( mPage->orientation() ) ); @@ -59,11 +60,14 @@ QgsLayoutPagePropertiesWidget::QgsLayoutPagePropertiesWidget( QWidget *parent, Q connect( mHeightSpin, static_cast< void ( QDoubleSpinBox::* )( double )>( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPagePropertiesWidget::updatePageSize ); connect( mWidthSpin, static_cast< void ( QDoubleSpinBox::* )( double )>( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPagePropertiesWidget::setToCustomSize ); connect( mHeightSpin, static_cast< void ( QDoubleSpinBox::* )( double )>( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPagePropertiesWidget::setToCustomSize ); + connect( mExcludePageCheckBox, &QCheckBox::toggled, this, &QgsLayoutPagePropertiesWidget::excludeExportsToggled ); connect( mSymbolButton, &QgsSymbolButton::changed, this, &QgsLayoutPagePropertiesWidget::symbolChanged ); registerDataDefinedButton( mPaperSizeDDBtn, QgsLayoutObject::PresetPaperSize ); registerDataDefinedButton( mWidthDDBtn, QgsLayoutObject::ItemWidth ); registerDataDefinedButton( mHeightDDBtn, QgsLayoutObject::ItemHeight ); + registerDataDefinedButton( mExcludePageDDBtn, QgsLayoutObject::ExcludeFromExports ); + mExcludePageDDBtn->registerEnabledWidget( mExcludePageCheckBox, false ); showCurrentPageSize(); } @@ -155,6 +159,13 @@ void QgsLayoutPagePropertiesWidget::symbolChanged() mPage->layout()->undoStack()->endCommand(); } +void QgsLayoutPagePropertiesWidget::excludeExportsToggled( bool checked ) +{ + mPage->beginCommand( !checked ? tr( "Include Page in Exports" ) : tr( "Exclude Page from Exports" ) ); + mPage->setExcludeFromExports( checked ); + mPage->endCommand(); +} + void QgsLayoutPagePropertiesWidget::showCurrentPageSize() { QgsLayoutSize paperSize = mPage->pageSize(); diff --git a/src/app/layout/qgslayoutpagepropertieswidget.h b/src/app/layout/qgslayoutpagepropertieswidget.h index bb25f7ed6adf..195b9ed4d273 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.h +++ b/src/app/layout/qgslayoutpagepropertieswidget.h @@ -48,6 +48,7 @@ class QgsLayoutPagePropertiesWidget : public QgsLayoutItemBaseWidget, private Ui void updatePageSize(); void setToCustomSize(); void symbolChanged(); + void excludeExportsToggled( bool checked ); private: diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 6bbcc7ac9917..737f595de3ed 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -672,6 +672,3 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext }; #endif //QGSLAYOUT_H - - - diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index 6b9028302d36..cfbfcf703b0e 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -236,7 +236,8 @@ class CORE_EXPORT QgsLayoutContext : public QObject bool mPagesVisible = true; friend class QgsLayoutExporter; - friend class LayoutItemCacheSettingRestorer; + friend class TestQgsLayout; + friend class LayoutContextPreviewSettingRestorer; }; diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 7188494e7c92..9eafe04db214 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -26,6 +26,30 @@ #include "gdal.h" #include "cpl_conv.h" +///@cond PRIVATE +class LayoutContextPreviewSettingRestorer +{ + public: + + LayoutContextPreviewSettingRestorer( QgsLayout *layout ) + : mLayout( layout ) + , mPreviousSetting( layout->context().mIsPreviewRender ) + { + mLayout->context().mIsPreviewRender = false; + } + + ~LayoutContextPreviewSettingRestorer() + { + mLayout->context().mIsPreviewRender = mPreviousSetting; + } + + private: + QgsLayout *mLayout = nullptr; + bool mPreviousSetting = false; +}; + +///@endcond PRIVATE + QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout ) : mLayout( layout ) { @@ -53,6 +77,9 @@ void QgsLayoutExporter::renderPage( QPainter *painter, int page ) const return; } + LayoutContextPreviewSettingRestorer restorer( mLayout ); + ( void )restorer; + QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() ); renderRegion( painter, paperRect ); } @@ -73,6 +100,9 @@ QImage QgsLayoutExporter::renderPageToImage( int page, QSize imageSize, double d return QImage(); } + LayoutContextPreviewSettingRestorer restorer( mLayout ); + ( void )restorer; + QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() ); return renderRegionToImage( paperRect, imageSize, dpi ); } @@ -85,8 +115,6 @@ class LayoutItemCacheSettingRestorer LayoutItemCacheSettingRestorer( QgsLayout *layout ) : mLayout( layout ) { - mLayout->context().mIsPreviewRender = false; - const QList< QGraphicsItem * > items = mLayout->items(); for ( QGraphicsItem *item : items ) { @@ -101,8 +129,6 @@ class LayoutItemCacheSettingRestorer { it.key()->setCacheMode( it.value() ); } - - mLayout->context().mIsPreviewRender = true; } private: @@ -122,6 +148,8 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) LayoutItemCacheSettingRestorer cacheRestorer( mLayout ); ( void )cacheRestorer; + LayoutContextPreviewSettingRestorer restorer( mLayout ); + ( void )restorer; #if 0 //TODO setSnapLinesVisible( false ); @@ -138,6 +166,9 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) QImage QgsLayoutExporter::renderRegionToImage( const QRectF ®ion, QSize imageSize, double dpi ) const { + LayoutContextPreviewSettingRestorer restorer( mLayout ); + ( void )restorer; + double resolution = mLayout->context().dpi(); double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) ); if ( imageSize.isValid() ) @@ -219,6 +250,8 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString pageDetails.baseName = fi.baseName(); pageDetails.extension = fi.completeSuffix(); + LayoutContextPreviewSettingRestorer restorer( mLayout ); + ( void )restorer; LayoutContextSettingsRestorer dpiRestorer( mLayout ); ( void )dpiRestorer; mLayout->context().setDpi( settings.dpi ); @@ -303,6 +336,8 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f mErrorFileName.clear(); + LayoutContextPreviewSettingRestorer restorer( mLayout ); + ( void )restorer; LayoutContextSettingsRestorer contextRestorer( mLayout ); ( void )contextRestorer; mLayout->context().setDpi( settings.dpi ); diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index 8c75460d8ef5..3d59f5a831e9 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -831,6 +831,11 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt */ void cancelCommand(); + /** + * Returns whether the item should be drawn in the current context. + */ + bool shouldDrawItem() const; + public slots: /** @@ -1049,11 +1054,6 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt */ virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); - /** - * Returns whether the item should be drawn in the current context. - */ - bool shouldDrawItem() const; - /** * Applies any present data defined size overrides to the specified layout \a size. */ diff --git a/src/core/layout/qgslayoutpagecollection.cpp b/src/core/layout/qgslayoutpagecollection.cpp index be7ace29d33c..14b8e5ad9821 100644 --- a/src/core/layout/qgslayoutpagecollection.cpp +++ b/src/core/layout/qgslayoutpagecollection.cpp @@ -482,6 +482,10 @@ bool QgsLayoutPageCollection::shouldExportPage( int page ) const return false; } + QgsLayoutItemPage *pageItem = mPages.at( page ); + if ( !pageItem->shouldDrawItem() ) + return false; + //check all frame items on page QList frames; itemsOnPage( frames, page ); diff --git a/src/ui/layout/qgslayoutpagepropertieswidget.ui b/src/ui/layout/qgslayoutpagepropertieswidget.ui index 7b808e3f568f..b34aa1dc20be 100644 --- a/src/ui/layout/qgslayoutpagepropertieswidget.ui +++ b/src/ui/layout/qgslayoutpagepropertieswidget.ui @@ -14,27 +14,7 @@ New Item Properties - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Background - - - - + true @@ -56,6 +36,13 @@ + + + + Background + + + @@ -204,6 +191,56 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + If checked, this page will not be included when exporting the layout + + + Exclude page from exports + + + false + + + + + + + … + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -244,6 +281,9 @@ mHeightSpin mHeightDDBtn mSizeUnitsComboBox + mExcludePageCheckBox + mExcludePageDDBtn + mSymbolButton diff --git a/src/ui/qgisapp.ui b/src/ui/qgisapp.ui index cc605c86a63c..bc9f780fe16e 100644 --- a/src/ui/qgisapp.ui +++ b/src/ui/qgisapp.ui @@ -17,7 +17,7 @@ 0 0 1018 - 20 + 25 diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index fd10006d8b00..5f993f11e50f 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -613,6 +613,7 @@ void TestQgsLayout::shouldExportPage() QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); page2->setPageSize( "A4" ); l.pageCollection()->addPage( page2 ); + l.context().mIsPreviewRender = false; QgsLayoutItemHtml *htmlItem = new QgsLayoutItemHtml( &l ); //frame on page 1 @@ -645,6 +646,33 @@ void TestQgsLayout::shouldExportPage() QVERIFY( l.pageCollection()->shouldExportPage( 0 ) ); QVERIFY( !l.pageCollection()->shouldExportPage( 1 ) ); + + // get rid of frames + l.removeItem( frame1 ); + l.removeItem( frame2 ); + l.removeMultiFrame( htmlItem ); + delete htmlItem; + QgsApplication::sendPostedEvents( nullptr, QEvent::DeferredDelete ); + + QVERIFY( l.pageCollection()->shouldExportPage( 0 ) ); + QVERIFY( l.pageCollection()->shouldExportPage( 1 ) ); + + // explicitly set exclude from exports + l.pageCollection()->page( 0 )->setExcludeFromExports( true ); + QVERIFY( !l.pageCollection()->shouldExportPage( 0 ) ); + QVERIFY( l.pageCollection()->shouldExportPage( 1 ) ); + + l.pageCollection()->page( 0 )->setExcludeFromExports( false ); + l.pageCollection()->page( 1 )->setExcludeFromExports( true ); + QVERIFY( l.pageCollection()->shouldExportPage( 0 ) ); + QVERIFY( !l.pageCollection()->shouldExportPage( 1 ) ); + + l.pageCollection()->page( 1 )->setExcludeFromExports( false ); + l.pageCollection()->page( 0 )->dataDefinedProperties().setProperty( QgsLayoutObject::ExcludeFromExports, QgsProperty::fromExpression( "1" ) ); + l.pageCollection()->page( 1 )->dataDefinedProperties().setProperty( QgsLayoutObject::ExcludeFromExports, QgsProperty::fromValue( true ) ); + l.refresh(); + QVERIFY( !l.pageCollection()->shouldExportPage( 0 ) ); + QVERIFY( !l.pageCollection()->shouldExportPage( 1 ) ); } void TestQgsLayout::pageIsEmpty() diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index fcb95b40f834..5f7063b74437 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -317,6 +317,52 @@ def testExportWorldFile(self): self.assertAlmostEqual(values[4], 1925.000000000000, 2) self.assertAlmostEqual(values[5], 3050.000000000000, 2) + def testExcludePagesImage(self): + l = QgsLayout(QgsProject.instance()) + l.initializeDefaults() + # add a second page + page2 = QgsLayoutItemPage(l) + page2.setPageSize('A5') + l.pageCollection().addPage(page2) + + exporter = QgsLayoutExporter(l) + # setup settings + settings = QgsLayoutExporter.ImageExportSettings() + settings.dpi = 80 + settings.generateWorldFile = False + + rendered_file_path = os.path.join(self.basetestpath, 'test_exclude_export.png') + details = QgsLayoutExporter.PageExportDetails() + details.directory = self.basetestpath + details.baseName = 'test_exclude_export' + details.extension = 'png' + details.page = 0 + + self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success) + self.assertTrue(os.path.exists(exporter.generateFileName(details))) + details.page = 1 + self.assertTrue(os.path.exists(exporter.generateFileName(details))) + + # exclude a page + l.pageCollection().page(0).setExcludeFromExports(True) + rendered_file_path = os.path.join(self.basetestpath, 'test_exclude_export_excluded.png') + details.baseName = 'test_exclude_export_excluded' + details.page = 0 + self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success) + self.assertFalse(os.path.exists(exporter.generateFileName(details))) + details.page = 1 + self.assertTrue(os.path.exists(exporter.generateFileName(details))) + + # exclude second page + l.pageCollection().page(1).setExcludeFromExports(True) + rendered_file_path = os.path.join(self.basetestpath, 'test_exclude_export_excluded_all.png') + details.baseName = 'test_exclude_export_excluded_all' + details.page = 0 + self.assertEqual(exporter.exportToImage(rendered_file_path, settings), QgsLayoutExporter.Success) + self.assertFalse(os.path.exists(exporter.generateFileName(details))) + details.page = 1 + self.assertFalse(os.path.exists(exporter.generateFileName(details))) + def testPageFileName(self): l = QgsLayout(QgsProject.instance()) exporter = QgsLayoutExporter(l) From 447a94909f6772235502ed3f94a2425de5974c7a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 18:41:54 +1000 Subject: [PATCH 45/56] Fix items moving after altering page size or inserting/deleting pages --- .../core/layout/qgslayoutpagecollection.sip | 16 +++ .../layout/qgslayoutpagepropertieswidget.cpp | 2 + src/core/composer/qgslayoutmanager.cpp | 3 + src/core/layout/qgslayout.cpp | 3 + src/core/layout/qgslayoutpagecollection.cpp | 76 +++++++++++- src/core/layout/qgslayoutpagecollection.h | 19 +++ .../python/test_qgslayoutpagecollection.py | 114 ++++++++++++++++++ 7 files changed, 232 insertions(+), 1 deletion(-) diff --git a/python/core/layout/qgslayoutpagecollection.sip b/python/core/layout/qgslayoutpagecollection.sip index 68906d575ab6..ce2510b792bb 100644 --- a/python/core/layout/qgslayoutpagecollection.sip +++ b/python/core/layout/qgslayoutpagecollection.sip @@ -184,6 +184,22 @@ Ownership is not transferred, and a copy of the symbol is created internally. Returns the symbol to use for drawing pages in the collection. .. seealso:: :py:func:`setPageStyleSymbol()` +%End + + void beginPageSizeChange(); +%Docstring + Should be called before changing any page item sizes, and followed by a call to + endPageSizeChange(). If page size changes are wrapped in these calls, then items + will maintain their same relative position on pages after the page sizes are updated. +.. seealso:: :py:func:`endPageSizeChange()` +%End + + void endPageSizeChange(); +%Docstring + Should be called after changing any page item sizes, and preceded by a call to + beginPageSizeChange(). If page size changes are wrapped in these calls, then items + will maintain their same relative position on pages after the page sizes are updated. +.. seealso:: :py:func:`beginPageSizeChange()` %End void reflow(); diff --git a/src/app/layout/qgslayoutpagepropertieswidget.cpp b/src/app/layout/qgslayoutpagepropertieswidget.cpp index 22150b9d693d..c2d418f71fb9 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.cpp +++ b/src/app/layout/qgslayoutpagepropertieswidget.cpp @@ -138,10 +138,12 @@ void QgsLayoutPagePropertiesWidget::orientationChanged( int ) void QgsLayoutPagePropertiesWidget::updatePageSize() { + mPage->layout()->pageCollection()->beginPageSizeChange(); mPage->layout()->undoStack()->beginCommand( mPage, tr( "Change Page Size" ), 1 + mPage->layout()->pageCollection()->pageNumber( mPage ) ); mPage->setPageSize( QgsLayoutSize( mWidthSpin->value(), mHeightSpin->value(), mSizeUnitsComboBox->unit() ) ); mPage->layout()->undoStack()->endCommand(); mPage->layout()->pageCollection()->reflow(); + mPage->layout()->pageCollection()->endPageSizeChange(); } void QgsLayoutPagePropertiesWidget::setToCustomSize() diff --git a/src/core/composer/qgslayoutmanager.cpp b/src/core/composer/qgslayoutmanager.cpp index 445c304e56d3..15166eb66f76 100644 --- a/src/core/composer/qgslayoutmanager.cpp +++ b/src/core/composer/qgslayoutmanager.cpp @@ -17,6 +17,7 @@ #include "qgslayout.h" #include "qgsproject.h" #include "qgslogger.h" +#include "qgslayoutundostack.h" QgsLayoutManager::QgsLayoutManager( QgsProject *project ) : QObject( project ) @@ -193,11 +194,13 @@ bool QgsLayoutManager::readXml( const QDomElement &element, const QDomDocument & for ( int i = 0; i < layoutNodes.size(); ++i ) { std::unique_ptr< QgsLayout > l = qgis::make_unique< QgsLayout >( mProject ); + l->undoStack()->blockCommands( true ); if ( !l->readXml( layoutNodes.at( i ).toElement(), doc, context ) ) { result = false; continue; } + l->undoStack()->blockCommands( false ); if ( addLayout( l.get() ) ) { ( void )l.release(); // ownership was transferred successfully diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 71995f177ec7..4e1a4dd5a39a 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -701,6 +701,9 @@ QList QgsLayout::ungroupItems( QgsLayoutItemGroup *group ) void QgsLayout::refresh() { emit refreshed(); + mPageCollection->beginPageSizeChange(); + mPageCollection->reflow(); + mPageCollection->endPageSizeChange(); update(); } diff --git a/src/core/layout/qgslayoutpagecollection.cpp b/src/core/layout/qgslayoutpagecollection.cpp index 14b8e5ad9821..9afb44102017 100644 --- a/src/core/layout/qgslayoutpagecollection.cpp +++ b/src/core/layout/qgslayoutpagecollection.cpp @@ -54,6 +54,37 @@ void QgsLayoutPageCollection::setPageStyleSymbol( QgsFillSymbol *symbol ) } +void QgsLayoutPageCollection::beginPageSizeChange() +{ + mPreviousItemPositions.clear(); + QList< QgsLayoutItem * > items; + mLayout->layoutItems( items ); + + for ( QgsLayoutItem *item : qgis::as_const( items ) ) + { + if ( item->type() == QgsLayoutItemRegistry::LayoutPage ) + continue; + + mPreviousItemPositions.insert( item->uuid(), qMakePair( item->page(), item->pagePositionWithUnits() ) ); + } +} + +void QgsLayoutPageCollection::endPageSizeChange() +{ + for ( auto it = mPreviousItemPositions.constBegin(); it != mPreviousItemPositions.constEnd(); ++it ) + { + if ( QgsLayoutItem *item = mLayout->itemByUuid( it.key() ) ) + { + if ( !mBlockUndoCommands ) + item->beginCommand( QString() ); + item->attemptMove( it.value().second, true, false, it.value().first ); + if ( !mBlockUndoCommands ) + item->endCommand(); + } + } + mPreviousItemPositions.clear(); +} + void QgsLayoutPageCollection::reflow() { double currentY = 0; @@ -526,11 +557,15 @@ QgsLayoutItemPage *QgsLayoutPageCollection::extendByNewPage() void QgsLayoutPageCollection::insertPage( QgsLayoutItemPage *page, int beforePage ) { if ( !mBlockUndoCommands ) + { + mLayout->undoStack()->beginMacro( tr( "Add Page" ) ); mLayout->undoStack()->beginCommand( this, tr( "Add Page" ) ); + } if ( beforePage < 0 ) beforePage = 0; + beginPageSizeChange(); if ( beforePage >= mPages.count() ) { mPages.append( page ); @@ -541,8 +576,22 @@ void QgsLayoutPageCollection::insertPage( QgsLayoutItemPage *page, int beforePag } mLayout->addItem( page ); reflow(); + + // bump up stored page numbers to account + for ( auto it = mPreviousItemPositions.begin(); it != mPreviousItemPositions.end(); ++it ) + { + if ( it.value().first < beforePage ) + continue; + + it.value().first = it.value().first + 1; + } + + endPageSizeChange(); if ( ! mBlockUndoCommands ) + { mLayout->undoStack()->endCommand(); + mLayout->undoStack()->endMacro(); + } } void QgsLayoutPageCollection::deletePage( int pageNumber ) @@ -556,10 +605,22 @@ void QgsLayoutPageCollection::deletePage( int pageNumber ) mLayout->undoStack()->beginCommand( this, tr( "Remove Page" ) ); } emit pageAboutToBeRemoved( pageNumber ); + beginPageSizeChange(); QgsLayoutItemPage *page = mPages.takeAt( pageNumber ); mLayout->removeItem( page ); page->deleteLater(); reflow(); + + // bump stored page numbers to account + for ( auto it = mPreviousItemPositions.begin(); it != mPreviousItemPositions.end(); ++it ) + { + if ( it.value().first <= pageNumber ) + continue; + + it.value().first = it.value().first - 1; + } + + endPageSizeChange(); if ( ! mBlockUndoCommands ) { mLayout->undoStack()->endCommand(); @@ -577,10 +638,23 @@ void QgsLayoutPageCollection::deletePage( QgsLayoutItemPage *page ) mLayout->undoStack()->beginMacro( tr( "Remove Page" ) ); mLayout->undoStack()->beginCommand( this, tr( "Remove Page" ) ); } - emit pageAboutToBeRemoved( mPages.indexOf( page ) ); + int pageIndex = mPages.indexOf( page ); + emit pageAboutToBeRemoved( pageIndex ); + beginPageSizeChange(); mPages.removeAll( page ); page->deleteLater(); reflow(); + + // bump stored page numbers to account + for ( auto it = mPreviousItemPositions.begin(); it != mPreviousItemPositions.end(); ++it ) + { + if ( it.value().first <= pageIndex ) + continue; + + it.value().first = it.value().first - 1; + } + + endPageSizeChange(); if ( !mBlockUndoCommands ) { mLayout->undoStack()->endCommand(); diff --git a/src/core/layout/qgslayoutpagecollection.h b/src/core/layout/qgslayoutpagecollection.h index f0b97b34ca83..364674551331 100644 --- a/src/core/layout/qgslayoutpagecollection.h +++ b/src/core/layout/qgslayoutpagecollection.h @@ -24,6 +24,7 @@ #include "qgslayoutitempage.h" #include "qgslayoutitem.h" #include "qgslayoutserializableobject.h" +#include "qgslayoutpoint.h" #include #include @@ -220,6 +221,22 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri */ const QgsFillSymbol *pageStyleSymbol() const { return mPageStyleSymbol.get(); } + /** + * Should be called before changing any page item sizes, and followed by a call to + * endPageSizeChange(). If page size changes are wrapped in these calls, then items + * will maintain their same relative position on pages after the page sizes are updated. + * \see endPageSizeChange() + */ + void beginPageSizeChange(); + + /** + * Should be called after changing any page item sizes, and preceded by a call to + * beginPageSizeChange(). If page size changes are wrapped in these calls, then items + * will maintain their same relative position on pages after the page sizes are updated. + * \see beginPageSizeChange() + */ + void endPageSizeChange(); + /** * Forces the page collection to reflow the arrangement of pages, e.g. to account * for page size/orientation change. @@ -391,6 +408,8 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri bool mBlockUndoCommands = false; + QMap< QString, QPair< int, QgsLayoutPoint > > mPreviousItemPositions; + void createDefaultPageStyleSymbol(); friend class QgsLayoutPageCollectionUndoCommand; diff --git a/tests/src/python/test_qgslayoutpagecollection.py b/tests/src/python/test_qgslayoutpagecollection.py index b1d85b01e0c0..cfd7c79391f0 100644 --- a/tests/src/python/test_qgslayoutpagecollection.py +++ b/tests/src/python/test_qgslayoutpagecollection.py @@ -365,6 +365,120 @@ def testReflow(self): self.assertEqual(page3.pos().x(), 0) self.assertEqual(page3.pos().y(), 130) + def testInsertPageWithItems(self): + p = QgsProject() + l = QgsLayout(p) + collection = l.pageCollection() + + # add a page + page = QgsLayoutItemPage(l) + page.setPageSize('A4') + collection.addPage(page) + page2 = QgsLayoutItemPage(l) + page2.setPageSize('A5') + collection.addPage(page2) + + # item on pages + shape1 = QgsLayoutItemShape(l) + shape1.attemptResize(QgsLayoutSize(90, 50)) + shape1.attemptMove(QgsLayoutPoint(90, 50), page=0) + l.addLayoutItem(shape1) + + shape2 = QgsLayoutItemShape(l) + shape2.attemptResize(QgsLayoutSize(110, 50)) + shape2.attemptMove(QgsLayoutPoint(100, 150), page=1) + l.addLayoutItem(shape2) + + self.assertEqual(shape1.page(), 0) + self.assertEqual(shape2.page(), 1) + + # third page, slotted in middle + page3 = QgsLayoutItemPage(l) + page3.setPageSize('A3') + collection.insertPage(page3, 0) + + # check item position + self.assertEqual(shape1.page(), 1) + self.assertEqual(shape1.pagePositionWithUnits(), QgsLayoutPoint(90, 50)) + self.assertEqual(shape2.page(), 2) + self.assertEqual(shape2.pagePositionWithUnits(), QgsLayoutPoint(100, 150)) + + def testDeletePageWithItems(self): + p = QgsProject() + l = QgsLayout(p) + collection = l.pageCollection() + + # add a page + page = QgsLayoutItemPage(l) + page.setPageSize('A4') + collection.addPage(page) + page2 = QgsLayoutItemPage(l) + page2.setPageSize('A4') + collection.addPage(page2) + page3 = QgsLayoutItemPage(l) + page3.setPageSize('A4') + collection.addPage(page3) + + # item on pages + shape1 = QgsLayoutItemShape(l) + shape1.attemptResize(QgsLayoutSize(90, 50)) + shape1.attemptMove(QgsLayoutPoint(90, 50), page=0) + l.addLayoutItem(shape1) + + shape2 = QgsLayoutItemShape(l) + shape2.attemptResize(QgsLayoutSize(110, 50)) + shape2.attemptMove(QgsLayoutPoint(100, 150), page=2) + l.addLayoutItem(shape2) + + self.assertEqual(shape1.page(), 0) + self.assertEqual(shape2.page(), 2) + + collection.deletePage(1) + + # check item position + self.assertEqual(shape1.page(), 0) + self.assertEqual(shape1.pagePositionWithUnits(), QgsLayoutPoint(90, 50)) + self.assertEqual(shape2.page(), 1) + self.assertEqual(shape2.pagePositionWithUnits(), QgsLayoutPoint(100, 150)) + + def testDeletePageWithItems2(self): + p = QgsProject() + l = QgsLayout(p) + collection = l.pageCollection() + + # add a page + page = QgsLayoutItemPage(l) + page.setPageSize('A4') + collection.addPage(page) + page2 = QgsLayoutItemPage(l) + page2.setPageSize('A4') + collection.addPage(page2) + page3 = QgsLayoutItemPage(l) + page3.setPageSize('A4') + collection.addPage(page3) + + # item on pages + shape1 = QgsLayoutItemShape(l) + shape1.attemptResize(QgsLayoutSize(90, 50)) + shape1.attemptMove(QgsLayoutPoint(90, 50), page=0) + l.addLayoutItem(shape1) + + shape2 = QgsLayoutItemShape(l) + shape2.attemptResize(QgsLayoutSize(110, 50)) + shape2.attemptMove(QgsLayoutPoint(100, 150), page=2) + l.addLayoutItem(shape2) + + self.assertEqual(shape1.page(), 0) + self.assertEqual(shape2.page(), 2) + + collection.deletePage(page2) + + # check item position + self.assertEqual(shape1.page(), 0) + self.assertEqual(shape1.pagePositionWithUnits(), QgsLayoutPoint(90, 50)) + self.assertEqual(shape2.page(), 1) + self.assertEqual(shape2.pagePositionWithUnits(), QgsLayoutPoint(100, 150)) + def testDataDefinedSize(self): p = QgsProject() l = QgsLayout(p) From 65f4c4acef97d1a9c298e1cac8228ff54b728885 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 20:46:23 +1000 Subject: [PATCH 46/56] Port orientation decoding code from composer --- python/core/layout/qgslayoututils.sip | 7 +++++++ src/core/layout/qgslayoututils.cpp | 16 ++++++++++++++++ src/core/layout/qgslayoututils.h | 7 +++++++ tests/src/core/testqgslayoututils.cpp | 18 ++++++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/python/core/layout/qgslayoututils.sip b/python/core/layout/qgslayoututils.sip index 1ceb6bcf9b8b..8777962853aa 100644 --- a/python/core/layout/qgslayoututils.sip +++ b/python/core/layout/qgslayoututils.sip @@ -214,6 +214,13 @@ the a specified ``rotation`` amount. :return: largest scaled version of the rectangle possible %End + static QgsLayoutItemPage::Orientation decodePaperOrientation( const QString &string, bool &ok ); +%Docstring + Decodes a ``string`` representing a paper orientation and returns the + decoded orientation. + If the string was correctly decoded, ``ok`` will be set to true. + :rtype: QgsLayoutItemPage.Orientation +%End }; diff --git a/src/core/layout/qgslayoututils.cpp b/src/core/layout/qgslayoututils.cpp index 221197183476..2d98aea2c303 100644 --- a/src/core/layout/qgslayoututils.cpp +++ b/src/core/layout/qgslayoututils.cpp @@ -362,7 +362,23 @@ QRectF QgsLayoutUtils::largestRotatedRectWithinBounds( const QRectF &originalRec offsetY += std::fabs( minY ); return QRectF( offsetX, offsetY, rectScaledWidth, rectScaledHeight ); +} +QgsLayoutItemPage::Orientation QgsLayoutUtils::decodePaperOrientation( const QString &string, bool &ok ) +{ + QString s = string.trimmed(); + if ( s.compare( QLatin1String( "Portrait" ), Qt::CaseInsensitive ) == 0 ) + { + ok = true; + return QgsLayoutItemPage::Portrait; + } + else if ( s.compare( QLatin1String( "Landscape" ), Qt::CaseInsensitive ) == 0 ) + { + ok = true; + return QgsLayoutItemPage::Landscape; + } + ok = false; + return QgsLayoutItemPage::Landscape; // default to landscape } double QgsLayoutUtils::pointsToMM( const double pointSize ) diff --git a/src/core/layout/qgslayoututils.h b/src/core/layout/qgslayoututils.h index e5b59ad19074..b19d8d9d5035 100644 --- a/src/core/layout/qgslayoututils.h +++ b/src/core/layout/qgslayoututils.h @@ -18,6 +18,7 @@ #define QGSLAYOUTUTILS_H #include "qgis_core.h" +#include "qgslayoutitempage.h" #include #include @@ -198,6 +199,12 @@ class CORE_EXPORT QgsLayoutUtils */ static QRectF largestRotatedRectWithinBounds( const QRectF &originalRect, const QRectF &boundsRect, const double rotation ); + /** + * Decodes a \a string representing a paper orientation and returns the + * decoded orientation. + * If the string was correctly decoded, \a ok will be set to true. + */ + static QgsLayoutItemPage::Orientation decodePaperOrientation( const QString &string, bool &ok ); private: diff --git a/tests/src/core/testqgslayoututils.cpp b/tests/src/core/testqgslayoututils.cpp index c8a3c820c8bb..2f7336192ca1 100644 --- a/tests/src/core/testqgslayoututils.cpp +++ b/tests/src/core/testqgslayoututils.cpp @@ -51,6 +51,7 @@ class TestQgsLayoutUtils: public QObject void drawTextPos(); //test drawing text at a pos void drawTextRect(); //test drawing text in a rect void largestRotatedRect(); //test largest rotated rect helper function + void decodePaperOrientation(); private: @@ -609,6 +610,23 @@ void TestQgsLayoutUtils::largestRotatedRect() } } +void TestQgsLayoutUtils::decodePaperOrientation() +{ + QgsLayoutItemPage::Orientation orientation; + bool ok = false; + orientation = QgsLayoutUtils::decodePaperOrientation( QStringLiteral( "bad string" ), ok ); + QVERIFY( !ok ); + QCOMPARE( orientation, QgsLayoutItemPage::Landscape ); //should default to landscape + ok = false; + orientation = QgsLayoutUtils::decodePaperOrientation( QStringLiteral( "portrait" ), ok ); + QVERIFY( ok ); + QCOMPARE( orientation, QgsLayoutItemPage::Portrait ); + ok = false; + orientation = QgsLayoutUtils::decodePaperOrientation( QStringLiteral( " LANDSCAPE " ), ok ); + QVERIFY( ok ); + QCOMPARE( orientation, QgsLayoutItemPage::Landscape ); +} + bool TestQgsLayoutUtils::renderCheck( const QString &testName, QImage &image, int mismatchCount ) { mReport += "

    " + testName + "

    \n"; From 5d6a5096367fa0920891e52abe19b1a77ae705df Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 20:52:40 +1000 Subject: [PATCH 47/56] Restore data defined page orientation --- src/core/layout/qgslayoutitem.cpp | 39 +++++++++++++++++++++++++++- src/core/layout/qgslayoutitem.h | 2 ++ src/core/layout/qgslayoutobject.h | 2 +- tests/src/core/testqgslayoutitem.cpp | 17 ++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/core/layout/qgslayoutitem.cpp b/src/core/layout/qgslayoutitem.cpp index bb141e0d6cff..110ade203628 100644 --- a/src/core/layout/qgslayoutitem.cpp +++ b/src/core/layout/qgslayoutitem.cpp @@ -26,6 +26,7 @@ #include "qgslayouteffect.h" #include "qgslayoutundostack.h" #include "qgslayoutpagecollection.h" +#include "qgslayoutitempage.h" #include #include #include @@ -932,6 +933,37 @@ QgsLayoutPoint QgsLayoutItem::applyDataDefinedPosition( const QgsLayoutPoint &po return QgsLayoutPoint( evaluatedX, evaluatedY, position.units() ); } +void QgsLayoutItem::applyDataDefinedOrientation( double &width, double &height, const QgsExpressionContext &context ) +{ + bool ok = false; + QString orientationString = mDataDefinedProperties.valueAsString( QgsLayoutObject::PaperOrientation, context, QString(), &ok ); + if ( ok && !orientationString.isEmpty() ) + { + QgsLayoutItemPage::Orientation orientation = QgsLayoutUtils::decodePaperOrientation( orientationString, ok ); + if ( ok ) + { + double heightD, widthD; + switch ( orientation ) + { + case QgsLayoutItemPage::Portrait: + { + heightD = std::max( height, width ); + widthD = std::min( height, width ); + break; + } + case QgsLayoutItemPage::Landscape: + { + heightD = std::min( height, width ); + widthD = std::max( height, width ); + break; + } + } + width = widthD; + height = heightD; + } + } +} + QgsLayoutSize QgsLayoutItem::applyDataDefinedSize( const QgsLayoutSize &size ) { if ( !mLayout ) @@ -941,7 +973,8 @@ QgsLayoutSize QgsLayoutItem::applyDataDefinedSize( const QgsLayoutSize &size ) if ( !mDataDefinedProperties.isActive( QgsLayoutObject::PresetPaperSize ) && !mDataDefinedProperties.isActive( QgsLayoutObject::ItemWidth ) && - !mDataDefinedProperties.isActive( QgsLayoutObject::ItemHeight ) ) + !mDataDefinedProperties.isActive( QgsLayoutObject::ItemHeight ) && + !mDataDefinedProperties.isActive( QgsLayoutObject::PaperOrientation ) ) return size; @@ -962,6 +995,10 @@ QgsLayoutSize QgsLayoutItem::applyDataDefinedSize( const QgsLayoutSize &size ) // highest priority is dd width/height evaluatedWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::ItemWidth, context, evaluatedWidth ); evaluatedHeight = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::ItemHeight, context, evaluatedHeight ); + + //which is finally overwritten by data defined orientation + applyDataDefinedOrientation( evaluatedWidth, evaluatedHeight, context ); + return QgsLayoutSize( evaluatedWidth, evaluatedHeight, size.units() ); } diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index 3d59f5a831e9..c4e70cd3927f 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -1138,6 +1138,8 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt void setScenePos( const QPointF &destinationPos ); bool shouldBlockUndoCommands() const; + void applyDataDefinedOrientation( double &width, double &height, const QgsExpressionContext &context ); + friend class TestQgsLayoutItem; friend class TestQgsLayoutView; friend class QgsLayoutItemGroup; diff --git a/src/core/layout/qgslayoutobject.h b/src/core/layout/qgslayoutobject.h index 7e1b99e811cd..e173c444f88c 100644 --- a/src/core/layout/qgslayoutobject.h +++ b/src/core/layout/qgslayoutobject.h @@ -53,7 +53,7 @@ class CORE_EXPORT QgsLayoutObject: public QObject, public QgsExpressionContextGe PresetPaperSize, //!< Preset paper size for composition PaperWidth, //!< Paper width (deprecated) PaperHeight, //!< Paper height (deprecated) - NumPages, //!< Number of pages in composition + NumPages, //!< Number of pages in composition (deprecated) PaperOrientation, //!< Paper orientation //general composer item properties PageNumber, //!< Page number for item placement diff --git a/tests/src/core/testqgslayoutitem.cpp b/tests/src/core/testqgslayoutitem.cpp index 42ed5f41e5f5..481db438ec73 100644 --- a/tests/src/core/testqgslayoutitem.cpp +++ b/tests/src/core/testqgslayoutitem.cpp @@ -607,9 +607,26 @@ void TestQgsLayoutItem::dataDefinedSize() QCOMPARE( item->sizeWithUnits().units(), QgsUnitTypes::LayoutCentimeters ); QCOMPARE( item->rect().width(), 130.0 ); //mm QCOMPARE( item->rect().height(), 30.0 ); //mm + // data defined orientation + item->dataDefinedProperties().setProperty( QgsLayoutObject::PaperOrientation, QgsProperty::fromValue( "portrait" ) ); + item->attemptResize( QgsLayoutSize( 7.0, 1.50, QgsUnitTypes::LayoutCentimeters ) ); + QCOMPARE( item->sizeWithUnits().width(), 3.0 ); + QCOMPARE( item->sizeWithUnits().height(), 13.0 ); + QCOMPARE( item->sizeWithUnits().units(), QgsUnitTypes::LayoutCentimeters ); + QCOMPARE( item->rect().width(), 30.0 ); //mm + QCOMPARE( item->rect().height(), 130.0 ); //mm + item->dataDefinedProperties().setProperty( QgsLayoutObject::ItemWidth, QgsProperty() ); item->dataDefinedProperties().setProperty( QgsLayoutObject::ItemHeight, QgsProperty() ); item->dataDefinedProperties().setProperty( QgsLayoutObject::PresetPaperSize, QgsProperty() ); + item->dataDefinedProperties().setProperty( QgsLayoutObject::PaperOrientation, QgsProperty::fromValue( "landscape" ) ); + item->attemptResize( QgsLayoutSize( 1.0, 1.50, QgsUnitTypes::LayoutCentimeters ) ); + QCOMPARE( item->sizeWithUnits().width(), 1.5 ); + QCOMPARE( item->sizeWithUnits().height(), 1.0 ); + QCOMPARE( item->sizeWithUnits().units(), QgsUnitTypes::LayoutCentimeters ); + QCOMPARE( item->rect().width(), 15.0 ); //mm + QCOMPARE( item->rect().height(), 10.0 ); //mm + item->dataDefinedProperties().setProperty( QgsLayoutObject::PaperOrientation, QgsProperty() ); //check change of units should apply to data defined size item->dataDefinedProperties().setProperty( QgsLayoutObject::ItemWidth, QgsProperty::fromExpression( QStringLiteral( "4+8" ) ) ); From db17c2c43a5a2eabd66ccbae1f211c1fc0e84443 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 21:27:46 +1000 Subject: [PATCH 48/56] Restore data defined page orientation control --- .../layout/qgslayoutpagepropertieswidget.cpp | 12 +++++++ .../layout/qgslayoutpagepropertieswidget.h | 1 + src/core/layout/qgslayout.cpp | 2 +- .../layout/qgslayoutpagepropertieswidget.ui | 36 ++++++++++++------- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/app/layout/qgslayoutpagepropertieswidget.cpp b/src/app/layout/qgslayoutpagepropertieswidget.cpp index c2d418f71fb9..fda767be6ebd 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.cpp +++ b/src/app/layout/qgslayoutpagepropertieswidget.cpp @@ -66,7 +66,14 @@ QgsLayoutPagePropertiesWidget::QgsLayoutPagePropertiesWidget( QWidget *parent, Q registerDataDefinedButton( mPaperSizeDDBtn, QgsLayoutObject::PresetPaperSize ); registerDataDefinedButton( mWidthDDBtn, QgsLayoutObject::ItemWidth ); registerDataDefinedButton( mHeightDDBtn, QgsLayoutObject::ItemHeight ); + registerDataDefinedButton( mOrientationDDBtn, QgsLayoutObject::PaperOrientation ); registerDataDefinedButton( mExcludePageDDBtn, QgsLayoutObject::ExcludeFromExports ); + + connect( mPaperSizeDDBtn, &QgsPropertyOverrideButton::changed, this, &QgsLayoutPagePropertiesWidget::refreshLayout ); + connect( mWidthDDBtn, &QgsPropertyOverrideButton::changed, this, &QgsLayoutPagePropertiesWidget::refreshLayout ); + connect( mHeightDDBtn, &QgsPropertyOverrideButton::changed, this, &QgsLayoutPagePropertiesWidget::refreshLayout ); + connect( mOrientationDDBtn, &QgsPropertyOverrideButton::changed, this, &QgsLayoutPagePropertiesWidget::refreshLayout ); + mExcludePageDDBtn->registerEnabledWidget( mExcludePageCheckBox, false ); showCurrentPageSize(); @@ -168,6 +175,11 @@ void QgsLayoutPagePropertiesWidget::excludeExportsToggled( bool checked ) mPage->endCommand(); } +void QgsLayoutPagePropertiesWidget::refreshLayout() +{ + mPage->layout()->refresh(); +} + void QgsLayoutPagePropertiesWidget::showCurrentPageSize() { QgsLayoutSize paperSize = mPage->pageSize(); diff --git a/src/app/layout/qgslayoutpagepropertieswidget.h b/src/app/layout/qgslayoutpagepropertieswidget.h index 195b9ed4d273..662b8c230e32 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.h +++ b/src/app/layout/qgslayoutpagepropertieswidget.h @@ -49,6 +49,7 @@ class QgsLayoutPagePropertiesWidget : public QgsLayoutItemBaseWidget, private Ui void setToCustomSize(); void symbolChanged(); void excludeExportsToggled( bool checked ); + void refreshLayout(); private: diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 4e1a4dd5a39a..4ff91595c244 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -700,8 +700,8 @@ QList QgsLayout::ungroupItems( QgsLayoutItemGroup *group ) void QgsLayout::refresh() { - emit refreshed(); mPageCollection->beginPageSizeChange(); + emit refreshed(); mPageCollection->reflow(); mPageCollection->endPageSizeChange(); update(); diff --git a/src/ui/layout/qgslayoutpagepropertieswidget.ui b/src/ui/layout/qgslayoutpagepropertieswidget.ui index b34aa1dc20be..c8c8fd5b2ece 100644 --- a/src/ui/layout/qgslayoutpagepropertieswidget.ui +++ b/src/ui/layout/qgslayoutpagepropertieswidget.ui @@ -6,8 +6,8 @@ 0 0 - 397 - 409 + 717 + 473 @@ -49,13 +49,6 @@ Page size - - - - Orientation - - - @@ -161,9 +154,6 @@ - - - @@ -188,6 +178,27 @@ + + + + + + + + + … + + + + + + + + + Orientation + + + @@ -275,6 +286,7 @@ mPageSizeComboBox mPaperSizeDDBtn mPageOrientationComboBox + mOrientationDDBtn mWidthSpin mWidthDDBtn mLockAspectRatio From 8f0144b32b53272dcb4c92a2b05a11696dcd0e5d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 21:48:44 +1000 Subject: [PATCH 49/56] Fix some multiple updates from gui, squash some undo commands --- src/app/layout/qgslayoutpagepropertieswidget.cpp | 11 +++++++++-- src/app/layout/qgslayoutpagepropertieswidget.h | 1 + src/core/layout/qgslayout.cpp | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/layout/qgslayoutpagepropertieswidget.cpp b/src/app/layout/qgslayoutpagepropertieswidget.cpp index fda767be6ebd..20b680b6bf8f 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.cpp +++ b/src/app/layout/qgslayoutpagepropertieswidget.cpp @@ -81,6 +81,7 @@ QgsLayoutPagePropertiesWidget::QgsLayoutPagePropertiesWidget( QWidget *parent, Q void QgsLayoutPagePropertiesWidget::pageSizeChanged( int ) { + mBlockPageUpdate = true; if ( mPageSizeComboBox->currentData().toString().isEmpty() ) { //custom size @@ -111,6 +112,7 @@ void QgsLayoutPagePropertiesWidget::pageSizeChanged( int ) } mSettingPresetSize = false; } + mBlockPageUpdate = false; updatePageSize(); } @@ -145,12 +147,17 @@ void QgsLayoutPagePropertiesWidget::orientationChanged( int ) void QgsLayoutPagePropertiesWidget::updatePageSize() { + if ( mBlockPageUpdate ) + return; + + mPage->layout()->undoStack()->beginMacro( tr( "Change Page Size" ) ); mPage->layout()->pageCollection()->beginPageSizeChange(); mPage->layout()->undoStack()->beginCommand( mPage, tr( "Change Page Size" ), 1 + mPage->layout()->pageCollection()->pageNumber( mPage ) ); mPage->setPageSize( QgsLayoutSize( mWidthSpin->value(), mHeightSpin->value(), mSizeUnitsComboBox->unit() ) ); mPage->layout()->undoStack()->endCommand(); mPage->layout()->pageCollection()->reflow(); mPage->layout()->pageCollection()->endPageSizeChange(); + mPage->layout()->undoStack()->endMacro(); } void QgsLayoutPagePropertiesWidget::setToCustomSize() @@ -186,7 +193,7 @@ void QgsLayoutPagePropertiesWidget::showCurrentPageSize() QString pageSize = QgsApplication::pageSizeRegistry()->find( paperSize ); if ( !pageSize.isEmpty() ) { - mPageSizeComboBox->setCurrentIndex( mPageSizeComboBox->findData( pageSize ) ); + whileBlocking( mPageSizeComboBox )->setCurrentIndex( mPageSizeComboBox->findData( pageSize ) ); mLockAspectRatio->setEnabled( false ); mLockAspectRatio->setLocked( false ); mSizeUnitsComboBox->setEnabled( false ); @@ -195,7 +202,7 @@ void QgsLayoutPagePropertiesWidget::showCurrentPageSize() else { // custom - mPageSizeComboBox->setCurrentIndex( mPageSizeComboBox->count() - 1 ); + whileBlocking( mPageSizeComboBox )->setCurrentIndex( mPageSizeComboBox->count() - 1 ); mLockAspectRatio->setEnabled( true ); mSizeUnitsComboBox->setEnabled( true ); mPageOrientationComboBox->setEnabled( false ); diff --git a/src/app/layout/qgslayoutpagepropertieswidget.h b/src/app/layout/qgslayoutpagepropertieswidget.h index 662b8c230e32..6ca344c52f8e 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.h +++ b/src/app/layout/qgslayoutpagepropertieswidget.h @@ -58,6 +58,7 @@ class QgsLayoutPagePropertiesWidget : public QgsLayoutItemBaseWidget, private Ui QgsLayoutMeasurementConverter mConverter; bool mSettingPresetSize = false; + bool mBlockPageUpdate = false; void showCurrentPageSize(); diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 4ff91595c244..496e8bf6efa1 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -700,10 +700,12 @@ QList QgsLayout::ungroupItems( QgsLayoutItemGroup *group ) void QgsLayout::refresh() { + mUndoStack->blockCommands( true ); mPageCollection->beginPageSizeChange(); emit refreshed(); mPageCollection->reflow(); mPageCollection->endPageSizeChange(); + mUndoStack->blockCommands( false ); update(); } From eb25ab79009604b922c4eb866ec72cf12beb13fa Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 22:18:11 +1000 Subject: [PATCH 50/56] Fix pdf export page sizes --- src/core/layout/qgslayoutexporter.cpp | 67 ++++++++++++--------------- src/core/layout/qgslayoutexporter.h | 3 +- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 9eafe04db214..cc105d09e882 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -362,10 +362,10 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasterizeWholeImage ); p.end(); -#if 0//TODO - georeferenceOutput( filePath ); -#endif - + if ( mLayout->pageCollection()->pageCount() == 1 ) + { + georeferenceOutput( filePath, nullptr, QRectF(), settings.dpi ); + } return result; } @@ -380,17 +380,7 @@ void QgsLayoutExporter::preparePrintAsPdf( QPrinter &printer, const QString &fil // Also an issue with PDF paper size using QPrinter::NativeFormat on Mac (always outputs portrait letter-size) printer.setOutputFormat( QPrinter::PdfFormat ); -#if 0 //TODO - refreshPageSize(); -#endif - - //must set orientation to portrait before setting paper size, otherwise size will be flipped - //for landscape sized outputs (#11352) - printer.setOrientation( QPrinter::Portrait ); - -#if 0 //TODO - printer.setPaperSize( QSizeF( paperWidth(), paperHeight() ), QPrinter::Millimeter ); -#endif + updatePrinterPageSize( printer, 0 ); // TODO: add option for this in Composer // May not work on Windows or non-X11 Linux. Works fine on Mac using QPrinter::NativeFormat @@ -399,7 +389,7 @@ void QgsLayoutExporter::preparePrintAsPdf( QPrinter &printer, const QString &fil QgsPaintEngineHack::fixEngineFlags( printer.paintEngine() ); } -void QgsLayoutExporter::preparePrint( QPrinter &printer, bool evaluateDDPageSize ) +void QgsLayoutExporter::preparePrint( QPrinter &printer, bool setFirstPageSize ) { printer.setFullPage( true ); printer.setColorMode( QPrinter::Color ); @@ -407,17 +397,10 @@ void QgsLayoutExporter::preparePrint( QPrinter &printer, bool evaluateDDPageSize //set user-defined resolution printer.setResolution( mLayout->context().dpi() ); -#if 0 //TODO - if ( evaluateDDPageSize && ddPageSizeActive() ) + if ( setFirstPageSize ) { - //set data defined page size - refreshPageSize(); - //must set orientation to portrait before setting paper size, otherwise size will be flipped - //for landscape sized outputs (#11352) - printer.setOrientation( QPrinter::Portrait ); - printer.setPaperSize( QSizeF( paperWidth(), paperHeight() ), QPrinter::Millimeter ); + updatePrinterPageSize( printer, 0 ); } -#endif } QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QPrinter &printer ) @@ -437,18 +420,6 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QPrinter &printer ) QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage, double dpi, bool rasterise ) { -#if 0 //TODO - if ( ddPageSizeActive() ) - { - //set the page size again so that data defined page size takes effect - refreshPageSize(); - //must set orientation to portrait before setting paper size, otherwise size will be flipped - //for landscape sized outputs (#11352) - printer.setOrientation( QPrinter::Portrait ); - printer.setPaperSize( QSizeF( paperWidth(), paperHeight() ), QPrinter::Millimeter ); - } -#endif - //layout starts page numbering at 0 int fromPage = ( printer.fromPage() < 1 ) ? 0 : printer.fromPage() - 1; int toPage = ( printer.toPage() < 1 ) ? mLayout->pageCollection()->pageCount() - 1 : printer.toPage() - 1; @@ -462,6 +433,12 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &print { continue; } + + if ( i > 0 ) + { + updatePrinterPageSize( printer, i ); + } + if ( ( pageExported && i > fromPage ) || startNewPage ) { printer.newPage(); @@ -488,6 +465,12 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &print { continue; } + + if ( i > 0 ) + { + updatePrinterPageSize( printer, i ); + } + if ( ( pageExported && i > fromPage ) || startNewPage ) { printer.newPage(); @@ -499,6 +482,16 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &print return Success; } +void QgsLayoutExporter::updatePrinterPageSize( QPrinter &printer, int page ) +{ + //must set orientation to portrait before setting paper size, otherwise size will be flipped + //for landscape sized outputs (#11352) + printer.setOrientation( QPrinter::Portrait ); + QgsLayoutSize pageSize = mLayout->pageCollection()->page( page )->sizeWithUnits(); + QgsLayoutSize pageSizeMM = mLayout->context().measurementConverter().convert( pageSize, QgsUnitTypes::LayoutMillimeters ); + printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter ); +} + double *QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF ®ion, double dpi ) const { if ( !map ) diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 0aae845a8da3..e55a1cb16850 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -331,7 +331,7 @@ class CORE_EXPORT QgsLayoutExporter */ void preparePrintAsPdf( QPrinter &printer, const QString &filePath ); - void preparePrint( QPrinter &printer, bool evaluateDDPageSize = false ); + void preparePrint( QPrinter &printer, bool setFirstPageSize = false ); /** * Convenience function that prepares the printer and prints. @@ -346,6 +346,7 @@ class CORE_EXPORT QgsLayoutExporter */ ExportResult printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage = false, double dpi = -1, bool rasterize = false ); + void updatePrinterPageSize( QPrinter &printer, int page ); friend class TestQgsLayout; From 8d1e7170d5fe0cf2ca339106491ea0d55ea76247 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 22:19:57 +1000 Subject: [PATCH 51/56] Add missing parents to message boxes --- src/app/layout/qgslayoutdesignerdialog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 7dce81cbc660..04174148f4ab 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1304,7 +1304,7 @@ void QgsLayoutDesignerDialog::saveAsTemplate() context.setPathResolver( QgsProject::instance()->pathResolver() ); if ( !currentLayout()->saveAsTemplate( saveFileName, context ) ) { - QMessageBox::warning( nullptr, tr( "Save template" ), tr( "Error creating template file." ) ); + QMessageBox::warning( this, tr( "Save template" ), tr( "Error creating template file." ) ); } } @@ -1444,7 +1444,7 @@ void QgsLayoutDesignerDialog::exportToRaster() if ( memuse > 400 ) // about 4500x4500 { - int answer = QMessageBox::warning( nullptr, tr( "Export layout" ), + int answer = QMessageBox::warning( this, tr( "Export layout" ), tr( "To create an image of %1x%2 requires about %3 MB of memory. Proceed?" ) .arg( width ).arg( height ).arg( memuse ), QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok ); @@ -1548,7 +1548,7 @@ void QgsLayoutDesignerDialog::exportToRaster() break; case QgsLayoutExporter::MemoryError: - QMessageBox::warning( nullptr, tr( "Memory Allocation Error" ), + QMessageBox::warning( this, tr( "Memory Allocation Error" ), tr( "Trying to create image %1 (%2×%3 @ %4dpi ) " "resulted in a memory overflow.\n\n" "Please try a lower resolution or a smaller paper size." ) @@ -1657,7 +1657,7 @@ void QgsLayoutDesignerDialog::exportToPdf() case QgsLayoutExporter::MemoryError: - QMessageBox::warning( nullptr, tr( "Memory Allocation Error" ), + QMessageBox::warning( this, tr( "Memory Allocation Error" ), tr( "Exporting the PDF " "resulted in a memory overflow.\n\n" "Please try a lower resolution or a smaller paper size." ), From 9ab813f344637532d982dc16b1395551559f6239 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 22:23:12 +1000 Subject: [PATCH 52/56] Better memory management --- src/app/layout/qgslayoutdesignerdialog.cpp | 37 +++++++++---------- .../qgslayoutimageexportoptionsdialog.h | 1 + 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 04174148f4ab..ee458d4bb261 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1847,19 +1847,17 @@ void QgsLayoutDesignerDialog::showRasterizationWarning() mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool() ) return; - QgsMessageViewer *m = new QgsMessageViewer( this, QgsGuiUtils::ModalDialogFlags, false ); - m->setWindowTitle( tr( "Composition Effects" ) ); - m->setMessage( tr( "Advanced composition effects such as blend modes or vector layer transparency are enabled in this layout, which cannot be printed as vectors. Printing as a raster is recommended." ), QgsMessageOutput::MessageText ); - m->setCheckBoxText( tr( "Print as raster" ) ); - m->setCheckBoxState( Qt::Checked ); - m->setCheckBoxVisible( true ); - m->showMessage( true ); - - mLayout->setCustomProperty( QStringLiteral( "rasterise" ), m->checkBoxState() == Qt::Checked ); + QgsMessageViewer m( this, QgsGuiUtils::ModalDialogFlags, false ); + m.setWindowTitle( tr( "Composition Effects" ) ); + m.setMessage( tr( "Advanced composition effects such as blend modes or vector layer transparency are enabled in this layout, which cannot be printed as vectors. Printing as a raster is recommended." ), QgsMessageOutput::MessageText ); + m.setCheckBoxText( tr( "Print as raster" ) ); + m.setCheckBoxState( Qt::Checked ); + m.setCheckBoxVisible( true ); + m.showMessage( true ); + + mLayout->setCustomProperty( QStringLiteral( "rasterise" ), m.checkBoxState() == Qt::Checked ); //make sure print as raster checkbox is updated mLayoutPropertiesWidget->updateGui(); - - delete m; } void QgsLayoutDesignerDialog::showForceVectorWarning() @@ -1868,19 +1866,18 @@ void QgsLayoutDesignerDialog::showForceVectorWarning() if ( settings.value( QStringLiteral( "LayoutDesigner/hideForceVectorWarning" ), false, QgsSettings::App ).toBool() ) return; - QgsMessageViewer *m = new QgsMessageViewer( this, QgsGuiUtils::ModalDialogFlags, false ); - m->setWindowTitle( tr( "Force Vector" ) ); - m->setMessage( tr( "This layout has the \"Always export as vectors\" option enabled, but the layout contains effects such as blend modes or vector layer transparency, which cannot be printed as vectors. The generated file will differ from the layout contents." ), QgsMessageOutput::MessageText ); - m->setCheckBoxText( tr( "Never show this message again" ) ); - m->setCheckBoxState( Qt::Unchecked ); - m->setCheckBoxVisible( true ); - m->showMessage( true ); + QgsMessageViewer m( this, QgsGuiUtils::ModalDialogFlags, false ); + m.setWindowTitle( tr( "Force Vector" ) ); + m.setMessage( tr( "This layout has the \"Always export as vectors\" option enabled, but the layout contains effects such as blend modes or vector layer transparency, which cannot be printed as vectors. The generated file will differ from the layout contents." ), QgsMessageOutput::MessageText ); + m.setCheckBoxText( tr( "Never show this message again" ) ); + m.setCheckBoxState( Qt::Unchecked ); + m.setCheckBoxVisible( true ); + m.showMessage( true ); - if ( m->checkBoxState() == Qt::Checked ) + if ( m.checkBoxState() == Qt::Checked ) { settings.setValue( QStringLiteral( "LayoutDesigner/hideForceVectorWarning" ), true, QgsSettings::App ); } - delete m; } void QgsLayoutDesignerDialog::selectItems( const QList items ) diff --git a/src/app/layout/qgslayoutimageexportoptionsdialog.h b/src/app/layout/qgslayoutimageexportoptionsdialog.h index 46653455d123..ce6cac2f064e 100644 --- a/src/app/layout/qgslayoutimageexportoptionsdialog.h +++ b/src/app/layout/qgslayoutimageexportoptionsdialog.h @@ -24,6 +24,7 @@ /** * A dialog for customising the properties of an exported image file. + * \since QGIS 3.0 */ class QgsLayoutImageExportOptionsDialog: public QDialog, private Ui::QgsLayoutImageExportOptionsDialog { From ffb9d0cbcfc96f78b3b90a271affcab3a06f9dd1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 22:25:00 +1000 Subject: [PATCH 53/56] Guard QgsLayoutExporter against nullptrs --- src/core/layout/qgslayoutexporter.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index cc105d09e882..6cc68307a40e 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -166,6 +166,9 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) QImage QgsLayoutExporter::renderRegionToImage( const QRectF ®ion, QSize imageSize, double dpi ) const { + if ( !mLayout ) + return QImage(); + LayoutContextPreviewSettingRestorer restorer( mLayout ); ( void )restorer; @@ -231,6 +234,9 @@ class LayoutContextSettingsRestorer QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &s ) { + if ( !mLayout ) + return PrintError; + ImageExportSettings settings = s; if ( settings.dpi <= 0 ) settings.dpi = mLayout->context().dpi(); @@ -330,6 +336,9 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &s ) { + if ( !mLayout ) + return PrintError; + PdfExportSettings settings = s; if ( settings.dpi <= 0 ) settings.dpi = mLayout->context().dpi(); @@ -597,6 +606,9 @@ void QgsLayoutExporter::writeWorldFile( const QString &worldFileName, double a, bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMap *map, const QRectF &exportRegion, double dpi ) const { + if ( !mLayout ) + return false; + if ( !map ) map = mLayout->referenceMap(); @@ -630,6 +642,9 @@ bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMa void QgsLayoutExporter::computeWorldFileParameters( double &a, double &b, double &c, double &d, double &e, double &f, double dpi ) const { + if ( !mLayout ) + return; + QgsLayoutItemMap *map = mLayout->referenceMap(); if ( !map ) { @@ -646,6 +661,9 @@ void QgsLayoutExporter::computeWorldFileParameters( double &a, double &b, double void QgsLayoutExporter::computeWorldFileParameters( const QRectF &exportRegion, double &a, double &b, double &c, double &d, double &e, double &f, double dpi ) const { + if ( !mLayout ) + return; + // World file parameters : affine transformation parameters from pixel coordinates to map coordinates QgsLayoutItemMap *map = mLayout->referenceMap(); if ( !map ) From 662ec7a77c88ec4a5bc8e76b3ef0fc0e34d4367c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Dec 2017 22:30:37 +1000 Subject: [PATCH 54/56] Use unique_ptr over raw array --- src/core/layout/qgslayoutexporter.cpp | 10 +++++----- src/core/layout/qgslayoutexporter.h | 2 +- tests/src/core/testqgslayout.cpp | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 6cc68307a40e..c9e193d4935b 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -501,7 +501,7 @@ void QgsLayoutExporter::updatePrinterPageSize( QPrinter &printer, int page ) printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter ); } -double *QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF ®ion, double dpi ) const +std::unique_ptr QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, const QRectF ®ion, double dpi ) const { if ( !map ) map = mLayout->referenceMap(); @@ -574,7 +574,7 @@ double *QgsLayoutExporter::computeGeoTransform( const QgsLayoutItemMap *map, con double pixelHeightScale = paperExtent.height() / pageHeightPixels; // transform matrix - double *t = new double[6]; + std::unique_ptr t( new double[6] ); t[0] = X0; t[1] = cosAlpha * pixelWidthScale; t[2] = -sinAlpha * pixelWidthScale; @@ -618,7 +618,7 @@ bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMa if ( dpi < 0 ) dpi = mLayout->context().dpi(); - double *t = computeGeoTransform( map, exportRegion, dpi ); + std::unique_ptr t = computeGeoTransform( map, exportRegion, dpi ); if ( !t ) return false; @@ -628,7 +628,7 @@ bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMa gdal::dataset_unique_ptr outputDS( GDALOpen( file.toLocal8Bit().constData(), GA_Update ) ); if ( outputDS ) { - GDALSetGeoTransform( outputDS.get(), t ); + GDALSetGeoTransform( outputDS.get(), t.get() ); #if 0 //TODO - metadata can be set here, e.g.: GDALSetMetadataItem( outputDS, "AUTHOR", "me", nullptr ); @@ -636,7 +636,7 @@ bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMa GDALSetProjection( outputDS.get(), map->crs().toWkt().toLocal8Bit().constData() ); } CPLSetConfigOption( "GDAL_PDF_DPI", nullptr ); - delete[] t; + return true; } diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index e55a1cb16850..96cc62aba2ad 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -321,7 +321,7 @@ class CORE_EXPORT QgsLayoutExporter * * \see georeferenceOutput() */ - double *computeGeoTransform( const QgsLayoutItemMap *referenceMap = nullptr, const QRectF &exportRegion = QRectF(), double dpi = -1 ) const; + std::unique_ptr computeGeoTransform( const QgsLayoutItemMap *referenceMap = nullptr, const QRectF &exportRegion = QRectF(), double dpi = -1 ) const; //! Write a world file void writeWorldFile( const QString &fileName, double a, double b, double c, double d, double e, double f ) const; diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index 5f993f11e50f..42c94c8e49ab 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -762,7 +762,7 @@ void TestQgsLayout::georeference() QgsLayoutExporter exporter( &l ); // no map - double *t = exporter.computeGeoTransform( nullptr ); + std::unique_ptr< double [] > t = exporter.computeGeoTransform( nullptr ); QVERIFY( !t ); QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); @@ -777,7 +777,7 @@ void TestQgsLayout::georeference() QGSCOMPARENEAR( t[3], 3050, 1 ); QGSCOMPARENEAR( t[4], 0.0, 4 * DBL_EPSILON ); QGSCOMPARENEAR( t[5], -0.211694, 0.0001 ); - delete[] t; + t.reset(); // don't specify map l.setReferenceMap( map ); @@ -788,7 +788,7 @@ void TestQgsLayout::georeference() QGSCOMPARENEAR( t[3], 3050, 1 ); QGSCOMPARENEAR( t[4], 0.0, 4 * DBL_EPSILON ); QGSCOMPARENEAR( t[5], -0.211694, 0.0001 ); - delete[] t; + t.reset(); // specify extent t = exporter.computeGeoTransform( map, QRectF( 70, 100, 50, 60 ) ); @@ -798,7 +798,7 @@ void TestQgsLayout::georeference() QGSCOMPARENEAR( t[3], 2800, 1 ); QGSCOMPARENEAR( t[4], 0.0, 4 * DBL_EPSILON ); QGSCOMPARENEAR( t[5], -0.211864, 0.0001 ); - delete[] t; + t.reset(); // specify dpi t = exporter.computeGeoTransform( map, QRectF(), 75 ); @@ -808,7 +808,7 @@ void TestQgsLayout::georeference() QGSCOMPARENEAR( t[3], 3050.0, 1 ); QGSCOMPARENEAR( t[4], 0.0, 4 * DBL_EPSILON ); QGSCOMPARENEAR( t[5], -0.846774, 0.0001 ); - delete[] t; + t.reset(); // rotation map->setMapRotation( 45 ); @@ -819,7 +819,7 @@ void TestQgsLayout::georeference() QGSCOMPARENEAR( t[3], 2761.611652, 1 ); QGSCOMPARENEAR( t[4], 0.14969, 0.0001 ); QGSCOMPARENEAR( t[5], -0.14969, 0.0001 ); - delete[] t; + t.reset(); } From 831732f3a31fccaed103cc88757582857aad4194 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 16 Dec 2017 08:16:52 +1000 Subject: [PATCH 55/56] Sipify --- python/core/layout/qgslayout.sip | 51 ++++-- python/core/layout/qgslayoutexporter.sip | 149 +++++++++--------- python/core/layout/qgslayoutitem.sip | 25 ++- python/core/layout/qgslayoutitemmap.sip | 9 +- .../core/layout/qgslayoutpagecollection.sip | 51 ++++-- python/core/layout/qgslayoututils.sip | 7 +- .../gui/layout/qgslayoutdesignerinterface.sip | 3 +- src/app/layout/qgslayoutdesignerdialog.cpp | 6 +- src/app/layout/qgslayoutpropertieswidget.cpp | 12 +- src/app/layout/qgslayoutpropertieswidget.h | 2 +- src/core/layout/qgslayoutexporter.cpp | 4 +- src/core/layout/qgslayoutexporter.h | 2 + src/core/layout/qgslayoutitem.h | 2 +- src/core/layout/qgslayoutitemmap.cpp | 2 +- src/ui/layout/qgslayoutwidgetbase.ui | 2 +- tests/src/core/testqgslayoutmap.cpp | 4 +- 16 files changed, 188 insertions(+), 143 deletions(-) diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index eb3e5cd12035..0f1d4f461805 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -13,6 +13,12 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoOb %Docstring Base class for layouts, which can contain items such as maps, labels, scalebars, etc. +While the raw QGraphicsScene API can be used to render the contents of a QgsLayout +to a QPainter, it is recommended to instead use a QgsLayoutExporter to handle rendering +layouts instead. QgsLayoutExporter automatically takes care of the intracacies of +preparing the layout and paint devices for correct exports, respecting various +user settings such as the layout context DPI. + .. versionadded:: 3.0 %End @@ -72,12 +78,6 @@ relations and various other bits. It is never null. QgsLayoutModel *itemsModel(); %Docstring Returns the items model attached to the layout. -%End - - QgsLayoutExporter &exporter(); -%Docstring -Returns the layout's exporter, which is used for rendering the layout and exporting -to various formats. %End QString name() const; @@ -182,7 +182,7 @@ z order list. This should be called after any stacking changes which deferred z-order updates. %End - QgsLayoutItem *itemByUuid( const QString &uuid, bool includeTemplateUuids = false ); + QgsLayoutItem *itemByUuid( const QString &uuid, bool includeTemplateUuids = false ) const; %Docstring Returns the layout item with matching ``uuid`` unique identifier, or a None if a matching item could not be found. @@ -387,8 +387,25 @@ Return list of keys stored in custom properties for the layout. %End QgsLayoutItemMap *referenceMap() const; +%Docstring +Returns the map item which will be used to generate corresponding world files when the +layout is exported. If no map was explicitly set via setReferenceMap(), the largest +map in the layout will be returned (or None if there are no maps in the layout). + +.. seealso:: :py:func:`setReferenceMap()` + +.. seealso:: :py:func:`generateWorldFile()` +%End void setReferenceMap( QgsLayoutItemMap *map ); +%Docstring +Sets the ``map`` item which will be used to generate corresponding world files when the +layout is exported. + +.. seealso:: :py:func:`referenceMap()` + +.. seealso:: :py:func:`setGenerateWorldFile()` +%End QgsLayoutPageCollection *pageCollection(); %Docstring @@ -406,6 +423,20 @@ and other cosmetic items. :param margin: optional marginal (in percent, e.g., 0.05 = 5% ) to add around items :return: layout bounds, in layout units. + +.. seealso:: :py:func:`pageItemBounds()` +%End + + QRectF pageItemBounds( int page, bool visibleOnly = false ) const; +%Docstring +Returns the bounding box of the items contained on a specified ``page``. +A page number of 0 represents the first page in the layout. + +Set ``visibleOnly`` to true to only include visible items. + +The returned bounds are in layout units. + +.. seealso:: :py:func:`layoutBounds()` %End void addLayoutItem( QgsLayoutItem *item /Transfer/ ); @@ -549,9 +580,9 @@ Updates the scene bounds of the layout. void changed(); %Docstring - Is emitted when properties of the layout change. This signal is only - emitted for settings directly managed by the layout, and is not emitted - when child items change. +Is emitted when properties of the layout change. This signal is only +emitted for settings directly managed by the layout, and is not emitted +when child items change. %End void variablesChanged(); diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index c4bad6876913..0e9b9e86bb20 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -53,8 +53,7 @@ Constructor for QgsLayoutExporter, for the specified ``layout``. QgsLayout *layout() const; %Docstring - Returns the layout linked to this exporter. - :rtype: QgsLayout +Returns the layout linked to this exporter. %End void renderPage( QPainter *painter, int page ) const; @@ -69,23 +68,23 @@ are 0 based, such that the first page in a layout is page 0. QImage renderPageToImage( int page, QSize imageSize = QSize(), double dpi = 0 ) const; %Docstring - Renders a full page to an image. +Renders a full page to an image. - The ``page`` argument specifies the page number to render. Page numbers - are 0 based, such that the first page in a layout is page 0. +The ``page`` argument specifies the page number to render. Page numbers +are 0 based, such that the first page in a layout is page 0. - The optional ``imageSize`` parameter can specify the target image size, in pixels. - It is the caller's responsibility to ensure that the ratio of the target image size - matches the ratio of the corresponding layout page size. +The optional ``imageSize`` parameter can specify the target image size, in pixels. +It is the caller's responsibility to ensure that the ratio of the target image size +matches the ratio of the corresponding layout page size. - The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print - resolution. This parameter has no effect if ``imageSize`` is specified. +The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print +resolution. This parameter has no effect if ``imageSize`` is specified. - Returns the rendered image, or a null QImage if the image does not fit into available memory. +Returns the rendered image, or a null QImage if the image does not fit into available memory. .. seealso:: :py:func:`renderPage()` + .. seealso:: :py:func:`renderRegionToImage()` - :rtype: QImage %End void renderRegion( QPainter *painter, const QRectF ®ion ) const; @@ -94,26 +93,27 @@ Renders a ``region`` from the layout to a ``painter``. This method can be used to render sections of pages rather than full pages. .. seealso:: :py:func:`renderPage()` + .. seealso:: :py:func:`renderRegionToImage()` %End QImage renderRegionToImage( const QRectF ®ion, QSize imageSize = QSize(), double dpi = 0 ) const; %Docstring - Renders a ``region`` of the layout to an image. This method can be used to render - sections of pages rather than full pages. +Renders a ``region`` of the layout to an image. This method can be used to render +sections of pages rather than full pages. - The optional ``imageSize`` parameter can specify the target image size, in pixels. - It is the caller's responsibility to ensure that the ratio of the target image size - matches the ratio of the specified region of the layout. +The optional ``imageSize`` parameter can specify the target image size, in pixels. +It is the caller's responsibility to ensure that the ratio of the target image size +matches the ratio of the specified region of the layout. - The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print - resolution. This parameter has no effect if ``imageSize`` is specified. +The ``dpi`` parameter is an optional dpi override. Set to 0 to use the default layout print +resolution. This parameter has no effect if ``imageSize`` is specified. - Returns the rendered image, or a null QImage if the image does not fit into available memory. +Returns the rendered image, or a null QImage if the image does not fit into available memory. .. seealso:: :py:func:`renderRegion()` + .. seealso:: :py:func:`renderPageToImage()` - :rtype: QImage %End @@ -139,63 +139,62 @@ Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. QSize imageSize; %Docstring - Manual size in pixels for output image. If imageSize is not - set then it will be automatically calculated based on the - output dpi and layout size. +Manual size in pixels for output image. If imageSize is not +set then it will be automatically calculated based on the +output dpi and layout size. - If cropToContents is true then imageSize has no effect. +If cropToContents is true then imageSize has no effect. - Be careful when specifying manual sizes if pages in the layout - have differing sizes! It's likely not going to give a reasonable - output in this case, and the automatic dpi-based image size should be - used instead. +Be careful when specifying manual sizes if pages in the layout +have differing sizes! It's likely not going to give a reasonable +output in this case, and the automatic dpi-based image size should be +used instead. %End bool cropToContents; %Docstring - Set to true if image should be cropped so only parts of the layout - containing items are exported. +Set to true if image should be cropped so only parts of the layout +containing items are exported. %End QgsMargins cropMargins; %Docstring - Crop to content margins, in pixels. These margins will be added - to the bounds of the exported layout if cropToContents is true. +Crop to content margins, in pixels. These margins will be added +to the bounds of the exported layout if cropToContents is true. %End QList< int > pages; %Docstring - List of specific pages to export, or an empty list to - export all pages. +List of specific pages to export, or an empty list to +export all pages. - Page numbers are 0 index based, so the first page in the - layout corresponds to page 0. +Page numbers are 0 index based, so the first page in the +layout corresponds to page 0. %End bool generateWorldFile; %Docstring - Set to true to generate an external world file alongside - exported images. +Set to true to generate an external world file alongside +exported images. %End QgsLayoutContext::Flags flags; %Docstring - Layout context flags, which control how the export will be created. +Layout context flags, which control how the export will be created. %End }; ExportResult exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ); %Docstring - Exports the layout to the a ``filePath``, using the specified export ``settings``. +Exports the layout to the a ``filePath``, using the specified export ``settings``. - If the layout is a multi-page layout, then filenames for each page will automatically - be generated by appending "_1", "_2", etc to the image file's base name. +If the layout is a multi-page layout, then filenames for each page will automatically +be generated by appending "_1", "_2", etc to the image file's base name. - Returns a result code indicating whether the export was successful or an - error was encountered. If an error code is returned, errorFile() can be called - to determine the filename for the export which encountered the error. - :rtype: ExportResult +Returns a result code indicating whether the export was successful or an +error was encountered. If an error code is returned, errorFile() can be called +to determine the filename for the export which encountered the error. %End struct PdfExportSettings @@ -212,85 +211,81 @@ Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. bool rasterizeWholeImage; %Docstring - Set to true to force whole layout to be rasterized while exporting. +Set to true to force whole layout to be rasterized while exporting. - This option is mutually exclusive with forceVectorOutput. +This option is mutually exclusive with forceVectorOutput. %End bool forceVectorOutput; %Docstring - Set to true to force vector object exports, even when the resultant appearance will differ - from the layout. If false, some items may be rasterized in order to maintain their - correct appearance in the output. +Set to true to force vector object exports, even when the resultant appearance will differ +from the layout. If false, some items may be rasterized in order to maintain their +correct appearance in the output. - This option is mutually exclusive with rasterizeWholeImage. +This option is mutually exclusive with rasterizeWholeImage. %End QgsLayoutContext::Flags flags; %Docstring - Layout context flags, which control how the export will be created. +Layout context flags, which control how the export will be created. %End }; ExportResult exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &settings ); %Docstring - Exports the layout as a PDF to the a ``filePath``, using the specified export ``settings``. +Exports the layout as a PDF to the a ``filePath``, using the specified export ``settings``. - Returns a result code indicating whether the export was successful or an - error was encountered. - :rtype: ExportResult +Returns a result code indicating whether the export was successful or an +error was encountered. %End QString errorFile() const; %Docstring - Returns the file name corresponding to the last error encountered during - an export. - :rtype: str +Returns the file name corresponding to the last error encountered during +an export. %End bool georeferenceOutput( const QString &file, QgsLayoutItemMap *referenceMap = 0, const QRectF &exportRegion = QRectF(), double dpi = -1 ) const; %Docstring - Georeferences a ``file`` (image of PDF) exported from the layout. +Georeferences a ``file`` (image of PDF) exported from the layout. - The ``referenceMap`` argument specifies a map item to use for georeferencing. If left as None, the - default layout QgsLayout.referenceMap() will be used. +The ``referenceMap`` argument specifies a map item to use for georeferencing. If left as None, the +default layout QgsLayout.referenceMap() will be used. - The ``exportRegion`` argument can be set to a valid rectangle to indicate that only part of the layout was - exported. +The ``exportRegion`` argument can be set to a valid rectangle to indicate that only part of the layout was +exported. - Similarly, the ``dpi`` can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. +Similarly, the ``dpi`` can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. - The function will return true if the output was successfully georeferenced. +The function will return true if the output was successfully georeferenced. .. seealso:: :py:func:`computeGeoTransform()` - :rtype: bool %End void computeWorldFileParameters( double &a, double &b, double &c, double &d, double &e, double &f, double dpi = -1 ) const; %Docstring - Compute world file parameters. Assumes the whole page containing the reference map item - will be exported. +Compute world file parameters. Assumes the whole page containing the reference map item +will be exported. - The ``dpi`` argument can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. +The ``dpi`` argument can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. %End void computeWorldFileParameters( const QRectF ®ion, double &a, double &b, double &c, double &d, double &e, double &f, double dpi = -1 ) const; %Docstring - Computes the world file parameters for a specified ``region`` of the layout. +Computes the world file parameters for a specified ``region`` of the layout. - The ``dpi`` argument can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. +The ``dpi`` argument can be set to the actual DPI of exported file, or left as -1 to use the layout's default DPI. %End protected: virtual QString generateFileName( const PageExportDetails &details ) const; %Docstring - Generates the file name for a page during export. +Generates the file name for a page during export. - Subclasses can override this method to customise page file naming. - :rtype: str +Subclasses can override this method to customise page file naming. %End }; diff --git a/python/core/layout/qgslayoutitem.sip b/python/core/layout/qgslayoutitem.sip index 41048d0595ac..9079bdfb88fa 100644 --- a/python/core/layout/qgslayoutitem.sip +++ b/python/core/layout/qgslayoutitem.sip @@ -822,22 +822,21 @@ Sets whether the item should be excluded from composer exports and prints. virtual bool containsAdvancedEffects() const; %Docstring - Returns true if the item contains contents with blend modes or transparency - effects which can only be reproduced by rastering the item. +Returns true if the item contains contents with blend modes or transparency +effects which can only be reproduced by rastering the item. - Subclasses should ensure that implemented overrides of this method - also check the base class result. +Subclasses should ensure that implemented overrides of this method +also check the base class result. .. seealso:: :py:func:`requiresRasterization()` - :rtype: bool %End virtual bool requiresRasterization() const; %Docstring - Returns true if the item is drawn in such a way that forces the whole layout - to be rasterised when exporting to vector formats. +Returns true if the item is drawn in such a way that forces the whole layout +to be rasterized when exporting to vector formats. + .. seealso:: :py:func:`containsAdvancedEffects()` - :rtype: bool %End virtual double estimatedFrameBleed() const; @@ -920,6 +919,11 @@ Cancels the current item command and discards it. .. seealso:: :py:func:`beginCommand()` .. seealso:: :py:func:`endCommand()` +%End + + bool shouldDrawItem() const; +%Docstring +Returns whether the item should be drawn in the current context. %End public slots: @@ -1161,11 +1165,6 @@ in finalizeRestoreFromXml(), not readPropertiesFromElement(). .. seealso:: :py:func:`writePropertiesToElement()` .. seealso:: :py:func:`readXml()` -%End - - bool shouldDrawItem() const; -%Docstring -Returns whether the item should be drawn in the current context. %End QgsLayoutSize applyDataDefinedSize( const QgsLayoutSize &size ); diff --git a/python/core/layout/qgslayoutitemmap.sip b/python/core/layout/qgslayoutitemmap.sip index fb8c4c4f3cbd..b47b1475f527 100644 --- a/python/core/layout/qgslayoutitemmap.sip +++ b/python/core/layout/qgslayoutitemmap.sip @@ -62,7 +62,6 @@ The caller takes responsibility for deleting the returned object. virtual void setFrameStrokeWidth( const QgsLayoutMeasurement &width ); - double scale() const; %Docstring Returns the map scale. @@ -280,10 +279,10 @@ Sets preset name for map rendering. See followVisibilityPresetName() for more de Returns true if the map contains a WMS layer. %End - bool containsAdvancedEffects() const; -%Docstring -Returns true if the map contains layers with blend modes or flattened layers for vectors -%End + virtual bool requiresRasterization() const; + + virtual bool containsAdvancedEffects() const; + void setMapRotation( double rotation ); %Docstring diff --git a/python/core/layout/qgslayoutpagecollection.sip b/python/core/layout/qgslayoutpagecollection.sip index ce2510b792bb..44dbfe2d7c20 100644 --- a/python/core/layout/qgslayoutpagecollection.sip +++ b/python/core/layout/qgslayoutpagecollection.sip @@ -85,6 +85,8 @@ Returns a list of the page numbers which are visible within the specified %Docstring Returns whether a given ``page`` index is empty, ie, it contains no items except for the background paper item. + +.. seealso:: :py:func:`shouldExportPage()` %End QList< QgsLayoutItem *> itemsOnPage( int page ) const; @@ -92,6 +94,14 @@ paper item. Returns a list of layout items on the specified ``page`` index. %End + + bool shouldExportPage( int page ) const; +%Docstring +Returns whether the specified ``page`` number should be included in exports of the layouts. + +.. seealso:: :py:func:`pageIsEmpty()` +%End + void addPage( QgsLayoutItemPage *page /Transfer/ ); %Docstring Adds a ``page`` to the collection. Ownership of the ``page`` is transferred @@ -188,17 +198,19 @@ Returns the symbol to use for drawing pages in the collection. void beginPageSizeChange(); %Docstring - Should be called before changing any page item sizes, and followed by a call to - endPageSizeChange(). If page size changes are wrapped in these calls, then items - will maintain their same relative position on pages after the page sizes are updated. +Should be called before changing any page item sizes, and followed by a call to +endPageSizeChange(). If page size changes are wrapped in these calls, then items +will maintain their same relative position on pages after the page sizes are updated. + .. seealso:: :py:func:`endPageSizeChange()` %End void endPageSizeChange(); %Docstring - Should be called after changing any page item sizes, and preceded by a call to - beginPageSizeChange(). If page size changes are wrapped in these calls, then items - will maintain their same relative position on pages after the page sizes are updated. +Should be called after changing any page item sizes, and preceded by a call to +beginPageSizeChange(). If page size changes are wrapped in these calls, then items +will maintain their same relative position on pages after the page sizes are updated. + .. seealso:: :py:func:`beginPageSizeChange()` %End @@ -212,15 +224,24 @@ for page size/orientation change. %Docstring Returns the maximum width of pages in the collection. The returned value is in layout units. + +.. seealso:: :py:func:`maximumPageSize()` +%End + + QSizeF maximumPageSize() const; +%Docstring +Returns the maximum size of any page in the collection, by area. The returned value +is in layout units. + +.. seealso:: :py:func:`maximumPageWidth()` %End bool hasUniformPageSizes() const; %Docstring - Returns true if the layout has uniform page sizes, e.g. all pages are the same size. +Returns true if the layout has uniform page sizes, e.g. all pages are the same size. - This method does not consider differing units as non-uniform sizes, only the actual - physical size of the pages. - :rtype: bool +This method does not consider differing units as non-uniform sizes, only the actual +physical size of the pages. %End int pageNumberForPoint( QPointF point ) const; @@ -309,12 +330,12 @@ Returns the size of the page shadow, in layout units. void resizeToContents( const QgsMargins &margins, QgsUnitTypes::LayoutUnit marginUnits ); %Docstring - Resizes the layout to a single page which fits the current contents of the layout. +Resizes the layout to a single page which fits the current contents of the layout. - Calling this method resets the number of pages to 1, with the size set to the - minimum size required to fit all existing layout items. Items will also be - repositioned so that the new top-left bounds of the layout is at the point - (marginLeft, marginTop). An optional margin can be specified. +Calling this method resets the number of pages to 1, with the size set to the +minimum size required to fit all existing layout items. Items will also be +repositioned so that the new top-left bounds of the layout is at the point +(marginLeft, marginTop). An optional margin can be specified. %End virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const; diff --git a/python/core/layout/qgslayoututils.sip b/python/core/layout/qgslayoututils.sip index 8777962853aa..b0e6c82a0b55 100644 --- a/python/core/layout/qgslayoututils.sip +++ b/python/core/layout/qgslayoututils.sip @@ -216,10 +216,9 @@ the a specified ``rotation`` amount. static QgsLayoutItemPage::Orientation decodePaperOrientation( const QString &string, bool &ok ); %Docstring - Decodes a ``string`` representing a paper orientation and returns the - decoded orientation. - If the string was correctly decoded, ``ok`` will be set to true. - :rtype: QgsLayoutItemPage.Orientation +Decodes a ``string`` representing a paper orientation and returns the +decoded orientation. +If the string was correctly decoded, ``ok`` will be set to true. %End }; diff --git a/python/gui/layout/qgslayoutdesignerinterface.sip b/python/gui/layout/qgslayoutdesignerinterface.sip index 1e4db3a83c39..bd3dfdda4790 100644 --- a/python/gui/layout/qgslayoutdesignerinterface.sip +++ b/python/gui/layout/qgslayoutdesignerinterface.sip @@ -48,8 +48,7 @@ Returns the layout view utilized by the designer. virtual QgsMessageBar *messageBar() = 0; %Docstring - Returns the designer's message bar. - :rtype: QgsMessageBar +Returns the designer's message bar. %End virtual void selectItems( const QList< QgsLayoutItem * > items ) = 0; diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index ee458d4bb261..7ab0df3d46ae 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1624,7 +1624,7 @@ void QgsLayoutDesignerDialog::exportToPdf() QApplication::setOverrideCursor( Qt::BusyCursor ); QgsLayoutExporter::PdfExportSettings pdfSettings; - pdfSettings.rasterizeWholeImage = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); + pdfSettings.rasterizeWholeImage = mLayout->customProperty( QStringLiteral( "rasterize" ), false ).toBool(); pdfSettings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); // force a refresh, to e.g. update data defined properties, tables, etc @@ -1843,7 +1843,7 @@ bool QgsLayoutDesignerDialog::containsAdvancedEffects() const void QgsLayoutDesignerDialog::showRasterizationWarning() { - if ( mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool() || + if ( mLayout->customProperty( QStringLiteral( "rasterize" ), false ).toBool() || mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool() ) return; @@ -1855,7 +1855,7 @@ void QgsLayoutDesignerDialog::showRasterizationWarning() m.setCheckBoxVisible( true ); m.showMessage( true ); - mLayout->setCustomProperty( QStringLiteral( "rasterise" ), m.checkBoxState() == Qt::Checked ); + mLayout->setCustomProperty( QStringLiteral( "rasterize" ), m.checkBoxState() == Qt::Checked ); //make sure print as raster checkbox is updated mLayoutPropertiesWidget->updateGui(); } diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index 54d14a9ca180..8ebd70fd108f 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -58,7 +58,7 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout mGenerateWorldFileCheckBox->setChecked( exportWorldFile ); connect( mGenerateWorldFileCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::worldFileToggled ); - connect( mRasterizeCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::rasteriseToggled ); + connect( mRasterizeCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::rasterizeToggled ); connect( mForceVectorCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::forceVectorToggled ); mTopMarginSpinBox->setValue( topMargin ); @@ -92,13 +92,13 @@ void QgsLayoutPropertiesWidget::updateGui() whileBlocking( mReferenceMapComboBox )->setItem( mLayout->referenceMap() ); whileBlocking( mResolutionSpinBox )->setValue( mLayout->context().dpi() ); - bool rasterise = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool(); - whileBlocking( mRasterizeCheckBox )->setChecked( rasterise ); + bool rasterize = mLayout->customProperty( QStringLiteral( "rasterize" ), false ).toBool(); + whileBlocking( mRasterizeCheckBox )->setChecked( rasterize ); bool forceVectors = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); whileBlocking( mForceVectorCheckBox )->setChecked( forceVectors ); - if ( rasterise ) + if ( rasterize ) { mForceVectorCheckBox->setChecked( false ); mForceVectorCheckBox->setEnabled( false ); @@ -208,9 +208,9 @@ void QgsLayoutPropertiesWidget::worldFileToggled() mLayout->setCustomProperty( QStringLiteral( "exportWorldFile" ), mGenerateWorldFileCheckBox->isChecked() ); } -void QgsLayoutPropertiesWidget::rasteriseToggled() +void QgsLayoutPropertiesWidget::rasterizeToggled() { - mLayout->setCustomProperty( QStringLiteral( "rasterise" ), mRasterizeCheckBox->isChecked() ); + mLayout->setCustomProperty( QStringLiteral( "rasterize" ), mRasterizeCheckBox->isChecked() ); if ( mRasterizeCheckBox->isChecked() ) { diff --git a/src/app/layout/qgslayoutpropertieswidget.h b/src/app/layout/qgslayoutpropertieswidget.h index e65bdabed0bd..642bb243a85b 100644 --- a/src/app/layout/qgslayoutpropertieswidget.h +++ b/src/app/layout/qgslayoutpropertieswidget.h @@ -46,7 +46,7 @@ class QgsLayoutPropertiesWidget: public QgsPanelWidget, private Ui::QgsLayoutWid void referenceMapChanged( QgsLayoutItem *item ); void dpiChanged( int value ); void worldFileToggled(); - void rasteriseToggled(); + void rasterizeToggled(); void forceVectorToggled(); private: diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index c9e193d4935b..7e198c458e9f 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -427,14 +427,14 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QPrinter &printer ) return Success; } -QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage, double dpi, bool rasterise ) +QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage, double dpi, bool rasterize ) { //layout starts page numbering at 0 int fromPage = ( printer.fromPage() < 1 ) ? 0 : printer.fromPage() - 1; int toPage = ( printer.toPage() < 1 ) ? mLayout->pageCollection()->pageCount() - 1 : printer.toPage() - 1; bool pageExported = false; - if ( rasterise ) + if ( rasterize ) { for ( int i = fromPage; i <= toPage; ++i ) { diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 96cc62aba2ad..bd3f49b15009 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -343,6 +343,8 @@ class CORE_EXPORT QgsLayoutExporter * \param printer QPrinter destination * \param painter QPainter source * \param startNewPage set to true to begin the print on a new page + * \param dpi set to a value > 0 to manually override the layout's default dpi + * \param rasterize set to true to force print as a raster image */ ExportResult printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage = false, double dpi = -1, bool rasterize = false ); diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index c4e70cd3927f..2067c31e9f11 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -755,7 +755,7 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt /** * Returns true if the item is drawn in such a way that forces the whole layout - * to be rasterised when exporting to vector formats. + * to be rasterized when exporting to vector formats. * \see containsAdvancedEffects() */ virtual bool requiresRasterization() const; diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 3818744fd670..6a2d1a0d4398 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -834,7 +834,7 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem if ( containsAdvancedEffects() && ( !mLayout || !( mLayout->context().flags() & QgsLayoutContext::FlagForceVectorOutput ) ) ) { - // rasterise + // rasterize double destinationDpi = style->matrix.m11() * 25.4; double layoutUnitsInInches = mLayout ? mLayout->convertFromLayoutUnits( 1, QgsUnitTypes::LayoutInches ).length() : 1; int widthInPixels = std::round( boundingRect().width() * layoutUnitsInInches * destinationDpi ); diff --git a/src/ui/layout/qgslayoutwidgetbase.ui b/src/ui/layout/qgslayoutwidgetbase.ui index 0bb37df5afc0..58f9b5502f5a 100644 --- a/src/ui/layout/qgslayoutwidgetbase.ui +++ b/src/ui/layout/qgslayoutwidgetbase.ui @@ -262,7 +262,7 @@ - If checked, the layout will always be kept as vector objects when exported to a compatible format, even if the appearance of the resultant file does not match the layouts settings. If unchecked, some elements in the layout may be rasterised in order to keep their appearance intact. + If checked, the layout will always be kept as vector objects when exported to a compatible format, even if the appearance of the resultant file does not match the layouts settings. If unchecked, some elements in the layout may be rasterized in order to keep their appearance intact. Always export as vectors diff --git a/tests/src/core/testqgslayoutmap.cpp b/tests/src/core/testqgslayoutmap.cpp index dbdf563d376b..4fce7944437d 100644 --- a/tests/src/core/testqgslayoutmap.cpp +++ b/tests/src/core/testqgslayoutmap.cpp @@ -444,7 +444,7 @@ void TestQgsLayoutMap::dataDefinedStyles() void TestQgsLayoutMap::rasterized() { - // test a map which must be rasterised + // test a map which must be rasterized QgsLayout l( QgsProject::instance() ); l.initializeDefaults(); @@ -496,7 +496,7 @@ void TestQgsLayoutMap::rasterized() QVERIFY( checker.testLayout( mReport, 0, 0 ) ); // try rendering again, without requiring rasterization, for comparison - // (we can use the same test image, because CompositionMode_Darken doesn't actually have any noticable + // (we can use the same test image, because CompositionMode_Darken doesn't actually have any noticeable // rendering differences for the black grid!) grid->setBlendMode( QPainter::CompositionMode_SourceOver ); QVERIFY( !map->containsAdvancedEffects() ); From 492f9ea18c185f56d87c4fdf4b887e63b015452d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 16 Dec 2017 11:25:22 +1000 Subject: [PATCH 56/56] Update test mask images --- .../expected_layoutmap_rasterized_mask.png | Bin 40729 -> 44417 bytes .../fedora/expected_composermap_grid_mask.png | Bin 0 -> 5486 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/testdata/control_images/composer_mapgrid/expected_composermap_grid/fedora/expected_composermap_grid_mask.png diff --git a/tests/testdata/control_images/composer_map/expected_layoutmap_rasterized/expected_layoutmap_rasterized_mask.png b/tests/testdata/control_images/composer_map/expected_layoutmap_rasterized/expected_layoutmap_rasterized_mask.png index b149afd792e620537309f1c2ca83f310fc9226a0..3a95170e1c3304cf4c0555f76e166505e43cfef7 100644 GIT binary patch literal 44417 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 KxvX