Add a 'gdal raster edit' command to override SRS, extent and metadata

This commit is contained in:
Even Rouault 2024-12-12 20:21:57 +01:00
parent 49367db76d
commit e4bb0c3de7
No known key found for this signature in database
GPG Key ID: 33EBBFC47B3DD87D
18 changed files with 999 additions and 60 deletions

View File

@ -13,6 +13,7 @@ add_library(
gdalalg_raster.cpp
gdalalg_raster_info.cpp
gdalalg_raster_convert.cpp
gdalalg_raster_edit.cpp
gdalalg_raster_pipeline.cpp
gdalalg_raster_read.cpp
gdalalg_raster_reproject.cpp

View File

@ -760,7 +760,8 @@ GDALDatasetH GDALTranslate(const char *pszDest, GDALDatasetH hSrcDataset,
CPLFree(pszSRS);
}
if (!psOptions->osOutputSRS.empty())
if (!psOptions->osOutputSRS.empty() && psOptions->osOutputSRS != "null" &&
psOptions->osOutputSRS != "none")
{
OGRSpatialReference oOutputSRS;
if (oOutputSRS.SetFromUserInput(psOptions->osOutputSRS.c_str()) !=
@ -1520,23 +1521,31 @@ GDALDatasetH GDALTranslate(const char *pszDest, GDALDatasetH hSrcDataset,
if (psOptions->nGCPCount == 0)
{
OGRSpatialReference oSRS;
if (!psOptions->osOutputSRS.empty())
if (psOptions->osOutputSRS == "null" ||
psOptions->osOutputSRS == "none")
{
oSRS.SetFromUserInput(psOptions->osOutputSRS.c_str());
oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
poVDS->SetSpatialRef(nullptr);
}
else
{
const OGRSpatialReference *poSrcSRS = poSrcDS->GetSpatialRef();
if (poSrcSRS)
oSRS = *poSrcSRS;
}
if (!oSRS.IsEmpty())
{
if (psOptions->dfOutputCoordinateEpoch > 0)
oSRS.SetCoordinateEpoch(psOptions->dfOutputCoordinateEpoch);
poVDS->SetSpatialRef(&oSRS);
OGRSpatialReference oSRS;
if (!psOptions->osOutputSRS.empty())
{
oSRS.SetFromUserInput(psOptions->osOutputSRS.c_str());
oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
}
else
{
const OGRSpatialReference *poSrcSRS = poSrcDS->GetSpatialRef();
if (poSrcSRS)
oSRS = *poSrcSRS;
}
if (!oSRS.IsEmpty())
{
if (psOptions->dfOutputCoordinateEpoch > 0)
oSRS.SetCoordinateEpoch(psOptions->dfOutputCoordinateEpoch);
poVDS->SetSpatialRef(&oSRS);
}
}
}
@ -1598,7 +1607,12 @@ GDALDatasetH GDALTranslate(const char *pszDest, GDALDatasetH hSrcDataset,
if (psOptions->nGCPCount != 0)
{
OGRSpatialReference oSRS;
if (!psOptions->osOutputSRS.empty())
if (psOptions->osOutputSRS == "null" ||
psOptions->osOutputSRS == "none")
{
// nothing to do
}
else if (!psOptions->osOutputSRS.empty())
{
oSRS.SetFromUserInput(psOptions->osOutputSRS.c_str());
oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);

View File

@ -14,6 +14,7 @@
#include "gdalalg_raster_info.h"
#include "gdalalg_raster_convert.h"
#include "gdalalg_raster_edit.h"
#include "gdalalg_raster_pipeline.h"
#include "gdalalg_raster_reproject.h"
@ -37,6 +38,7 @@ class GDALRasterAlgorithm final : public GDALAlgorithm
{
RegisterSubAlgorithm<GDALRasterInfoAlgorithm>();
RegisterSubAlgorithm<GDALRasterConvertAlgorithm>();
RegisterSubAlgorithm<GDALRasterEditAlgorithmStandalone>();
RegisterSubAlgorithm<GDALRasterPipelineAlgorithm>();
RegisterSubAlgorithm<GDALRasterReprojectAlgorithmStandalone>();
}

View File

@ -0,0 +1,233 @@
/******************************************************************************
*
* Project: GDAL
* Purpose: "edit" step of "raster pipeline"
* Author: Even Rouault <even dot rouault at spatialys.com>
*
******************************************************************************
* Copyright (c) 2024, Even Rouault <even dot rouault at spatialys.com>
*
* SPDX-License-Identifier: MIT
****************************************************************************/
#include "gdalalg_raster_edit.h"
#include "gdal_priv.h"
#include "gdal_utils.h"
//! @cond Doxygen_Suppress
#ifndef _
#define _(x) (x)
#endif
/************************************************************************/
/* GDALRasterEditAlgorithm::GDALRasterEditAlgorithm() */
/************************************************************************/
GDALRasterEditAlgorithm::GDALRasterEditAlgorithm(bool standaloneStep)
: GDALRasterPipelineStepAlgorithm(
NAME, DESCRIPTION, HELP_URL,
// Avoid automatic addition of input/output arguments
/*standaloneStep = */ false)
{
if (standaloneStep)
{
AddArg("dataset", 0, _("Dataset (in-place updated)"), &m_dataset,
GDAL_OF_RASTER | GDAL_OF_UPDATE)
.SetPositional()
.SetRequired();
m_standaloneStep = true;
}
AddArg("crs", 0, _("Override CRS (without reprojection)"), &m_overrideCrs)
.AddHiddenAlias("a_srs")
.SetIsCRSArg(/*noneAllowed=*/true);
{
auto &arg =
AddArg("extent", 0, _("Extent as xmin,ymin,xmax,ymax"), &m_extent)
.SetRepeatedArgAllowed(false)
.SetMinCount(4)
.SetMaxCount(4)
.SetDisplayHintAboutRepetition(false);
arg.AddValidationAction(
[&arg]()
{
const auto &val = arg.Get<std::vector<double>>();
CPLAssert(val.size() == 4);
if (!(val[0] <= val[2]) || !(val[1] <= val[3]))
{
CPLError(
CE_Failure, CPLE_AppDefined,
"Value of 'extent' should be xmin,ymin,xmax,ymax with "
"xmin <= xmax and ymin <= ymax");
return false;
}
return true;
});
}
{
auto &arg = AddArg("metadata", 0, _("Add/update dataset metadata item"),
&m_metadata)
.SetMetaVar("<KEY>=<VALUE>");
arg.AddValidationAction([this, &arg]()
{ return ValidateKeyValue(arg); });
arg.AddHiddenAlias("mo");
}
AddArg("unset-metadata", 0, _("Remove dataset metadata item"),
&m_unsetMetadata)
.SetMetaVar("<KEY>");
}
/************************************************************************/
/* GDALRasterEditAlgorithm::RunImpl() */
/************************************************************************/
bool GDALRasterEditAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
void *pProgressData)
{
if (m_standaloneStep)
{
auto poDS = m_dataset.GetDatasetRef();
CPLAssert(poDS);
if (poDS->GetAccess() != GA_Update)
{
ReportError(CE_Failure, CPLE_AppDefined,
"Dataset should be opened in update mode");
return false;
}
if (m_overrideCrs == "null" || m_overrideCrs == "none")
{
if (poDS->SetSpatialRef(nullptr) != CE_None)
{
ReportError(CE_Failure, CPLE_AppDefined,
"SetSpatialRef(%s) failed", m_overrideCrs.c_str());
return false;
}
}
else if (!m_overrideCrs.empty())
{
OGRSpatialReference oSRS;
oSRS.SetFromUserInput(m_overrideCrs.c_str());
oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
if (poDS->SetSpatialRef(&oSRS) != CE_None)
{
ReportError(CE_Failure, CPLE_AppDefined,
"SetSpatialRef(%s) failed", m_overrideCrs.c_str());
return false;
}
}
if (!m_extent.empty())
{
if (poDS->GetRasterXSize() == 0 || poDS->GetRasterYSize() == 0)
{
ReportError(
CE_Failure, CPLE_AppDefined,
"Cannot set extent because dataset has 0x0 dimension");
return false;
}
double adfGT[6];
adfGT[0] = m_extent[0];
adfGT[1] = (m_extent[2] - m_extent[0]) / poDS->GetRasterXSize();
adfGT[2] = 0;
adfGT[3] = m_extent[3];
adfGT[4] = 0;
adfGT[5] = -(m_extent[3] - m_extent[1]) / poDS->GetRasterYSize();
if (poDS->SetGeoTransform(adfGT) != CE_None)
{
ReportError(CE_Failure, CPLE_AppDefined,
"Setting extent failed");
return false;
}
}
const CPLStringList aosMD(m_metadata);
for (const auto &[key, value] : cpl::IterateNameValue(aosMD))
{
if (poDS->SetMetadataItem(key, value) != CE_None)
{
ReportError(CE_Failure, CPLE_AppDefined,
"SetMetadataItem('%s', '%s') failed", key, value);
return false;
}
}
for (const std::string &key : m_unsetMetadata)
{
if (poDS->SetMetadataItem(key.c_str(), nullptr) != CE_None)
{
ReportError(CE_Failure, CPLE_AppDefined,
"SetMetadataItem('%s', NULL) failed", key.c_str());
return false;
}
}
return true;
}
else
{
return RunStep(pfnProgress, pProgressData);
}
}
/************************************************************************/
/* GDALRasterEditAlgorithm::RunStep() */
/************************************************************************/
bool GDALRasterEditAlgorithm::RunStep(GDALProgressFunc, void *)
{
CPLAssert(m_inputDataset.GetDatasetRef());
CPLAssert(m_outputDataset.GetName().empty());
CPLAssert(!m_outputDataset.GetDatasetRef());
CPLStringList aosOptions;
aosOptions.AddString("-of");
aosOptions.AddString("VRT");
if (!m_overrideCrs.empty())
{
aosOptions.AddString("-a_srs");
aosOptions.AddString(m_overrideCrs.c_str());
}
if (!m_extent.empty())
{
aosOptions.AddString("-a_ullr");
aosOptions.AddString(CPLSPrintf("%.17g", m_extent[0])); // upper-left X
aosOptions.AddString(CPLSPrintf("%.17g", m_extent[3])); // upper-left Y
aosOptions.AddString(
CPLSPrintf("%.17g", m_extent[2])); // lower-right X
aosOptions.AddString(
CPLSPrintf("%.17g", m_extent[1])); // lower-right Y
}
for (const auto &val : m_metadata)
{
aosOptions.AddString("-mo");
aosOptions.AddString(val.c_str());
}
for (const std::string &key : m_unsetMetadata)
{
aosOptions.AddString("-mo");
aosOptions.AddString((key + "=").c_str());
}
GDALTranslateOptions *psOptions =
GDALTranslateOptionsNew(aosOptions.List(), nullptr);
GDALDatasetH hSrcDS = GDALDataset::ToHandle(m_inputDataset.GetDatasetRef());
auto poRetDS =
GDALDataset::FromHandle(GDALTranslate("", hSrcDS, psOptions, nullptr));
GDALTranslateOptionsFree(psOptions);
const bool ok = poRetDS != nullptr;
if (ok)
m_outputDataset.Set(std::unique_ptr<GDALDataset>(poRetDS));
return ok;
}
//! @endcond

View File

@ -0,0 +1,65 @@
/******************************************************************************
*
* Project: GDAL
* Purpose: "edit" step of "raster pipeline"
* Author: Even Rouault <even dot rouault at spatialys.com>
*
******************************************************************************
* Copyright (c) 2024, Even Rouault <even dot rouault at spatialys.com>
*
* SPDX-License-Identifier: MIT
****************************************************************************/
#ifndef GDALALG_RASTER_EDIT_INCLUDED
#define GDALALG_RASTER_EDIT_INCLUDED
#include "gdalalg_raster_pipeline.h"
//! @cond Doxygen_Suppress
/************************************************************************/
/* GDALRasterEditAlgorithm */
/************************************************************************/
class GDALRasterEditAlgorithm /* non final */
: public GDALRasterPipelineStepAlgorithm
{
public:
static constexpr const char *NAME = "edit";
static constexpr const char *DESCRIPTION = "Edit a raster dataset.";
static constexpr const char *HELP_URL = "/programs/gdal_raster_edit.html";
static std::vector<std::string> GetAliases()
{
return {};
}
explicit GDALRasterEditAlgorithm(bool standaloneStep = false);
private:
bool RunImpl(GDALProgressFunc pfnProgress, void *pProgressData) override;
bool RunStep(GDALProgressFunc pfnProgress, void *pProgressData) override;
GDALArgDatasetValue m_dataset{}; // standalone mode only
std::string m_overrideCrs{};
std::vector<double> m_extent{};
std::vector<std::string> m_metadata{};
std::vector<std::string> m_unsetMetadata{};
};
/************************************************************************/
/* GDALRasterEditAlgorithmStandalone */
/************************************************************************/
class GDALRasterEditAlgorithmStandalone final : public GDALRasterEditAlgorithm
{
public:
GDALRasterEditAlgorithmStandalone()
: GDALRasterEditAlgorithm(/* standaloneStep = */ true)
{
}
};
//! @endcond
#endif /* GDALALG_RASTER_EDIT_INCLUDED */

View File

@ -12,6 +12,7 @@
#include "gdalalg_raster_pipeline.h"
#include "gdalalg_raster_read.h"
#include "gdalalg_raster_edit.h"
#include "gdalalg_raster_reproject.h"
#include "gdalalg_raster_write.h"
@ -159,6 +160,7 @@ GDALRasterPipelineAlgorithm::GDALRasterPipelineAlgorithm(
m_stepRegistry.Register<GDALRasterReadAlgorithm>();
m_stepRegistry.Register<GDALRasterWriteAlgorithm>();
m_stepRegistry.Register<GDALRasterEditAlgorithm>();
m_stepRegistry.Register<GDALRasterReprojectAlgorithm>();
}

View File

@ -29,8 +29,11 @@ GDALRasterReprojectAlgorithm::GDALRasterReprojectAlgorithm(bool standaloneStep)
: GDALRasterPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
standaloneStep)
{
AddArg("src-crs", 's', _("Source CRS"), &m_srsCrs).AddHiddenAlias("s_srs");
AddArg("src-crs", 's', _("Source CRS"), &m_srsCrs)
.SetIsCRSArg()
.AddHiddenAlias("s_srs");
AddArg("dst-crs", 'd', _("Destination CRS"), &m_dstCrs)
.SetIsCRSArg()
.AddHiddenAlias("t_srs");
AddArg("resampling", 'r', _("Resampling method"), &m_resampling)
.SetChoices("near", "bilinear", "cubic", "cubicspline", "lanczos",
@ -98,28 +101,6 @@ bool GDALRasterReprojectAlgorithm::RunStep(GDALProgressFunc, void *)
CPLAssert(m_outputDataset.GetName().empty());
CPLAssert(!m_outputDataset.GetDatasetRef());
if (!m_srsCrs.empty())
{
OGRSpatialReference oSRS;
if (oSRS.SetFromUserInput(m_srsCrs.c_str()) != OGRERR_NONE)
{
ReportError(CE_Failure, CPLE_AppDefined,
"Invalid value for '--src-crs'");
return false;
}
}
if (!m_dstCrs.empty())
{
OGRSpatialReference oSRS;
if (oSRS.SetFromUserInput(m_dstCrs.c_str()) != OGRERR_NONE)
{
ReportError(CE_Failure, CPLE_AppDefined,
"Invalid value for '--dst-crs'");
return false;
}
}
CPLStringList aosOptions;
aosOptions.AddString("-of");
aosOptions.AddString("VRT");

View File

@ -31,8 +31,11 @@ GDALVectorReprojectAlgorithm::GDALVectorReprojectAlgorithm(bool standaloneStep)
: GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
standaloneStep)
{
AddArg("src-crs", 's', _("Source CRS"), &m_srsCrs).AddHiddenAlias("s_srs");
AddArg("src-crs", 's', _("Source CRS"), &m_srsCrs)
.SetIsCRSArg()
.AddHiddenAlias("s_srs");
AddArg("dst-crs", 'd', _("Destination CRS"), &m_dstCrs)
.SetIsCRSArg()
.SetRequired()
.AddHiddenAlias("t_srs");
}
@ -82,22 +85,12 @@ bool GDALVectorReprojectAlgorithm::RunStep(GDALProgressFunc, void *)
if (!m_srsCrs.empty())
{
poSrcCRS = std::make_unique<OGRSpatialReference>();
if (poSrcCRS->SetFromUserInput(m_srsCrs.c_str()) != OGRERR_NONE)
{
ReportError(CE_Failure, CPLE_AppDefined,
"Invalid value for '--src-crs'");
return false;
}
poSrcCRS->SetFromUserInput(m_srsCrs.c_str());
poSrcCRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
}
OGRSpatialReference oDstCRS;
if (oDstCRS.SetFromUserInput(m_dstCrs.c_str()) != OGRERR_NONE)
{
ReportError(CE_Failure, CPLE_AppDefined,
"Invalid value for '--dst-crs'");
return false;
}
oDstCRS.SetFromUserInput(m_dstCrs.c_str());
oDstCRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
auto poSrcDS = m_inputDataset.GetDatasetRef();

View File

@ -455,6 +455,19 @@ TEST_F(test_gdal_algorithm, RunValidationActions)
EXPECT_FALSE(arg.Set(2));
}
TEST_F(test_gdal_algorithm, SetIsCRSArg_wrong_type)
{
int val = 0;
auto arg = GDALInConstructionAlgorithmArg(
nullptr, GDALAlgorithmArgDecl("", 0, "", GAAT_INTEGER), &val);
{
CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
CPLErrorReset();
arg.SetIsCRSArg();
EXPECT_EQ(CPLGetLastErrorType(), CE_Failure);
}
}
class MyAlgorithmWithDummyRun : public GDALAlgorithm
{
public:
@ -3070,4 +3083,229 @@ TEST_F(test_gdal_algorithm, DispatcherGetUsageForCLI)
}
}
TEST_F(test_gdal_algorithm, raster_edit_failures_dataset_0_0)
{
auto &singleton = GDALGlobalAlgorithmRegistry::GetSingleton();
auto raster = singleton.Instantiate("raster");
ASSERT_NE(raster, nullptr);
auto edit = raster->InstantiateSubAlgorithm("edit");
ASSERT_NE(edit, nullptr);
class MyDataset : public GDALDataset
{
public:
MyDataset()
{
nRasterXSize = 0;
nRasterYSize = 0;
eAccess = GA_Update;
}
};
auto datasetArg = edit->GetArg("dataset");
ASSERT_NE(datasetArg, nullptr);
datasetArg->Get<GDALArgDatasetValue>().Set(std::make_unique<MyDataset>());
auto extentArg = edit->GetArg("extent");
ASSERT_NE(extentArg, nullptr);
extentArg->Set(std::vector<double>{2, 49, 3, 50});
CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
CPLErrorReset();
EXPECT_FALSE(edit->Run());
EXPECT_EQ(CPLGetLastErrorType(), CE_Failure);
EXPECT_STREQ(CPLGetLastErrorMsg(),
"edit: Cannot set extent because dataset has 0x0 dimension");
}
TEST_F(test_gdal_algorithm, raster_edit_failures_set_spatial_ref_none)
{
auto &singleton = GDALGlobalAlgorithmRegistry::GetSingleton();
auto raster = singleton.Instantiate("raster");
ASSERT_NE(raster, nullptr);
auto edit = raster->InstantiateSubAlgorithm("edit");
ASSERT_NE(edit, nullptr);
class MyDataset : public GDALDataset
{
public:
MyDataset()
{
eAccess = GA_Update;
}
CPLErr SetSpatialRef(const OGRSpatialReference *) override
{
return CE_Failure;
}
};
auto datasetArg = edit->GetArg("dataset");
ASSERT_NE(datasetArg, nullptr);
datasetArg->Get<GDALArgDatasetValue>().Set(std::make_unique<MyDataset>());
auto crsArg = edit->GetArg("crs");
ASSERT_NE(crsArg, nullptr);
crsArg->Set("none");
CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
CPLErrorReset();
EXPECT_FALSE(edit->Run());
EXPECT_EQ(CPLGetLastErrorType(), CE_Failure);
EXPECT_STREQ(CPLGetLastErrorMsg(), "edit: SetSpatialRef(none) failed");
}
TEST_F(test_gdal_algorithm, raster_edit_failures_set_spatial_ref_regular)
{
auto &singleton = GDALGlobalAlgorithmRegistry::GetSingleton();
auto raster = singleton.Instantiate("raster");
ASSERT_NE(raster, nullptr);
auto edit = raster->InstantiateSubAlgorithm("edit");
ASSERT_NE(edit, nullptr);
class MyDataset : public GDALDataset
{
public:
MyDataset()
{
eAccess = GA_Update;
}
CPLErr SetSpatialRef(const OGRSpatialReference *) override
{
return CE_Failure;
}
};
auto datasetArg = edit->GetArg("dataset");
ASSERT_NE(datasetArg, nullptr);
datasetArg->Get<GDALArgDatasetValue>().Set(std::make_unique<MyDataset>());
auto crsArg = edit->GetArg("crs");
ASSERT_NE(crsArg, nullptr);
crsArg->Set("EPSG:32632");
CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
CPLErrorReset();
EXPECT_FALSE(edit->Run());
EXPECT_EQ(CPLGetLastErrorType(), CE_Failure);
EXPECT_STREQ(CPLGetLastErrorMsg(),
"edit: SetSpatialRef(EPSG:32632) failed");
}
TEST_F(test_gdal_algorithm, raster_edit_failures_set_geo_transform)
{
auto &singleton = GDALGlobalAlgorithmRegistry::GetSingleton();
auto raster = singleton.Instantiate("raster");
ASSERT_NE(raster, nullptr);
auto edit = raster->InstantiateSubAlgorithm("edit");
ASSERT_NE(edit, nullptr);
class MyDataset : public GDALDataset
{
public:
MyDataset()
{
eAccess = GA_Update;
}
CPLErr SetGeoTransform(double *) override
{
return CE_Failure;
}
};
auto datasetArg = edit->GetArg("dataset");
ASSERT_NE(datasetArg, nullptr);
datasetArg->Get<GDALArgDatasetValue>().Set(std::make_unique<MyDataset>());
auto extentArg = edit->GetArg("extent");
ASSERT_NE(extentArg, nullptr);
extentArg->Set(std::vector<double>{2, 49, 3, 50});
CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
CPLErrorReset();
EXPECT_FALSE(edit->Run());
EXPECT_EQ(CPLGetLastErrorType(), CE_Failure);
EXPECT_STREQ(CPLGetLastErrorMsg(), "edit: Setting extent failed");
}
TEST_F(test_gdal_algorithm, raster_edit_failures_set_metadata)
{
auto &singleton = GDALGlobalAlgorithmRegistry::GetSingleton();
auto raster = singleton.Instantiate("raster");
ASSERT_NE(raster, nullptr);
auto edit = raster->InstantiateSubAlgorithm("edit");
ASSERT_NE(edit, nullptr);
class MyDataset : public GDALDataset
{
public:
MyDataset()
{
eAccess = GA_Update;
}
CPLErr SetMetadataItem(const char *, const char *,
const char *) override
{
return CE_Failure;
}
};
auto datasetArg = edit->GetArg("dataset");
ASSERT_NE(datasetArg, nullptr);
datasetArg->Get<GDALArgDatasetValue>().Set(std::make_unique<MyDataset>());
auto extentArg = edit->GetArg("metadata");
ASSERT_NE(extentArg, nullptr);
extentArg->Set(std::vector<std::string>{"foo=bar"});
CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
CPLErrorReset();
EXPECT_FALSE(edit->Run());
EXPECT_EQ(CPLGetLastErrorType(), CE_Failure);
EXPECT_STREQ(CPLGetLastErrorMsg(),
"edit: SetMetadataItem('foo', 'bar') failed");
}
TEST_F(test_gdal_algorithm, raster_edit_failures_unset_metadata)
{
auto &singleton = GDALGlobalAlgorithmRegistry::GetSingleton();
auto raster = singleton.Instantiate("raster");
ASSERT_NE(raster, nullptr);
auto edit = raster->InstantiateSubAlgorithm("edit");
ASSERT_NE(edit, nullptr);
class MyDataset : public GDALDataset
{
public:
MyDataset()
{
eAccess = GA_Update;
}
CPLErr SetMetadataItem(const char *, const char *,
const char *) override
{
return CE_Failure;
}
};
auto datasetArg = edit->GetArg("dataset");
ASSERT_NE(datasetArg, nullptr);
datasetArg->Get<GDALArgDatasetValue>().Set(std::make_unique<MyDataset>());
auto extentArg = edit->GetArg("unset-metadata");
ASSERT_NE(extentArg, nullptr);
extentArg->Set(std::vector<std::string>{"foo"});
CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
CPLErrorReset();
EXPECT_FALSE(edit->Run());
EXPECT_EQ(CPLGetLastErrorType(), CE_Failure);
EXPECT_STREQ(CPLGetLastErrorMsg(),
"edit: SetMetadataItem('foo', NULL) failed");
}
} // namespace test_gdal_algorithm

View File

@ -0,0 +1,238 @@
#!/usr/bin/env pytest
# -*- coding: utf-8 -*-
###############################################################################
# Project: GDAL/OGR Test Suite
# Purpose: 'gdal raster edit' testing
# Author: Even Rouault <even dot rouault @ spatialys.com>
#
###############################################################################
# Copyright (c) 2024, Even Rouault <even dot rouault at spatialys.com>
#
# SPDX-License-Identifier: MIT
###############################################################################
import pytest
from osgeo import gdal
def get_edit_alg():
reg = gdal.GetGlobalAlgorithmRegistry()
raster = reg.InstantiateAlg("raster")
return raster.InstantiateSubAlgorithm("edit")
def test_gdalalg_raster_edit_read_only(tmp_vsimem):
tmp_filename = str(tmp_vsimem / "tmp.tif")
gdal.FileFromMemBuffer(tmp_filename, open("../gcore/data/byte.tif", "rb").read())
pipeline = get_edit_alg()
pipeline.GetArg("dataset").Get().SetDataset(gdal.OpenEx(tmp_filename))
with pytest.raises(
Exception, match="edit: Dataset should be opened in update mode"
):
pipeline.Run()
def test_gdalalg_raster_edit_crs(tmp_vsimem):
tmp_filename = str(tmp_vsimem / "tmp.tif")
gdal.FileFromMemBuffer(tmp_filename, open("../gcore/data/byte.tif", "rb").read())
pipeline = get_edit_alg()
assert pipeline.ParseRunAndFinalize(
[
"--crs=EPSG:32611",
tmp_filename,
]
)
with gdal.OpenEx(tmp_filename) as ds:
assert ds.GetSpatialRef().GetAuthorityCode(None) == "32611"
def test_gdalalg_raster_edit_crs_none(tmp_vsimem):
tmp_filename = str(tmp_vsimem / "tmp.tif")
gdal.FileFromMemBuffer(tmp_filename, open("../gcore/data/byte.tif", "rb").read())
pipeline = get_edit_alg()
assert pipeline.ParseRunAndFinalize(
[
"--crs=none",
tmp_filename,
]
)
with gdal.OpenEx(tmp_filename) as ds:
assert ds.GetSpatialRef() is None
def test_gdalalg_raster_edit_extent(tmp_vsimem):
tmp_filename = str(tmp_vsimem / "tmp.tif")
gdal.FileFromMemBuffer(tmp_filename, open("../gcore/data/byte.tif", "rb").read())
pipeline = get_edit_alg()
assert pipeline.ParseRunAndFinalize(
[
"--extent=1,2,10,200",
tmp_filename,
]
)
with gdal.OpenEx(tmp_filename) as ds:
assert ds.GetGeoTransform() == pytest.approx((1.0, 0.45, 0.0, 200.0, 0.0, -9.9))
def test_gdalalg_raster_edit_extent_invalid(tmp_vsimem):
tmp_filename = str(tmp_vsimem / "tmp.tif")
gdal.FileFromMemBuffer(tmp_filename, open("../gcore/data/byte.tif", "rb").read())
pipeline = get_edit_alg()
with pytest.raises(
Exception,
match="Value of 'extent' should be xmin,ymin,xmax,ymax with xmin <= xmax and ymin <= ymax",
):
pipeline.ParseRunAndFinalize(
[
"--extent=1,200,10,2",
tmp_filename,
]
)
def test_gdalalg_raster_edit_metadata(tmp_vsimem):
tmp_filename = str(tmp_vsimem / "tmp.tif")
gdal.FileFromMemBuffer(tmp_filename, open("../gcore/data/byte.tif", "rb").read())
pipeline = get_edit_alg()
assert pipeline.ParseRunAndFinalize(
[
"--metadata",
"foo=bar",
"--metadata",
"bar=baz",
tmp_filename,
]
)
with gdal.OpenEx(tmp_filename) as ds:
assert ds.GetMetadata() == {"AREA_OR_POINT": "Area", "foo": "bar", "bar": "baz"}
pipeline = get_edit_alg()
assert pipeline.ParseRunAndFinalize(
[
"--unset-metadata",
"foo",
tmp_filename,
]
)
with gdal.OpenEx(tmp_filename) as ds:
assert ds.GetMetadata() == {"AREA_OR_POINT": "Area", "bar": "baz"}
def get_pipeline_alg():
reg = gdal.GetGlobalAlgorithmRegistry()
raster = reg.InstantiateAlg("raster")
return raster.InstantiateSubAlgorithm("pipeline")
def test_gdalalg_raster_pipeline_edit_crs(tmp_vsimem):
out_filename = str(tmp_vsimem / "out.tif")
pipeline = get_pipeline_alg()
assert pipeline.ParseRunAndFinalize(
[
"read",
"../gcore/data/byte.tif",
"!",
"edit",
"--crs=EPSG:32611",
"!",
"write",
"--overwrite",
out_filename,
]
)
with gdal.OpenEx(out_filename) as ds:
assert ds.GetSpatialRef().GetAuthorityCode(None) == "32611"
assert ds.GetRasterBand(1).Checksum() == 4672
def test_gdalalg_raster_pipeline_edit_crs_none(tmp_vsimem):
out_filename = str(tmp_vsimem / "out.tif")
pipeline = get_pipeline_alg()
assert pipeline.ParseRunAndFinalize(
[
"read",
"../gcore/data/byte.tif",
"!",
"edit",
"--crs=none",
"!",
"write",
"--overwrite",
out_filename,
]
)
with gdal.OpenEx(out_filename) as ds:
assert ds.GetSpatialRef() is None
def test_gdalalg_raster_pipeline_edit_extent(tmp_vsimem):
out_filename = str(tmp_vsimem / "out.tif")
pipeline = get_pipeline_alg()
assert pipeline.ParseRunAndFinalize(
[
"read",
"../gcore/data/byte.tif",
"!",
"edit",
"--extent=1,2,10,200",
"!",
"write",
"--overwrite",
out_filename,
]
)
with gdal.OpenEx(out_filename) as ds:
assert ds.GetGeoTransform() == pytest.approx((1.0, 0.45, 0.0, 200.0, 0.0, -9.9))
def test_gdalalg_raster_pipeline_edit_metadata(tmp_vsimem):
out_filename = str(tmp_vsimem / "out.tif")
pipeline = get_pipeline_alg()
assert pipeline.ParseRunAndFinalize(
[
"read",
"../gcore/data/byte.tif",
"!",
"edit",
"--metadata=foo=bar,bar=baz",
"!",
"edit",
"--unset-metadata=foo",
"!",
"write",
"--overwrite",
out_filename,
]
)
with gdal.OpenEx(out_filename) as ds:
assert ds.GetMetadata() == {"AREA_OR_POINT": "Area", "bar": "baz"}

View File

@ -372,7 +372,7 @@ def test_gdalalg_raster_pipeline_reproject_invalid_src_crs(tmp_vsimem):
pipeline = get_pipeline_alg()
with pytest.raises(
Exception,
match="reproject: Invalid value for '--src-crs'",
match="reproject: Invalid value for 'src-crs' argument",
):
pipeline.ParseRunAndFinalize(
[
@ -396,7 +396,7 @@ def test_gdalalg_raster_pipeline_reproject_invalid_dst_crs(tmp_vsimem):
pipeline = get_pipeline_alg()
with pytest.raises(
Exception,
match="reproject: Invalid value for '--dst-crs'",
match="reproject: Invalid value for 'dst-crs' argument",
):
pipeline.ParseRunAndFinalize(
[

View File

@ -577,7 +577,7 @@ def test_gdalalg_vector_pipeline_reproject_invalid_src_crs(tmp_vsimem):
pipeline = get_pipeline_alg()
with pytest.raises(
Exception,
match="reproject: Invalid value for '--src-crs'",
match="reproject: Invalid value for 'src-crs' argument",
):
pipeline.ParseRunAndFinalize(
[
@ -601,7 +601,7 @@ def test_gdalalg_vector_pipeline_reproject_invalid_dst_crs(tmp_vsimem):
pipeline = get_pipeline_alg()
with pytest.raises(
Exception,
match="reproject: Invalid value for '--dst-crs'",
match="reproject: Invalid value for 'dst-crs' argument",
):
pipeline.ParseRunAndFinalize(
[

View File

@ -229,6 +229,13 @@ man_pages = [
[author_evenr],
1,
),
(
"programs/gdal_raster_edit",
"gdal-raster-edit",
"Edit in place a raster dataset",
[author_evenr],
1,
),
(
"programs/gdal_raster_pipeline",
"gdal-raster-pipeline",
@ -239,7 +246,7 @@ man_pages = [
(
"programs/gdal_raster_reproject",
"gdal-raster-reproject",
"Reproect a raster dataset",
"Reproject a raster dataset",
[author_evenr],
1,
),

View File

@ -0,0 +1,106 @@
.. _gdal_raster_edit_subcommand:
================================================================================
"gdal raster edit" sub-command
================================================================================
.. versionadded:: 3.11
.. only:: html
Edit in place a raster dataset.
.. Index:: gdal raster edit
Synopsis
--------
.. code-block::
Usage: gdal raster edit [OPTIONS] <DATASET>
Edit a raster dataset.
Positional arguments:
--dataset <DATASET> Dataset (in-place updated) [required]
Common Options:
-h, --help Display help message and exit
--version Display GDAL version and exit
--json-usage Display usage as JSON document and exit
--drivers Display driver list as JSON document and exit
Options:
--crs <CRS> Override CRS (without reprojection)
--extent <EXTENT> Extent as xmin,ymin,xmax,ymax
--metadata <KEY>=<VALUE> Add/update dataset metadata item [may be repeated]
--unset-metadata <KEY> Remove dataset metadata item [may be repeated]
Description
-----------
:program:`gdal raster edit` can be used to edit a raster dataset.
This subcommand is also available as a potential step of :ref:`gdal_raster_pipeline_subcommand`
.. option:: --dataset <DATASET>
Dataset name, to be in-place updated. Required.
.. option:: --crs <CRS>
Override CRS, without reprojecting.
The coordinate reference systems that can be passed are anything supported by the
:cpp:func:`OGRSpatialReference::SetFromUserInput` call, which includes EPSG Projected,
Geographic or Compound CRS (i.e. EPSG:4296), a well known text (WKT) CRS definition,
PROJ.4 declarations, or the name of a .prj file containing a WKT CRS definition.
``null`` or ``none`` can be specified to unset an existing CRS.
Note that the spatial extent is also left unchanged.
.. option:: --extent <xmin>,<ymin>,<xmax>,ymax>
Override the spatial extent, without reprojecting or subsetting.
.. option:: --metadata <KEY>=<VALUE>
Add/update dataset metadata item, at the dataset level.
.. option:: --unset-metadata <KEY>
Remove dataset metadata item, at the dataset level.
Examples
--------
.. example::
:title: Override (without reprojecting) the CRS of a dataset
.. code-block:: bash
$ gdal raster edit --crs=EPSG:32632 my.tif
.. example::
:title: Override (without reprojecting or subsetting) the extent of a dataset
.. code-block:: bash
$ gdal raster edit --extent=2,49,3,50 my.tif
.. example::
:title: Add a metadata item
.. code-block:: bash
$ gdal raster edit --metadata AUTHOR=EvenR my.tif
.. example::
:title: Remove a metadata item
.. code-block:: bash
$ gdal raster edit --unset-metadata AUTHOR my.tif

View File

@ -49,6 +49,19 @@ Potential steps are:
--if, --input-format <INPUT-FORMAT> Input formats [may be repeated]
--oo, --open-option <KEY=VALUE> Open options [may be repeated]
* edit [OPTIONS]
.. code-block::
Edit a raster dataset.
Options:
--crs <CRS> Override CRS (without reprojection)
--extent <EXTENT> Extent as xmin,ymin,xmax,ymax
--metadata <KEY>=<VALUE> Add/update dataset metadata item [may be repeated]
--unset-metadata <KEY> Remove dataset metadata item [may be repeated]
Details for options can be found in :ref:`gdal_raster_edit_subcommand`.
* reproject [OPTIONS]
@ -64,6 +77,7 @@ Potential steps are:
--extent <xmin>,<ymin>,<xmax>,<ymax> Target extent (in destination CRS units)
--target-aligned-pixels Round target extent to target resolution
Details for options can be found in :ref:`gdal_raster_reproject_subcommand`.
* write [OPTIONS] <OUTPUT>
@ -91,8 +105,8 @@ Examples
--------
.. example::
:title: Reproject a GeoTIFF file to CRS EPSG:32632 ("WGS 84 / UTM zone 32N")
:title: Reproject a GeoTIFF file to CRS EPSG:32632 ("WGS 84 / UTM zone 32N") and adding a metadata item
.. code-block:: bash
$ gdal raster pipeline --progress ! read in.tif ! reproject --dst-crs=EPSG:32632 ! write out.tif --overwrite
$ gdal raster pipeline --progress ! read in.tif ! reproject --dst-crs=EPSG:32632 ! edit --metadata AUTHOR=EvenR ! write out.tif --overwrite

View File

@ -31,6 +31,7 @@ single :program:`gdal` program that accepts commands and subcommands.
gdal_raster
gdal_raster_info
gdal_raster_convert
gdal_raster_edit
gdal_raster_pipeline
gdal_raster_reproject
gdal_vector
@ -46,6 +47,7 @@ single :program:`gdal` program that accepts commands and subcommands.
- :ref:`gdal_raster_command`: Entry point for raster commands
- :ref:`gdal_raster_info_subcommand`: Get information on a raster dataset
- :ref:`gdal_raster_convert_subcommand`: Convert a raster dataset
- :ref:`gdal_raster_edit_subcommand`: Edit in place a raster dataset
- :ref:`gdal_raster_pipeline_subcommand`: Process a raster dataset
- :ref:`gdal_raster_reproject_subcommand`: Reproject a raster dataset
- :ref:`gdal_vector_command`: Entry point for vector commands

View File

@ -699,6 +699,40 @@ GDALArgDatasetValue::GDALArgDatasetValue(GDALArgDatasetValue &&other)
other.m_name.clear();
}
/************************************************************************/
/* GDALInConstructionAlgorithmArg::SetIsCRSArg() */
/************************************************************************/
GDALInConstructionAlgorithmArg &
GDALInConstructionAlgorithmArg::SetIsCRSArg(bool noneAllowed)
{
if (GetType() != GAAT_STRING)
{
CPLError(CE_Failure, CPLE_AppDefined,
"SetIsCRSArg() can only be called on a String argument");
return *this;
}
return AddValidationAction(
[this, noneAllowed]()
{
const std::string &osVal =
static_cast<const GDALInConstructionAlgorithmArg *>(this)
->Get<std::string>();
if (!noneAllowed || (osVal != "none" && osVal != "null"))
{
OGRSpatialReference oSRS;
if (oSRS.SetFromUserInput(osVal.c_str()) != OGRERR_NONE)
{
m_owner->ReportError(CE_Failure, CPLE_AppDefined,
"Invalid value for '%s' argument",
GetName().c_str());
return false;
}
}
return true;
});
}
/************************************************************************/
/* GDALAlgorithm::GDALAlgorithm() */
/************************************************************************/

View File

@ -449,7 +449,7 @@ class CPL_DLL GDALArgDatasetValue final
/** Get which type of dataset is allowed / generated.
* Binary-or combination of GDAL_OF_RASTER, GDAL_OF_VECTOR and
* GDAL_OF_MULTIDIM_RASTER.
* GDAL_OF_MULTIDIM_RASTER, possibly combined with GDAL_OF_UPDATE.
*/
GDALArgDatasetValueType GetType() const
{
@ -1583,6 +1583,13 @@ class CPL_DLL GDALInConstructionAlgorithmArg final : public GDALAlgorithmArg
return *this;
}
/** Register an action to validate that the argument value is a valid
* CRS definition.
* @param noneAllowed Set to true to mean that "null" or "none" are allowed
* to mean to unset CRS.
*/
GDALInConstructionAlgorithmArg &SetIsCRSArg(bool noneAllowed = false);
private:
GDALAlgorithm *const m_owner;
@ -2060,6 +2067,9 @@ class CPL_DLL GDALAlgorithmRegistry
/** Add --progress argument. */
GDALInConstructionAlgorithmArg &AddProgressArg();
/** Validation function to use for key=value type of arguments. */
bool ValidateKeyValue(const GDALAlgorithmArg &arg) const;
//! @cond Doxygen_Suppress
void AddAliasFor(GDALInConstructionAlgorithmArg *arg,
const std::string &alias);
@ -2112,7 +2122,6 @@ class CPL_DLL GDALAlgorithmRegistry
&inConstructionValues);
bool ValidateFormat(const GDALAlgorithmArg &arg) const;
bool ValidateKeyValue(const GDALAlgorithmArg &arg) const;
virtual bool RunImpl(GDALProgressFunc pfnProgress, void *pProgressData) = 0;