Coverage Report

Created: 2026-09-14 06:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_raster_clip.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  "clip" step of "raster pipeline", or "gdal raster clip" standalone
5
 * Author:   Even Rouault <even dot rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_raster_clip.h"
14
15
#include "gdal_priv.h"
16
#include "gdal_utils.h"
17
18
#include <algorithm>
19
#include <cmath>
20
21
//! @cond Doxygen_Suppress
22
23
#ifndef _
24
0
#define _(x) (x)
25
#endif
26
27
/************************************************************************/
28
/*          GDALRasterClipAlgorithm::GDALRasterClipAlgorithm()          */
29
/************************************************************************/
30
31
GDALRasterClipAlgorithm::GDALRasterClipAlgorithm(bool standaloneStep)
32
0
    : GDALRasterPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
33
0
                                      standaloneStep)
34
0
{
35
0
    constexpr const char *EXCLUSION_GROUP = "bbox-window-geometry-like";
36
0
    AddBBOXArg(&m_bbox, _("Clipping bounding box as xmin,ymin,xmax,ymax"))
37
0
        .SetMutualExclusionGroup(EXCLUSION_GROUP);
38
0
    AddArg("bbox-crs", 0, _("CRS of clipping bounding box"), &m_bboxCrs)
39
0
        .SetIsCRSArg()
40
0
        .AddHiddenAlias("bbox_srs");
41
42
0
    AddArg("window", 0, _("Raster window as col,line,width,height in pixels"),
43
0
           &m_window)
44
0
        .SetRepeatedArgAllowed(false)
45
0
        .SetMinCount(4)
46
0
        .SetMaxCount(4)
47
0
        .SetDisplayHintAboutRepetition(false)
48
0
        .SetMutualExclusionGroup(EXCLUSION_GROUP)
49
0
        .AddValidationAction(
50
0
            [this]()
51
0
            {
52
0
                CPLAssert(m_window.size() == 4);
53
0
                if (m_window[2] <= 0 || m_window[3] <= 0)
54
0
                {
55
0
                    CPLError(CE_Failure, CPLE_AppDefined,
56
0
                             "Value of 'window' should be "
57
0
                             "col,line,width,height with "
58
0
                             "width > 0 and height > 0");
59
0
                    return false;
60
0
                }
61
0
                return true;
62
0
            });
63
64
0
    AddArg("geometry", 0, _("Clipping geometry (WKT or GeoJSON)"), &m_geometry)
65
0
        .SetMutualExclusionGroup(EXCLUSION_GROUP);
66
0
    AddArg("geometry-crs", 0, _("CRS of clipping geometry"), &m_geometryCrs)
67
0
        .SetIsCRSArg()
68
0
        .AddHiddenAlias("geometry_srs");
69
0
    AddArg("like", 0, _("Dataset to use as a template for bounds"),
70
0
           &m_likeDataset, GDAL_OF_RASTER | GDAL_OF_VECTOR)
71
0
        .SetMetaVar("DATASET")
72
0
        .SetMutualExclusionGroup(EXCLUSION_GROUP);
73
0
    AddArg("like-sql", 0, ("SELECT statement to run on the 'like' dataset"),
74
0
           &m_likeSQL)
75
0
        .SetMetaVar("SELECT-STATEMENT")
76
0
        .SetMutualExclusionGroup("sql-where");
77
0
    AddArg("like-layer", 0, ("Name of the layer of the 'like' dataset"),
78
0
           &m_likeLayer)
79
0
        .SetMetaVar("LAYER-NAME");
80
0
    AddArg("like-where", 0, ("WHERE SQL clause to run on the 'like' dataset"),
81
0
           &m_likeWhere)
82
0
        .SetMetaVar("WHERE-EXPRESSION")
83
0
        .SetMutualExclusionGroup("sql-where");
84
0
    AddArg("only-bbox", 0,
85
0
           _("For 'geometry' and 'like', only consider their bounding box"),
86
0
           &m_onlyBBOX);
87
0
    AddArg("allow-bbox-outside-source", 0,
88
0
           _("Allow clipping box to include pixels outside input dataset"),
89
0
           &m_allowExtentOutsideSource);
90
    // Note: this would be a use case for multiple exclusion groups because it is not
91
    //       compatible with "window"
92
0
    AddArg("add-alpha", 0,
93
0
           _("Adds an alpha mask band to the destination when the source "
94
0
             "raster have none."),
95
0
           &m_addAlpha);
96
0
}
97
98
/************************************************************************/
99
/*                  GDALRasterClipAlgorithm::RunStep()                  */
100
/************************************************************************/
101
102
bool GDALRasterClipAlgorithm::RunStep(GDALPipelineStepRunContext &)
103
0
{
104
0
    auto poSrcDS = m_inputDataset[0].GetDatasetRef();
105
0
    CPLAssert(poSrcDS);
106
0
    CPLAssert(m_outputDataset.GetName().empty());
107
0
    CPLAssert(!m_outputDataset.GetDatasetRef());
108
109
0
    if (!m_window.empty())
110
0
    {
111
0
        if (m_addAlpha)
112
0
        {
113
0
            ReportError(CE_Failure, CPLE_NotSupported,
114
0
                        "'alpha' argument is not supported with 'window'");
115
0
            return false;
116
0
        }
117
118
0
        CPLStringList aosOptions;
119
0
        aosOptions.AddString("-of");
120
0
        aosOptions.AddString("VRT");
121
0
        aosOptions.AddString("--invoked-from-gdal-algorithm");
122
123
0
        aosOptions.AddString("-srcwin");
124
0
        aosOptions.AddString(CPLSPrintf("%d", m_window[0]));
125
0
        aosOptions.AddString(CPLSPrintf("%d", m_window[1]));
126
0
        aosOptions.AddString(CPLSPrintf("%d", m_window[2]));
127
0
        aosOptions.AddString(CPLSPrintf("%d", m_window[3]));
128
129
0
        if (!m_allowExtentOutsideSource)
130
0
        {
131
            // Unless we've specifically allowed the bounding box to extend beyond
132
            // the source raster, raise an error.
133
0
            aosOptions.AddString("-epo");
134
0
        }
135
136
0
        GDALTranslateOptions *psOptions =
137
0
            GDALTranslateOptionsNew(aosOptions.List(), nullptr);
138
139
0
        GDALDatasetH hSrcDS = GDALDataset::ToHandle(poSrcDS);
140
0
        auto poRetDS = GDALDataset::FromHandle(
141
0
            GDALTranslate("", hSrcDS, psOptions, nullptr));
142
0
        GDALTranslateOptionsFree(psOptions);
143
144
0
        const bool bOK = poRetDS != nullptr;
145
0
        if (bOK)
146
0
        {
147
0
            m_outputDataset.Set(std::unique_ptr<GDALDataset>(poRetDS));
148
0
        }
149
150
0
        return bOK;
151
0
    }
152
153
0
    GDALGeoTransform gt;
154
0
    if (poSrcDS->GetGeoTransform(gt) != CE_None)
155
0
    {
156
0
        ReportError(
157
0
            CE_Failure, CPLE_NotSupported,
158
0
            "Clipping is not supported on a raster without a geotransform");
159
0
        return false;
160
0
    }
161
0
    if (!gt.IsAxisAligned())
162
0
    {
163
0
        ReportError(CE_Failure, CPLE_NotSupported,
164
0
                    "Clipping is not supported on a raster whose geotransform "
165
0
                    "has rotation terms");
166
0
        return false;
167
0
    }
168
169
0
    auto [poClipGeom, errMsg] = GetClipGeometry();
170
0
    if (!poClipGeom)
171
0
    {
172
0
        ReportError(CE_Failure, CPLE_AppDefined, "%s", errMsg.c_str());
173
0
        return false;
174
0
    }
175
176
0
    auto poLikeDS = m_likeDataset.GetDatasetRef();
177
0
    if (!poClipGeom->getSpatialReference() && poLikeDS &&
178
0
        poLikeDS->GetLayerCount() == 0)
179
0
    {
180
0
        ReportError(CE_Failure, CPLE_AppDefined,
181
0
                    "Dataset '%s' has no CRS. Its bounds cannot be used.",
182
0
                    poLikeDS->GetDescription());
183
0
        return false;
184
0
    }
185
186
0
    CPLStringList aosOptions;
187
0
    aosOptions.AddString("-of");
188
0
    aosOptions.AddString("VRT");
189
0
    aosOptions.AddString("--invoked-from-gdal-algorithm");
190
191
0
    OGREnvelope env;
192
0
    poClipGeom->getEnvelope(&env);
193
194
0
    if (m_onlyBBOX)
195
0
    {
196
0
        auto poPoly = std::make_unique<OGRPolygon>(env);
197
0
        poPoly->assignSpatialReference(poClipGeom->getSpatialReference());
198
0
        poClipGeom = std::move(poPoly);
199
0
    }
200
201
0
    const bool bBottomUpRaster = gt.yscale > 0;
202
203
0
    if (poClipGeom->IsRectangle() && !m_addAlpha && !bBottomUpRaster)
204
0
    {
205
0
        aosOptions.AddString("-projwin");
206
0
        aosOptions.AddString(env.MinX);
207
0
        aosOptions.AddString(env.MaxY);
208
0
        aosOptions.AddString(env.MaxX);
209
0
        aosOptions.AddString(env.MinY);
210
211
0
        auto poClipGeomSRS = poClipGeom->getSpatialReference();
212
0
        if (poClipGeomSRS)
213
0
        {
214
0
            const char *const apszOptions[] = {"FORMAT=WKT2", nullptr};
215
0
            const std::string osWKT = poClipGeomSRS->exportToWkt(apszOptions);
216
0
            aosOptions.AddString("-projwin_srs");
217
0
            aosOptions.AddString(osWKT.c_str());
218
0
        }
219
220
0
        if (m_allowExtentOutsideSource)
221
0
        {
222
0
            aosOptions.AddString("--no-warn-about-outside-window");
223
0
        }
224
0
        else
225
0
        {
226
            // Unless we've specifically allowed the bounding box to extend beyond
227
            // the source raster, raise an error.
228
0
            aosOptions.AddString("-epo");
229
0
        }
230
231
0
        GDALTranslateOptions *psOptions =
232
0
            GDALTranslateOptionsNew(aosOptions.List(), nullptr);
233
234
0
        GDALDatasetH hSrcDS = GDALDataset::ToHandle(poSrcDS);
235
0
        auto poRetDS = GDALDataset::FromHandle(
236
0
            GDALTranslate("", hSrcDS, psOptions, nullptr));
237
0
        GDALTranslateOptionsFree(psOptions);
238
239
0
        const bool bOK = poRetDS != nullptr;
240
0
        if (bOK)
241
0
        {
242
0
            m_outputDataset.Set(std::unique_ptr<GDALDataset>(poRetDS));
243
0
        }
244
245
0
        return bOK;
246
0
    }
247
0
    else
248
0
    {
249
0
        if (bBottomUpRaster)
250
0
        {
251
0
            gt.yorig += gt.yscale * poSrcDS->GetRasterYSize();
252
0
            gt.yscale = -gt.yscale;
253
0
        }
254
255
0
        {
256
0
            auto poClipGeomInSrcSRS =
257
0
                std::unique_ptr<OGRGeometry>(poClipGeom->clone());
258
0
            if (poClipGeom->getSpatialReference() && poSrcDS->GetSpatialRef())
259
0
                poClipGeomInSrcSRS->transformTo(poSrcDS->GetSpatialRef());
260
0
            poClipGeomInSrcSRS->getEnvelope(&env);
261
0
        }
262
263
0
        OGREnvelope rasterEnv;
264
0
        poSrcDS->GetExtent(&rasterEnv, nullptr);
265
0
        if (!m_allowExtentOutsideSource && !rasterEnv.Contains(env))
266
0
        {
267
0
            ReportError(CE_Failure, CPLE_AppDefined,
268
0
                        "Clipping geometry is partially or totally outside the "
269
0
                        "extent of the raster. You can set the "
270
0
                        "'allow-bbox-outside-source' argument to proceed.");
271
0
            return false;
272
0
        }
273
274
0
        if (m_addAlpha)
275
0
        {
276
0
            aosOptions.AddString("-dstalpha");
277
0
        }
278
279
0
        aosOptions.AddString("-cutline");
280
0
        aosOptions.AddString(poClipGeom->exportToWkt());
281
282
0
        aosOptions.AddString("-wo");
283
0
        aosOptions.AddString("CUTLINE_ALL_TOUCHED=YES");
284
285
0
        auto poClipGeomSRS = poClipGeom->getSpatialReference();
286
0
        if (poClipGeomSRS)
287
0
        {
288
0
            const char *const apszOptions[] = {"FORMAT=WKT2", nullptr};
289
0
            const std::string osWKT = poClipGeomSRS->exportToWkt(apszOptions);
290
0
            aosOptions.AddString("-cutline_srs");
291
0
            aosOptions.AddString(osWKT.c_str());
292
0
        }
293
294
0
        constexpr double REL_EPS_PIXEL = 1e-3;
295
0
        const double dfMinX =
296
0
            gt.xorig +
297
0
            floor((env.MinX - gt.xorig) / gt.xscale + REL_EPS_PIXEL) *
298
0
                gt.xscale;
299
0
        const double dfMinY =
300
0
            gt.yorig +
301
0
            ceil((env.MinY - gt.yorig) / gt.yscale - REL_EPS_PIXEL) * gt.yscale;
302
0
        const double dfMaxX =
303
0
            gt.xorig +
304
0
            ceil((env.MaxX - gt.xorig) / gt.xscale - REL_EPS_PIXEL) * gt.xscale;
305
0
        const double dfMaxY =
306
0
            gt.yorig +
307
0
            floor((env.MaxY - gt.yorig) / gt.yscale + REL_EPS_PIXEL) *
308
0
                gt.yscale;
309
310
0
        aosOptions.AddString("-te");
311
0
        aosOptions.AddString(dfMinX);
312
0
        aosOptions.AddString(bBottomUpRaster ? dfMaxY : dfMinY);
313
0
        aosOptions.AddString(dfMaxX);
314
0
        aosOptions.AddString(bBottomUpRaster ? dfMinY : dfMaxY);
315
316
0
        aosOptions.AddString("-tr");
317
0
        aosOptions.AddString(gt.xscale);
318
0
        aosOptions.AddString(std::fabs(gt.yscale));
319
320
0
        GDALWarpAppOptions *psOptions =
321
0
            GDALWarpAppOptionsNew(aosOptions.List(), nullptr);
322
323
0
        GDALDatasetH hSrcDS = GDALDataset::ToHandle(poSrcDS);
324
0
        auto poRetDS = GDALDataset::FromHandle(
325
0
            GDALWarp("", nullptr, 1, &hSrcDS, psOptions, nullptr));
326
0
        GDALWarpAppOptionsFree(psOptions);
327
328
0
        const bool bOK = poRetDS != nullptr;
329
0
        if (bOK)
330
0
        {
331
0
            m_outputDataset.Set(std::unique_ptr<GDALDataset>(poRetDS));
332
0
        }
333
334
0
        return bOK;
335
0
    }
336
0
}
337
338
0
GDALRasterClipAlgorithmStandalone::~GDALRasterClipAlgorithmStandalone() =
339
    default;
340
341
//! @endcond