From dd759370bd90784de0332fd1568a5945f99df5d9 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 10 Sep 2015 21:26:16 +1000 Subject: [PATCH] [FEATURE][composer] Option to restrict image/SVG outputs to content If selected, then the images output by composer will include only the area of the composition with content. There's also an option for margins to add around the item bounds if required. If the composition includes a single page, then the output will be sized to include EVERYTHING on the composition. If it's a multi-page composition, then each page will be cropped to only include the area of that page with items. A new image export options dialog has been added to facilitate this, which also includes handy shortcuts for overriding the print resolution or exported image dimensions. Sponsored by NIWA --- python/core/composer/qgscomposition.sip | 65 +++- src/app/CMakeLists.txt | 2 + src/app/composer/qgscomposer.cpp | 249 +++++++++++--- .../qgscomposerimageexportoptionsdialog.cpp | 160 +++++++++ .../qgscomposerimageexportoptionsdialog.h | 112 +++++++ src/core/composer/qgscomposition.cpp | 84 ++++- src/core/composer/qgscomposition.h | 70 +++- .../composer/qgscomposerimageexportoptions.ui | 317 ++++++++++++++++++ src/ui/composer/qgssvgexportoptions.ui | 229 ++++++++++--- tests/src/core/testqgscomposermap.cpp | 11 + tests/src/core/testqgscomposition.cpp | 59 ++++ 11 files changed, 1252 insertions(+), 106 deletions(-) create mode 100644 src/app/composer/qgscomposerimageexportoptionsdialog.cpp create mode 100644 src/app/composer/qgscomposerimageexportoptionsdialog.h create mode 100644 src/ui/composer/qgscomposerimageexportoptions.ui diff --git a/python/core/composer/qgscomposition.sip b/python/core/composer/qgscomposition.sip index c393fb1e949..58ee719a61a 100644 --- a/python/core/composer/qgscomposition.sip +++ b/python/core/composer/qgscomposition.sip @@ -601,18 +601,59 @@ class QgsComposition : QGraphicsScene */ bool exportAsPDF( const QString& file ); - //! print composer page to image - //! If the image does not fit into memory, a null image is returned + /** Renders a composer page to an image. + * @param page page number, 0 based such that the first page is page 0 + * @returns rendered image, or null image if image does not fit into available memory + * @see renderRectAsRaster() + * @see renderPage() + */ QImage printPageAsRaster( int page ); - /** Render a page to a paint device + /** Renders a portion of the composition to an image. This method can be used to render + * sections of pages rather than full pages. + * @param rect region of composition to render + * @returns rendered image, or null image if image does not fit into available memory + * @note added in QGIS 2.12 + * @see printPageAsRaster() + * @see renderRect() + */ + QImage renderRectAsRaster( const QRectF& rect ); + + /** Renders a full page to a paint device. * @param p destination painter - * @param page page number, 0 based such that the first page is page 0 */ + * @param page page number, 0 based such that the first page is page 0 + * @see renderRect() + * @see printPageAsRaster() + */ void renderPage( QPainter* p, int page ); - /** Compute world file parameters */ + /** Renders a portion of the composition to a paint device. This method can be used + * to render sections of pages rather than full pages. + * @param p destination painter + * @param rect region of composition to render + * @note added in QGIS 2.12 + * @see renderPage() + * @see renderRectAsRaster() + */ + void renderRect( QPainter* p, const QRectF& rect ); + + /** Compute world file parameters. Assumes the whole page containing the associated map item + * will be exported. + */ void computeWorldFileParameters( double& a, double& b, double& c, double& d, double& e, double& f ) const; + /** Computes the world file parameters for a specified region of the composition. + * @param exportRegion region of the composition which will be associated with world file + * @param a + * @param b + * @param c + * @param d + * @param e + * @param f + * @note added in QGIS 2.12 + */ + void computeWorldFileParameters( const QRectF& exportRegion, double& a, double& b, double& c, double& d, double& e, double& f ) const; + QgsAtlasComposition& atlasComposition(); /** Resizes a QRectF relative to the change from boundsBefore to boundsAfter @@ -703,6 +744,20 @@ class QgsComposition : QGraphicsScene */ QStringList customProperties() const; + /** Returns the bounding box of the items contained on a specified page. + * @param pageNumber page number, where 0 is the first page + * @param visibleOnly set to true to only include visible items + * @note added in QGIS 2.12 + */ + QRectF pageItemBounds( int pageNumber, bool visibleOnly = false ) const; + + /** Calculates the bounds of all non-gui items in the composition. Ignores snap lines and mouse handles. + * @param ignorePages set to true to ignore page items + * @param margin optional marginal (in percent, eg 0.05 = 5% ) to add around items + */ + QRectF compositionBounds( bool ignorePages = false, double margin = 0.0 ) const; + + public slots: /** Casts object to the proper subclass type and calls corresponding itemAdded signal*/ void sendItemAddedSignal( QgsComposerItem* item ); diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 58a7f7c16f4..c91ded30fa8 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -124,6 +124,7 @@ SET(QGIS_APP_SRCS composer/qgscomposerarrowwidget.cpp composer/qgscomposerattributetablewidget.cpp composer/qgscomposerhtmlwidget.cpp + composer/qgscomposerimageexportoptionsdialog.cpp composer/qgscomposeritemwidget.cpp composer/qgscomposerlabelwidget.cpp composer/qgscomposerpicturewidget.cpp @@ -278,6 +279,7 @@ SET (QGIS_APP_MOC_HDRS composer/qgscomposerarrowwidget.h composer/qgscomposerattributetablewidget.h composer/qgscomposerhtmlwidget.h + composer/qgscomposerimageexportoptionsdialog.h composer/qgscomposeritemwidget.h composer/qgscomposerlabelwidget.h composer/qgscomposerlegendwidget.h diff --git a/src/app/composer/qgscomposer.cpp b/src/app/composer/qgscomposer.cpp index bb4fb5a9357..8d8e8344f71 100644 --- a/src/app/composer/qgscomposer.cpp +++ b/src/app/composer/qgscomposer.cpp @@ -61,6 +61,7 @@ #include "qgspaperitem.h" #include "qgsmaplayerregistry.h" #include "qgsprevieweffect.h" +#include "qgscomposerimageexportoptionsdialog.h" #include "ui_qgssvgexportoptions.h" #include @@ -1965,6 +1966,19 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode ) return; } + //get some defaults from the composition + bool cropToContents = mComposition->customProperty( "imageCropToContents", false ).toBool(); + int marginTop = mComposition->customProperty( "imageCropMarginTop", 0 ).toInt(); + int marginRight = mComposition->customProperty( "imageCropMarginRight", 0 ).toInt(); + int marginBottom = mComposition->customProperty( "imageCropMarginBottom", 0 ).toInt(); + int marginLeft = mComposition->customProperty( "imageCropMarginLeft", 0 ).toInt(); + + QgsComposerImageExportOptionsDialog imageDlg( this ); + imageDlg.setImageSize( QSizeF( mComposition->paperWidth(), mComposition->paperHeight() ) ); + imageDlg.setResolution( mComposition->printResolution() ); + imageDlg.setCropToContents( cropToContents ); + imageDlg.setCropMargins( marginTop, marginRight, marginBottom, marginLeft ); + QgsAtlasComposition* atlasMap = &mComposition->atlasComposition(); if ( mode == QgsComposer::Single ) { @@ -1983,6 +1997,17 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode ) return; } + if ( !imageDlg.exec() ) + return; + + cropToContents = imageDlg.cropToContents(); + imageDlg.getCropMargins( marginTop, marginRight, marginBottom, marginLeft ); + mComposition->setCustomProperty( "imageCropToContents", cropToContents ); + mComposition->setCustomProperty( "imageCropMarginTop", marginTop ); + mComposition->setCustomProperty( "imageCropMarginRight", marginRight ); + mComposition->setCustomProperty( "imageCropMarginBottom", marginBottom ); + mComposition->setCustomProperty( "imageCropMarginLeft", marginLeft ); + mView->setPaintingEnabled( false ); int worldFilePageNo = -1; @@ -1997,7 +2022,38 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode ) { continue; } - QImage image = mComposition->printPageAsRaster( i ); + + QImage image; + QRectF bounds; + if ( cropToContents ) + { + if ( mComposition->numPages() == 1 ) + { + // single page, so include everything + bounds = mComposition->compositionBounds( true ); + } + else + { + // multi page, so just clip to items on current page + bounds = mComposition->pageItemBounds( i, true ); + } + if ( bounds.width() <= 0 || bounds.height() <= 0 ) + { + //invalid size, skip page + continue; + } + double pixelToMm = 25.4 / mComposition->printResolution(); + bounds = bounds.adjusted( -marginLeft * pixelToMm, + -marginTop * pixelToMm, + marginRight * pixelToMm, + marginBottom * pixelToMm ); + image = mComposition->renderRectAsRaster( bounds ); + } + else + { + image = mComposition->printPageAsRaster( i ); + } + if ( image.isNull() ) { QMessageBox::warning( 0, tr( "Memory Allocation Error" ), @@ -2037,7 +2093,10 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode ) { // should generate world file for this page double a, b, c, d, e, f; - mComposition->computeWorldFileParameters( a, b, c, d, e, f ); + if ( bounds.isValid() ) + mComposition->computeWorldFileParameters( bounds, a, b, c, d, e, f ); + else + mComposition->computeWorldFileParameters( a, b, c, d, e, f ); QFileInfo fi( outputFilePath ); // build the world file name @@ -2127,6 +2186,17 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode ) return; } + if ( !imageDlg.exec() ) + return; + + cropToContents = imageDlg.cropToContents(); + imageDlg.getCropMargins( marginTop, marginRight, marginBottom, marginLeft ); + mComposition->setCustomProperty( "imageCropToContents", cropToContents ); + mComposition->setCustomProperty( "imageCropMarginTop", marginTop ); + mComposition->setCustomProperty( "imageCropMarginRight", marginRight ); + mComposition->setCustomProperty( "imageCropMarginBottom", marginBottom ); + mComposition->setCustomProperty( "imageCropMarginLeft", marginLeft ); + myQSettings.setValue( "/UI/lastSaveAtlasAsImagesDir", dir ); // So, now we can render the atlas @@ -2184,7 +2254,38 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode ) { continue; } - QImage image = mComposition->printPageAsRaster( i ); + + QImage image; + QRectF bounds; + if ( cropToContents ) + { + if ( mComposition->numPages() == 1 ) + { + // single page, so include everything + bounds = mComposition->compositionBounds( true ); + } + else + { + // multi page, so just clip to items on current page + bounds = mComposition->pageItemBounds( i, true ); + } + if ( bounds.width() <= 0 || bounds.height() <= 0 ) + { + //invalid size, skip page + continue; + } + double pixelToMm = 25.4 / mComposition->printResolution(); + bounds = bounds.adjusted( -marginLeft * pixelToMm, + -marginTop * pixelToMm, + marginRight * pixelToMm, + marginBottom * pixelToMm ); + image = mComposition->renderRectAsRaster( bounds ); + } + else + { + image = mComposition->printPageAsRaster( i ); + } + QString imageFilename = filename; if ( i != 0 ) @@ -2210,7 +2311,10 @@ void QgsComposer::exportCompositionAsImage( QgsComposer::OutputMode mode ) { // should generate world file for this page double a, b, c, d, e, f; - mComposition->computeWorldFileParameters( a, b, c, d, e, f ); + if ( bounds.isValid() ) + mComposition->computeWorldFileParameters( bounds, a, b, c, d, e, f ); + else + mComposition->computeWorldFileParameters( a, b, c, d, e, f ); QFileInfo fi( imageFilename ); // build the world file name @@ -2317,6 +2421,11 @@ void QgsComposer::exportCompositionAsSVG( QgsComposer::OutputMode mode ) QString outputDir; bool groupLayers = false; bool prevSettingLabelsAsOutlines = QgsProject::instance()->readBoolEntry( "PAL", "/DrawOutlineLabels", true ); + bool clipToContent = false; + double marginTop = 0.0; + double marginRight = 0.0; + double marginBottom = 0.0; + double marginLeft = 0.0; if ( mode == QgsComposer::Single ) { @@ -2335,26 +2444,13 @@ void QgsComposer::exportCompositionAsSVG( QgsComposer::OutputMode mode ) // open file dialog outputFileName = QFileDialog::getSaveFileName( this, - tr( "Choose a file name to save the map as" ), + tr( "Choose a file name to save the composition as" ), outputFileName, tr( "SVG Format" ) + " (*.svg *.SVG)" ); if ( outputFileName.isEmpty() ) return; - // open otions dialog - { - QDialog dialog; - Ui::QgsSvgExportOptionsDialog options; - options.setupUi( &dialog ); - options.chkTextAsOutline->setChecked( prevSettingLabelsAsOutlines ); - - dialog.exec(); - groupLayers = options.chkMapLayersAsGroup->isChecked(); - //temporarily override label draw outlines setting - QgsProject::instance()->writeEntry( "PAL", "/DrawOutlineLabels", options.chkTextAsOutline->isChecked() ); - } - if ( !outputFileName.endsWith( ".svg", Qt::CaseInsensitive ) ) { outputFileName += ".svg"; @@ -2400,24 +2496,42 @@ void QgsComposer::exportCompositionAsSVG( QgsComposer::OutputMode mode ) QMessageBox::Ok ); return; } - - // open otions dialog - { - QDialog dialog; - Ui::QgsSvgExportOptionsDialog options; - options.setupUi( &dialog ); - options.chkTextAsOutline->setChecked( prevSettingLabelsAsOutlines ); - - dialog.exec(); - groupLayers = options.chkMapLayersAsGroup->isChecked(); - //temporarily override label draw outlines setting - QgsProject::instance()->writeEntry( "PAL", "/DrawOutlineLabels", options.chkTextAsOutline->isChecked() ); - } - - myQSettings.setValue( "/UI/lastSaveAtlasAsSvgDir", outputDir ); } + // open options dialog + QDialog dialog; + Ui::QgsSvgExportOptionsDialog options; + options.setupUi( &dialog ); + options.chkTextAsOutline->setChecked( prevSettingLabelsAsOutlines ); + options.chkMapLayersAsGroup->setChecked( mComposition->customProperty( "svgGroupLayers", false ).toBool() ); + options.mClipToContentGroupBox->setChecked( mComposition->customProperty( "svgCropToContents", false ).toBool() ); + options.mTopMarginSpinBox->setValue( mComposition->customProperty( "svgCropMarginTop", 0 ).toInt() ); + options.mRightMarginSpinBox->setValue( mComposition->customProperty( "svgCropMarginRight", 0 ).toInt() ); + options.mBottomMarginSpinBox->setValue( mComposition->customProperty( "svgCropMarginBottom", 0 ).toInt() ); + options.mLeftMarginSpinBox->setValue( mComposition->customProperty( "svgCropMarginLeft", 0 ).toInt() ); + + if ( dialog.exec() != QDialog::Accepted ) + return; + + groupLayers = options.chkMapLayersAsGroup->isChecked(); + clipToContent = options.mClipToContentGroupBox->isChecked(); + marginTop = options.mTopMarginSpinBox->value(); + marginRight = options.mRightMarginSpinBox->value(); + marginBottom = options.mBottomMarginSpinBox->value(); + marginLeft = options.mLeftMarginSpinBox->value(); + + //save dialog settings + mComposition->setCustomProperty( "svgGroupLayers", groupLayers ); + mComposition->setCustomProperty( "svgCropToContents", clipToContent ); + mComposition->setCustomProperty( "svgCropMarginTop", marginTop ); + mComposition->setCustomProperty( "svgCropMarginRight", marginRight ); + mComposition->setCustomProperty( "svgCropMarginBottom", marginBottom ); + mComposition->setCustomProperty( "svgCropMarginLeft", marginLeft ); + + //temporarily override label draw outlines setting + QgsProject::instance()->writeEntry( "PAL", "/DrawOutlineLabels", options.chkTextAsOutline->isChecked() ); + mView->setPaintingEnabled( false ); int featureI = 0; @@ -2488,10 +2602,33 @@ void QgsComposer::exportCompositionAsSVG( QgsComposer::OutputMode mode ) generator.setFileName( currentFileName ); } + QRectF bounds; + if ( clipToContent ) + { + if ( mComposition->numPages() == 1 ) + { + // single page, so include everything + bounds = mComposition->compositionBounds( true ); + } + else + { + // multi page, so just clip to items on current page + bounds = mComposition->pageItemBounds( i, true ); + } + bounds = bounds.adjusted( -marginLeft, -marginTop, marginRight, marginBottom ); + } + else + bounds = QRectF( 0, 0, mComposition->paperWidth(), mComposition->paperHeight() ); + //width in pixel - int width = ( int )( mComposition->paperWidth() * mComposition->printResolution() / 25.4 ); + int width = ( int )( bounds.width() * mComposition->printResolution() / 25.4 ); //height in pixel - int height = ( int )( mComposition->paperHeight() * mComposition->printResolution() / 25.4 ); + int height = ( int )( bounds.height() * mComposition->printResolution() / 25.4 ); + if ( width == 0 || height == 0 ) + { + //invalid size, skip this page + continue; + } generator.setSize( QSize( width, height ) ); generator.setViewBox( QRect( 0, 0, width, height ) ); generator.setResolution( mComposition->printResolution() ); //because the rendering is done in mm, convert the dpi @@ -2509,15 +2646,18 @@ void QgsComposer::exportCompositionAsSVG( QgsComposer::OutputMode mode ) return; } - mComposition->renderPage( &p, i ); + if ( clipToContent ) + mComposition->renderRect( &p, bounds ); + else + mComposition->renderPage( &p, i ); p.end(); } } else { //width and height in pixel - const int width = ( int )( mComposition->paperWidth() * mComposition->printResolution() / 25.4 ); - const int height = ( int )( mComposition->paperHeight() * mComposition->printResolution() / 25.4 ); + const int pageWidth = ( int )( mComposition->paperWidth() * mComposition->printResolution() / 25.4 ); + const int pageHeight = ( int )( mComposition->paperHeight() * mComposition->printResolution() / 25.4 ); QList< QgsPaperItem* > paperItems( mComposition->pages() ); for ( int i = 0; i < mComposition->numPages(); ++i ) @@ -2526,6 +2666,34 @@ void QgsComposer::exportCompositionAsSVG( QgsComposer::OutputMode mode ) { continue; } + + int width = pageWidth; + int height = pageHeight; + + QRectF bounds; + if ( clipToContent ) + { + if ( mComposition->numPages() == 1 ) + { + // single page, so include everything + bounds = mComposition->compositionBounds( true ); + } + else + { + // multi page, so just clip to items on current page + bounds = mComposition->pageItemBounds( i, true ); + } + bounds = bounds.adjusted( -marginLeft, -marginTop, marginRight, marginBottom ); + width = bounds.width() * mComposition->printResolution() / 25.4; + height = bounds.height() * mComposition->printResolution() / 25.4; + } + + if ( width == 0 || height == 0 ) + { + //invalid size, skip this page + continue; + } + QDomDocument svg; QDomNode svgDocRoot; QgsPaperItem * paperItem = paperItems[i]; @@ -2581,7 +2749,10 @@ void QgsComposer::exportCompositionAsSVG( QgsComposer::OutputMode mode ) generator.setResolution( mComposition->printResolution() ); //because the rendering is done in mm, convert the dpi QPainter p( &generator ); - mComposition->renderPage( &p, i ); + if ( clipToContent ) + mComposition->renderRect( &p, bounds ); + else + mComposition->renderPage( &p, i ); } // post-process svg output to create groups in a single svg file // we create inkscape layers since it's nice and clean and free diff --git a/src/app/composer/qgscomposerimageexportoptionsdialog.cpp b/src/app/composer/qgscomposerimageexportoptionsdialog.cpp new file mode 100644 index 00000000000..b602f8ce1fd --- /dev/null +++ b/src/app/composer/qgscomposerimageexportoptionsdialog.cpp @@ -0,0 +1,160 @@ +/*************************************************************************** + qgscomposerimageexportoptionsdialog.cpp + --------------------------------------- + begin : September 2015 + copyright : (C) 2015 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 "qgscomposerimageexportoptionsdialog.h" +#include +#include +#include + +QgsComposerImageExportOptionsDialog::QgsComposerImageExportOptionsDialog( QWidget* parent, Qt::WindowFlags flags ) + : QDialog( parent, flags ) +{ + setupUi( this ); + + connect( mClipToContentGroupBox, SIGNAL(toggled(bool)), this, SLOT(clipToContentsToggled(bool))); + + QSettings settings; + restoreGeometry( settings.value( "/Windows/ComposerImageExportOptionsDialog/geometry" ).toByteArray() ); +} + +QgsComposerImageExportOptionsDialog::~QgsComposerImageExportOptionsDialog() +{ + QSettings settings; + settings.setValue( "/Windows/ComposerImageExportOptionsDialog/geometry", saveGeometry() ); +} + +void QgsComposerImageExportOptionsDialog::setResolution(int resolution) +{ + mResolutionSpinBox->setValue( resolution ); + + if ( mImageSize.isValid() ) + { + mWidthSpinBox->blockSignals( true ); + mHeightSpinBox->blockSignals( true ); + mWidthSpinBox->setValue( mImageSize.width() * resolution / 25.4 ); + mHeightSpinBox->setValue( mImageSize.height() * resolution / 25.4 ); + mWidthSpinBox->blockSignals( false ); + mHeightSpinBox->blockSignals( false ); + } +} + +int QgsComposerImageExportOptionsDialog::resolution() const +{ + return mResolutionSpinBox->value(); +} + +void QgsComposerImageExportOptionsDialog::setImageSize( const 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 QgsComposerImageExportOptionsDialog::width() const +{ + return mWidthSpinBox->value(); +} + +int QgsComposerImageExportOptionsDialog::height() const +{ + return mHeightSpinBox->value(); +} + +void QgsComposerImageExportOptionsDialog::setCropToContents(bool crop) +{ + mClipToContentGroupBox->setChecked( crop ); +} + +bool QgsComposerImageExportOptionsDialog::cropToContents() const +{ + return mClipToContentGroupBox->isChecked(); +} + +void QgsComposerImageExportOptionsDialog::getCropMargins(int& topMargin, int& rightMargin, int& bottomMargin, int& leftMargin) const +{ + topMargin = mTopMarginSpinBox->value(); + rightMargin = mRightMarginSpinBox->value(); + bottomMargin = mBottomMarginSpinBox->value(); + leftMargin = mLeftMarginSpinBox->value(); +} + +void QgsComposerImageExportOptionsDialog::setCropMargins(int topMargin, int rightMargin, int bottomMargin, int leftMargin) +{ + mTopMarginSpinBox->setValue( topMargin ); + mRightMarginSpinBox->setValue( rightMargin ); + mBottomMarginSpinBox->setValue( bottomMargin ); + mLeftMarginSpinBox->setValue( leftMargin ); +} + +void QgsComposerImageExportOptionsDialog::on_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 QgsComposerImageExportOptionsDialog::on_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 QgsComposerImageExportOptionsDialog::on_mResolutionSpinBox_valueChanged(int value) +{ + mWidthSpinBox->blockSignals( true ); + mHeightSpinBox->blockSignals( true ); + mWidthSpinBox->setValue( mImageSize.width() * value / 25.4 ); + mHeightSpinBox->setValue( mImageSize.height() * value / 25.4 ); + mWidthSpinBox->blockSignals( false ); + mHeightSpinBox->blockSignals( false); +} + +void QgsComposerImageExportOptionsDialog::clipToContentsToggled(bool state) +{ + mWidthSpinBox->setEnabled( !state ); + mHeightSpinBox->setEnabled( !state ); + + if ( state ) + { + mWidthSpinBox->blockSignals( true ); + mWidthSpinBox->setValue( 0 ); + mWidthSpinBox->blockSignals( false ); + mHeightSpinBox->blockSignals( true ); + mHeightSpinBox->setValue( 0 ); + mHeightSpinBox->blockSignals( false ); + } + else + { + mWidthSpinBox->blockSignals( true ); + mWidthSpinBox->setValue( mImageSize.width() * mResolutionSpinBox->value() / 25.4 ); + mWidthSpinBox->blockSignals( false ); + mHeightSpinBox->blockSignals( true ); + mHeightSpinBox->setValue( mImageSize.height() * mResolutionSpinBox->value() / 25.4 ); + mHeightSpinBox->blockSignals( false ); + } +} diff --git a/src/app/composer/qgscomposerimageexportoptionsdialog.h b/src/app/composer/qgscomposerimageexportoptionsdialog.h new file mode 100644 index 00000000000..5147c51c7f1 --- /dev/null +++ b/src/app/composer/qgscomposerimageexportoptionsdialog.h @@ -0,0 +1,112 @@ +/*************************************************************************** + qgscomposerimageexportoptionsdialog.h + ------------------------------------- + begin : September 2015 + copyright : (C) 2015 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 QGSCOMPOSERIMAGEEXPORTOPTIONSDIALOG_H +#define QGSCOMPOSERIMAGEEXPORTOPTIONSDIALOG_H + +#include +#include "ui_qgscomposerimageexportoptions.h" +#include "qgscomposertablev2.h" + + +/** A dialog for customising the properties of an exported image file. + * /note added in QGIS 2.12 +*/ +class QgsComposerImageExportOptionsDialog: public QDialog, private Ui::QgsComposerImageExportOptionsDialog +{ + Q_OBJECT + + public: + + /** Constructor for QgsComposerImageExportOptionsDialog + * @param parent parent widget + * @param flags window flags + */ + QgsComposerImageExportOptionsDialog( QWidget* parent = 0, Qt::WindowFlags flags = 0 ); + + ~QgsComposerImageExportOptionsDialog(); + + /** Sets the initial resolution displayed in the dialog. + * @param resolution default resolution in DPI + * @see resolution() + */ + void setResolution( int resolution ); + + /** Returns the selected resolution from the dialog. + * @returns image resolution in DPI + * @see setResolution() + */ + int 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( const QSizeF& size ); + + /** Returns the user-set image width in pixels. + * @see height + */ + int width() const; + + /** Returns the user-set image height in pixels. + * @see width + */ + int height() 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 on_mWidthSpinBox_valueChanged( int value ); + void on_mHeightSpinBox_valueChanged( int value ); + void on_mResolutionSpinBox_valueChanged( int value ); + void clipToContentsToggled( bool state ); + + private: + + QSizeF mImageSize; + + +}; + +#endif // QGSCOMPOSERIMAGEEXPORTOPTIONSDIALOG_H diff --git a/src/core/composer/qgscomposition.cpp b/src/core/composer/qgscomposition.cpp index 366f611a70b..f06ed6d1828 100644 --- a/src/core/composer/qgscomposition.cpp +++ b/src/core/composer/qgscomposition.cpp @@ -325,6 +325,34 @@ QRectF QgsComposition::compositionBounds( bool ignorePages, double margin ) cons return bounds; } +QRectF QgsComposition::pageItemBounds( int pageNumber, bool visibleOnly ) const +{ + //start with an empty rectangle + QRectF bounds; + + //add all QgsComposerItems on page + QList itemList = items(); + QList::iterator itemIt = itemList.begin(); + for ( ; itemIt != itemList.end(); ++itemIt ) + { + const QgsComposerItem* composerItem = dynamic_cast( *itemIt ); + const QgsPaperItem* paperItem = dynamic_cast( *itemIt ); + if ( composerItem && !paperItem && itemPageNumber( composerItem ) == pageNumber ) + { + if ( visibleOnly && !composerItem->isVisible() ) + continue; + + //expand bounds with current item's bounds + if ( bounds.isValid() ) + bounds = bounds.united(( *itemIt )->sceneBoundingRect() ); + else + bounds = ( *itemIt )->sceneBoundingRect(); + } + } + + return bounds; +} + void QgsComposition::setPaperSize( const double width, const double height, bool keepRelativeItemPosition ) { if ( width == mPageWidth && height == mPageHeight ) @@ -2877,6 +2905,23 @@ QImage QgsComposition::printPageAsRaster( int page ) return image; } +QImage QgsComposition::renderRectAsRaster( const QRectF& rect ) +{ + int width = ( int )( printResolution() * rect.width() / 25.4 ); + int height = ( int )( printResolution() * rect.height() / 25.4 ); + QImage image( QSize( width, height ), QImage::Format_ARGB32 ); + if ( !image.isNull() ) + { + image.setDotsPerMeterX( printResolution() / 25.4 * 1000 ); + image.setDotsPerMeterY( printResolution() / 25.4 * 1000 ); + image.fill( Qt::transparent ); + QPainter imagePainter( &image ); + renderRect( &imagePainter, rect ); + if ( !imagePainter.isActive() ) return QImage(); + } + return image; +} + void QgsComposition::renderPage( QPainter* p, int page ) { if ( mPages.size() <= page ) @@ -2890,21 +2935,25 @@ void QgsComposition::renderPage( QPainter* p, int page ) return; } + QRectF paperRect = QRectF( paperItem->pos().x(), paperItem->pos().y(), paperItem->rect().width(), paperItem->rect().height() ); + renderRect( p, paperRect ); +} + +void QgsComposition::renderRect( QPainter* p, const QRectF& rect ) +{ QPaintDevice* paintDevice = p->device(); if ( !paintDevice ) { return; } - QRectF paperRect = QRectF( paperItem->pos().x(), paperItem->pos().y(), paperItem->rect().width(), paperItem->rect().height() ); - QgsComposition::PlotStyle savedPlotStyle = mPlotStyle; mPlotStyle = QgsComposition::Print; setSnapLinesVisible( false ); //hide background before rendering setBackgroundBrush( Qt::NoBrush ); - render( p, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), paperRect ); + render( p, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), rect ); //show background after rendering setBackgroundBrush( QColor( 215, 215, 215 ) ); setSnapLinesVisible( true ); @@ -2937,6 +2986,19 @@ QGraphicsView *QgsComposition::graphicsView() const } void QgsComposition::computeWorldFileParameters( double& a, double& b, double& c, double& d, double& e, double& f ) const +{ + if ( !mWorldFileMap ) + { + return; + } + + int pageNumber = mWorldFileMap->page() - 1; + double pageY = pageNumber * ( mPageHeight + mSpaceBetweenPages ); + QRectF pageRect( 0, pageY, mPageWidth, mPageHeight ); + computeWorldFileParameters( pageRect, a, b, c, d, e, f ); +} + +void QgsComposition::computeWorldFileParameters( const QRectF& exportRegion, double& a, double& b, double& c, double& d, double& e, double& f ) const { // World file parameters : affine transformation parameters from pixel coordinates to map coordinates @@ -2945,8 +3007,8 @@ void QgsComposition::computeWorldFileParameters( double& a, double& b, double& c return; } - double destinationHeight = paperHeight(); - double destinationWidth = paperWidth(); + double destinationHeight = exportRegion.height(); + double destinationWidth = exportRegion.width(); QRectF mapItemSceneRect = mWorldFileMap->mapRectToScene( mWorldFileMap->rect() ); QgsRectangle mapExtent = *mWorldFileMap->currentMapExtent(); @@ -2959,10 +3021,14 @@ void QgsComposition::computeWorldFileParameters( double& a, double& b, double& c double xCenter = mapExtent.center().x(); double yCenter = mapExtent.center().y(); - // get the extent (in map units) for the page - QPointF mapItemPosOnPage = mWorldFileMap->pagePos(); - double xmin = mapExtent.xMinimum() - mapItemPosOnPage.x() * xRatio; - double ymax = mapExtent.yMaximum() + mapItemPosOnPage.y() * yRatio; + // get the extent (in map units) for the region + QPointF mapItemPos = mWorldFileMap->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(); diff --git a/src/core/composer/qgscomposition.h b/src/core/composer/qgscomposition.h index 61e58414e43..46281cec3ff 100644 --- a/src/core/composer/qgscomposition.h +++ b/src/core/composer/qgscomposition.h @@ -664,18 +664,59 @@ class CORE_EXPORT QgsComposition : public QGraphicsScene */ bool exportAsPDF( const QString& file ); - //! print composer page to image - //! If the image does not fit into memory, a null image is returned + /** Renders a composer page to an image. + * @param page page number, 0 based such that the first page is page 0 + * @returns rendered image, or null image if image does not fit into available memory + * @see renderRectAsRaster() + * @see renderPage() + */ QImage printPageAsRaster( int page ); - /** Render a page to a paint device + /** Renders a portion of the composition to an image. This method can be used to render + * sections of pages rather than full pages. + * @param rect region of composition to render + * @returns rendered image, or null image if image does not fit into available memory + * @note added in QGIS 2.12 + * @see printPageAsRaster() + * @see renderRect() + */ + QImage renderRectAsRaster( const QRectF& rect ); + + /** Renders a full page to a paint device. * @param p destination painter - * @param page page number, 0 based such that the first page is page 0 */ + * @param page page number, 0 based such that the first page is page 0 + * @see renderRect() + * @see printPageAsRaster() + */ void renderPage( QPainter* p, int page ); - /** Compute world file parameters */ + /** Renders a portion of the composition to a paint device. This method can be used + * to render sections of pages rather than full pages. + * @param p destination painter + * @param rect region of composition to render + * @note added in QGIS 2.12 + * @see renderPage() + * @see renderRectAsRaster() + */ + void renderRect( QPainter* p, const QRectF& rect ); + + /** Compute world file parameters. Assumes the whole page containing the associated map item + * will be exported. + */ void computeWorldFileParameters( double& a, double& b, double& c, double& d, double& e, double& f ) const; + /** Computes the world file parameters for a specified region of the composition. + * @param exportRegion region of the composition which will be associated with world file + * @param a + * @param b + * @param c + * @param d + * @param e + * @param f + * @note added in QGIS 2.12 + */ + void computeWorldFileParameters( const QRectF& exportRegion, double& a, double& b, double& c, double& d, double& e, double& f ) const; + QgsAtlasComposition& atlasComposition() { return mAtlasComposition; } /** Resizes a QRectF relative to the change from boundsBefore to boundsAfter @@ -766,6 +807,19 @@ class CORE_EXPORT QgsComposition : public QGraphicsScene */ QStringList customProperties() const; + /** Returns the bounding box of the items contained on a specified page. + * @param pageNumber page number, where 0 is the first page + * @param visibleOnly set to true to only include visible items + * @note added in QGIS 2.12 + */ + QRectF pageItemBounds( int pageNumber, bool visibleOnly = false ) const; + + /** Calculates the bounds of all non-gui items in the composition. Ignores snap lines and mouse handles. + * @param ignorePages set to true to ignore page items + * @param margin optional marginal (in percent, eg 0.05 = 5% ) to add around items + */ + QRectF compositionBounds( bool ignorePages = false, double margin = 0.0 ) const; + public slots: /** Casts object to the proper subclass type and calls corresponding itemAdded signal*/ void sendItemAddedSignal( QgsComposerItem* item ); @@ -894,12 +948,6 @@ class CORE_EXPORT QgsComposition : public QGraphicsScene QgsComposition(); //default constructor is forbidden - /** Calculates the bounds of all non-gui items in the composition. Ignores snap lines and mouse handles. - * @param ignorePages set to true to ignore page items - * @param margin optional marginal (in percent, eg 0.05 = 5% ) to add around items - */ - QRectF compositionBounds( bool ignorePages = false, double margin = 0.0 ) const; - /** Reset z-values of items based on position in z list*/ void updateZValues( const bool addUndoCommands = true ); diff --git a/src/ui/composer/qgscomposerimageexportoptions.ui b/src/ui/composer/qgscomposerimageexportoptions.ui new file mode 100644 index 00000000000..d91e8e0bb72 --- /dev/null +++ b/src/ui/composer/qgscomposerimageexportoptions.ui @@ -0,0 +1,317 @@ + + + QgsComposerImageExportOptionsDialog + + + + 0 + 0 + 489 + 325 + + + + 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 + + + + + + + + QgsSpinBox + QSpinBox +
qgsspinbox.h
+
+ + QgsCollapsibleGroupBoxBasic + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+
+ + mClipToContentGroupBox + mTopMarginSpinBox + mLeftMarginSpinBox + mRightMarginSpinBox + mBottomMarginSpinBox + + + + + buttonBox + accepted() + QgsComposerImageExportOptionsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + QgsComposerImageExportOptionsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/ui/composer/qgssvgexportoptions.ui b/src/ui/composer/qgssvgexportoptions.ui index 4279abad44b..75f8821ea1e 100644 --- a/src/ui/composer/qgssvgexportoptions.ui +++ b/src/ui/composer/qgssvgexportoptions.ui @@ -6,56 +6,201 @@ 0 0 - 471 - 103 + 489 + 282 SVG export options - - - - - - - Export map layers as svg groups (may affect label placement) - - - false - - - - - - - true - - - Uncheck to render map labels as text objects. This will degrade the quality of the map labels but allow editing in vector illustration software. - - - Render map labels as outlines - - - true - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Ok - - - - + + + + + SVG options + + + + + + Export map layers as svg groups (may affect label placement) + + + false + + + + + + + true + + + Uncheck to render map labels as text objects. This will degrade the quality of the map labels but allow editing in vector illustration software. + + + Render map labels as outlines + + + true + + + + + + + + + + Crop to content + + + true + + + false + + + + + + + + 0.100000000000000 + + + + + + + + + Left + + + + + + + 0.100000000000000 + + + + + + + Right + + + + + + + 0.100000000000000 + + + + + + + + + Bottom + + + + + + + 0.100000000000000 + + + + + + + Top margin (mm) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + QgsCollapsibleGroupBoxBasic + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsDoubleSpinBox + QDoubleSpinBox +
qgsdoublespinbox.h
+
+
+ + chkMapLayersAsGroup + chkTextAsOutline + mClipToContentGroupBox + mTopMarginSpinBox + mLeftMarginSpinBox + mRightMarginSpinBox + mBottomMarginSpinBox + diff --git a/tests/src/core/testqgscomposermap.cpp b/tests/src/core/testqgscomposermap.cpp index 1269f32d27d..d510a9a29b1 100644 --- a/tests/src/core/testqgscomposermap.cpp +++ b/tests/src/core/testqgscomposermap.cpp @@ -201,6 +201,17 @@ void TestQgsComposerMap::worldFileGeneration() QVERIFY( qgsDoubleNear( e, -4.17997, 0.001 ) ); QVERIFY( qgsDoubleNear( 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 ); + + QVERIFY( qgsDoubleNear( a, 4.18061, 0.001 ) ); + QVERIFY( qgsDoubleNear( b, 2.41321, 0.001 ) ); + QVERIFY( qgsDoubleNear( c, 773810, 1 ) ); + QVERIFY( qgsDoubleNear( d, 2.4137, 0.001 ) ); + QVERIFY( qgsDoubleNear( e, -4.1798, 0.001 ) ); + QVERIFY( qgsDoubleNear( f, 3.35331e+06, 1e+03 ) ); + mComposition->setGenerateWorldFile( false ); mComposerMap->setMapRotation( 0.0 ); diff --git a/tests/src/core/testqgscomposition.cpp b/tests/src/core/testqgscomposition.cpp index 09288f8f79f..f4041d2e0aa 100644 --- a/tests/src/core/testqgscomposition.cpp +++ b/tests/src/core/testqgscomposition.cpp @@ -47,6 +47,7 @@ class TestQgsComposition : public QObject void pageIsEmpty(); //test the pageIsEmpty method void customProperties(); void writeRetrieveCustomProperties(); + void bounds(); void resizeToContents(); void resizeToContentsMargin(); void resizeToContentsMultiPage(); @@ -326,6 +327,64 @@ void TestQgsComposition::writeRetrieveCustomProperties() delete readComposition; } +void TestQgsComposition::bounds() +{ + //add some items to a composition + QgsComposition* composition = new QgsComposition( *mMapSettings ); + QgsComposerShape* shape1 = new QgsComposerShape( composition ); + shape1->setShapeType( QgsComposerShape::Rectangle ); + composition->addComposerShape( shape1 ); + shape1->setItemPosition( 90, 50, 90, 50, QgsComposerItem::UpperLeft, false, 1 ); + shape1->setItemRotation( 45 ); + QgsComposerShape* shape2 = new QgsComposerShape( composition ); + shape2->setShapeType( QgsComposerShape::Rectangle ); + composition->addComposerShape( shape2 ); + shape2->setItemPosition( 100, 150, 110, 50, QgsComposerItem::UpperLeft, false, 1 ); + QgsComposerShape* shape3 = new QgsComposerShape( composition ); + shape3->setShapeType( QgsComposerShape::Rectangle ); + composition->addComposerShape( shape3 ); + shape3->setItemPosition( 210, 30, 50, 100, QgsComposerItem::UpperLeft, false, 2 ); + QgsComposerShape* shape4 = new QgsComposerShape( composition ); + shape4->setShapeType( QgsComposerShape::Rectangle ); + composition->addComposerShape( shape4 ); + shape4->setItemPosition( 10, 120, 50, 30, QgsComposerItem::UpperLeft, false, 2 ); + shape4->setVisibility( false ); + + //check bounds + QRectF compositionBounds = composition->compositionBounds( false ); + QVERIFY( qgsDoubleNear( compositionBounds.height(), 372.15, 0.01 ) ); + QVERIFY( qgsDoubleNear( compositionBounds.width(), 301.00, 0.01 ) ); + QVERIFY( qgsDoubleNear( compositionBounds.left(), -2, 0.01 ) ); + QVERIFY( qgsDoubleNear( compositionBounds.top(), -2, 0.01 ) ); + + QRectF compositionBoundsNoPage = composition->compositionBounds( true ); + QVERIFY( qgsDoubleNear( compositionBoundsNoPage.height(), 320.36, 0.01 ) ); + QVERIFY( qgsDoubleNear( compositionBoundsNoPage.width(), 250.30, 0.01 ) ); + QVERIFY( qgsDoubleNear( compositionBoundsNoPage.left(), 9.85, 0.01 ) ); + QVERIFY( qgsDoubleNear( compositionBoundsNoPage.top(), 49.79, 0.01 ) ); + + QRectF page1Bounds = composition->pageItemBounds( 0, true ); + QVERIFY( qgsDoubleNear( page1Bounds.height(), 150.36, 0.01 ) ); + QVERIFY( qgsDoubleNear( page1Bounds.width(), 155.72, 0.01 ) ); + QVERIFY( qgsDoubleNear( page1Bounds.left(), 54.43, 0.01 ) ); + QVERIFY( qgsDoubleNear( page1Bounds.top(), 49.79, 0.01 ) ); + + QRectF page2Bounds = composition->pageItemBounds( 1, true ); + QVERIFY( qgsDoubleNear( page2Bounds.height(), 100.30, 0.01 ) ); + QVERIFY( qgsDoubleNear( page2Bounds.width(), 50.30, 0.01 ) ); + QVERIFY( qgsDoubleNear( page2Bounds.left(), 209.85, 0.01 ) ); + QVERIFY( qgsDoubleNear( page2Bounds.top(), 249.85, 0.01 ) ); + + QRectF page2BoundsWithHidden = composition->pageItemBounds( 1, false ); + QVERIFY( qgsDoubleNear( page2BoundsWithHidden.height(), 120.30, 0.01 ) ); + QVERIFY( qgsDoubleNear( page2BoundsWithHidden.width(), 250.30, 0.01 ) ); + QVERIFY( qgsDoubleNear( page2BoundsWithHidden.left(), 9.85, 0.01 ) ); + QVERIFY( qgsDoubleNear( page2BoundsWithHidden.top(), 249.85, 0.01 ) ); + + delete composition; +} + + void TestQgsComposition::resizeToContents() { //add some items to a composition