Skip to content

Commit

Permalink
[FEATURE] Add option to georeference PDFs and TIFs to save map canvas…
Browse files Browse the repository at this point in the history
… as image/PDF
  • Loading branch information
nirvn committed Aug 12, 2019
1 parent c567974 commit 2e4a567
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 23 deletions.
2 changes: 1 addition & 1 deletion python/core/auto_generated/qgsmaprenderertask.sip.in
Expand Up @@ -57,7 +57,7 @@ Adds ``decorations`` to be rendered on the map.

void setSaveWorldFile( bool save );
%Docstring
Sets whether a world file will be created alongside an image file.
Sets whether the image file will be georeferenced (embedded or via a world file).
%End

virtual void cancel();
Expand Down
14 changes: 14 additions & 0 deletions python/core/auto_generated/qgsmapsettingsutils.sip.in
Expand Up @@ -28,6 +28,20 @@ Utilities for map settings.
Checks whether any of the layers attached to a map settings object contain advanced effects

:param mapSettings: map settings
%End

static void worldFileParameters( const QgsMapSettings &mapSettings, double &a /Out/, double &b /Out/, double &c /Out/, double &d /Out/, double &e /Out/, double &f /Out/ );
%Docstring
Computes the six parameters of a world file.

:param mapSettings: map settings
:param b: the b parameter
:param c: the c parameter
:param d: the d parameter
:param e: the e parameter
:param f: the f parameter

.. versionadded:: 3.10
%End

