Coverage Report

Created: 2026-02-14 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_raster_clean_collar.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  gdal "raster clean-collar" subcommand
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_clean_collar.h"
14
15
#include "cpl_conv.h"
16
#include "cpl_vsi_virtual.h"
17
18
#include "gdal_priv.h"
19
#include "gdal_utils.h"
20
21
//! @cond Doxygen_Suppress
22
23
#ifndef _
24
0
#define _(x) (x)
25
#endif
26
27
/************************************************************************/
28
/*   GDALRasterCleanCollarAlgorithm::GDALRasterCleanCollarAlgorithm()   */
29
/************************************************************************/
30
31
GDALRasterCleanCollarAlgorithm::GDALRasterCleanCollarAlgorithm()
32
0
    : GDALAlgorithm(NAME, DESCRIPTION, HELP_URL)
33
0
{
34
0
    AddProgressArg();
35
36
0
    AddOpenOptionsArg(&m_openOptions);
37
0
    AddInputFormatsArg(&m_inputFormats)
38
0
        .AddMetadataItem(GAAMDI_REQUIRED_CAPABILITIES, {GDAL_DCAP_RASTER});
39
0
    AddInputDatasetArg(&m_inputDataset, GDAL_OF_RASTER);
40
41
0
    AddOutputDatasetArg(&m_outputDataset, GDAL_OF_RASTER,
42
0
                        /* positionalAndRequired = */ false)
43
0
        .SetPositional()
44
0
        .SetDatasetInputFlags(GADV_NAME | GADV_OBJECT);
45
0
    AddOutputFormatArg(&m_format, /* bStreamAllowed = */ false,
46
0
                       /* bGDALGAllowed = */ false)
47
0
        .AddMetadataItem(GAAMDI_REQUIRED_CAPABILITIES,
48
0
                         {GDAL_DCAP_CREATE, GDAL_DCAP_RASTER});
49
0
    AddCreationOptionsArg(&m_creationOptions);
50
0
    AddOverwriteArg(&m_overwrite);
51
0
    AddUpdateArg(&m_update);
52
53
0
    AddArg("color", 0,
54
0
           _("Transparent color(s): tuple of integer (like 'r,g,b'), 'black', "
55
0
             "'white'"),
56
0
           &m_color)
57
0
        .SetDefault("black")
58
0
        .SetPackedValuesAllowed(false)
59
0
        .AddValidationAction(
60
0
            [this]()
61
0
            {
62
0
                for (const auto &c : m_color)
63
0
                {
64
0
                    if (c != "white" && c != "black")
65
0
                    {
66
0
                        const CPLStringList aosTokens(
67
0
                            CSLTokenizeString2(c.c_str(), ",", 0));
68
0
                        for (const char *pszToken : aosTokens)
69
0
                        {
70
0
                            if (CPLGetValueType(pszToken) != CPL_VALUE_INTEGER)
71
0
                            {
72
0
                                ReportError(CE_Failure, CPLE_IllegalArg,
73
0
                                            "Value for 'color' should be tuple "
74
0
                                            "of integer (like 'r,g,b'), "
75
0
                                            "'black' or 'white'");
76
0
                                return false;
77
0
                            }
78
0
                        }
79
0
                    }
80
0
                }
81
0
                return true;
82
0
            });
83
0
    AddArg("color-threshold", 0,
84
0
           _("Select how far from specified transparent colors the pixel "
85
0
             "values are considered transparent."),
86
0
           &m_colorThreshold)
87
0
        .SetDefault(m_colorThreshold)
88
0
        .SetMinValueIncluded(0);
89
0
    AddArg("pixel-distance", 0,
90
0
           _("Number of consecutive transparent pixels that can be encountered "
91
0
             "before the giving up search inwards."),
92
0
           &m_pixelDistance)
93
0
        .SetDefault(m_pixelDistance)
94
0
        .SetMinValueIncluded(0);
95
0
    AddArg("add-alpha", 0, _("Adds an alpha band to the output dataset."),
96
0
           &m_addAlpha)
97
0
        .SetMutualExclusionGroup("addalpha-addmask");
98
0
    AddArg("add-mask", 0, _("Adds a mask band to the output dataset."),
99
0
           &m_addMask)
100
0
        .SetMutualExclusionGroup("addalpha-addmask");
101
0
    AddArg("algorithm", 0, _("Algorithm to apply"), &m_algorithm)
102
0
        .SetChoices("floodfill", "twopasses")
103
0
        .SetDefault(m_algorithm);
104
0
}
105
106
/************************************************************************/
107
/*              GDALRasterCleanCollarAlgorithm::RunImpl()               */
108
/************************************************************************/
109
110
bool GDALRasterCleanCollarAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
111
                                             void *pProgressData)
