diff --git a/python/core/raster/qgsrasterfilewriter.sip b/python/core/raster/qgsrasterfilewriter.sip index 24f0067a49f..c68805456d7 100644 --- a/python/core/raster/qgsrasterfilewriter.sip +++ b/python/core/raster/qgsrasterfilewriter.sip @@ -35,6 +35,13 @@ class QgsRasterFileWriter WriteCanceled, }; + enum RasterFormatOption + { + SortRecommended, + }; + typedef QFlags RasterFormatOptions; + + QgsRasterFileWriter( const QString &outputUrl ); QgsRasterDataProvider *createOneBandRaster( Qgis::DataType dataType, @@ -156,6 +163,49 @@ class QgsRasterFileWriter :rtype: list of str %End + static QString filterForDriver( const QString &driverName ); +%Docstring +Creates a filter for an GDAL driver key + :rtype: str +%End + + struct FilterFormatDetails + { + QString driverName; +%Docstring +Unique driver name +%End + + QString filterString; +%Docstring +Filter string for file picker dialogs +%End + }; + + static QList< QgsRasterFileWriter::FilterFormatDetails > supportedFiltersAndFormats( RasterFormatOptions options = SortRecommended ); +%Docstring + Returns a list or pairs, with format filter string as first element and GDAL format key as second element. + Relies on GDAL_DMD_EXTENSIONS metadata, if it is empty corresponding driver will be skipped even if supported. + + The ``options`` argument can be used to control the sorting and filtering of + returned formats. + +.. seealso:: :py:func:`supportedOutputRasterLayerExtensions()` + :rtype: list of QgsRasterFileWriter.FilterFormatDetails +%End + + static QStringList supportedFormatExtensions( RasterFormatOptions options = SortRecommended ); +%Docstring + Returns a list of file extensions for supported formats. + + The ``options`` argument can be used to control the sorting and filtering of + returned formats. + +.. versionadded:: 3.0 +.. seealso:: :py:func:`supportedFiltersAndFormats()` + :rtype: list of str +%End + static QString driverForExtension( const QString &extension ); %Docstring Returns the GDAL driver name for a specified file ``extension``. E.g. the diff --git a/python/plugins/processing/core/ProcessingConfig.py b/python/plugins/processing/core/ProcessingConfig.py index dc836edf851..0f220a7260e 100644 --- a/python/plugins/processing/core/ProcessingConfig.py +++ b/python/plugins/processing/core/ProcessingConfig.py @@ -31,7 +31,8 @@ from qgis.PyQt.QtCore import QCoreApplication, QObject, pyqtSignal from qgis.core import (NULL, QgsApplication, QgsSettings, - QgsVectorFileWriter) + QgsVectorFileWriter, + QgsRasterFileWriter) from processing.tools.system import defaultOutputFolder import processing.tools.dataobjects @@ -170,7 +171,7 @@ class ProcessingConfig: valuetype=Setting.SELECTION, options=extensions)) - extensions = processing.tools.dataobjects.getSupportedOutputRasterLayerExtensions() + extensions = QgsRasterFileWriter.supportedFormatExtensions() ProcessingConfig.addSetting(Setting( ProcessingConfig.tr('General'), ProcessingConfig.DEFAULT_OUTPUT_RASTER_LAYER_EXT, diff --git a/python/plugins/processing/gui/ParameterGuiUtils.py b/python/plugins/processing/gui/ParameterGuiUtils.py index c956ca01544..09d4a361a4e 100644 --- a/python/plugins/processing/gui/ParameterGuiUtils.py +++ b/python/plugins/processing/gui/ParameterGuiUtils.py @@ -29,7 +29,8 @@ __revision__ = '$Format:%H$' from qgis.core import (QgsProcessing, QgsProviderRegistry, QgsProcessingFeatureSourceDefinition, - QgsVectorFileWriter) + QgsVectorFileWriter, + QgsRasterFileWriter) from qgis.PyQt.QtCore import QCoreApplication from processing.tools import dataobjects @@ -48,7 +49,7 @@ def getFileFilter(param): """ if param.type() == 'multilayer': if param.layerType() == QgsProcessing.TypeRaster: - exts = dataobjects.getSupportedOutputRasterLayerExtensions() + exts = QgsRasterFileWriter.supportedFormatExtensions() elif param.layerType() == QgsProcessing.TypeFile: return tr('All files (*.*)', 'QgsProcessingParameterMultipleLayers') else: @@ -59,7 +60,12 @@ def getFileFilter(param): elif param.type() == 'raster': return QgsProviderRegistry.instance().fileRasterFilters() elif param.type() == 'rasterDestination': - exts = dataobjects.getSupportedOutputRasterFilters() + if param.provider() is not None: + exts = param.provider().supportedOutputRasterLayerExtensions() + else: + exts = QgsRasterFileWriter.supportedFormatExtensions() + for i in range(len(exts)): + exts[i] = tr('{0} files (*.{1})', 'ParameterRaster').format(exts[i].upper(), exts[i].lower()) return ';;'.join(exts) + ';;' + tr('All files (*.*)') elif param.type() in ('sink', 'vectorDestination'): if param.provider() is not None: diff --git a/python/plugins/processing/tools/dataobjects.py b/python/plugins/processing/tools/dataobjects.py index 6c0fe427379..c216f4c78ff 100644 --- a/python/plugins/processing/tools/dataobjects.py +++ b/python/plugins/processing/tools/dataobjects.py @@ -105,35 +105,6 @@ def createExpressionContext(): return context -def getSupportedOutputRasterLayerExtensions(): - allexts = [] - for exts in list(GdalUtils.getSupportedRasters().values()): - for ext in exts: - if ext != 'tif' and ext not in allexts: - allexts.append(ext) - allexts.sort() - allexts.insert(0, 'tif') # tif is the default, should be the first - return allexts - - -def getSupportedOutputRasterFilters(): - """ - Return a list of file filters for supported raster formats. - Supported formats come from Gdal. - :return: a list of strings for Qt file filters. - """ - allFilters = [] - supported = GdalUtils.getSupportedOutputRasters() - formatList = sorted(supported.keys()) - # Place GTiff as the first format - if 'GTiff' in formatList: - formatList.pop(formatList.index('GTiff')) - formatList.insert(0, 'GTiff') - for f in formatList: - allFilters.append('{0} files (*.{1})'.format(f, ' *.'.join(supported[f]))) - return allFilters - - def load(fileName, name=None, crs=None, style=None, isRaster=False): """Loads a layer/table into the current project, given its file. """ diff --git a/src/core/processing/qgsprocessingprovider.cpp b/src/core/processing/qgsprocessingprovider.cpp index 326a709d323..184e650038b 100644 --- a/src/core/processing/qgsprocessingprovider.cpp +++ b/src/core/processing/qgsprocessingprovider.cpp @@ -18,6 +18,7 @@ #include "qgsprocessingprovider.h" #include "qgsapplication.h" #include "qgsvectorfilewriter.h" +#include "qgsrasterfilewriter.h" #include "qgssettings.h" QgsProcessingProvider::QgsProcessingProvider( QObject *parent SIP_TRANSFERTHIS ) @@ -47,7 +48,7 @@ QString QgsProcessingProvider::longName() const QStringList QgsProcessingProvider::supportedOutputRasterLayerExtensions() const { - return QStringList() << QStringLiteral( "tif" ); + return QgsRasterFileWriter::supportedFormatExtensions(); } void QgsProcessingProvider::refreshAlgorithms() @@ -136,4 +137,3 @@ QString QgsProcessingProvider::defaultRasterFileExtension() const return defaultExtension; } } - diff --git a/src/core/raster/qgsrasterfilewriter.cpp b/src/core/raster/qgsrasterfilewriter.cpp index 4b4599a8f79..a2470a313a7 100644 --- a/src/core/raster/qgsrasterfilewriter.cpp +++ b/src/core/raster/qgsrasterfilewriter.cpp @@ -1023,3 +1023,103 @@ QStringList QgsRasterFileWriter::extensionsForFormat( const QString &format ) } return QStringList(); } + +QString QgsRasterFileWriter::filterForDriver( const QString &driverName ) +{ + GDALDriverH drv = GDALGetDriverByName( driverName.toLocal8Bit().data() ); + if ( drv ) + { + QString drvName = GDALGetDriverLongName( drv ); + QString extensionsString = QString( GDALGetMetadataItem( drv, GDAL_DMD_EXTENSIONS, nullptr ) ); + if ( extensionsString.isEmpty() ) + { + return QString(); + } + QStringList extensions = extensionsString.split( ' ' ); + QString filter = drvName + " ("; + for ( const QString &ext : extensions ) + { + filter.append( QStringLiteral( "*.%1 *.%2 " ).arg( ext.toLower(), ext.toUpper() ) ); + } + filter = filter.trimmed().append( QStringLiteral( ")" ) ); + return filter; + } + + return QString(); +} + +QList< QgsRasterFileWriter::FilterFormatDetails > QgsRasterFileWriter::supportedFiltersAndFormats( RasterFormatOptions options ) +{ + QList< FilterFormatDetails > results; + + GDALAllRegister(); + int const drvCount = GDALGetDriverCount(); + + FilterFormatDetails tifFormat; + + for ( int i = 0; i < drvCount; ++i ) + { + GDALDriverH drv = GDALGetDriver( i ); + if ( drv ) + { + QString drvName = GDALGetDriverShortName( drv ); + char **driverMetadata = GDALGetMetadata( drv, nullptr ); + if ( CSLFetchBoolean( driverMetadata, GDAL_DCAP_CREATE, false ) && CSLFetchBoolean( driverMetadata, GDAL_DCAP_RASTER, false ) ) + { + QString filterString = filterForDriver( drvName ); + if ( filterString.isEmpty() ) + continue; + + FilterFormatDetails details; + details.driverName = drvName; + details.filterString = filterString; + + if ( options & SortRecommended ) + { + if ( drvName == QLatin1String( "GTiff" ) ) + { + tifFormat = details; + continue; + } + } + + results << details; + } + } + } + + std::sort( results.begin(), results.end(), []( const FilterFormatDetails & a, const FilterFormatDetails & b ) -> bool + { + return a.driverName < b.driverName; + } ); + + if ( options & SortRecommended ) + { + if ( !tifFormat.filterString.isEmpty() ) + { + results.insert( 0, tifFormat ); + } + } + + return results; +} + +QStringList QgsRasterFileWriter::supportedFormatExtensions( const RasterFormatOptions options ) +{ + const auto formats = supportedFiltersAndFormats( options ); + QStringList extensions; + + QRegularExpression rx( QStringLiteral( "\\*\\.([a-zA-Z0-9]*)" ) ); + + for ( const FilterFormatDetails &format : formats ) + { + QString ext = format.filterString; + QRegularExpressionMatch match = rx.match( ext ); + if ( !match.hasMatch() ) + continue; + + QString matched = match.captured( 1 ); + extensions << matched; + } + return extensions; +} diff --git a/src/core/raster/qgsrasterfilewriter.h b/src/core/raster/qgsrasterfilewriter.h index 4ad4215806b..e0334611c6e 100644 --- a/src/core/raster/qgsrasterfilewriter.h +++ b/src/core/raster/qgsrasterfilewriter.h @@ -54,6 +54,16 @@ class CORE_EXPORT QgsRasterFileWriter WriteCanceled = 6, //!< Writing was manually canceled }; + /** + * Options for sorting and filtering raster formats. + * \since QGIS 3.0 + */ + enum RasterFormatOption + { + SortRecommended = 1 << 1, //!< Use recommended sort order, with extremely commonly used formats listed first + }; + Q_DECLARE_FLAGS( RasterFormatOptions, RasterFormatOption ) + QgsRasterFileWriter( const QString &outputUrl ); /** @@ -134,6 +144,44 @@ class CORE_EXPORT QgsRasterFileWriter void setPyramidsConfigOptions( const QStringList &list ) { mPyramidsConfigOptions = list; } QStringList pyramidsConfigOptions() const { return mPyramidsConfigOptions; } + //! Creates a filter for an GDAL driver key + static QString filterForDriver( const QString &driverName ); + + /** + * Details of available filters and formats. + * \since QGIS 3.0 + */ + struct FilterFormatDetails + { + //! Unique driver name + QString driverName; + + //! Filter string for file picker dialogs + QString filterString; + }; + + /** + * Returns a list or pairs, with format filter string as first element and GDAL format key as second element. + * Relies on GDAL_DMD_EXTENSIONS metadata, if it is empty corresponding driver will be skipped even if supported. + * + * The \a options argument can be used to control the sorting and filtering of + * returned formats. + * + * \see supportedOutputRasterLayerExtensions() + */ + static QList< QgsRasterFileWriter::FilterFormatDetails > supportedFiltersAndFormats( RasterFormatOptions options = SortRecommended ); + + /** + * Returns a list of file extensions for supported formats. + * + * The \a options argument can be used to control the sorting and filtering of + * returned formats. + * + * \since QGIS 3.0 + * \see supportedFiltersAndFormats() + */ + static QStringList supportedFormatExtensions( RasterFormatOptions options = SortRecommended ); + /** * Returns the GDAL driver name for a specified file \a extension. E.g. the * driver name for the ".tif" extension is "GTiff". diff --git a/tests/src/python/test_qgsrasterfilewriter.py b/tests/src/python/test_qgsrasterfilewriter.py index ec62e8a6a2c..5e0d27f3d6a 100644 --- a/tests/src/python/test_qgsrasterfilewriter.py +++ b/tests/src/python/test_qgsrasterfilewriter.py @@ -114,6 +114,32 @@ class TestQgsRasterFileWriter(unittest.TestCase): self.assertCountEqual(QgsRasterFileWriter.extensionsForFormat('GTiff'), ['tiff', 'tif']) self.assertCountEqual(QgsRasterFileWriter.extensionsForFormat('GPKG'), ['gpkg']) + def testSupportedFiltersAndFormat(self): + # test with formats in recommended order + formats = QgsRasterFileWriter.supportedFiltersAndFormats(QgsRasterFileWriter.SortRecommended) + self.assertEqual(formats[0].filterString, 'GeoTIFF (*.tif *.TIF *.tiff *.TIFF)') + self.assertEqual(formats[0].driverName, 'GTiff') + self.assertTrue('netCDF' in [f.driverName for f in formats]) + + # alphabetical sorting + formats2 = QgsRasterFileWriter.supportedFiltersAndFormats(QgsRasterFileWriter.RasterFormatOptions()) + self.assertTrue(formats2[0].driverName < formats2[1].driverName) + self.assertCountEqual([f.driverName for f in formats], [f.driverName for f in formats2]) + self.assertNotEqual(formats2[0].driverName, 'GTiff') + + def testSupportedFormatExtensions(self): + formats = QgsRasterFileWriter.supportedFormatExtensions() + self.assertTrue('tif' in formats) + self.assertFalse('exe' in formats) + self.assertEqual(formats[0], 'tif') + self.assertTrue('nc' in formats) + + # alphabetical sorting + formats2 = QgsRasterFileWriter.supportedFormatExtensions(QgsRasterFileWriter.RasterFormatOptions()) + self.assertTrue(formats2[1] < formats2[2]) + self.assertCountEqual(formats, formats2) + self.assertNotEqual(formats2[0], 'tif') + def testImportIntoGpkg(self): # init target file test_gpkg = tempfile.mktemp(suffix='.gpkg', dir=self.testDataDir)