diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index aed88bae547..69ba29edbca 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -158,6 +158,7 @@ %Include composer/qgscomposertexttable.sip %Include composer/qgspaperitem.sip %Include layout/qgslayoutaligner.sip +%Include layout/qgslayoutexporter.sip %Include layout/qgslayoutgridsettings.sip %Include layout/qgslayoutmeasurement.sip %Include layout/qgslayoutmeasurementconverter.sip diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index 1fff7e217a3..702cc84bf09 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -62,6 +62,13 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoOb :rtype: QgsLayoutModel %End + QgsLayoutExporter &exporter(); +%Docstring + Returns the layout's exporter, which is used for rendering the layout and exporting + to various formats. + :rtype: QgsLayoutExporter +%End + QString name() const; %Docstring Returns the layout's name. diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip new file mode 100644 index 00000000000..0d6b2604491 --- /dev/null +++ b/python/core/layout/qgslayoutexporter.sip @@ -0,0 +1,57 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutexporter.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsLayoutExporter +{ +%Docstring + Handles rendering and exports of layouts to various formats. +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgslayoutexporter.h" +%End + public: + + QgsLayoutExporter( QgsLayout *layout ); +%Docstring + Constructor for QgsLayoutExporter, for the specified ``layout``. +%End + + void renderPage( QPainter *painter, int page ); +%Docstring + Renders a full page to a destination ``painter``. + + 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. + +.. seealso:: renderRect() +%End + + void renderRegion( QPainter *painter, const QRectF ®ion ); +%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:: renderPage() +%End + +}; + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutexporter.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/layout/qgslayoutitem.sip b/python/core/layout/qgslayoutitem.sip index 6ff5314a109..929bda95766 100644 --- a/python/core/layout/qgslayoutitem.sip +++ b/python/core/layout/qgslayoutitem.sip @@ -589,14 +589,6 @@ class QgsLayoutItem : QgsLayoutObject, QGraphicsRectItem, QgsLayoutUndoObjectInt Draws the background for the item. %End - bool isPreviewRender( QPainter *painter ) const; -%Docstring - Returns true if the render to the specified ``painter`` is a preview render, - i.e. is being rendered inside a QGraphicsView widget as opposed to a destination - device (such as an image). - :rtype: bool -%End - virtual void setFixedSize( const QgsLayoutSize &size ); %Docstring Sets a fixed ``size`` for the layout item, which prevents it from being freely diff --git a/python/core/layout/qgslayoututils.sip b/python/core/layout/qgslayoututils.sip index d43b4d4a184..7b1ccfba144 100644 --- a/python/core/layout/qgslayoututils.sip +++ b/python/core/layout/qgslayoututils.sip @@ -72,6 +72,15 @@ class QgsLayoutUtils :rtype: float %End + + static bool isPreviewRender( QPainter *painter ); +%Docstring + Returns true if the render to the specified ``painter`` is a preview render, + i.e. is being rendered inside a QGraphicsView widget as opposed to a destination + device (such as an image). + :rtype: bool +%End + }; /************************************************************************ diff --git a/python/core/qgsmultirenderchecker.sip b/python/core/qgsmultirenderchecker.sip index 5f90af98883..4d67c590fde 100644 --- a/python/core/qgsmultirenderchecker.sip +++ b/python/core/qgsmultirenderchecker.sip @@ -152,6 +152,44 @@ class QgsCompositionChecker : QgsMultiRenderChecker }; +class QgsLayoutChecker : QgsMultiRenderChecker +{ +%Docstring + Renders a layout to an image and compares with an expected output +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsmultirenderchecker.h" +%End + public: + + QgsLayoutChecker( const QString &testName, QgsLayout *layout ); +%Docstring + Constructor for QgsLayoutChecker. +%End + + void setSize( QSize size ); +%Docstring + Sets the output (reference) image ``size``. +%End + + bool runTest( QString &report, int page = 0, int pixelDiff = 0 ); +%Docstring + Runs a render check on the layout, adding results to the specified ``report``. + + The maximum number of allowable pixels differing from the reference image is + specified via the ``pixelDiff`` argument. + + The page number is specified via ``page``, where 0 corresponds to the first + page in the layout. + + Returns false if the rendered layout differs from the expected reference image. + :rtype: bool +%End + +}; + %End diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index dc036abab78..073fd7e9b57 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -363,6 +363,7 @@ SET(QGIS_CORE_SRCS layout/qgslayoutaligner.cpp layout/qgslayoutcontext.cpp layout/qgslayouteffect.cpp + layout/qgslayoutexporter.cpp layout/qgslayoutgridsettings.cpp layout/qgslayoutguidecollection.cpp layout/qgslayoutitem.cpp @@ -975,6 +976,7 @@ SET(QGIS_CORE_HDRS composer/qgspaperitem.h layout/qgslayoutaligner.h + layout/qgslayoutexporter.h layout/qgslayoutgridsettings.h layout/qgslayoutitemundocommand.h layout/qgslayoutmeasurement.h diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index d2b07cdf930..0f6dae82234 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -31,6 +31,7 @@ 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 ); @@ -87,6 +88,11 @@ QgsLayoutModel *QgsLayout::itemsModel() return mItemsModel.get(); } +QgsLayoutExporter &QgsLayout::exporter() +{ + return mExporter; +} + QList QgsLayout::selectedLayoutItems( const bool includeLockedItems ) { QList layoutItemList; diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 9c9ae600f3c..23457928833 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -25,6 +25,7 @@ #include "qgslayoutgridsettings.h" #include "qgslayoutguidecollection.h" #include "qgslayoutundostack.h" +#include "qgslayoutexporter.h" class QgsLayoutItemMap; class QgsLayoutModel; @@ -83,6 +84,12 @@ 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() @@ -517,6 +524,7 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext std::unique_ptr< QgsLayoutPageCollection > mPageCollection; std::unique_ptr< QgsLayoutUndoStack > mUndoStack; + QgsLayoutExporter mExporter; bool mBlockUndoCommands = false; diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp new file mode 100644 index 00000000000..f6207b94268 --- /dev/null +++ b/src/core/layout/qgslayoutexporter.cpp @@ -0,0 +1,64 @@ +/*************************************************************************** + qgslayoutexporter.cpp + ------------------- + begin : October 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 "qgslayoutexporter.h" +#include "qgslayout.h" + +QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout ) + : mLayout( layout ) +{ + +} + +void QgsLayoutExporter::renderPage( QPainter *painter, int page ) +{ + if ( !mLayout ) + return; + + if ( mLayout->pageCollection()->pageCount() <= page || page < 0 ) + { + return; + } + + QgsLayoutItemPage *pageItem = mLayout->pageCollection()->page( page ); + if ( !pageItem ) + { + return; + } + + QRectF paperRect = QRectF( pageItem->pos().x(), pageItem->pos().y(), pageItem->rect().width(), pageItem->rect().height() ); + renderRegion( painter, paperRect ); +} + +void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) +{ + QPaintDevice *paintDevice = painter->device(); + if ( !paintDevice || !mLayout ) + { + return; + } + +#if 0 //TODO + setSnapLinesVisible( false ); +#endif + + mLayout->render( painter, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), region ); + +#if 0 // TODO + setSnapLinesVisible( true ); +#endif +} + diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h new file mode 100644 index 00000000000..9f89820c27b --- /dev/null +++ b/src/core/layout/qgslayoutexporter.h @@ -0,0 +1,67 @@ +/*************************************************************************** + qgslayoutexporter.h + ------------------- + begin : October 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 QGSLAYOUTEXPORTER_H +#define QGSLAYOUTEXPORTER_H + +#include "qgis_core.h" +#include + +class QgsLayout; +class QPainter; + +/** + * \ingroup core + * \class QgsLayoutExporter + * \brief Handles rendering and exports of layouts to various formats. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsLayoutExporter +{ + + public: + + /** + * Constructor for QgsLayoutExporter, for the specified \a layout. + */ + QgsLayoutExporter( QgsLayout *layout ); + + /** + * Renders a full page to a destination \a painter. + * + * 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. + * + * \see renderRect() + */ + void renderPage( QPainter *painter, int page ); + + /** + * 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() + */ + void renderRegion( QPainter *painter, const QRectF ®ion ); + + private: + + QPointer< QgsLayout > mLayout; +}; + +#endif //QGSLAYOUTEXPORTER_H + + + diff --git a/src/core/layout/qgslayoutitem.cpp b/src/core/layout/qgslayoutitem.cpp index d96a7c15373..7e10d0780e1 100644 --- a/src/core/layout/qgslayoutitem.cpp +++ b/src/core/layout/qgslayoutitem.cpp @@ -433,7 +433,7 @@ bool QgsLayoutItem::shouldBlockUndoCommands() const bool QgsLayoutItem::shouldDrawItem( QPainter *painter ) const { - if ( isPreviewRender( painter ) ) + if ( QgsLayoutUtils::isPreviewRender( painter ) ) { //preview mode so OK to draw item return true; @@ -767,31 +767,6 @@ void QgsLayoutItem::drawBackground( QgsRenderContext &context ) p->restore(); } -bool QgsLayoutItem::isPreviewRender( QPainter *painter ) const -{ - if ( !painter || !painter->device() ) - return false; - - // if rendering to a QGraphicsView, we are in preview mode - QPaintDevice *device = painter->device(); - if ( dynamic_cast< QPixmap * >( device ) ) - return true; - - QObject *obj = dynamic_cast< QObject *>( device ); - if ( !obj ) - return false; - - const QMetaObject *mo = obj->metaObject(); - while ( mo ) - { - if ( mo->className() == QStringLiteral( "QGraphicsView" ) ) - return true; - - mo = mo->superClass(); - } - return false; -} - void QgsLayoutItem::setFixedSize( const QgsLayoutSize &size ) { mFixedSize = size; diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index 8d01adebf75..28b817b4979 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -583,13 +583,6 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt */ virtual void drawBackground( QgsRenderContext &context ); - /** - * Returns true if the render to the specified \a painter is a preview render, - * i.e. is being rendered inside a QGraphicsView widget as opposed to a destination - * device (such as an image). - */ - bool isPreviewRender( QPainter *painter ) const; - /** * Sets a fixed \a size for the layout item, which prevents it from being freely * resized. Set an empty size if item can be freely resized. diff --git a/src/core/layout/qgslayoutitempage.cpp b/src/core/layout/qgslayoutitempage.cpp index 87abb739a20..d51f19324c2 100644 --- a/src/core/layout/qgslayoutitempage.cpp +++ b/src/core/layout/qgslayoutitempage.cpp @@ -178,9 +178,7 @@ void QgsLayoutItemPage::draw( QgsRenderContext &context, const QStyleOptionGraph QPainter *painter = context.painter(); painter->save(); -#if 0 //TODO - if ( mComposition->plotStyle() == QgsComposition::Preview ) -#endif + if ( QgsLayoutUtils::isPreviewRender( context.painter() ) ) { //if in preview mode, draw page border and shadow so that it's //still possible to tell where pages with a transparent style begin and end @@ -256,6 +254,9 @@ void QgsLayoutItemPageGrid::paint( QPainter *painter, const QStyleOptionGraphics if ( !mLayout ) return; + if ( !QgsLayoutUtils::isPreviewRender( painter ) ) + return; + const QgsLayoutContext &context = mLayout->context(); const QgsLayoutGridSettings &grid = mLayout->gridSettings(); diff --git a/src/core/layout/qgslayoututils.cpp b/src/core/layout/qgslayoututils.cpp index 004f78a50e7..6029d779a1d 100644 --- a/src/core/layout/qgslayoututils.cpp +++ b/src/core/layout/qgslayoututils.cpp @@ -109,3 +109,28 @@ double QgsLayoutUtils::relativePosition( const double position, const double bef //return linearly scaled position return m * position + c; } + +bool QgsLayoutUtils::isPreviewRender( QPainter *painter ) +{ + if ( !painter || !painter->device() ) + return false; + + // if rendering to a QGraphicsView, we are in preview mode + QPaintDevice *device = painter->device(); + if ( dynamic_cast< QPixmap * >( device ) ) + return true; + + QObject *obj = dynamic_cast< QObject *>( device ); + if ( !obj ) + return false; + + const QMetaObject *mo = obj->metaObject(); + while ( mo ) + { + if ( mo->className() == QStringLiteral( "QGraphicsView" ) ) + return true; + + mo = mo->superClass(); + } + return false; +} diff --git a/src/core/layout/qgslayoututils.h b/src/core/layout/qgslayoututils.h index 5e7b9beea55..02cad0718ac 100644 --- a/src/core/layout/qgslayoututils.h +++ b/src/core/layout/qgslayoututils.h @@ -81,6 +81,14 @@ class CORE_EXPORT QgsLayoutUtils */ static double relativePosition( const double position, const double beforeMin, const double beforeMax, const double afterMin, const double afterMax ); + + /** + * Returns true if the render to the specified \a painter is a preview render, + * i.e. is being rendered inside a QGraphicsView widget as opposed to a destination + * device (such as an image). + */ + static bool isPreviewRender( QPainter *painter ); + }; #endif //QGSLAYOUTUTILS_H diff --git a/src/core/qgsmultirenderchecker.cpp b/src/core/qgsmultirenderchecker.cpp index 2142bb9263b..98eba84424c 100644 --- a/src/core/qgsmultirenderchecker.cpp +++ b/src/core/qgsmultirenderchecker.cpp @@ -15,6 +15,7 @@ #include "qgsmultirenderchecker.h" #include "qgscomposition.h" +#include "qgslayout.h" #include void QgsMultiRenderChecker::setControlName( const QString &name ) @@ -170,6 +171,70 @@ bool QgsCompositionChecker::testComposition( QString &checkedReport, int page, i return testResult; } + + +// +// QgsLayoutChecker +// + +QgsLayoutChecker::QgsLayoutChecker( const QString &testName, QgsLayout *layout ) + : QgsMultiRenderChecker() + , mTestName( testName ) + , mLayout( layout ) + , mSize( 1122, 794 ) + , mDotsPerMeter( 96 / 25.4 * 1000 ) +{ + // Qt has some slight render inconsistencies on the whole image sometimes + setColorTolerance( 5 ); +} + +bool QgsLayoutChecker::runTest( QString &checkedReport, int page, int pixelDiff ) +{ + if ( !mLayout ) + { + return false; + } + + setControlName( "expected_" + mTestName ); + +#if 0 + //fake mode to generate expected image + //assume 96 dpi and size of the control image 1122 * 794 + QImage newImage( QSize( 1122, 794 ), QImage::Format_RGB32 ); + mComposition->setPlotStyle( QgsComposition::Print ); + newImage.setDotsPerMeterX( 96 / 25.4 * 1000 ); + newImage.setDotsPerMeterY( 96 / 25.4 * 1000 ); + drawBackground( &newImage ); + QPainter expectedPainter( &newImage ); + //QRectF sourceArea( 0, 0, mComposition->paperWidth(), mComposition->paperHeight() ); + //QRectF targetArea( 0, 0, 3507, 2480 ); + mComposition->renderPage( &expectedPainter, page ); + expectedPainter.end(); + newImage.save( controlImagePath() + QDir::separator() + "expected_" + mTestName + ".png", "PNG" ); + return true; +#endif //0 + + QImage outputImage( mSize, QImage::Format_RGB32 ); + outputImage.setDotsPerMeterX( mDotsPerMeter ); + outputImage.setDotsPerMeterY( mDotsPerMeter ); + drawBackground( &outputImage ); + QPainter p( &outputImage ); + mLayout->exporter().renderPage( &p, page ); + p.end(); + + QString renderedFilePath = QDir::tempPath() + '/' + QFileInfo( mTestName ).baseName() + "_rendered.png"; + outputImage.save( renderedFilePath, "PNG" ); + + setRenderedImage( renderedFilePath ); + + bool testResult = runTest( mTestName, pixelDiff ); + + checkedReport += report(); + + return testResult; +} + + ///@endcond #endif diff --git a/src/core/qgsmultirenderchecker.h b/src/core/qgsmultirenderchecker.h index fe74188dcac..39fca4e5528 100644 --- a/src/core/qgsmultirenderchecker.h +++ b/src/core/qgsmultirenderchecker.h @@ -165,6 +165,48 @@ class CORE_EXPORT QgsCompositionChecker : public QgsMultiRenderChecker QSize mSize; int mDotsPerMeter; }; + +/** + * \ingroup core + * \class QgsLayoutChecker + * Renders a layout to an image and compares with an expected output + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsLayoutChecker : public QgsMultiRenderChecker +{ + public: + + /** + * Constructor for QgsLayoutChecker. + */ + QgsLayoutChecker( const QString &testName, QgsLayout *layout ); + + /** + * Sets the output (reference) image \a size. + */ + void setSize( QSize size ) { mSize = size; } + + /** + * Runs a render check on the layout, adding results to the specified \a report. + * + * The maximum number of allowable pixels differing from the reference image is + * specified via the \a pixelDiff argument. + * + * The page number is specified via \a page, where 0 corresponds to the first + * page in the layout. + * + * Returns false if the rendered layout differs from the expected reference image. + */ + bool runTest( QString &report, int page = 0, int pixelDiff = 0 ); + + private: + QgsLayoutChecker() = delete; + + QString mTestName; + QgsLayout *mLayout = nullptr; + QSize mSize; + int mDotsPerMeter; +}; ///@endcond SIP_END diff --git a/src/gui/layout/qgslayoutmousehandles.cpp b/src/gui/layout/qgslayoutmousehandles.cpp index 0f3d349073a..c3862563a37 100644 --- a/src/gui/layout/qgslayoutmousehandles.cpp +++ b/src/gui/layout/qgslayoutmousehandles.cpp @@ -57,14 +57,11 @@ void QgsLayoutMouseHandles::paint( QPainter *painter, const QStyleOptionGraphics Q_UNUSED( itemStyle ); Q_UNUSED( pWidget ); - //TODO -#if 0 - if ( mLayout->plotStyle() != QgsComposition::Preview ) + if ( !QgsLayoutUtils::isPreviewRender( painter ) ) { - //don't draw selection handles in composition outputs + //don't draw selection handles in layout outputs return; } -#endif if ( mLayout->context().boundingBoxesVisible() ) { diff --git a/tests/src/gui/testqgslayoutview.cpp b/tests/src/gui/testqgslayoutview.cpp index c49c8b1b5cb..8de64f08861 100644 --- a/tests/src/gui/testqgslayoutview.cpp +++ b/tests/src/gui/testqgslayoutview.cpp @@ -26,6 +26,7 @@ #include "qgslayoutitemshape.h" #include "qgsproject.h" #include "qgsgui.h" +#include "qgslayoututils.h" #include #include #include @@ -344,7 +345,7 @@ class TestViewItem : public QgsLayoutItem void paint( QPainter *painter, const QStyleOptionGraphicsItem *, QWidget * ) override { mDrawn = true; - mPreview = isPreviewRender( painter ); + mPreview = QgsLayoutUtils::isPreviewRender( painter ); } void draw( QgsRenderContext &, const QStyleOptionGraphicsItem * ) override { @@ -369,18 +370,18 @@ void TestQgsLayoutView::isPreviewRender() // render to image - QVERIFY( !item->isPreviewRender( nullptr ) ); + QVERIFY( !QgsLayoutUtils::isPreviewRender( nullptr ) ); QImage im = QImage( 250, 250, QImage::Format_RGB32 ); QPainter painter; QVERIFY( painter.begin( &im ) ); - QVERIFY( !item->isPreviewRender( &painter ) ); + QVERIFY( !QgsLayoutUtils::isPreviewRender( &painter ) ); painter.end(); // render to svg QSvgGenerator generator; generator.setFileName( QDir::tempPath() + "/layout_text.svg" ); QVERIFY( painter.begin( &generator ) ); - QVERIFY( !item->isPreviewRender( &painter ) ); + QVERIFY( !QgsLayoutUtils::isPreviewRender( &painter ) ); painter.end(); // render to pdf @@ -389,7 +390,7 @@ void TestQgsLayoutView::isPreviewRender() printer.setOutputFormat( QPrinter::PdfFormat ); printer.setOutputFileName( QDir::tempPath() + "/layout_text.pdf" ); QVERIFY( painter.begin( &printer ) ); - QVERIFY( !item->isPreviewRender( &painter ) ); + QVERIFY( !QgsLayoutUtils::isPreviewRender( &painter ) ); painter.end(); // render in view - kinda gross!