diff --git a/python/core/composer/qgscomposerpicture.sip b/python/core/composer/qgscomposerpicture.sip index 4238496a91a..ad0177b4bfb 100644 --- a/python/core/composer/qgscomposerpicture.sip +++ b/python/core/composer/qgscomposerpicture.sip @@ -117,6 +117,22 @@ class QgsComposerPicture: QgsComposerItem */ ResizeMode resizeMode() const; + /**Sets the picture's anchor point, which controls how it is placed + * within the picture item's frame. + * @param anchor anchor point for picture + * @note added in 2.3 + * @see pictureAnchor + */ + void setPictureAnchor( QgsComposerItem::ItemPositionMode anchor ); + + /**Returns the picture's current anchor, which controls how it is placed + * within the picture item's frame. + * @returns anchor point for picture + * @note added in 2.3 + * @see setPictureAnchor + */ + ItemPositionMode pictureAnchor() const; + /**Returns whether the picture item is using an expression for the image source. * @returns true if the picture is using an expression for the source, false if * it is using a single static file path for the source. diff --git a/src/app/composer/qgscomposerpicturewidget.cpp b/src/app/composer/qgscomposerpicturewidget.cpp index d8687a46cde..2373a4101a3 100644 --- a/src/app/composer/qgscomposerpicturewidget.cpp +++ b/src/app/composer/qgscomposerpicturewidget.cpp @@ -245,6 +245,29 @@ void QgsComposerPictureWidget::on_mResizeModeComboBox_currentIndexChanged( int i //disable picture rotation for non-zoom modes mRotationGroupBox->setEnabled( mPicture->resizeMode() == QgsComposerPicture::Zoom ); + + //disable anchor point control for certain zoom modes + if ( mPicture->resizeMode() == QgsComposerPicture::Zoom || + mPicture->resizeMode() == QgsComposerPicture::Clip ) + { + mAnchorPointComboBox->setEnabled( true ); + } + else + { + mAnchorPointComboBox->setEnabled( false ); + } +} + +void QgsComposerPictureWidget::on_mAnchorPointComboBox_currentIndexChanged( int index ) +{ + if ( !mPicture ) + { + return; + } + + mPicture->beginCommand( tr( "Picture placement changed" ) ); + mPicture->setPictureAnchor(( QgsComposerItem::ItemPositionMode )index ); + mPicture->endCommand(); } void QgsComposerPictureWidget::on_mRadioPath_clicked() @@ -422,12 +445,12 @@ void QgsComposerPictureWidget::setGuiElementValues() mComposerMapComboBox->blockSignals( true ); mRotationFromComposerMapCheckBox->blockSignals( true ); mResizeModeComboBox->blockSignals( true ); + mAnchorPointComboBox->blockSignals( true ); mRadioPath->blockSignals( true ); mRadioExpression->blockSignals( true ); mPictureExpressionLineEdit->blockSignals( true ); mPictureLineEdit->setText( mPicture->pictureFile() ); -// QRectF pictureRect = mPicture->rect(); mPictureRotationSpinBox->setValue( mPicture->pictureRotation() ); refreshMapComboBox(); @@ -455,6 +478,18 @@ void QgsComposerPictureWidget::setGuiElementValues() //disable picture rotation for non-zoom modes mRotationGroupBox->setEnabled( mPicture->resizeMode() == QgsComposerPicture::Zoom ); + mAnchorPointComboBox->setCurrentIndex(( int )mPicture->pictureAnchor() ); + //disable anchor point control for certain zoom modes + if ( mPicture->resizeMode() == QgsComposerPicture::Zoom || + mPicture->resizeMode() == QgsComposerPicture::Clip ) + { + mAnchorPointComboBox->setEnabled( true ); + } + else + { + mAnchorPointComboBox->setEnabled( false ); + } + mRadioPath->setChecked( !( mPicture->usePictureExpression() ) ); mRadioExpression->setChecked( mPicture->usePictureExpression() ); mPictureLineEdit->setEnabled( !( mPicture->usePictureExpression() ) ); @@ -469,6 +504,7 @@ void QgsComposerPictureWidget::setGuiElementValues() mPictureLineEdit->blockSignals( false ); mComposerMapComboBox->blockSignals( false ); mResizeModeComboBox->blockSignals( false ); + mAnchorPointComboBox->blockSignals( false ); mRadioPath->blockSignals( false ); mRadioExpression->blockSignals( false ); mPictureExpressionLineEdit->blockSignals( false ); diff --git a/src/app/composer/qgscomposerpicturewidget.h b/src/app/composer/qgscomposerpicturewidget.h index 725b1e54c0e..e142fd37604 100644 --- a/src/app/composer/qgscomposerpicturewidget.h +++ b/src/app/composer/qgscomposerpicturewidget.h @@ -30,6 +30,7 @@ class QgsComposerPictureWidget: public QWidget, private Ui::QgsComposerPictureWi Q_OBJECT public: + QgsComposerPictureWidget( QgsComposerPicture* picture ); ~QgsComposerPictureWidget(); @@ -47,6 +48,7 @@ class QgsComposerPictureWidget: public QWidget, private Ui::QgsComposerPictureWi void on_mRotationFromComposerMapCheckBox_stateChanged( int state ); void on_mComposerMapComboBox_activated( const QString & text ); void on_mResizeModeComboBox_currentIndexChanged( int index ); + void on_mAnchorPointComboBox_currentIndexChanged( int index ); void on_mRadioPath_clicked(); void on_mRadioExpression_clicked(); void setPictureExpression(); diff --git a/src/core/composer/qgscomposerpicture.cpp b/src/core/composer/qgscomposerpicture.cpp index 12e2eb02ed6..22a30b83377 100644 --- a/src/core/composer/qgscomposerpicture.cpp +++ b/src/core/composer/qgscomposerpicture.cpp @@ -38,6 +38,7 @@ QgsComposerPicture::QgsComposerPicture( QgsComposition *composition ) : mPictureRotation( 0 ), mRotationMap( 0 ), mResizeMode( QgsComposerPicture::Zoom ), + mPictureAnchor( UpperLeft ), mPictureExpr( 0 ) { mPictureWidth = rect().width(); @@ -50,6 +51,7 @@ QgsComposerPicture::QgsComposerPicture() : QgsComposerItem( 0 ), mPictureRotation( 0 ), mRotationMap( 0 ), mResizeMode( QgsComposerPicture::Zoom ), + mPictureAnchor( UpperLeft ), mPictureExpr( 0 ) { mPictureHeight = rect().height(); @@ -90,40 +92,99 @@ void QgsComposerPicture::paint( QPainter* painter, const QStyleOptionGraphicsIte //int newDpi = ( painter->device()->logicalDpiX() + painter->device()->logicalDpiY() ) / 2; + //picture resizing if ( mMode != Unknown ) { double boundRectWidthMM; double boundRectHeightMM; - double imageRectWidthMM; - double imageRectHeightMM; + QRect imageRect; if ( mResizeMode == QgsComposerPicture::Zoom || mResizeMode == QgsComposerPicture::ZoomResizeFrame ) { boundRectWidthMM = mPictureWidth; boundRectHeightMM = mPictureHeight; - imageRectWidthMM = mImage.width(); - imageRectHeightMM = mImage.height(); + imageRect = QRect( 0, 0, mImage.width(), mImage.height() ); } else if ( mResizeMode == QgsComposerPicture::Stretch ) { boundRectWidthMM = rect().width(); boundRectHeightMM = rect().height(); - imageRectWidthMM = mImage.width(); - imageRectHeightMM = mImage.height(); + imageRect = QRect( 0, 0, mImage.width(), mImage.height() ); + } + else if ( mResizeMode == QgsComposerPicture::Clip ) + { + boundRectWidthMM = rect().width(); + boundRectHeightMM = rect().height(); + int imageRectWidthPixels = mImage.width(); + int imageRectHeightPixels = mImage.height(); + imageRect = clippedImageRect( boundRectWidthMM, boundRectHeightMM , + QSize( imageRectWidthPixels, imageRectHeightPixels ) ); } else { boundRectWidthMM = rect().width(); boundRectHeightMM = rect().height(); - imageRectWidthMM = rect().width() * mComposition->printResolution() / 25.4; - imageRectHeightMM = rect().height() * mComposition->printResolution() / 25.4; + imageRect = QRect( 0, 0, rect().width() * mComposition->printResolution() / 25.4, + rect().height() * mComposition->printResolution() / 25.4 ); } painter->save(); + //zoom mode - calculate anchor point and rotation if ( mResizeMode == Zoom ) { - painter->translate( rect().width() / 2.0, rect().height() / 2.0 ); - painter->rotate( mPictureRotation ); - painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 ); + //TODO - allow placement modes with rotation set. for now, setting a rotation + //always places picture in center of frame + if ( mPictureRotation != 0 ) + { + painter->translate( rect().width() / 2.0, rect().height() / 2.0 ); + painter->rotate( mPictureRotation ); + painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 ); + } + else + { + //shift painter to edge/middle of frame depending on placement + double diffX = rect().width() - boundRectWidthMM; + double diffY = rect().height() - boundRectHeightMM; + + double dX = 0; + double dY = 0; + switch ( mPictureAnchor ) + { + case UpperLeft: + case MiddleLeft: + case LowerLeft: + //nothing to do + break; + case UpperMiddle: + case Middle: + case LowerMiddle: + dX = diffX / 2.0; + break; + case UpperRight: + case MiddleRight: + case LowerRight: + dX = diffX; + break; + } + switch ( mPictureAnchor ) + { + case UpperLeft: + case UpperMiddle: + case UpperRight: + //nothing to do + break; + case MiddleLeft: + case Middle: + case MiddleRight: + dY = diffY / 2.0; + break; + case LowerLeft: + case LowerMiddle: + case LowerRight: + dY = diffY; + break; + } + painter->translate( dX, dY ); + } } if ( mMode == SVG ) @@ -132,7 +193,7 @@ void QgsComposerPicture::paint( QPainter* painter, const QStyleOptionGraphicsIte } else if ( mMode == RASTER ) { - painter->drawImage( QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ), mImage, QRectF( 0, 0, imageRectWidthMM, imageRectHeightMM ) ); + painter->drawImage( QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ), mImage, imageRect ); } painter->restore(); @@ -146,6 +207,64 @@ void QgsComposerPicture::paint( QPainter* painter, const QStyleOptionGraphicsIte } } +QRect QgsComposerPicture::clippedImageRect( double &boundRectWidthMM, double &boundRectHeightMM, QSize imageRectPixels ) +{ + int boundRectWidthPixels = boundRectWidthMM * mComposition->printResolution() / 25.4; + int boundRectHeightPixels = boundRectHeightMM * mComposition->printResolution() / 25.4; + + //update boundRectWidth/Height so that they exactly match pixel bounds + boundRectWidthMM = boundRectWidthPixels * 25.4 / mComposition->printResolution(); + boundRectHeightMM = boundRectHeightPixels * 25.4 / mComposition->printResolution(); + + //calculate part of image which fits in bounds + int leftClip = 0; + int topClip = 0; + + //calculate left crop + switch ( mPictureAnchor ) + { + case UpperLeft: + case MiddleLeft: + case LowerLeft: + leftClip = 0; + break; + case UpperMiddle: + case Middle: + case LowerMiddle: + leftClip = ( imageRectPixels.width() - boundRectWidthPixels ) / 2; + break; + case UpperRight: + case MiddleRight: + case LowerRight: + leftClip = imageRectPixels.width() - boundRectWidthPixels; + break; + } + + //calculate top crop + switch ( mPictureAnchor ) + { + case UpperLeft: + case UpperMiddle: + case UpperRight: + topClip = 0; + break; + case MiddleLeft: + case Middle: + case MiddleRight: + topClip = ( imageRectPixels.height() - boundRectHeightPixels ) / 2; + break; + case LowerLeft: + case LowerMiddle: + case LowerRight: + topClip = imageRectPixels.height() - boundRectHeightPixels; + break; + } + + + return QRect( leftClip, topClip, boundRectWidthPixels, boundRectHeightPixels ); + +} + void QgsComposerPicture::setPictureFile( const QString& path ) { mSourceFile.setFileName( path ); @@ -525,6 +644,8 @@ bool QgsComposerPicture::writeXML( QDomElement& elem, QDomDocument & doc ) const composerPictureElem.setAttribute( "pictureWidth", QString::number( mPictureWidth ) ); composerPictureElem.setAttribute( "pictureHeight", QString::number( mPictureHeight ) ); composerPictureElem.setAttribute( "resizeMode", QString::number(( int )mResizeMode ) ); + composerPictureElem.setAttribute( "anchorPoint", QString::number(( int )mPictureAnchor ) ); + if ( mUseSourceExpression ) { composerPictureElem.setAttribute( "useExpression", "true" ); @@ -561,6 +682,7 @@ bool QgsComposerPicture::readXML( const QDomElement& itemElem, const QDomDocumen mPictureWidth = itemElem.attribute( "pictureWidth", "10" ).toDouble(); mPictureHeight = itemElem.attribute( "pictureHeight", "10" ).toDouble(); mResizeMode = QgsComposerPicture::ResizeMode( itemElem.attribute( "resizeMode", "0" ).toInt() ); + mPictureAnchor = QgsComposerItem::ItemPositionMode( itemElem.attribute( "anchorPoint", QString( QgsComposerItem::UpperLeft ) ).toInt() ); QDomNodeList composerItemList = itemElem.elementsByTagName( "ComposerItem" ); if ( composerItemList.size() > 0 ) @@ -594,8 +716,6 @@ bool QgsComposerPicture::readXML( const QDomElement& itemElem, const QDomDocumen QString fileName = QgsProject::instance()->readPath( itemElem.attribute( "file" ) ); mSourceFile.setFileName( fileName ); - refreshPicture(); - //picture rotation if ( itemElem.attribute( "pictureRotation", "0" ).toDouble() != 0 ) { @@ -619,6 +739,8 @@ bool QgsComposerPicture::readXML( const QDomElement& itemElem, const QDomDocumen QObject::connect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setRotation( double ) ) ); } + refreshPicture(); + emit itemChanged(); return true; } @@ -635,6 +757,12 @@ int QgsComposerPicture::rotationMap() const } } +void QgsComposerPicture::setPictureAnchor( QgsComposerItem::ItemPositionMode anchor ) +{ + mPictureAnchor = anchor; + update(); +} + bool QgsComposerPicture::imageSizeConsideringRotation( double& width, double& height ) const { //kept for api compatibility with QGIS 2.0 - use mPictureRotation diff --git a/src/core/composer/qgscomposerpicture.h b/src/core/composer/qgscomposerpicture.h index ea1371c8664..63135bb0d48 100644 --- a/src/core/composer/qgscomposerpicture.h +++ b/src/core/composer/qgscomposerpicture.h @@ -141,6 +141,22 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem */ ResizeMode resizeMode() const { return mResizeMode; } + /**Sets the picture's anchor point, which controls how it is placed + * within the picture item's frame. + * @param anchor anchor point for picture + * @note added in 2.3 + * @see pictureAnchor + */ + void setPictureAnchor( QgsComposerItem::ItemPositionMode anchor ); + + /**Returns the picture's current anchor, which controls how it is placed + * within the picture item's frame. + * @returns anchor point for picture + * @note added in 2.3 + * @see setPictureAnchor + */ + ItemPositionMode pictureAnchor() const { return mPictureAnchor; } + /**Returns whether the picture item is using an expression for the image source. * @returns true if the picture is using an expression for the source, false if * it is using a single static file path for the source. @@ -275,6 +291,7 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem double mPictureHeight; ResizeMode mResizeMode; + QgsComposerItem::ItemPositionMode mPictureAnchor; QgsExpression* mPictureExpr; @@ -286,6 +303,11 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem /**sets up the picture item and connects to relevant signals*/ void init(); + + /**Returns part of a raster image which will be shown, given current picture + * anchor settings + */ + QRect clippedImageRect( double &boundRectWidthMM, double &boundRectHeightMM, QSize imageRectPixels ); }; #endif diff --git a/src/ui/qgscomposerpicturewidgetbase.ui b/src/ui/qgscomposerpicturewidgetbase.ui index 188527a1b06..c431489b9f8 100644 --- a/src/ui/qgscomposerpicturewidgetbase.ui +++ b/src/ui/qgscomposerpicturewidgetbase.ui @@ -6,7 +6,7 @@ 0 0 - 312 + 315 572 @@ -52,8 +52,8 @@ 0 0 - 310 - 549 + 297 + 589 @@ -69,13 +69,6 @@ false - - - - Image source - - - @@ -109,36 +102,19 @@ - - - - - - Expression - - - - - - - - - - - 0 - 0 - - - - ... - - - - :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg - - - - + + + + Image source + + + + + + + Placement + + @@ -176,6 +152,86 @@ + + + + + + Expression + + + + + + + + + + + 0 + 0 + + + + ... + + + + :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg + + + + + + + + + + Top left + + + + + Top center + + + + + Top right + + + + + Middle left + + + + + Middle + + + + + Middle right + + + + + Bottom left + + + + + Bottom center + + + + + Bottom right + + + + diff --git a/tests/src/core/testqgscomposerpicture.cpp b/tests/src/core/testqgscomposerpicture.cpp index e7cf3486105..1846f9fc3fd 100644 --- a/tests/src/core/testqgscomposerpicture.cpp +++ b/tests/src/core/testqgscomposerpicture.cpp @@ -43,6 +43,10 @@ class TestQgsComposerPicture: public QObject void pictureResizeZoomAndResize(); void pictureResizeFrameToImage(); + void pictureClipAnchor(); + void pictureClipAnchorOversize(); + void pictureZoomAnchor(); + void pictureSvgZoom(); void pictureSvgStretch(); void pictureSvgZoomAndResize(); @@ -214,6 +218,56 @@ void TestQgsComposerPicture::pictureResizeFrameToImage() mComposerPicture->setSceneRect( QRectF( 70, 70, 100, 100 ) ); } +void TestQgsComposerPicture::pictureClipAnchor() +{ + //test picture anchor in Clip mode + mComposition->addComposerPicture( mComposerPicture ); + mComposerPicture->setResizeMode( QgsComposerPicture::Clip ); + mComposerPicture->setSceneRect( QRectF( 70, 70, 30, 50 ) ); + mComposerPicture->setPictureAnchor( QgsComposerItem::LowerRight ); + + QgsCompositionChecker checker( "composerpicture_clip_anchor", mComposition ); + QVERIFY( checker.testComposition( mReport, 0, 0 ) ); + + mComposition->removeItem( mComposerPicture ); + mComposerPicture->setResizeMode( QgsComposerPicture::Zoom ); + mComposerPicture->setPictureAnchor( QgsComposerItem::UpperLeft ); + mComposerPicture->setSceneRect( QRectF( 70, 70, 100, 100 ) ); +} + +void TestQgsComposerPicture::pictureClipAnchorOversize() +{ + //test picture anchor in Clip mode + mComposition->addComposerPicture( mComposerPicture ); + mComposerPicture->setResizeMode( QgsComposerPicture::Clip ); + mComposerPicture->setSceneRect( QRectF( 70, 70, 150, 120 ) ); + mComposerPicture->setPictureAnchor( QgsComposerItem::LowerMiddle ); + + QgsCompositionChecker checker( "expected_composerpicture_clip_anchoroversize", mComposition ); + QVERIFY( checker.testComposition( mReport, 0, 0 ) ); + + mComposition->removeItem( mComposerPicture ); + mComposerPicture->setResizeMode( QgsComposerPicture::Zoom ); + mComposerPicture->setPictureAnchor( QgsComposerItem::UpperLeft ); + mComposerPicture->setSceneRect( QRectF( 70, 70, 100, 100 ) ); +} + +void TestQgsComposerPicture::pictureZoomAnchor() +{ + //test picture anchor in Zoom mode + mComposition->addComposerPicture( mComposerPicture ); + mComposerPicture->setResizeMode( QgsComposerPicture::Zoom ); + mComposerPicture->setSceneRect( QRectF( 70, 10, 30, 100 ) ); + mComposerPicture->setPictureAnchor( QgsComposerItem::LowerMiddle ); + + QgsCompositionChecker checker( "composerpicture_zoom_anchor", mComposition ); + QVERIFY( checker.testComposition( mReport, 0, 100 ) ); + + mComposition->removeItem( mComposerPicture ); + mComposerPicture->setPictureAnchor( QgsComposerItem::UpperLeft ); + mComposerPicture->setSceneRect( QRectF( 70, 70, 100, 100 ) ); +} + void TestQgsComposerPicture::pictureSvgZoom() { //test picture resize Zoom mode diff --git a/tests/testdata/control_images/expected_composerpicture_clip_anchor/expected_composerpicture_clip_anchor.png b/tests/testdata/control_images/expected_composerpicture_clip_anchor/expected_composerpicture_clip_anchor.png new file mode 100644 index 00000000000..376e05363d1 Binary files /dev/null and b/tests/testdata/control_images/expected_composerpicture_clip_anchor/expected_composerpicture_clip_anchor.png differ diff --git a/tests/testdata/control_images/expected_composerpicture_clip_anchoroversize/expected_composerpicture_clip_anchoroversize.png b/tests/testdata/control_images/expected_composerpicture_clip_anchoroversize/expected_composerpicture_clip_anchoroversize.png new file mode 100644 index 00000000000..bc0ef1766c8 Binary files /dev/null and b/tests/testdata/control_images/expected_composerpicture_clip_anchoroversize/expected_composerpicture_clip_anchoroversize.png differ diff --git a/tests/testdata/control_images/expected_composerpicture_expression/expected_composerpicture_expression.png b/tests/testdata/control_images/expected_composerpicture_expression/expected_composerpicture_expression.png index 0a631778e2b..0cd6449a637 100644 Binary files a/tests/testdata/control_images/expected_composerpicture_expression/expected_composerpicture_expression.png and b/tests/testdata/control_images/expected_composerpicture_expression/expected_composerpicture_expression.png differ diff --git a/tests/testdata/control_images/expected_composerpicture_itemrotation/expected_composerpicture_itemrotation.png b/tests/testdata/control_images/expected_composerpicture_itemrotation/expected_composerpicture_itemrotation.png index 8443dbde4ce..3d0508e8c69 100644 Binary files a/tests/testdata/control_images/expected_composerpicture_itemrotation/expected_composerpicture_itemrotation.png and b/tests/testdata/control_images/expected_composerpicture_itemrotation/expected_composerpicture_itemrotation.png differ diff --git a/tests/testdata/control_images/expected_composerpicture_resize_zoom/expected_composerpicture_resize_zoom.png b/tests/testdata/control_images/expected_composerpicture_resize_zoom/expected_composerpicture_resize_zoom.png index 8778ecc521f..2ff89753ea6 100644 Binary files a/tests/testdata/control_images/expected_composerpicture_resize_zoom/expected_composerpicture_resize_zoom.png and b/tests/testdata/control_images/expected_composerpicture_resize_zoom/expected_composerpicture_resize_zoom.png differ diff --git a/tests/testdata/control_images/expected_composerpicture_svg_zoom/expected_composerpicture_svg_zoom.png b/tests/testdata/control_images/expected_composerpicture_svg_zoom/expected_composerpicture_svg_zoom.png index 0a631778e2b..0cd6449a637 100644 Binary files a/tests/testdata/control_images/expected_composerpicture_svg_zoom/expected_composerpicture_svg_zoom.png and b/tests/testdata/control_images/expected_composerpicture_svg_zoom/expected_composerpicture_svg_zoom.png differ diff --git a/tests/testdata/control_images/expected_composerpicture_zoom_anchor/expected_composerpicture_zoom_anchor.png b/tests/testdata/control_images/expected_composerpicture_zoom_anchor/expected_composerpicture_zoom_anchor.png new file mode 100644 index 00000000000..432644d42c0 Binary files /dev/null and b/tests/testdata/control_images/expected_composerpicture_zoom_anchor/expected_composerpicture_zoom_anchor.png differ