Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[layouts] Show an explicit warning when exporting a layout which cont…
…ains a broken image
  • Loading branch information
nyalldawson committed Jan 30, 2019
1 parent 4559d9e commit e670371
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 0 deletions.
20 changes: 20 additions & 0 deletions python/core/auto_generated/layout/qgslayoutitempicture.sip.in
Expand Up @@ -79,6 +79,8 @@ this value. The path can either be a local path or a remote (http) path.
:return: path for the source image

.. seealso:: :py:func:`setPicturePath`

.. seealso:: :py:func:`evaluatedPath`
%End

double pictureRotation() const;
Expand Down Expand Up @@ -252,6 +254,24 @@ Returns the current picture mode (image format).
virtual void finalizeRestoreFromXml();


bool isMissingImage() const;
%Docstring
Returns true if the source image is missing and the picture
cannot be rendered.

.. versionadded:: 3.6
%End

QString evaluatedPath() const;
%Docstring
Returns the current evaluated picture path, which includes
the result of data defined path overrides.

.. seealso:: :py:func:`picturePath`

.. versionadded:: 3.6
%End

public slots:

void setPictureRotation( double rotation );
Expand Down
59 changes: 59 additions & 0 deletions src/app/layout/qgslayoutvaliditychecks.cpp
Expand Up @@ -18,6 +18,7 @@
#include "qgsvaliditycheckcontext.h"
#include "qgslayoutitemscalebar.h"
#include "qgslayoutitemmap.h"
#include "qgslayoutitempicture.h"
#include "qgslayout.h"

//
Expand Down Expand Up @@ -126,3 +127,61 @@ QList<QgsValidityCheckResult> QgsLayoutOverviewValidityCheck::runCheck( const Qg
{
return mResults;
}



//
// QgsLayoutPictureSourceValidityCheck
//

QgsLayoutPictureSourceValidityCheck *QgsLayoutPictureSourceValidityCheck::create() const
{
return new QgsLayoutPictureSourceValidityCheck();
}

QString QgsLayoutPictureSourceValidityCheck::id() const
{
return QStringLiteral( "layout_picture_source_check" );
}

int QgsLayoutPictureSourceValidityCheck::checkType() const
{
return QgsAbstractValidityCheck::TypeLayoutCheck;
}

bool QgsLayoutPictureSourceValidityCheck::prepareCheck( const QgsValidityCheckContext *context, QgsFeedback * )
{
if ( context->type() != QgsValidityCheckContext::TypeLayoutContext )
return false;

const QgsLayoutValidityCheckContext *layoutContext = static_cast< const QgsLayoutValidityCheckContext * >( context );
if ( !layoutContext )
return false;

QList< QgsLayoutItemPicture * > pictureItems;
layoutContext->layout->layoutItems( pictureItems );
for ( QgsLayoutItemPicture *picture : qgis::as_const( pictureItems ) )
{
if ( picture->isMissingImage() )
{
QgsValidityCheckResult res;
res.type = QgsValidityCheckResult::Warning;
res.title = QObject::tr( "Picture source is missing or corrupt" );
const QString name = picture->displayName().toHtmlEscaped();

const QUrl picUrl = QUrl::fromUserInput( picture->evaluatedPath() );
const bool isLocalFile = picUrl.isLocalFile();

res.detailedDescription = QObject::tr( "The source for picture “%1” could not be loaded or is corrupt:<p>%2" ).arg( name,
isLocalFile ? QDir::toNativeSeparators( picture->evaluatedPath() ) : picture->evaluatedPath() );
mResults.append( res );
}
}

return true;
}

QList<QgsValidityCheckResult> QgsLayoutPictureSourceValidityCheck::runCheck( const QgsValidityCheckContext *, QgsFeedback * )
{
return mResults;
}
15 changes: 15 additions & 0 deletions src/app/layout/qgslayoutvaliditychecks.h
Expand Up @@ -44,3 +44,18 @@ class APP_EXPORT QgsLayoutOverviewValidityCheck : public QgsAbstractValidityChec
private:
QList<QgsValidityCheckResult> mResults;
};

class APP_EXPORT QgsLayoutPictureSourceValidityCheck : public QgsAbstractValidityCheck
{
public:

QgsLayoutPictureSourceValidityCheck *create() const override;
QString id() const override;
int checkType() const override;
bool prepareCheck( const QgsValidityCheckContext *context, QgsFeedback *feedback ) override;
QList< QgsValidityCheckResult > runCheck( const QgsValidityCheckContext *context, QgsFeedback *feedback ) override;

private:
QList<QgsValidityCheckResult> mResults;
};

