From 3037f22482eb4eb3a46481ff912871ad17ff7130 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Tue, 18 Jul 2017 10:47:06 +0700 Subject: [PATCH] Non-blocking save as image/PDF dialogs (#4874) --- python/core/qgsmaprenderertask.sip | 3 +- src/app/qgisapp.cpp | 155 ++--------------------------- src/app/qgsmapsavedialog.cpp | 108 +++++++++++++++++++- src/app/qgsmapsavedialog.h | 13 ++- src/core/qgsmaprenderertask.cpp | 73 ++++++++++---- src/core/qgsmaprenderertask.h | 4 +- 6 files changed, 183 insertions(+), 173 deletions(-) diff --git a/python/core/qgsmaprenderertask.sip b/python/core/qgsmaprenderertask.sip index b4f34f922c9..26429a2b048 100644 --- a/python/core/qgsmaprenderertask.sip +++ b/python/core/qgsmaprenderertask.sip @@ -31,7 +31,8 @@ class QgsMapRendererTask : QgsTask QgsMapRendererTask( const QgsMapSettings &ms, const QString &fileName, - const QString &fileFormat = QString( "PNG" ) ); + const QString &fileFormat = QString( "PNG" ), + const bool forceRaster = false ); %Docstring Constructor for QgsMapRendererTask to render a map to an image file. %End diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index d43ddb726de..b05e7124355 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -5740,173 +5740,34 @@ void QgisApp::updateFilterLegend() void QgisApp::saveMapAsImage() { - QList< QgsMapDecoration * > decorations; - QString activeDecorations; + QList< QgsDecorationItem * > decorations; Q_FOREACH ( QgsDecorationItem *decoration, mDecorationItems ) { if ( decoration->enabled() ) { decorations << decoration; - if ( activeDecorations.isEmpty() ) - activeDecorations = decoration->name().toLower(); - else - activeDecorations += QString( ", %1" ).arg( decoration->name().toLower() ); } } - QgsMapSaveDialog dlg( this, mMapCanvas, activeDecorations ); - if ( !dlg.exec() ) - return; - - QPair< QString, QString> fileNameAndFilter = QgsGuiUtils::getSaveAsImageName( this, tr( "Choose a file name to save the map image as" ) ); - if ( fileNameAndFilter.first != QLatin1String( "" ) ) - { - QgsMapSettings ms = QgsMapSettings(); - dlg.applyMapSettings( ms ); - - QgsMapRendererTask *mapRendererTask = new QgsMapRendererTask( ms, fileNameAndFilter.first, fileNameAndFilter.second ); - - if ( dlg.drawAnnotations() ) - { - mapRendererTask->addAnnotations( QgsProject::instance()->annotationManager()->annotations() ); - } - - if ( dlg.drawDecorations() ) - { - mapRendererTask->addDecorations( decorations ); - } - - mapRendererTask->setSaveWorldFile( dlg.saveWorldFile() ); - - connect( mapRendererTask, &QgsMapRendererTask::renderingComplete, this, [ = ] - { - messageBar()->pushSuccess( tr( "Save as image" ), tr( "Successfully saved map to image" ) ); - } ); - connect( mapRendererTask, &QgsMapRendererTask::errorOccurred, this, [ = ]( int error ) - { - switch ( error ) - { - case QgsMapRendererTask::ImageAllocationFail: - { - messageBar()->pushWarning( tr( "Save as image" ), tr( "Could not allocate required memory for image" ) ); - break; - } - case QgsMapRendererTask::ImageSaveFail: - { - messageBar()->pushWarning( tr( "Save as image" ), tr( "Could not save the image to file" ) ); - break; - } - } - } ); - - QgsApplication::taskManager()->addTask( mapRendererTask ); - } - + QgsMapSaveDialog *dlg = new QgsMapSaveDialog( this, mMapCanvas, decorations, QgsProject::instance()->annotationManager()->annotations() ); + dlg->setAttribute( Qt::WA_DeleteOnClose ); + dlg->show(); } // saveMapAsImage void QgisApp::saveMapAsPdf() { - QList< QgsMapDecoration * > decorations; - QString activeDecorations; + QList< QgsDecorationItem * > decorations; Q_FOREACH ( QgsDecorationItem *decoration, mDecorationItems ) { if ( decoration->enabled() ) { decorations << decoration; - if ( activeDecorations.isEmpty() ) - activeDecorations = decoration->name().toLower(); - else - activeDecorations += QString( ", %1" ).arg( decoration->name().toLower() ); } } - QgsMapSaveDialog dlg( this, mMapCanvas, activeDecorations, QgsMapSaveDialog::Pdf ); - if ( !dlg.exec() ) - return; - - QgsSettings settings; - QString lastUsedDir = settings.value( QStringLiteral( "UI/lastSaveAsImageDir" ), QDir::homePath() ).toString(); - QString fileName = QFileDialog::getSaveFileName( this, tr( "Save map as" ), lastUsedDir, tr( "PDF Format" ) + " (*.pdf *.PDF)" ); - if ( !fileName.isEmpty() ) - { - QgsMapSettings ms = QgsMapSettings(); - dlg.applyMapSettings( ms ); - - QPrinter *printer = new QPrinter(); - printer->setOutputFileName( fileName ); - printer->setOutputFormat( QPrinter::PdfFormat ); - printer->setOrientation( QPrinter::Portrait ); - // paper size needs to be given in millimeters in order to be able to set a resolution to pass onto the map renderer - printer->setPaperSize( dlg.size() * 25.4 / dlg.dpi(), QPrinter::Millimeter ); - printer->setPageMargins( 0, 0, 0, 0, QPrinter::Millimeter ); - printer->setResolution( dlg.dpi() ); - - QPainter *p = new QPainter(); - QImage *image = nullptr; - if ( dlg.saveAsRaster() ) - { - image = new QImage( dlg.size(), QImage::Format_ARGB32 ); - if ( image->isNull() ) - { - messageBar()->pushWarning( tr( "Save as PDF" ), tr( "Could not allocate required memory for image" ) ); - delete p; - delete image; - delete printer; - - return; - } - - image->setDotsPerMeterX( 1000 * dlg.dpi() / 25.4 ); - image->setDotsPerMeterY( 1000 * dlg.dpi() / 25.4 ); - p->begin( image ); - } - else - { - p->begin( printer ); - } - - QgsMapRendererTask *mapRendererTask = new QgsMapRendererTask( ms, p ); - - if ( dlg.drawAnnotations() ) - { - mapRendererTask->addAnnotations( QgsProject::instance()->annotationManager()->annotations() ); - } - - if ( dlg.drawDecorations() ) - { - mapRendererTask->addDecorations( decorations ); - } - - mapRendererTask->setSaveWorldFile( dlg.saveWorldFile() ); - - connect( mapRendererTask, &QgsMapRendererTask::renderingComplete, this, [ this, p, image, printer ] - { - p->end(); - - if ( image ) - { - QPainter pp; - pp.begin( printer ); - QRectF rect( 0, 0, image->width(), image->height() ); - pp.drawImage( rect, *image, rect ); - pp.end(); - } - - messageBar()->pushSuccess( tr( "Save as PDF" ), tr( "Successfully saved map to PDF" ) ); - delete p; - delete image; - delete printer; - } ); - connect( mapRendererTask, &QgsMapRendererTask::errorOccurred, this, [ this, p, image, printer ]( int ) - { - delete p; - delete image; - delete printer; - } ); - - QgsApplication::taskManager()->addTask( mapRendererTask ); - } - + QgsMapSaveDialog *dlg = new QgsMapSaveDialog( this, mMapCanvas, decorations, QgsProject::instance()->annotationManager()->annotations(), QgsMapSaveDialog::Pdf ); + dlg->setAttribute( Qt::WA_DeleteOnClose ); + dlg->show(); } // saveMapAsPdf //overloaded version of the above function diff --git a/src/app/qgsmapsavedialog.cpp b/src/app/qgsmapsavedialog.cpp index ab0fb5f9231..be15d956623 100644 --- a/src/app/qgsmapsavedialog.cpp +++ b/src/app/qgsmapsavedialog.cpp @@ -18,25 +18,32 @@ #include "qgsmapsavedialog.h" #include "qgis.h" +#include "qgisapp.h" #include "qgsscalecalculator.h" #include "qgsdecorationitem.h" #include "qgsexpressioncontext.h" #include "qgsextentgroupbox.h" #include "qgsmapsettings.h" #include "qgsmapsettingsutils.h" +#include "qgsmaprenderertask.h" #include "qgsproject.h" #include "qgssettings.h" #include -#include +#include +#include #include +#include +#include +#include Q_GUI_EXPORT extern int qt_defaultDpiX(); -QgsMapSaveDialog::QgsMapSaveDialog( QWidget *parent, QgsMapCanvas *mapCanvas, const QString &activeDecorations, DialogType type ) +QgsMapSaveDialog::QgsMapSaveDialog( QWidget *parent, QgsMapCanvas *mapCanvas, QList< QgsDecorationItem * > decorations, QList< QgsAnnotation *> annotations, DialogType type ) : QDialog( parent ) , mDialogType( type ) , mMapCanvas( mapCanvas ) + , mAnnotations( annotations ) { setupUi( this ); @@ -57,6 +64,15 @@ QgsMapSaveDialog::QgsMapSaveDialog( QWidget *parent, QgsMapCanvas *mapCanvas, co mScaleWidget->setMapCanvas( mMapCanvas ); mScaleWidget->setShowCurrentScaleButton( true ); + QString activeDecorations; + Q_FOREACH ( QgsDecorationItem *decoration, decorations ) + { + mDecorations << decoration; + if ( activeDecorations.isEmpty() ) + activeDecorations = decoration->name().toLower(); + else + activeDecorations += QString( ", %1" ).arg( decoration->name().toLower() ); + } mDrawDecorations->setText( tr( "Draw active decorations: %1" ).arg( !activeDecorations.isEmpty() ? activeDecorations : tr( "none" ) ) ); connect( mResolutionSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsMapSaveDialog::updateDpi ); @@ -92,6 +108,8 @@ QgsMapSaveDialog::QgsMapSaveDialog( QWidget *parent, QgsMapCanvas *mapCanvas, co this->setWindowTitle( tr( "Save map as PDF" ) ); } + + connect( buttonBox, &QDialogButtonBox::accepted, this, &QgsMapSaveDialog::accepted ); } void QgsMapSaveDialog::updateDpi( int dpi ) @@ -222,3 +240,89 @@ void QgsMapSaveDialog::applyMapSettings( QgsMapSettings &mapSettings ) mapSettings.setExpressionContext( expressionContext ); } + +void QgsMapSaveDialog::accepted() +{ + if ( mDialogType == Image ) + { + QPair< QString, QString> fileNameAndFilter = QgsGuiUtils::getSaveAsImageName( QgisApp::instance(), tr( "Choose a file name to save the map image as" ) ); + if ( fileNameAndFilter.first != QLatin1String( "" ) ) + { + QgsMapSettings ms = QgsMapSettings(); + applyMapSettings( ms ); + + QgsMapRendererTask *mapRendererTask = new QgsMapRendererTask( ms, fileNameAndFilter.first, fileNameAndFilter.second ); + + if ( drawAnnotations() ) + { + mapRendererTask->addAnnotations( mAnnotations ); + } + + if ( drawDecorations() ) + { + mapRendererTask->addDecorations( mDecorations ); + } + + mapRendererTask->setSaveWorldFile( saveWorldFile() ); + + connect( mapRendererTask, &QgsMapRendererTask::renderingComplete, [ = ] + { + QgisApp::instance()->messageBar()->pushSuccess( tr( "Save as image" ), tr( "Successfully saved map to image" ) ); + } ); + connect( mapRendererTask, &QgsMapRendererTask::errorOccurred, [ = ]( int error ) + { + switch ( error ) + { + case QgsMapRendererTask::ImageAllocationFail: + { + QgisApp::instance()->messageBar()->pushWarning( tr( "Save as image" ), tr( "Could not allocate required memory for image" ) ); + break; + } + case QgsMapRendererTask::ImageSaveFail: + { + QgisApp::instance()->messageBar()->pushWarning( tr( "Save as image" ), tr( "Could not save the map to file" ) ); + break; + } + } + } ); + + QgsApplication::taskManager()->addTask( mapRendererTask ); + } + } + else + { + QgsSettings settings; + QString lastUsedDir = settings.value( QStringLiteral( "UI/lastSaveAsImageDir" ), QDir::homePath() ).toString(); + QString fileName = QFileDialog::getSaveFileName( QgisApp::instance(), tr( "Save map as" ), lastUsedDir, tr( "PDF Format" ) + " (*.pdf *.PDF)" ); + if ( !fileName.isEmpty() ) + { + QgsMapSettings ms = QgsMapSettings(); + applyMapSettings( ms ); + + QgsMapRendererTask *mapRendererTask = new QgsMapRendererTask( ms, fileName, QStringLiteral( "PDF" ), saveAsRaster() ); + + if ( drawAnnotations() ) + { + mapRendererTask->addAnnotations( mAnnotations ); + } + + if ( drawDecorations() ) + { + mapRendererTask->addDecorations( mDecorations ); + } + + mapRendererTask->setSaveWorldFile( saveWorldFile() ); + + connect( mapRendererTask, &QgsMapRendererTask::renderingComplete, [ = ] + { + QgisApp::instance()->messageBar()->pushSuccess( tr( "Save as PDF" ), tr( "Successfully saved map to PDF" ) ); + } ); + connect( mapRendererTask, &QgsMapRendererTask::errorOccurred, [ = ]( int ) + { + QgisApp::instance()->messageBar()->pushWarning( tr( "Save as PDF" ), tr( "Could not save the map to PDF..." ) ); + } ); + + QgsApplication::taskManager()->addTask( mapRendererTask ); + } + } +} diff --git a/src/app/qgsmapsavedialog.h b/src/app/qgsmapsavedialog.h index b6194f804fc..b1291b3c2f1 100644 --- a/src/app/qgsmapsavedialog.h +++ b/src/app/qgsmapsavedialog.h @@ -21,8 +21,9 @@ #include "ui_qgsmapsavedialog.h" #include "qgisapp.h" -#include "qgsrectangle.h" #include "qgsmapcanvas.h" +#include "qgsmapdecoration.h" +#include "qgsrectangle.h" #include #include @@ -45,7 +46,10 @@ class APP_EXPORT QgsMapSaveDialog: public QDialog, private Ui::QgsMapSaveDialog /** Constructor for QgsMapSaveDialog */ - QgsMapSaveDialog( QWidget *parent = nullptr, QgsMapCanvas *mapCanvas = nullptr, const QString &activeDecorations = QString(), DialogType type = Image ); + QgsMapSaveDialog( QWidget *parent = nullptr, QgsMapCanvas *mapCanvas = nullptr, + QList< QgsDecorationItem * > decorations = QList< QgsDecorationItem * >(), + QList< QgsAnnotation *> annotations = QList< QgsAnnotation * >(), + DialogType type = Image ); //! returns extent rectangle QgsRectangle extent() const; @@ -73,6 +77,8 @@ class APP_EXPORT QgsMapSaveDialog: public QDialog, private Ui::QgsMapSaveDialog private: + void accepted(); + void updateDpi( int dpi ); void updateOutputWidth( int width ); void updateOutputHeight( int height ); @@ -82,6 +88,9 @@ class APP_EXPORT QgsMapSaveDialog: public QDialog, private Ui::QgsMapSaveDialog DialogType mDialogType; QgsMapCanvas *mMapCanvas; + QList< QgsMapDecoration * > mDecorations; + QList< QgsAnnotation *> mAnnotations; + QgsRectangle mExtent; int mDpi; QSize mSize; diff --git a/src/core/qgsmaprenderertask.cpp b/src/core/qgsmaprenderertask.cpp index c80b337c2a0..bbf218ea626 100644 --- a/src/core/qgsmaprenderertask.cpp +++ b/src/core/qgsmaprenderertask.cpp @@ -22,17 +22,19 @@ #include #include +#include -QgsMapRendererTask::QgsMapRendererTask( const QgsMapSettings &ms, const QString &fileName, const QString &fileFormat ) +QgsMapRendererTask::QgsMapRendererTask( const QgsMapSettings &ms, const QString &fileName, const QString &fileFormat, const bool forceRaster ) : QgsTask( tr( "Saving as image" ) ) , mMapSettings( ms ) , mFileName( fileName ) , mFileFormat( fileFormat ) + , mForceRaster( forceRaster ) { } QgsMapRendererTask::QgsMapRendererTask( const QgsMapSettings &ms, QPainter *p ) - : QgsTask( tr( "Saving as image" ) ) + : QgsTask( tr( "Rendering to painter" ) ) , mMapSettings( ms ) , mPainter( p ) { @@ -70,8 +72,27 @@ bool QgsMapRendererTask::run() QImage img; std::unique_ptr< QPainter > tempPainter; QPainter *destPainter = mPainter; + std::unique_ptr< QPrinter > printer; - if ( !mPainter ) + if ( mFileFormat == QStringLiteral( "PDF" ) ) + { + printer.reset( new QPrinter() ); + printer->setOutputFileName( mFileName ); + printer->setOutputFormat( QPrinter::PdfFormat ); + printer->setOrientation( QPrinter::Portrait ); + // paper size needs to be given in millimeters in order to be able to set a resolution to pass onto the map renderer + printer->setPaperSize( mMapSettings.outputSize() * 25.4 / mMapSettings.outputDpi(), QPrinter::Millimeter ); + printer->setPageMargins( 0, 0, 0, 0, QPrinter::Millimeter ); + printer->setResolution( mMapSettings.outputDpi() ); + + if ( !mForceRaster ) + { + tempPainter.reset( new QPainter( printer.get() ) ); + destPainter = tempPainter.get(); + } + } + + if ( !destPainter ) { // save rendered map to an image file img = QImage( mMapSettings.outputSize(), QImage::Format_ARGB32 ); @@ -149,27 +170,39 @@ bool QgsMapRendererTask::run() if ( !mFileName.isEmpty() ) { destPainter->end(); - bool success = img.save( mFileName, mFileFormat.toLocal8Bit().data() ); - if ( !success ) + + if ( mForceRaster && mFileFormat == QStringLiteral( "PDF" ) ) { - mError = ImageSaveFail; - return false; + QPainter pp; + pp.begin( printer.get() ); + QRectF rect( 0, 0, img.width(), img.height() ); + pp.drawImage( rect, img, rect ); + pp.end(); } - - if ( mSaveWorldFile ) + else if ( mFileFormat != QStringLiteral( "PDF" ) ) { - QFileInfo info = QFileInfo( mFileName ); - - // build the world file name - QString outputSuffix = info.suffix(); - QString worldFileName = info.absolutePath() + '/' + info.baseName() + '.' - + outputSuffix.at( 0 ) + outputSuffix.at( info.suffix().size() - 1 ) + 'w'; - QFile worldFile( worldFileName ); - - if ( worldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) //don't use QIODevice::Text + bool success = img.save( mFileName, mFileFormat.toLocal8Bit().data() ); + if ( !success ) { - QTextStream stream( &worldFile ); - stream << QgsMapSettingsUtils::worldFileContent( mMapSettings ); + mError = ImageSaveFail; + return false; + } + + if ( mSaveWorldFile ) + { + QFileInfo info = QFileInfo( mFileName ); + + // build the world file name + QString outputSuffix = info.suffix(); + QString worldFileName = info.absolutePath() + '/' + info.baseName() + '.' + + outputSuffix.at( 0 ) + outputSuffix.at( info.suffix().size() - 1 ) + 'w'; + QFile worldFile( worldFileName ); + + if ( worldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) //don't use QIODevice::Text + { + QTextStream stream( &worldFile ); + stream << QgsMapSettingsUtils::worldFileContent( mMapSettings ); + } } } } diff --git a/src/core/qgsmaprenderertask.h b/src/core/qgsmaprenderertask.h index 521c84d197b..4484a84a3ce 100644 --- a/src/core/qgsmaprenderertask.h +++ b/src/core/qgsmaprenderertask.h @@ -55,7 +55,8 @@ class CORE_EXPORT QgsMapRendererTask : public QgsTask */ QgsMapRendererTask( const QgsMapSettings &ms, const QString &fileName, - const QString &fileFormat = QString( "PNG" ) ); + const QString &fileFormat = QString( "PNG" ), + const bool forceRaster = false ); /** * Constructor for QgsMapRendererTask to render a map to a painter object. @@ -108,6 +109,7 @@ class CORE_EXPORT QgsMapRendererTask : public QgsTask QString mFileName; QString mFileFormat; + bool mForceRaster = false; bool mSaveWorldFile = false; QList< QgsAnnotation * > mAnnotations;