112
0
{
113
0
    auto poSrcDS = m_inputDataset.GetDatasetRef();
114
0
    CPLAssert(poSrcDS);
115
116
0
    auto poDstDS = m_outputDataset.GetDatasetRef();
117
0
    if (poSrcDS == poDstDS && poSrcDS->GetAccess() == GA_ReadOnly)
118
0
    {
119
0
        ReportError(CE_Failure, CPLE_AppDefined,
120
0
                    "Dataset should be opened in update mode");
121
0
        return false;
122
0
    }
123
0
    if (!poDstDS && !m_outputDataset.IsNameSet() && m_update)
124
0
    {
125
0
        m_outputDataset.Set(poSrcDS);
126
0
        poDstDS = poSrcDS;
127
0
    }
128
129
0
    const bool dstDSWasNull = poDstDS == nullptr;
130
131
0
    if (dstDSWasNull && !m_outputDataset.IsNameSet() && !m_update)
132
0
    {
133
0
        ReportError(CE_Failure, CPLE_AppDefined,
134
0
                    "Output dataset is not specified. If you intend to update "
135
0
                    "the input dataset, set the 'update' option");
136
0
        return false;
137
0
    }
138
139
0
    if (!poDstDS && !m_outputDataset.GetName().empty() && poDstDS != poSrcDS)
140
0
    {
141
0
        VSIStatBufL sStat;
142
0
        bool fileExists{VSIStatL(m_outputDataset.GetName().c_str(), &sStat) ==
143
0
                        0};
144
145
0
        {
146
0
            CPLErrorStateBackuper oCPLErrorHandlerPusher(CPLQuietErrorHandler);
147
0
            poDstDS = GDALDataset::FromHandle(GDALOpenEx(
148
0
                m_outputDataset.GetName().c_str(),
149
0
                GDAL_OF_RASTER | GDAL_OF_VERBOSE_ERROR | GDAL_OF_UPDATE,
150
0
                nullptr, nullptr, nullptr));
151
0
            CPLErrorReset();
152
0
        }
153
154
0
        if ((poDstDS || fileExists) && !m_overwrite && !m_update)
155
0
        {
156
0
            CPLError(CE_Failure, CPLE_AppDefined,
157
0
                     "Dataset '%s' already exists. Specify the --overwrite "
158
0
                     "option to overwrite it or the --update option to "
159
0
                     "update it.",
160
0
                     m_outputDataset.GetName().c_str());
161
0
            delete poDstDS;
162
0
            return false;
163
0
        }
164
165
0
        if (poDstDS && fileExists && m_overwrite)
166
0
        {
167
            // Delete the existing file
168
0
            delete poDstDS;
169
0
            poDstDS = nullptr;
170
0
            if (VSIUnlink(m_outputDataset.GetName().c_str()) != 0)
171
0
            {
172
0
                CPLError(CE_Failure, CPLE_AppDefined,
173
0
                         "Failed to delete existing dataset '%s'.",
174
0
                         m_outputDataset.GetName().c_str());
175
0
                return false;
176
0
            }
177
0
        }
178
0
    }
179
180
0
    CPLStringList aosOptions;
181
182
0
    if (!m_format.empty())
183
0
    {
184
0
        aosOptions.push_back("-of");
185
0
        aosOptions.push_back(m_format.c_str());
186
0
    }
187
188
0
    for (const auto &co : m_creationOptions)
189
0
    {
190
0
        aosOptions.push_back("-co");
191
0
        aosOptions.push_back(co.c_str());
192
0
    }
193
194
0
    for (const auto &color : m_color)
195
0
    {
196
0
        aosOptions.push_back("-color");
197
0
        std::string osColor;
198
0
        int nNonAlphaSrcBands = poSrcDS->GetRasterCount();
199
0
        if (nNonAlphaSrcBands &&
200
0
            poSrcDS->GetRasterBand(nNonAlphaSrcBands)
201
0
                    ->GetColorInterpretation() == GCI_AlphaBand)
202
0
            --nNonAlphaSrcBands;
203
0
        if (color == "white")
204
0
        {
205
0
            for (int i = 0; i < nNonAlphaSrcBands; ++i)
206
0
            {
207
0
                if (i > 0)
208
0
                    osColor += ',';
209
0
                osColor += "255";
210
0
            }
211
0
        }
212
0
        else if (color == "black")
213
0
        {
214
0
            for (int i = 0; i < nNonAlphaSrcBands; ++i)
215
0
            {
216
0
                if (i > 0)
217
0
                    osColor += ',';
218
0
                osColor += "0";
219
0
            }
220
0
        }
221
0
        else
222
0
        {
223
0
            osColor = color;
224
0
        }
225
0
        aosOptions.push_back(osColor.c_str());
226
0
    }
227
228
0
    aosOptions.push_back("-near");
229
0
    aosOptions.push_back(CPLSPrintf("%d", m_colorThreshold));
230
231
0
    aosOptions.push_back("-nb");
232
0
    aosOptions.push_back(CPLSPrintf("%d", m_pixelDistance));
233
234
0
    if (m_addAlpha ||
235
0
        (!m_addMask && poDstDS == nullptr && poSrcDS->GetRasterCount() > 0 &&
236
0
         poSrcDS->GetRasterBand(poSrcDS->GetRasterCount())
237
0
                 ->GetColorInterpretation() == GCI_AlphaBand) ||
238
0
        (!m_addMask && poDstDS != nullptr && poDstDS->GetRasterCount() > 0 &&
239
0
         poDstDS->GetRasterBand(poDstDS->GetRasterCount())
240
0
                 ->GetColorInterpretation() == GCI_AlphaBand))
241
0
    {
242
0
        aosOptions.push_back("-setalpha");
243
0
    }
244
245
0
    if (m_addMask ||
246
0
        (!m_addAlpha && poDstDS == nullptr && poSrcDS->GetRasterCount() > 0 &&
247
0
         poSrcDS->GetRasterBand(1)->GetMaskFlags() == GMF_PER_DATASET) ||
248
0
        (!m_addAlpha && poDstDS != nullptr && poDstDS->GetRasterCount() > 0 &&
249
0
         poDstDS->GetRasterBand(1)->GetMaskFlags() == GMF_PER_DATASET))
250
0
    {
251
0
        aosOptions.push_back("-setmask");
252
0
    }
253
254
0
    aosOptions.push_back("-alg");
255
0
    aosOptions.push_back(m_algorithm.c_str());
256
257
0
    std::unique_ptr<GDALNearblackOptions, decltype(&GDALNearblackOptionsFree)>
258
0
        psOptions{GDALNearblackOptionsNew(aosOptions.List(), nullptr),
259
0
                  GDALNearblackOptionsFree};
260
0
    if (!psOptions)
261
0
        return false;
262
263
0
    GDALNearblackOptionsSetProgress(psOptions.get(), pfnProgress,
264
0
                                    pProgressData);
265
266
0
    auto poRetDS = GDALDataset::FromHandle(GDALNearblack(
267
0
        m_outputDataset.GetName().c_str(), GDALDataset::ToHandle(poDstDS),
268
0
        GDALDataset::ToHandle(poSrcDS), psOptions.get(), nullptr));
269
0
    if (!poRetDS)
270
0
        return false;
271
272
0
    if (poDstDS == nullptr)
273
0
    {
274
0
        m_outputDataset.Set(std::unique_ptr<GDALDataset>(poRetDS));
275
0
    }
276
0
    else if (dstDSWasNull)
277
0
    {
278
0
        const bool bCloseOK = poDstDS->Close() == CE_None;
279
0
        delete poDstDS;
280
0
        if (!bCloseOK)
281
0
        {
282
0
            CPLError(CE_Failure, CPLE_AppDefined,
283
0
                     "Failed to close output dataset");
284
0
            return false;
285
0
        }
286
0
    }
287
288
0
    return true;
289
0
}
290
291
//! @endcond