From 9e4518fe425f71f636ee955bd3d385ae9214f939 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 24 Oct 2017 11:16:38 +1000 Subject: [PATCH] [FEATURE] More output format choices in raster save as dialog Previously only geotiff format was available, even though the underlying QgsRasterFileWriter/GDAL libraries support other formats. This commit exposes those other formats to the dialog so that users can directly save rasters to them (including everyone's new BFF, geopackage). --- python/core/raster/qgsrasterfilewriter.sip | 11 ++ src/app/qgisapp.cpp | 4 + src/core/raster/qgsrasterfilewriter.cpp | 14 +++ src/core/raster/qgsrasterfilewriter.h | 10 ++ src/gui/qgsrasterlayersaveasdialog.cpp | 101 ++++++++++++++++--- src/gui/qgsrasterlayersaveasdialog.h | 1 + tests/src/python/test_qgsrasterfilewriter.py | 5 + 7 files changed, 131 insertions(+), 15 deletions(-) diff --git a/python/core/raster/qgsrasterfilewriter.sip b/python/core/raster/qgsrasterfilewriter.sip index 79daf017457..24f0067a49f 100644 --- a/python/core/raster/qgsrasterfilewriter.sip +++ b/python/core/raster/qgsrasterfilewriter.sip @@ -165,6 +165,17 @@ class QgsRasterFileWriter :rtype: str %End + static QStringList extensionsForFormat( const QString &format ); +%Docstring + Returns a list of known file extensions for the given GDAL driver ``format``. + E.g. returns "tif", "tiff" for the format "GTiff". + + If no matching format driver is found an empty list will be returned. + +.. versionadded:: 3.0 + :rtype: list of str +%End + }; /************************************************************************ diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index fc49cfccb51..fd27d33f18d 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -6750,6 +6750,10 @@ void QgisApp::saveAsRasterFile( QgsRasterLayer *rasterLayer ) fileWriter.setMaxTileWidth( d.maximumTileSizeX() ); fileWriter.setMaxTileHeight( d.maximumTileSizeY() ); } + else + { + fileWriter.setOutputFormat( d.outputFormat() ); + } // TODO: show error dialogs // TODO: this code should go somewhere else, but probably not into QgsRasterFileWriter diff --git a/src/core/raster/qgsrasterfilewriter.cpp b/src/core/raster/qgsrasterfilewriter.cpp index 22d5f77461f..4b4599a8f79 100644 --- a/src/core/raster/qgsrasterfilewriter.cpp +++ b/src/core/raster/qgsrasterfilewriter.cpp @@ -1009,3 +1009,17 @@ QString QgsRasterFileWriter::driverForExtension( const QString &extension ) } return QString(); } + +QStringList QgsRasterFileWriter::extensionsForFormat( const QString &format ) +{ + GDALDriverH drv = GDALGetDriverByName( format.toLocal8Bit().data() ); + if ( drv ) + { + char **driverMetadata = GDALGetMetadata( drv, nullptr ); + if ( CSLFetchBoolean( driverMetadata, GDAL_DCAP_CREATE, false ) && CSLFetchBoolean( driverMetadata, GDAL_DCAP_RASTER, false ) ) + { + return QString( GDALGetMetadataItem( drv, GDAL_DMD_EXTENSIONS, nullptr ) ).split( ' ' ); + } + } + return QStringList(); +} diff --git a/src/core/raster/qgsrasterfilewriter.h b/src/core/raster/qgsrasterfilewriter.h index 9ad09980a4f..4ad4215806b 100644 --- a/src/core/raster/qgsrasterfilewriter.h +++ b/src/core/raster/qgsrasterfilewriter.h @@ -142,6 +142,16 @@ class CORE_EXPORT QgsRasterFileWriter */ static QString driverForExtension( const QString &extension ); + /** + * Returns a list of known file extensions for the given GDAL driver \a format. + * E.g. returns "tif", "tiff" for the format "GTiff". + * + * If no matching format driver is found an empty list will be returned. + * + * \since QGIS 3.0 + */ + static QStringList extensionsForFormat( const QString &format ); + private: QgsRasterFileWriter(); //forbidden WriterError writeDataRaster( const QgsRasterPipe *pipe, QgsRasterIterator *iter, int nCols, int nRows, const QgsRectangle &outputExtent, diff --git a/src/gui/qgsrasterlayersaveasdialog.cpp b/src/gui/qgsrasterlayersaveasdialog.cpp index f5750f3d5f3..6ad6f9a7127 100644 --- a/src/gui/qgsrasterlayersaveasdialog.cpp +++ b/src/gui/qgsrasterlayersaveasdialog.cpp @@ -23,7 +23,8 @@ #include "qgsrastertransparency.h" #include "qgsprojectionselectiondialog.h" #include "qgssettings.h" - +#include "qgsrasterfilewriter.h" +#include "cpl_string.h" #include #include @@ -74,13 +75,7 @@ QgsRasterLayerSaveAsDialog::QgsRasterLayerSaveAsDialog( QgsRasterLayer *rasterLa toggleResolutionSize(); - //only one hardcoded format at the moment - QStringList myFormats; - myFormats << QStringLiteral( "GTiff" ); - Q_FOREACH ( const QString &myFormat, myFormats ) - { - mFormatComboBox->addItem( myFormat ); - } + insertAvailableOutputFormats(); //fill reasonable default values depending on the provider if ( mDataProvider ) @@ -104,7 +99,7 @@ QgsRasterLayerSaveAsDialog::QgsRasterLayerSaveAsDialog( QgsRasterLayer *rasterLa mCreateOptionsWidget->setProvider( mDataProvider->name() ); if ( mDataProvider->name() == QLatin1String( "gdal" ) ) { - mCreateOptionsWidget->setFormat( myFormats[0] ); + mCreateOptionsWidget->setFormat( mFormatComboBox->currentData().toString() ); } mCreateOptionsWidget->setRasterLayer( mRasterLayer ); mCreateOptionsWidget->update(); @@ -165,6 +160,68 @@ QgsRasterLayerSaveAsDialog::QgsRasterLayerSaveAsDialog( QgsRasterLayer *rasterLa recalcResolutionSize(); } +void QgsRasterLayerSaveAsDialog::insertAvailableOutputFormats() +{ + GDALAllRegister(); + + int nDrivers = GDALGetDriverCount(); + QMap< int, QPair< QString, QString > > topPriorityDrivers; + QMap< QString, QString > lowPriorityDrivers; + + for ( int i = 0; i < nDrivers; ++i ) + { + GDALDriverH driver = GDALGetDriver( i ); + if ( driver ) + { + char **driverMetadata = GDALGetMetadata( driver, nullptr ); + + if ( CSLFetchBoolean( driverMetadata, GDAL_DCAP_CREATE, false ) && CSLFetchBoolean( driverMetadata, GDAL_DCAP_RASTER, false ) ) + { + QString driverShortName = GDALGetDriverShortName( driver ); + QString driverLongName = GDALGetDriverLongName( driver ); + if ( driverShortName == QLatin1String( "MEM" ) ) + { + // in memory rasters are not (yet) supported because the GDAL dataset handle + // would need to be passed directly to QgsRasterLayer (it is not possible to + // close it in raster calculator and reopen the dataset again in raster layer) + continue; + } + else if ( driverShortName == QLatin1String( "VRT" ) ) + { + // skip GDAL vrt driver, since we handle that format manually + continue; + } + else if ( driverShortName == QStringLiteral( "GTiff" ) ) + { + // always list geotiff first + topPriorityDrivers.insert( 1, qMakePair( driverLongName, driverShortName ) ); + } + else if ( driverShortName == QStringLiteral( "GPKG" ) ) + { + // and gpkg second + topPriorityDrivers.insert( 2, qMakePair( driverLongName, driverShortName ) ); + } + else + { + lowPriorityDrivers.insert( driverLongName, driverShortName ); + } + } + } + } + + // will be sorted by priority, so that geotiff and geopackage are listed first + for ( auto priorityDriversIt = topPriorityDrivers.constBegin(); priorityDriversIt != topPriorityDrivers.constEnd(); ++priorityDriversIt ) + { + mFormatComboBox->addItem( priorityDriversIt.value().first, priorityDriversIt.value().second ); + } + // will be sorted by driver name + for ( auto lowPriorityDriversIt = lowPriorityDrivers.constBegin(); lowPriorityDriversIt != lowPriorityDrivers.constEnd(); ++lowPriorityDriversIt ) + { + mFormatComboBox->addItem( lowPriorityDriversIt.key(), lowPriorityDriversIt.value() ); + } + +} + void QgsRasterLayerSaveAsDialog::setValidators() { mXResolutionLineEdit->setValidator( new QDoubleValidator( this ) ); @@ -212,12 +269,26 @@ void QgsRasterLayerSaveAsDialog::mBrowseButton_clicked() } else { - fileName = QFileDialog::getSaveFileName( this, tr( "Select output file" ), dirName, tr( "GeoTIFF" ) + " (*.tif *.tiff *.TIF *.TIFF)" ); + QStringList extensions = QgsRasterFileWriter::extensionsForFormat( outputFormat() ); + QString filter; + QString defaultExt; + if ( extensions.empty() ) + filter = tr( "All files (*.*)" ); + else + { + filter = QStringLiteral( "%1 (*.%2);;%3" ).arg( mFormatComboBox->currentText(), + extensions.join( QStringLiteral( " *." ) ), + tr( "All files (*.*)" ) ); + defaultExt = extensions.at( 0 ); + } + + fileName = QFileDialog::getSaveFileName( this, tr( "Select output file" ), dirName, filter ); // ensure the user never omits the extension from the file name - if ( !fileName.isEmpty() && !fileName.endsWith( QLatin1String( ".tif" ), Qt::CaseInsensitive ) && !fileName.endsWith( QLatin1String( ".tiff" ), Qt::CaseInsensitive ) ) + QFileInfo fi( fileName ); + if ( !fileName.isEmpty() && fi.suffix().isEmpty() ) { - fileName += QLatin1String( ".tif" ); + fileName += '.' + defaultExt; } } @@ -239,12 +310,12 @@ void QgsRasterLayerSaveAsDialog::mSaveAsLineEdit_textChanged( const QString &tex } -void QgsRasterLayerSaveAsDialog::mFormatComboBox_currentIndexChanged( const QString &text ) +void QgsRasterLayerSaveAsDialog::mFormatComboBox_currentIndexChanged( const QString & ) { //gdal-specific if ( mDataProvider && mDataProvider->name() == QLatin1String( "gdal" ) ) { - mCreateOptionsWidget->setFormat( text ); + mCreateOptionsWidget->setFormat( outputFormat() ); mCreateOptionsWidget->update(); } } @@ -296,7 +367,7 @@ QString QgsRasterLayerSaveAsDialog::outputFileName() const QString QgsRasterLayerSaveAsDialog::outputFormat() const { - return mFormatComboBox->currentText(); + return mFormatComboBox->currentData().toString(); } QStringList QgsRasterLayerSaveAsDialog::createOptions() const diff --git a/src/gui/qgsrasterlayersaveasdialog.h b/src/gui/qgsrasterlayersaveasdialog.h index 791e1d27219..2ed9ee104ba 100644 --- a/src/gui/qgsrasterlayersaveasdialog.h +++ b/src/gui/qgsrasterlayersaveasdialog.h @@ -139,6 +139,7 @@ class GUI_EXPORT QgsRasterLayerSaveAsDialog: public QDialog, private Ui::QgsRast void adjustNoDataCellWidth( int row, int column ); bool validate() const; + void insertAvailableOutputFormats(); }; diff --git a/tests/src/python/test_qgsrasterfilewriter.py b/tests/src/python/test_qgsrasterfilewriter.py index 158dc10f4e0..ec62e8a6a2c 100644 --- a/tests/src/python/test_qgsrasterfilewriter.py +++ b/tests/src/python/test_qgsrasterfilewriter.py @@ -109,6 +109,11 @@ class TestQgsRasterFileWriter(unittest.TestCase): self.assertEqual(QgsRasterFileWriter.driverForExtension('not a format'), '') self.assertEqual(QgsRasterFileWriter.driverForExtension(''), '') + def testExtensionsForFormat(self): + self.assertCountEqual(QgsRasterFileWriter.extensionsForFormat('not format'), []) + self.assertCountEqual(QgsRasterFileWriter.extensionsForFormat('GTiff'), ['tiff', 'tif']) + self.assertCountEqual(QgsRasterFileWriter.extensionsForFormat('GPKG'), ['gpkg']) + def testImportIntoGpkg(self): # init target file test_gpkg = tempfile.mktemp(suffix='.gpkg', dir=self.testDataDir)