static QString worldFileContent( const QgsMapSettings &mapSettings );
Expand Down
2 changes: 0 additions & 2 deletions src/app/qgsmapsavedialog.cpp
Expand Up @@ -95,8 +95,6 @@ QgsMapSaveDialog::QgsMapSaveDialog( QWidget *parent, QgsMapCanvas *mapCanvas, co

if ( mDialogType == QgsMapSaveDialog::Pdf )
{
mSaveWorldFile->setVisible( false );

QStringList layers = QgsMapSettingsUtils::containsAdvancedEffects( mMapCanvas->mapSettings() );
if ( !layers.isEmpty() )
{
Expand Down
2 changes: 1 addition & 1 deletion src/app/qgsmapsavedialog.h
Expand Up @@ -71,7 +71,7 @@ class APP_EXPORT QgsMapSaveDialog: public QDialog, private Ui::QgsMapSaveDialog
//! returns whether the draw decorations element is checked
bool drawDecorations() const;

//! returns whether a world file will be created
//! returns whether the resulting image will be georeferenced (embedded or via world file)
bool saveWorldFile() const;

//! returns whether the map will be rasterized
Expand Down
62 changes: 54 additions & 8 deletions src/core/qgsmaprenderertask.cpp
Expand Up @@ -19,11 +19,15 @@
#include "qgsannotationmanager.h"
#include "qgsmaprenderertask.h"
#include "qgsmapsettingsutils.h"

#include "qgsogrutils.h"
#include "qgslogger.h"
#include <QFile>
#include <QTextStream>
#include <QPrinter>

#include "gdal.h"
#include "cpl_conv.h"

QgsMapRendererTask::QgsMapRendererTask( const QgsMapSettings &ms, const QString &fileName, const QString &fileFormat, const bool forceRaster )
: QgsTask( tr( "Saving as image" ) )
, mMapSettings( ms )
Expand Down Expand Up @@ -86,7 +90,8 @@ bool QgsMapRendererTask::run()
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 );
QSizeF outputSize = mMapSettings.outputSize();
printer->setPaperSize( outputSize * 25.4 / mMapSettings.outputDpi(), QPrinter::Millimeter );
printer->setPageMargins( 0, 0, 0, 0, QPrinter::Millimeter );
printer->setResolution( mMapSettings.outputDpi() );

Expand Down Expand Up @@ -190,6 +195,25 @@ bool QgsMapRendererTask::run()
QRectF rect( 0, 0, img.width(), img.height() );
pp.drawImage( rect, img, rect );
pp.end();

if ( mSaveWorldFile )
{
CPLSetThreadLocalConfigOption( "GDAL_PDF_DPI", QString::number( mMapSettings.outputDpi() ).toLocal8Bit().constData() );
gdal::dataset_unique_ptr outputDS( GDALOpen( mFileName.toLocal8Bit().constData(), GA_Update ) );
if ( outputDS )
{
double a, b, c, d, e, f;
QgsMapSettingsUtils::worldFileParameters( mMapSettings, a, b, c, d, e, f );
c -= 0.5 * a;
c -= 0.5 * b;
f -= 0.5 * d;
f -= 0.5 * e;
double geoTransform[6] = { c, a, b, f, d, e };
GDALSetGeoTransform( outputDS.get(), geoTransform );
GDALSetProjection( outputDS.get(), mMapSettings.destinationCrs().toWkt().toLocal8Bit().constData() );
}
CPLSetThreadLocalConfigOption( "GDAL_PDF_DPI", nullptr );
}
#else
mError = ImageUnsupportedFormat;
return false;
Expand All @@ -210,14 +234,36 @@ bool QgsMapRendererTask::run()

// 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 );
bool skipWorldFile = false;
if ( outputSuffix == QStringLiteral( "tif" ) || outputSuffix == QStringLiteral( "tiff" ) )
{
gdal::dataset_unique_ptr outputDS( GDALOpen( mFileName.toLocal8Bit().constData(), GA_Update ) );
if ( outputDS )
{
skipWorldFile = true;
double a, b, c, d, e, f;
QgsMapSettingsUtils::worldFileParameters( mMapSettings, a, b, c, d, e, f );
c -= 0.5 * a;
c -= 0.5 * b;
f -= 0.5 * d;
f -= 0.5 * e;
double geoTransform[] = { c, a, b, f, d, e };
GDALSetGeoTransform( outputDS.get(), geoTransform );
GDALSetProjection( outputDS.get(), mMapSettings.destinationCrs().toWkt().toLocal8Bit().constData() );
}
}

if ( worldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) //don't use QIODevice::Text
if ( !skipWorldFile )
{
QTextStream stream( &worldFile );
stream << QgsMapSettingsUtils::worldFileContent( mMapSettings );
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 );
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsmaprenderertask.h
Expand Up @@ -76,7 +76,7 @@ class CORE_EXPORT QgsMapRendererTask : public QgsTask
void addDecorations( const QList<QgsMapDecoration *> &decorations );

/**
* Sets whether a world file will be created alongside an image file.
* Sets whether the image file will be georeferenced (embedded or via a world file).
*/
void setSaveWorldFile( bool save ) { mSaveWorldFile = save; }

Expand Down
24 changes: 15 additions & 9 deletions src/core/qgsmapsettingsutils.cpp
Expand Up @@ -66,7 +66,7 @@ const QStringList QgsMapSettingsUtils::containsAdvancedEffects( const QgsMapSett
return layers.toList();
}

QString QgsMapSettingsUtils::worldFileContent( const QgsMapSettings &mapSettings )
void QgsMapSettingsUtils::worldFileParameters( const QgsMapSettings &mapSettings, double &a, double &b, double &c, double &d, double &e, double &f )
{
QgsMapSettings ms = mapSettings;

Expand Down Expand Up @@ -101,14 +101,20 @@ QString QgsMapSettingsUtils::worldFileContent( const QgsMapSettings &mapSettings
r[5] = - xCenter * std::sin( alpha ) + yCenter * ( 1 - std::cos( alpha ) );

// result = rotation x scaling = rotation(scaling(X))
double a = r[0] * s[0] + r[1] * s[3];
double b = r[0] * s[1] + r[1] * s[4];
double c = r[0] * s[2] + r[1] * s[5] + r[2];
double d = r[3] * s[0] + r[4] * s[3];
a = r[0] * s[0] + r[1] * s[3];
b = r[0] * s[1] + r[1] * s[4];
c = r[0] * s[2] + r[1] * s[5] + r[2];
d = r[3] * s[0] + r[4] * s[3];
// Pixel YDim - almost always negative
// See https://en.wikipedia.org/wiki/World_file#cite_ref-3, https://github.com/qgis/QGIS/issues/26379
double e = r[3] * s[1] + r[4] * s[4];
double f = r[3] * s[2] + r[4] * s[5] + r[5];
e = r[3] * s[1] + r[4] * s[4];
f = r[3] * s[2] + r[4] * s[5] + r[5];
}

QString QgsMapSettingsUtils::worldFileContent( const QgsMapSettings &mapSettings )
{
double a, b, c, d, e, f;
worldFileParameters( mapSettings, a, b, c, d, e, f );

QString content;
// Pixel XDim
Expand All @@ -119,9 +125,9 @@ QString QgsMapSettingsUtils::worldFileContent( const QgsMapSettings &mapSettings
content += qgsDoubleToString( b ) + "\r\n";
// Pixel YDim
content += qgsDoubleToString( e ) + "\r\n";
// Origin X (center of top left cell)
// Origin X (top left cell)
content += qgsDoubleToString( c ) + "\r\n";
// Origin Y (center of top left cell)
// Origin Y (top left cell)
content += qgsDoubleToString( f ) + "\r\n";

return content;
Expand Down
13 changes: 13 additions & 0 deletions src/core/qgsmapsettingsutils.h
Expand Up @@ -39,6 +39,19 @@ class CORE_EXPORT QgsMapSettingsUtils
*/
static const QStringList containsAdvancedEffects( const QgsMapSettings &mapSettings );

/**
* Computes the six parameters of a world file.
* \param mapSettings map settings
* \param a the a parameter
* \param b the b parameter
* \param c the c parameter
* \param d the d parameter
* \param e the e parameter
* \param f the f parameter
* \since QGIS 3.10
*/
static void worldFileParameters( const QgsMapSettings &mapSettings, double &a SIP_OUT, double &b SIP_OUT, double &c SIP_OUT, double &d SIP_OUT, double &e SIP_OUT, double &f SIP_OUT );

/**
* Creates the content of a world file.
* \param mapSettings map settings
Expand Down
2 changes: 1 addition & 1 deletion src/ui/qgsmapsavedialog.ui
Expand Up @@ -43,7 +43,7 @@ Rasterizing the map is recommended when such effects are used.</string>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="mSaveWorldFile">
<property name="text">
<string>Save world file</string>
<string>Append georeference information (embedded or via world file)</string>
</property>
<property name="checked">
<bool>true</bool>
Expand Down
9 changes: 9 additions & 0 deletions tests/src/core/testqgsmapsettingsutils.cpp
Expand Up @@ -48,6 +48,15 @@ void TestQgsMapSettingsUtils::initTestCase()

void TestQgsMapSettingsUtils::createWorldFileContent()
{
double a, b, c, d, e, f;
QgsMapSettingsUtils::worldFileParameters( mMapSettings, a, b, c, d, e, f );
QCOMPARE( a, 1.0 );
QCOMPARE( b, 0.0 );
QCOMPARE( c, 0.5 );
QCOMPARE( d, 0.0 );
QCOMPARE( e, -1.0 );
QCOMPARE( f, 0.5 );

QCOMPARE( QgsMapSettingsUtils::worldFileContent( mMapSettings ), QString( "1\r\n0\r\n0\r\n-1\r\n0.5\r\n0.5\r\n" ) );

mMapSettings.setRotation( 45 );
Expand Down

0 comments on commit 2e4a567

Please sign in to comment.