1 change: 1 addition & 0 deletions src/app/qgisapp.cpp
Expand Up @@ -1244,6 +1244,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh

QgsApplication::validityCheckRegistry()->addCheck( new QgsLayoutScaleBarValidityCheck() );
QgsApplication::validityCheckRegistry()->addCheck( new QgsLayoutOverviewValidityCheck() );
QgsApplication::validityCheckRegistry()->addCheck( new QgsLayoutPictureSourceValidityCheck() );

mSplash->showMessage( tr( "Initializing file filters" ), Qt::AlignHCenter | Qt::AlignBottom );
qApp->processEvents();
Expand Down
13 changes: 13 additions & 0 deletions src/core/layout/qgslayoutitempicture.cpp
Expand Up @@ -485,6 +485,8 @@ void QgsLayoutItemPicture::updateMapRotation()

void QgsLayoutItemPicture::loadPicture( const QString &path )
{
mIsMissingImage = false;
mEvaluatedPath = path;
if ( path.startsWith( QLatin1String( "http" ) ) )
{
//remote location
Expand All @@ -503,6 +505,7 @@ void QgsLayoutItemPicture::loadPicture( const QString &path )
{
//trying to load an invalid file or bad expression, show cross picture
mMode = FormatSVG;
mIsMissingImage = true;
QString badFile( QStringLiteral( ":/images/composer/missing_image.svg" ) );
mSVG.load( badFile );
if ( mSVG.isValid() )
Expand Down Expand Up @@ -569,6 +572,16 @@ QSizeF QgsLayoutItemPicture::pictureSize()
}
}

bool QgsLayoutItemPicture::isMissingImage() const
{
return mIsMissingImage;
}

QString QgsLayoutItemPicture::evaluatedPath() const
{
return mEvaluatedPath;
}

void QgsLayoutItemPicture::shapeChanged()
{
if ( mMode == FormatSVG && !mLoadingSvg )
Expand Down
20 changes: 20 additions & 0 deletions src/core/layout/qgslayoutitempicture.h
Expand Up @@ -94,6 +94,7 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem
* this value. The path can either be a local path or a remote (http) path.
* \returns path for the source image
* \see setPicturePath()
* \see evaluatedPath()
*/
QString picturePath() const;

Expand Down Expand Up @@ -230,6 +231,23 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem

void finalizeRestoreFromXml() override;

/**
* Returns true if the source image is missing and the picture
* cannot be rendered.
*
* \since QGIS 3.6
*/
bool isMissingImage() const;

/**
* Returns the current evaluated picture path, which includes
* the result of data defined path overrides.
*
* \see picturePath()
* \since QGIS 3.6
*/
QString evaluatedPath() const;

public slots:

/**
Expand Down Expand Up @@ -318,6 +336,8 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem
bool mHasExpressionError = false;
bool mLoaded = false;
bool mLoadingSvg = false;
bool mIsMissingImage = false;
QString mEvaluatedPath;

//! Loads an image file into the picture item and redraws the item
void loadPicture( const QString &path );
Expand Down
59 changes: 59 additions & 0 deletions tests/src/app/testqgsapplayoutvaliditychecks.cpp
Expand Up @@ -26,6 +26,7 @@
#include "qgslayout.h"
#include "qgslayoutitemscalebar.h"
#include "qgslayoutitemmap.h"
#include "qgslayoutitempicture.h"
#include "qgsabstractvaliditycheck.h"
#include "qgsvaliditycheckcontext.h"
#include "layout/qgslayoutvaliditychecks.h"
Expand All @@ -46,6 +47,7 @@ class TestQgsLayoutValidityChecks : public QObject

void testScaleBarValidity();
void testOverviewValidity();
void testPictureValidity();

private:
QString mTestDataDir;
Expand Down Expand Up @@ -158,6 +160,63 @@ void TestQgsLayoutValidityChecks::testOverviewValidity()
QCOMPARE( res.at( 1 ).type, QgsValidityCheckResult::Warning );
}

void TestQgsLayoutValidityChecks::testPictureValidity()
{
QgsProject p;
QgsLayout l( &p );

QgsLayoutItemPicture *picture = new QgsLayoutItemPicture( &l );
l.addItem( picture );

QgsLayoutValidityCheckContext context( &l );
QgsFeedback f;

// invalid picture source
picture->setPicturePath( QStringLiteral( "blaaaaaaaaaaaaaaaaah" ) );
QgsLayoutPictureSourceValidityCheck check;
QVERIFY( check.prepareCheck( &context, &f ) );
QList< QgsValidityCheckResult > res = check.runCheck( &context, &f );
QCOMPARE( res.size(), 1 );
QCOMPARE( res.at( 0 ).type, QgsValidityCheckResult::Warning );

QgsLayoutPictureSourceValidityCheck check2;
picture->setPicturePath( QString() );
QVERIFY( check2.prepareCheck( &context, &f ) );
res = check2.runCheck( &context, &f );
QCOMPARE( res.size(), 0 );

QgsLayoutPictureSourceValidityCheck check3;
picture->setPicturePath( QStringLiteral( TEST_DATA_DIR ) + "/sample_svg.svg" );
QVERIFY( check3.prepareCheck( &context, &f ) );
res = check3.runCheck( &context, &f );
QCOMPARE( res.size(), 0 );

QgsLayoutItemPicture *picture2 = new QgsLayoutItemPicture( &l );
l.addItem( picture2 );
picture2->dataDefinedProperties().setProperty( QgsLayoutObject::PictureSource, QgsProperty::fromExpression( QStringLiteral( "'d:/bad' || 'robot'" ) ) );
l.refresh();

QgsLayoutPictureSourceValidityCheck check4;
QVERIFY( check4.prepareCheck( &context, &f ) );
res = check4.runCheck( &context, &f );
QCOMPARE( res.size(), 1 );
QCOMPARE( res.at( 0 ).type, QgsValidityCheckResult::Warning );

picture2->dataDefinedProperties().setProperty( QgsLayoutObject::PictureSource, QgsProperty::fromExpression( QStringLiteral( "''" ) ) );
l.refresh();
QgsLayoutPictureSourceValidityCheck check5;
QVERIFY( check5.prepareCheck( &context, &f ) );
res = check5.runCheck( &context, &f );
QCOMPARE( res.size(), 0 );

picture2->dataDefinedProperties().setProperty( QgsLayoutObject::PictureSource, QgsProperty::fromExpression( QStringLiteral( "'%1'" ).arg( QStringLiteral( TEST_DATA_DIR ) + "/sam' || 'ple_svg.svg" ) ) );
l.refresh();
QgsLayoutPictureSourceValidityCheck check6;
QVERIFY( check6.prepareCheck( &context, &f ) );
res = check6.runCheck( &context, &f );
QCOMPARE( res.size(), 0 );
}



QGSTEST_MAIN( TestQgsLayoutValidityChecks )
Expand Down
28 changes: 28 additions & 0 deletions tests/src/core/testqgslayoutpicture.cpp
Expand Up @@ -60,6 +60,7 @@ class TestQgsLayoutPicture : public QObject

void pictureExpression();
void pictureInvalidExpression();
void valid();


private:
Expand Down Expand Up @@ -422,5 +423,32 @@ void TestQgsLayoutPicture::pictureInvalidExpression()
mPicture->dataDefinedProperties().setProperty( QgsLayoutObject::PictureSource, QgsProperty() );
}

void TestQgsLayoutPicture::valid()
{
QgsProject p;
QgsLayout l( &p );

QgsLayoutItemPicture *picture = new QgsLayoutItemPicture( &l );
l.addItem( picture );

picture->setPicturePath( mPngImage );
QVERIFY( !picture->isMissingImage() );
QCOMPARE( picture->evaluatedPath(), mPngImage );

picture->setPicturePath( QStringLiteral( "bad" ) );
QVERIFY( picture->isMissingImage() );
QCOMPARE( picture->evaluatedPath(), QStringLiteral( "bad" ) );

picture->dataDefinedProperties().setProperty( QgsLayoutObject::PictureSource, QgsProperty::fromExpression( QStringLiteral( "'%1'" ).arg( mSvgImage ) ) );
picture->refreshPicture();
QVERIFY( !picture->isMissingImage() );
QCOMPARE( picture->evaluatedPath(), mSvgImage );

picture->dataDefinedProperties().setProperty( QgsLayoutObject::PictureSource, QgsProperty::fromExpression( QStringLiteral( "'bad'" ) ) );
picture->refreshPicture();
QVERIFY( picture->isMissingImage() );
QCOMPARE( picture->evaluatedPath(), QStringLiteral( "bad" ) );
}

QGSTEST_MAIN( TestQgsLayoutPicture )
#include "testqgslayoutpicture.moc"

0 comments on commit e670371

Please sign in to comment.