[FEATURE] Save map/canvas as PDF (#4516)

This commit is contained in:
Mathieu Pellerin 2017-05-08 13:03:39 +07:00 committed by GitHub
parent 01f12221ad
commit a88cf7ad31
10 changed files with 230 additions and 42 deletions

View File

@ -22,6 +22,13 @@ class QgsMapSettingsUtils
%End
public:
static bool containsAdvancedEffects( const QgsMapSettings &mapSettings );
%Docstring
Checks whether any of the layers attached to a map settings object contain advanced effects
\param mapSettings map settings
:rtype: bool
%End
static QString worldFileContent( const QgsMapSettings &mapSettings );
%Docstring
Creates the content of a world file.

View File

@ -1706,6 +1706,7 @@ void QgisApp::createActions()
connect( mActionSaveProject, &QAction::triggered, this, &QgisApp::fileSave );
connect( mActionSaveProjectAs, &QAction::triggered, this, &QgisApp::fileSaveAs );
connect( mActionSaveMapAsImage, &QAction::triggered, this, [ = ] { saveMapAsImage(); } );
connect( mActionSaveMapAsPdf, &QAction::triggered, this, [ = ] { saveMapAsPdf(); } );
connect( mActionNewMapCanvas, &QAction::triggered, this, &QgisApp::newMapCanvas );
connect( mActionNewPrintComposer, &QAction::triggered, this, &QgisApp::newPrintComposer );
connect( mActionShowComposerManager, &QAction::triggered, this, &QgisApp::showComposerManager );
@ -2703,6 +2704,7 @@ void QgisApp::setTheme( const QString &themeName )
mActionNewPrintComposer->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionNewComposer.svg" ) ) );
mActionShowComposerManager->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionComposerManager.svg" ) ) );
mActionSaveMapAsImage->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionSaveMapAsImage.svg" ) ) );
mActionSaveMapAsPdf->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionSaveAsPDF.svg" ) ) );
mActionExit->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionFileExit.png" ) ) );
mActionAddOgrLayer->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddOgrLayer.svg" ) ) );
mActionAddRasterLayer->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddRasterLayer.svg" ) ) );
@ -5801,8 +5803,8 @@ void QgisApp::saveMapAsImage()
if ( !dlg.exec() )
return;
QPair< QString, QString> myFileNameAndFilter = QgisGui::getSaveAsImageName( this, tr( "Choose a file name to save the map image as" ) );
if ( myFileNameAndFilter.first != QLatin1String( "" ) )
QPair< QString, QString> fileNameAndFilter = QgisGui::getSaveAsImageName( this, tr( "Choose a file name to save the map image as" ) );
if ( fileNameAndFilter.first != QLatin1String( "" ) )
{
QgsMapSettings ms = QgsMapSettings();
ms.setDestinationCrs( QgsProject::instance()->crs() );
@ -5813,7 +5815,7 @@ void QgisApp::saveMapAsImage()
ms.setRotation( mMapCanvas->rotation() );
ms.setLayers( mMapCanvas->layers() );
QgsMapRendererTask *mapRendererTask = new QgsMapRendererTask( ms, myFileNameAndFilter.first, myFileNameAndFilter.second );
QgsMapRendererTask *mapRendererTask = new QgsMapRendererTask( ms, fileNameAndFilter.first, fileNameAndFilter.second );
if ( dlg.drawAnnotations() )
{
@ -5853,6 +5855,117 @@ void QgisApp::saveMapAsImage()
} // saveMapAsImage
void QgisApp::saveMapAsPdf()
{
QList< QgsMapDecoration * > decorations;
QString activeDecorations;
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();
ms.setDestinationCrs( QgsProject::instance()->crs() );
ms.setExtent( dlg.extent() );
ms.setOutputSize( dlg.size() );
ms.setOutputDpi( dlg.dpi() );
ms.setBackgroundColor( mMapCanvas->canvasColor() );
ms.setRotation( mMapCanvas->rotation() );
ms.setLayers( mMapCanvas->layers() );
QPrinter *printer = new QPrinter();
printer->setOutputFileName( fileName );
printer->setOutputFormat( QPrinter::PdfFormat );
printer->setOrientation( QPrinter::Portrait );
printer->setPaperSize( dlg.size(), QPrinter::DevicePixel );
printer->setPageMargins( 0, 0, 0, 0, QPrinter::DevicePixel );
QPainter *p = new QPainter();
QImage *image = nullptr;
if ( dlg.saveAsRaster() )
{
image = new QImage( dlg.size(), QImage::Format_ARGB32 );
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 ]
{
messageBar()->pushSuccess( tr( "Save as image" ), tr( "Successfully saved map to image" ) );
p->end();
if ( image )
{
QPainter pp;
pp.begin( printer );
QRectF rect( 0, 0, image->width(), image->height() );
pp.drawImage( rect, *image, rect );
pp.end();
}
delete p;
delete image;
delete printer;
} );
connect( mapRendererTask, &QgsMapRendererTask::errorOccurred, this, [ this, p, image, printer ]( 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;
}
}
delete p;
delete image;
delete printer;
} );
QgsApplication::taskManager()->addTask( mapRendererTask );
}
} // saveMapAsPdf
//overloaded version of the above function
void QgisApp::saveMapAsImage( const QString &imageFileNameQString, QPixmap *theQPixmap )
{

View File

@ -391,6 +391,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
QAction *actionSaveProject() { return mActionSaveProject; }
QAction *actionSaveProjectAs() { return mActionSaveProjectAs; }
QAction *actionSaveMapAsImage() { return mActionSaveMapAsImage; }
QAction *actionSaveMapAsPdf() { return mActionSaveMapAsPdf; }
QAction *actionProjectProperties() { return mActionProjectProperties; }
QAction *actionShowComposerManager() { return mActionShowComposerManager; }
QAction *actionNewPrintComposer() { return mActionNewPrintComposer; }
@ -1080,6 +1081,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
void runScript( const QString &filePath );
//! Save the map view as an image - user is prompted for image name using a dialog
void saveMapAsImage();
//! Save the map view as a pdf - user is prompted for image name using a dialog
void saveMapAsPdf();
//! Open a project
void fileOpen();
//! Create a new project

View File

@ -22,6 +22,7 @@
#include "qgsdecorationitem.h"
#include "qgsextentgroupbox.h"
#include "qgsmapsettings.h"
#include "qgsmapsettingsutils.h"
#include <QCheckBox>
#include <QSpinBox>
@ -29,7 +30,7 @@
Q_GUI_EXPORT extern int qt_defaultDpiX();
QgsMapSaveDialog::QgsMapSaveDialog( QWidget *parent, QgsMapCanvas *mapCanvas, const QString &activeDecorations )
QgsMapSaveDialog::QgsMapSaveDialog( QWidget *parent, QgsMapCanvas *mapCanvas, const QString &activeDecorations, DialogType type )
: QDialog( parent )
{
setupUi( this );
@ -60,6 +61,16 @@ QgsMapSaveDialog::QgsMapSaveDialog( QWidget *parent, QgsMapCanvas *mapCanvas, co
connect( mScaleWidget, &QgsScaleWidget::scaleChanged, this, &QgsMapSaveDialog::updateScale );
updateOutputSize();
if ( type == QgsMapSaveDialog::Pdf )
{
mSaveWorldFile->setVisible( false );
mSaveAsRaster->setChecked( QgsMapSettingsUtils::containsAdvancedEffects( mapCanvas->mapSettings() ) );
mSaveAsRaster->setVisible( true );
this->setWindowTitle( tr( "Save map as PDF" ) );
}
}
void QgsMapSaveDialog::updateDpi( int dpi )
@ -152,3 +163,8 @@ bool QgsMapSaveDialog::saveWorldFile() const
{
return mSaveWorldFile->isChecked();
}
bool QgsMapSaveDialog::saveAsRaster() const
{
return mSaveAsRaster->isChecked();
}

View File

@ -37,9 +37,15 @@ class APP_EXPORT QgsMapSaveDialog: public QDialog, private Ui::QgsMapSaveDialog
public:
enum DialogType
{
Image = 1, // Image-specific dialog
Pdf // PDF-specific dialog
};
/** Constructor for QgsMapSaveDialog
*/
QgsMapSaveDialog( QWidget *parent = nullptr, QgsMapCanvas *mapCanvas = nullptr, const QString &activeDecorations = QString() );
QgsMapSaveDialog( QWidget *parent = nullptr, QgsMapCanvas *mapCanvas = nullptr, const QString &activeDecorations = QString(), DialogType type = Image );
//! returns extent rectangle
QgsRectangle extent() const;
@ -59,6 +65,9 @@ class APP_EXPORT QgsMapSaveDialog: public QDialog, private Ui::QgsMapSaveDialog
//! returns whether a world file will be created
bool saveWorldFile() const;
//! returns whether the map will be rasterized
bool saveAsRaster() const;
private:
void updateDpi( int dpi );

View File

@ -25,6 +25,7 @@
#include "qgsmaplayerlistutils.h"
#include "qgsmaplayerstylemanager.h"
#include "qgsmaptopixel.h"
#include "qgsmapsettingsutils.h"
#include "qgspainting.h"
#include "qgsproject.h"
#include "qgsrasterdataprovider.h"
@ -1071,43 +1072,10 @@ bool QgsComposerMap::containsAdvancedEffects() const
return true;
}
// check if map contains advanced effects like blend modes, or flattened layers for transparency
QgsTextFormat layerFormat;
Q_FOREACH ( QgsMapLayer *layer, layersToRender() )
{
if ( layer )
{
if ( layer->blendMode() != QPainter::CompositionMode_SourceOver )
{
return true;
}
// if vector layer, check labels and feature blend mode
QgsVectorLayer *currentVectorLayer = qobject_cast<QgsVectorLayer *>( layer );
if ( currentVectorLayer )
{
if ( currentVectorLayer->layerTransparency() != 0 )
{
return true;
}
if ( currentVectorLayer->featureBlendMode() != QPainter::CompositionMode_SourceOver )
{
return true;
}
// check label blend modes
if ( QgsPalLabeling::staticWillUseLayer( currentVectorLayer ) )
{
// Check all label blending properties
layerFormat.readFromLayer( currentVectorLayer );
if ( layerFormat.containsAdvancedEffects() )
return true;
}
}
}
}
return false;
QgsMapSettings ms;
ms.setLayers( layersToRender() );
return QgsMapSettingsUtils::containsAdvancedEffects( ms );
}
void QgsComposerMap::connectUpdateSlot()

View File

@ -17,9 +17,49 @@
#include "qgsmapsettings.h"
#include "qgsmapsettingsutils.h"
#include "qgspallabeling.h"
#include "qgstextrenderer.h"
#include "qgsvectorlayer.h"
#include <QString>
bool QgsMapSettingsUtils::containsAdvancedEffects( const QgsMapSettings &mapSettings )
{
QgsTextFormat layerFormat;
Q_FOREACH ( QgsMapLayer *layer, mapSettings.layers() )
{
if ( layer )
{
if ( layer->blendMode() != QPainter::CompositionMode_SourceOver )
{
return true;
}
// if vector layer, check labels and feature blend mode
QgsVectorLayer *currentVectorLayer = qobject_cast<QgsVectorLayer *>( layer );
if ( currentVectorLayer )
{
if ( currentVectorLayer->layerTransparency() != 0 )
{
return true;
}
if ( currentVectorLayer->featureBlendMode() != QPainter::CompositionMode_SourceOver )
{
return true;
}
// check label blend modes
if ( QgsPalLabeling::staticWillUseLayer( currentVectorLayer ) )
{
// Check all label blending properties
layerFormat.readFromLayer( currentVectorLayer );
if ( layerFormat.containsAdvancedEffects() )
return true;
}
}
}
}
return false;
}
QString QgsMapSettingsUtils::worldFileContent( const QgsMapSettings &mapSettings )
{
QgsMapSettings ms = mapSettings;

View File

@ -32,6 +32,11 @@ class CORE_EXPORT QgsMapSettingsUtils
public:
/** Checks whether any of the layers attached to a map settings object contain advanced effects
* \param mapSettings map settings
*/
static bool containsAdvancedEffects( const QgsMapSettings &mapSettings );
/** Creates the content of a world file.
* \param mapSettings map settings
* \note Uses 17 places of precision for all numbers output

View File

@ -50,6 +50,7 @@
<addaction name="mActionSaveProject"/>
<addaction name="mActionSaveProjectAs"/>
<addaction name="mActionSaveMapAsImage"/>
<addaction name="mActionSaveMapAsPdf"/>
<addaction name="mActionDxfExport"/>
<addaction name="mActionDwgImport"/>
<addaction name="separator"/>
@ -640,7 +641,16 @@
<normaloff>:/images/themes/default/mActionSaveMapAsImage.svg</normaloff>:/images/themes/default/mActionSaveMapAsImage.svg</iconset>
</property>
<property name="text">
<string>Save as &amp;Image...</string>
<string>Save Map as &amp;Image...</string>
</property>
</action>
<action name="mActionSaveMapAsPdf">
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionSaveAsPDF.svg</normaloff>:/images/themes/default/mActionSaveAsPDF.svg</iconset>
</property>
<property name="text">
<string>Save Map as &amp;PDF...</string>
</property>
</action>
<action name="mActionNewMapCanvas">

View File

@ -16,6 +16,23 @@
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="mSaveAsRaster">
<property name="text">
<string>Rasterize map</string>
</property>
<property name="toolTip">
<string>Advanced effects such as blend modes or vector layer transparency cannot be exported as vectors.
Rasterizing the map is recommended when such effects are used.</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
<property name="visible">
<bool>false</bool>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="mSaveWorldFile">
<property name="text">