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_calc.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  "gdal raster calc" subcommand
5
 * Author:   Daniel Baston
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2025, ISciences LLC
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_raster_calc.h"
14
15
#include "../frmts/vrt/gdal_vrt.h"
16
#include "../frmts/vrt/vrtdataset.h"
17
18
#include "cpl_float.h"
19
#include "cpl_vsi_virtual.h"
20
#include "gdal_priv.h"
21
#include "gdal_utils.h"
22
#include "vrtdataset.h"
23
24
#include <algorithm>
25
#include <cmath>
26
#include <limits>
27
#include <optional>
28
29
//! @cond Doxygen_Suppress
30
31
#ifndef _
32
0
#define _(x) (x)
33
#endif
34
35
constexpr const char *DEFAULT_SOURCE_NAME = "X";
36
constexpr const char *PIPELINE_INPUT_DSN = "";
37
38
struct GDALCalcOptions
39
{
40
    GDALDataType dstType{GDT_Unknown};
41
    bool checkCRS{true};
42
    bool checkExtent{true};
43
};
44
45
static bool MatchIsCompleteVariableNameWithNoIndex(const std::string &str,
46
                                                   size_t from, size_t to)
47
0
{
48
0
    if (to < str.size())
49
0
    {
50
        // If the character after the end of the match is:
51
        // * alphanumeric or _ : we've matched only part of a variable name
52
        // * [ : we've matched a variable that already has an index
53
        // * ( : we've matched a function name
54
0
        if (std::isalnum(str[to]) || str[to] == '_' || str[to] == '[' ||
55
0
            str[to] == '(')
56
0
        {
57
0
            return false;
58
0
        }
59
0
    }
60
0
    if (from > 0)
61
0
    {
62
        // If the character before the start of the match is alphanumeric or _,
63
        // we've matched only part of a variable name.
64
0
        if (std::isalnum(str[from - 1]) || str[from - 1] == '_')
65
0
        {
66
0
            return false;
67
0
        }
68
0
    }
69
70
0
    return true;
71
0
}
72
73
/**
74
 *  Add a band subscript to all instances of a specified variable that
75
 *  do not already have such a subscript. For example, "X" would be
76
 *  replaced with "X[3]" but "X[1]" would be left untouched.
77
 */
78
static std::string SetBandIndices(const std::string &origExpression,
79
                                  const std::string &variable, int band,
80
                                  bool &expressionChanged)
81
0
{
82
0
    std::string expression = origExpression;
83
0
    expressionChanged = false;
84
85
0
    std::string::size_type seekPos = 0;
86
0
    auto pos = expression.find(variable, seekPos);
87
0
    while (pos != std::string::npos)
88
0
    {
89
0
        auto end = pos + variable.size();
90
91
0
        if (MatchIsCompleteVariableNameWithNoIndex(expression, pos, end))
92
0
        {
93
            // No index specified for variable
94
0
            expression = expression.substr(0, pos + variable.size()) + '[' +
95
0
                         std::to_string(band) + ']' + expression.substr(end);
96
0
            expressionChanged = true;
97
0
        }
98
99
0
        seekPos = end;
100
0
        pos = expression.find(variable, seekPos);
101
0
    }
102
103
0
    return expression;
104
0
}
105
106
static bool PosIsAggregateFunctionArgument(const std::string &expression,
107
                                           size_t pos)
108
0
{
109
    // If this position is a function argument, we should be able to
110
    // scan backwards for a ( and find only variable names, literals or commas.
111
0
    while (pos != 0)
112
0
    {
113
0
        const char c = expression[pos];
114
0
        if (c == '(')
115
0
        {
116
0
            pos--;
117
0
            break;
118
0
        }
119
0
        if (!(isspace(c) || isalnum(c) || c == ',' || c == '.' || c == '[' ||
120
0
              c == ']' || c == '_'))
121
0
        {
122
0
            return false;
123
0
        }
124
0
        pos--;
125
0
    }
126
127
    // Now what we've found the (, the preceding characters should be an
128
    // aggregate function name
129
0
    if (pos < 2)
130
0
    {
131
0
        return false;
132
0
    }
133
134
0
    if (STARTS_WITH_CI(expression.c_str() + (pos - 2), "avg") ||
135
0
        STARTS_WITH_CI(expression.c_str() + (pos - 2), "sum") ||
136
0
        STARTS_WITH_CI(expression.c_str() + (pos - 2), "min") ||
137
0
        STARTS_WITH_CI(expression.c_str() + (pos - 2), "max"))
138
0
    {
139
0
        return true;
140
0
    }
141
142
0
    return false;
143
0
}
144
145
/**
146
 *  Replace X by X[1],X[2],...X[n]
147
 */
148
static std::string
149
SetBandIndicesFlattenedExpression(const std::string &origExpression,
150
                                  const std::string &variable, int nBands)
151
0
{
152
0
    std::string expression = origExpression;
153
154
0
    std::string::size_type seekPos = 0;
155
0
    auto pos = expression.find(variable, seekPos);
156
0
    while (pos != std::string::npos)
157
0
    {
158
0
        auto end = pos + variable.size();
159
160
0
        if (MatchIsCompleteVariableNameWithNoIndex(expression, pos, end) &&
161
0
            PosIsAggregateFunctionArgument(expression, pos))
162
0
        {
163
0
            std::string newExpr = expression.substr(0, pos);
164
0
            for (int i = 1; i <= nBands; ++i)
165
0
            {
166
0
                if (i > 1)
167
0
                    newExpr += ',';
168
0
                newExpr += variable;
169
0
                newExpr += '[';
170
0
                newExpr += std::to_string(i);
171
0
                newExpr += ']';
172
0
            }
173
0
            const size_t oldExprSize = expression.size();
174
0
            newExpr += expression.substr(end);
175
0
            expression = std::move(newExpr);
176
0
            end += expression.size() - oldExprSize;
177
0
        }
178
179
0
        seekPos = end;
180
0
        pos = expression.find(variable, seekPos);
181
0
    }
182
183
0
    return expression;
184
0
}
185
186
struct SourceProperties
187
{
188
    int nBands{0};
189
    int nX{0};
190
    int nY{0};
191
    bool hasGT{false};
192
    GDALGeoTransform gt{};
193
    OGRSpatialReferenceRefCountedPtr srs{};
194
    std::vector<std::optional<double>> noData{};
195
    GDALDataType eDT{GDT_Unknown};
196
};
197
198
static std::optional<SourceProperties>
199
UpdateSourceProperties(SourceProperties &out, GDALDataset *ds,
200
                       const GDALCalcOptions &options)
201
0
{
202
0
    SourceProperties source;
203
0
    bool srsMismatch = false;
204
0
    bool extentMismatch = false;
205
0
    bool dimensionMismatch = false;
206
207
0
    {
208
0
        source.nBands = ds->GetRasterCount();
209
0
        source.nX = ds->GetRasterXSize();
210
0
        source.nY = ds->GetRasterYSize();
211
0
        source.noData.resize(source.nBands);
212
213
0
        if (options.checkExtent)
214
0
        {
215
0
            ds->GetGeoTransform(source.gt);
216
0
        }
217
218
0
        if (options.checkCRS && out.srs)
219
0
        {
220
0
            const OGRSpatialReference *srs = ds->GetSpatialRef();
221
0
            srsMismatch = srs && !srs->IsSame(out.srs.get());
222
0
        }
223
224
        // Store the source data type if it is the same for all bands in the source
225
0
        bool bandsHaveSameType = true;
226
0
        for (int i = 1; i <= source.nBands; ++i)
227
0
        {
228
0
            GDALRasterBand *band = ds->GetRasterBand(i);
229
230
0
            if (i == 1)
231
0
            {
232
0
                source.eDT = band->GetRasterDataType();
233
0
            }
234
0
            else if (bandsHaveSameType &&
235
0
                     source.eDT != band->GetRasterDataType())
236
0
            {
237
0
                source.eDT = GDT_Unknown;
238
0
                bandsHaveSameType = false;
239
0
            }
240
241
0
            int success;
242
0
            double noData = band->GetNoDataValue(&success);
243
0
            if (success)
244
0
            {
245
0
                source.noData[i - 1] = noData;
246
0
            }
247
0
        }
248
0
    }
249
250
0
    if (source.nX != out.nX || source.nY != out.nY)
251
0
    {
252
0
        dimensionMismatch = true;
253
0
    }
254
255
0
    if (source.gt.xorig != out.gt.xorig || source.gt.xrot != out.gt.xrot ||
256
0
        source.gt.yorig != out.gt.yorig || source.gt.yrot != out.gt.yrot)
257
0
    {
258
0
        extentMismatch = true;
259
0
    }
260
0
    if (source.gt.xscale != out.gt.xscale || source.gt.yscale != out.gt.yscale)
261
0
    {
262
        // Resolutions are different. Are the extents the same?
263
0
        double xmaxOut =
264
0
            out.gt.xorig + out.nX * out.gt.xscale + out.nY * out.gt.xrot;
265
0
        double yminOut =
266
0
            out.gt.yorig + out.nX * out.gt.yrot + out.nY * out.gt.yscale;
267
268
0
        double xmax = source.gt.xorig + source.nX * source.gt.xscale +
269
0
                      source.nY * source.gt.xrot;
270
0
        double ymin = source.gt.yorig + source.nX * source.gt.yrot +
271
0
                      source.nY * source.gt.yscale;
272
273
        // Max allowable extent misalignment, expressed as fraction of a pixel
274
0
        constexpr double EXTENT_RTOL = 1e-3;
275
276
0
        if (std::abs(xmax - xmaxOut) >
277
0
                EXTENT_RTOL * std::abs(source.gt.xscale) ||
278
0
            std::abs(ymin - yminOut) > EXTENT_RTOL * std::abs(source.gt.yscale))
279
0
        {
280
0
            extentMismatch = true;
281
0
        }
282
0
    }
283
284
0
    if (options.checkExtent && extentMismatch)
285
0
    {
286
0
        CPLError(CE_Failure, CPLE_AppDefined,
287
0
                 "Input extents are inconsistent.");
288
0
        return std::nullopt;
289
0
    }
290
291
0
    if (!options.checkExtent && dimensionMismatch)
292
0
    {
293
0
        CPLError(CE_Failure, CPLE_AppDefined,
294
0
                 "Inputs do not have the same dimensions.");
295
0
        return std::nullopt;
296
0
    }
297
298
    // Find a common resolution
299
0
    if (source.nX > out.nX)
300
0
    {
301
0
        auto dx = CPLGreatestCommonDivisor(out.gt.xscale, source.gt.xscale);
302
0
        if (std::fabs(dx) < std::numeric_limits<double>::min())
303
0
        {
304
0
            CPLError(CE_Failure, CPLE_AppDefined,
305
0
                     "Failed to find common resolution for inputs.");
306
0
            return std::nullopt;
307
0
        }
308
0
        out.nX = static_cast<int>(
309
0
            std::round(static_cast<double>(out.nX) * out.gt.xscale / dx));
310
0
        out.gt.xscale = dx;
311
0
    }
312
0
    if (source.nY > out.nY)
313
0
    {
314
0
        auto dy = CPLGreatestCommonDivisor(out.gt.yscale, source.gt.yscale);
315
0
        if (std::fabs(dy) < std::numeric_limits<double>::min())
316
0
        {
317
0
            CPLError(CE_Failure, CPLE_AppDefined,
318
0
                     "Failed to find common resolution for inputs.");
319
0
            return std::nullopt;
320
0
        }
321
0
        out.nY = static_cast<int>(
322
0
            std::round(static_cast<double>(out.nY) * out.gt.yscale / dy));
323
0
        out.gt.yscale = dy;
324
0
    }
325
326
0
    if (srsMismatch)
327
0
    {
328
0
        CPLError(CE_Failure, CPLE_AppDefined,
329
0
                 "Input spatial reference systems are inconsistent.");
330
0
        return std::nullopt;
331
0
    }
332
333
0
    return source;
334
0
}
335
336
/** Add one or more derived bands to a VRTDataset, representing the evaluation
337
 *  of a single expression
338
 *
339
 * @param poDS VRT dataset
340
 * @param bandType the type of the band(s) to create
341
 * @param expression Expression for which band(s) should be added
342
 * @param dialect Expression dialect
343
 * @param flatten Generate a single band output raster per expression, even if
344
 *                input datasets are multiband.
345
 * @param noDataText nodata value to use for the created band, or "none", or ""
346
 * @param pixelFunctionArguments Pixel function arguments.
347
 * @param sources Mapping of source names to DSNs
348
 * @param sourceProps Mapping of source names to properties
349
 * @param fakeSourceFilename If not empty, used instead of real input filenames.
350
 * @param pipelineInputSource A pointer to a dataset representing pipeline input.
351
 * @return true if the band(s) were added, false otherwise
352
 */
353
static bool CreateVRTDerivedBand(
354
    VRTDataset *poDS, GDALDataType bandType, const std::string &expression,
355
    const std::string &dialect, bool flatten, const std::string &noDataText,
356
    const std::vector<std::string> &pixelFunctionArguments,
357
    const std::map<std::string, std::string> &sources,
358
    const std::map<std::string, SourceProperties> &sourceProps,
359
    const std::string &fakeSourceFilename, GDALDataset *pipelineInputSource)
360
0
{
361
0
    const char *pszVRTFilename = poDS->GetDescription();
362
363
0
    const int nPrevBands = poDS->GetRasterCount();
364
0
    const int nXOut = poDS->GetRasterXSize();
365
0
    const int nYOut = poDS->GetRasterYSize();
366
367
0
    CPLStringList papszBandArgs;
368
0
    papszBandArgs.SetNameValue("subclass", "VRTDerivedRasterBand");
369
370
0
    int nOutBands = 1;  // By default, each expression produces a single output
371
                        // band. When processing the expression below, we may
372
                        // discover that the expression produces multiple bands,
373
                        // in which case this will be updated.
374
375
0
    for (int nOutBand = 1; nOutBand <= nOutBands; nOutBand++)
376
0
    {
377
        // Copy the expression for each output band, because we may modify it
378
        // when adding band indices (e.g., X -> X[1]) to the variables in the
379
        // expression.
380
0
        std::string bandExpression = expression;
381
382
0
        if (poDS->AddBand(bandType == GDT_Unknown ? GDT_Float64 : bandType,
383
0
                          papszBandArgs) != CE_None)
384
0
        {
385
0
            return false;
386
0
        }
387
0
        VRTDerivedRasterBand *poBand = cpl::down_cast<VRTDerivedRasterBand *>(
388
0
            poDS->GetRasterBand(nPrevBands + nOutBand));
389
390
0
        std::optional<double> dstNoData;
391
0
        bool autoSelectNoDataValue = false;
392
0
        if (noDataText.empty())
393
0
        {
394
0
            autoSelectNoDataValue = true;
395
0
        }
396
0
        else if (noDataText != "none")
397
0
        {
398
0
            if (auto parsed = cpl::strict_parse<double>(noDataText);
399
0
                parsed.has_value())
400
0
            {
401
0
                dstNoData = parsed.value();
402
0
            }
403
0
            else
404
0
            {
405
0
                CPLError(CE_Failure, CPLE_AppDefined,
406
0
                         "Invalid NoData value: %s", noDataText.c_str());
407
0
                return false;
408
0
            }
409
0
        }
410
411
0
        for (const auto &[source_name, dsn] : sources)
412
0
        {
413
0
            auto it = sourceProps.find(source_name);
414
0
            CPLAssert(it != sourceProps.end());
415
0
            const auto &props = it->second;
416
417
0
            bool expressionAppliedPerBand = false;
418
0
            if (dialect == "builtin")
419
0
            {
420
0
                expressionAppliedPerBand = !flatten;
421
0
            }
422
0
            else
423
0
            {
424
0
                const int nDefaultInBand = std::min(props.nBands, nOutBand);
425
426
0
                if (flatten)
427
0
                {
428
0
                    bandExpression = SetBandIndicesFlattenedExpression(
429
0
                        bandExpression, source_name, props.nBands);
430
0
                }
431
432
0
                bandExpression =
433
0
                    SetBandIndices(bandExpression, source_name, nDefaultInBand,
434
0
                                   expressionAppliedPerBand);
435
0
            }
436
437
0
            if (expressionAppliedPerBand)
438
0
            {
439
0
                if (nOutBands <= 1)
440
0
                {
441
0
                    nOutBands = props.nBands;
442
0
                }
443
0
                else if (props.nBands != 1 && props.nBands != nOutBands)
444
0
                {
445
0
                    CPLError(CE_Failure, CPLE_AppDefined,
446
0
                             "Expression cannot operate on all bands of "
447
0
                             "rasters with incompatible numbers of bands "
448
0
                             "(source %s has %d bands but expected to have "
449
0
                             "1 or %d bands).",
450
0
                             source_name.c_str(), props.nBands, nOutBands);
451
0
                    return false;
452
0
                }
453
0
            }
454
455
            // Create a source for each input band that is used in
456
            // the expression.
457
0
            for (int nInBand = 1; nInBand <= props.nBands; nInBand++)
458
0
            {
459
0
                CPLString inBandVariable;
460
0
                if (dialect == "builtin")
461
0
                {
462
0
                    if (!flatten && props.nBands >= 2 && nInBand != nOutBand)
463
0
                        continue;
464
0
                }
465
0
                else
466
0
                {
467
0
                    inBandVariable.Printf("%s[%d]", source_name.c_str(),
468
0
                                          nInBand);
469
0
                    if (bandExpression.find(inBandVariable) ==
470
0
                        std::string::npos)
471
0
                    {
472
0
                        continue;
473
0
                    }
474
0
                }
475
476
0
                const std::optional<double> &srcNoData =
477
0
                    props.noData[nInBand - 1];
478
479
0
                std::unique_ptr<VRTSimpleSource> poSource;
480
0
                if (srcNoData.has_value())
481
0
                {
482
0
                    poSource = std::make_unique<VRTComplexSource>();
483
0
                }
484
0
                else
485
0
                {
486
0
                    poSource = std::make_unique<VRTSimpleSource>();
487
0
                }
488
489
0
                if (!inBandVariable.empty())
490
0
                {
491
0
                    poSource->SetName(inBandVariable);
492
0
                }
493
494
0
                if (fakeSourceFilename.empty())
495
0
                {
496
0
                    if (dsn == PIPELINE_INPUT_DSN)
497
0
                    {
498
0
                        CPLAssertNotNull(pipelineInputSource);
499
0
                        pipelineInputSource->Reference();
500
0
                        poSource->SetSrcBand(
501
0
                            pipelineInputSource->GetRasterBand(nInBand));
502
0
                    }
503
0
                    else
504
0
                    {
505
0
                        std::string osSourceFilename = dsn;
506
0
                        bool bRelativeToVRT = false;
507
0
                        if (pszVRTFilename[0])
508
0
                        {
509
0
                            std::tie(osSourceFilename, bRelativeToVRT) =
510
0
                                VRTSimpleSource::
511
0
                                    ComputeSourceNameAndRelativeFlag(
512
0
                                        CPLGetPathSafe(pszVRTFilename).c_str(),
513
0
                                        dsn);
514
0
                        }
515
0
                        poSource->SetSrcBand(osSourceFilename.c_str(), nInBand);
516
0
                    }
517
0
                }
518
0
                else
519
0
                {
520
0
                    poSource->SetSrcBand(fakeSourceFilename.c_str(), nInBand);
521
0
                }
522
523
0
                if (srcNoData.has_value())
524
0
                {
525
0
                    cpl::down_cast<VRTComplexSource *>(poSource.get())
526
0
                        ->SetNoDataValue(srcNoData.value());
527
528
0
                    if (autoSelectNoDataValue && !dstNoData.has_value())
529
0
                    {
530
0
                        dstNoData = srcNoData;
531
0
                    }
532
0
                }
533
534
0
                if (fakeSourceFilename.empty())
535
0
                {
536
0
                    poSource->SetSrcWindow(0, 0, props.nX, props.nY);
537
0
                    poSource->SetDstWindow(0, 0, nXOut, nYOut);
538
0
                }
539
540
0
                poBand->AddSource(std::move(poSource));
541
0
            }
542
543
0
            if (dstNoData.has_value())
544
0
            {
545
0
                if (!GDALIsValueExactAs(dstNoData.value(), bandType))
546
0
                {
547
0
                    CPLError(
548
0
                        CE_Failure, CPLE_AppDefined,
549
0
                        "Band output type %s cannot represent NoData value %g",
550
0
                        GDALGetDataTypeName(bandType), dstNoData.value());
551
0
                    return false;
552
0
                }
553
554
0
                poBand->SetNoDataValue(dstNoData.value());
555
0
            }
556
0
        }
557
558
0
        if (dialect == "builtin")
559
0
        {
560
0
            poBand->SetPixelFunctionName(expression.c_str());
561
0
        }
562
0
        else
563
0
        {
564
0
            poBand->SetPixelFunctionName("expression");
565
0
            poBand->AddPixelFunctionArgument("dialect", "muparser");
566
            // Add the expression as a last step, because we may modify the
567
            // expression as we iterate through the bands.
568
0
            poBand->AddPixelFunctionArgument("expression",
569
0
                                             bandExpression.c_str());
570
0
        }
571
572
0
        if (!pixelFunctionArguments.empty())
573
0
        {
574
0
            const CPLStringList args(pixelFunctionArguments);
575
0
            for (const auto &[key, value] : cpl::IterateNameValue(args))
576
0
            {
577
0
                poBand->AddPixelFunctionArgument(key, value);
578
0
            }
579
0
        }
580
0
    }
581
582
0
    return true;
583
0
}
584
585
static bool ParseSourceDescriptors(const std::vector<std::string> &inputs,
586
                                   std::map<std::string, std::string> &datasets,
587
                                   std::string &firstSourceName,
588
                                   bool requireSourceNames)
589
0
{
590
0
    for (size_t iInput = 0; iInput < inputs.size(); iInput++)
591
0
    {
592
0
        const std::string &input = inputs[iInput];
593
0
        std::string name;
594
595
0
        const auto pos = input.find('=');
596
0
        if (pos == std::string::npos)
597
0
        {
598
0
            if (requireSourceNames && inputs.size() > 1)
599
0
            {
600
0
                CPLError(CE_Failure, CPLE_AppDefined,
601
0
                         "Inputs must be named when more than one input is "
602
0
                         "provided.");
603
0
                return false;
604
0
            }
605
0
            name = DEFAULT_SOURCE_NAME;
606
0
            if (iInput > 0)
607
0
            {
608
0
                name += std::to_string(iInput);
609
0
            }
610
0
        }
611
0
        else
612
0
        {
613
0
            name = input.substr(0, pos);
614
0
        }
615
616
        // Check input name is legal
617
0
        for (size_t i = 0; i < name.size(); ++i)
618
0
        {
619
0
            const char c = name[i];
620
0
            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
621
0
            {
622
                // ok
623
0
            }
624
0
            else if (c == '_' || (c >= '0' && c <= '9'))
625
0
            {
626
0
                if (i == 0)
627
0
                {
628
                    // Reserved constants in MuParser start with an underscore
629
0
                    CPLError(
630
0
                        CE_Failure, CPLE_AppDefined,
631
0
                        "Name '%s' is illegal because it starts with a '%c'",
632
0
                        name.c_str(), c);
633
0
                    return false;
634
0
                }
635
0
            }
636
0
            else
637
0
            {
638
0
                CPLError(CE_Failure, CPLE_AppDefined,
639
0
                         "Name '%s' is illegal because character '%c' is not "
640
0
                         "allowed",
641
0
                         name.c_str(), c);
642
0
                return false;
643
0
            }
644
0
        }
645
646
0
        std::string dsn =
647
0
            (pos == std::string::npos) ? input : input.substr(pos + 1);
648
649
0
        if (!dsn.empty() && dsn.front() == '[' && dsn.back() == ']')
650
0
        {
651
0
            dsn = "{\"type\":\"gdal_streamed_alg\", \"command_line\":\"gdal "
652
0
                  "raster pipeline " +
653
0
                  CPLString(dsn.substr(1, dsn.size() - 2))
654
0
                      .replaceAll('\\', "\\\\")
655
0
                      .replaceAll('"', "\\\"") +
656
0
                  "\"}";
657
0
        }
658
659
0
        if (datasets.find(name) != datasets.end())
660
0
        {
661
0
            CPLError(CE_Failure, CPLE_AppDefined,
662
0
                     "An input with name '%s' has already been provided",
663
0
                     name.c_str());
664
0
            return false;
665
0
        }
666
0
        datasets[name] = std::move(dsn);
667
668
0
        if (iInput == 0)
669
0
        {
670
0
            firstSourceName = std::move(name);
671
0
        }
672
0
    }
673
674
0
    return true;
675
0
}
676
677
static bool ReadFileLists(const std::vector<GDALArgDatasetValue> &inputDS,
678
                          std::vector<std::string> &inputFilenames)
679
0
{
680
0
    for (const auto &dsVal : inputDS)
681
0
    {
682
0
        const auto &input = dsVal.GetName();
683
0
        if (!input.empty() && input[0] == '@')
684
0
        {
685
0
            auto f =
686
0
                VSIVirtualHandleUniquePtr(VSIFOpenL(input.c_str() + 1, "r"));
687
0
            if (!f)
688
0
            {
689
0
                CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
690
0
                         input.c_str() + 1);
691
0
                return false;
692
0
            }
693
0
            while (const char *filename = CPLReadLineL(f.get()))
694
0
            {
695
0
                inputFilenames.push_back(filename);
696
0
            }
697
0
        }
698
0
        else
699
0
        {
700
0
            inputFilenames.push_back(input);
701
0
        }
702
0
    }
703
704
0
    return true;
705
0
}
706
707
/** Creates a VRT dataset with one or more derived raster bands containing
708
 *  results of an expression.
709
 *
710
 * To make this work with muparser (which does not support vector types), we
711
 * do a simple parsing of the expression internally, transforming it into
712
 * multiple expressions with explicit band indices. For example, for a two-band
713
 * raster "X", the expression "X + 3" will be transformed into "X[1] + 3" and
714
 * "X[2] + 3". The use of brackets is for readability only; as far as the
715
 * expression engine is concerned, the variables "X[1]" and "X[2]" have nothing
716
 * to do with each other.
717
 *
718
 * @param inputs Either:
719
 *               - a list of sources, expressed as NAME=DSN
720
 *               - pointer to a single opened dataset
721
 * @param expressions A list of expressions to be evaluated
722
 * @param dialect Expression dialect
723
 * @param flatten Generate a single band output raster per expression, even if
724
 *                input datasets are multiband.
725
 * @param noData NoData values to use for output bands, or "none", or ""
726
 * @param pixelFunctionArguments Pixel function arguments.
727
 * @param options flags controlling which checks should be performed on the inputs
728
 * @param[out] maxSourceBands Maximum number of bands in source dataset(s)
729
 * @param fakeSourceFilename If not empty, used instead of real input filenames.
730
 *
731
 * @return a newly created VRTDataset, or nullptr on error
732
 */
733
static std::unique_ptr<GDALDataset> GDALCalcCreateVRTDerived(
734
    std::variant<GDALDataset *, const std::vector<std::string> *> inputs,
735
    const std::vector<std::string> &expressions, const std::string &dialect,
736
    bool flatten, const std::string &noData,
737
    const std::vector<std::vector<std::string>> &pixelFunctionArguments,
738
    const GDALCalcOptions &options, int &maxSourceBands,
739
    const std::string &fakeSourceFilename = std::string())
740
0
{
741
0
    std::map<std::string, std::string> sources;
742
0
    std::map<std::string, SourceProperties> sourceProps;
743
0
    GDALDataset *pipelineInputDS = std::holds_alternative<GDALDataset *>(inputs)
744
0
                                       ? std::get<GDALDataset *>(inputs)
745
0
                                       : nullptr;
746
747
0
    maxSourceBands = 0;
748
749
    // Read properties from the first source
750
0
    SourceProperties out;
751
0
    {
752
0
        std::unique_ptr<GDALDataset> poTmpDS;
753
0
        const GDALDataset *poTemplateDS;
754
755
0
        if (pipelineInputDS)
756
0
        {
757
0
            poTemplateDS = pipelineInputDS;
758
0
        }
759
0
        else
760
0
        {
761
0
            const std::vector<std::string> &sourceDescriptors =
762
0
                *std::get<const std::vector<std::string> *>(inputs);
763
764
0
            if (sourceDescriptors.empty())
765
0
            {
766
0
                return nullptr;
767
0
            }
768
769
0
            const bool requireSourceNames = dialect != "builtin";
770
771
0
            std::string firstSource;
772
0
            if (!ParseSourceDescriptors(sourceDescriptors, sources, firstSource,
773
0
                                        requireSourceNames))
774
0
            {
775
0
                return nullptr;
776
0
            }
777
778
            // Use the first source provided to determine properties of the output
779
0
            const char *firstDSN = sources[firstSource].c_str();
780
781
0
            poTmpDS.reset(GDALDataset::Open(firstDSN, GDAL_OF_RASTER));
782
0
            if (!poTmpDS)
783
0
            {
784
0
                CPLError(CE_Failure, CPLE_AppDefined, "Failed to open %s",
785
0
                         firstDSN);
786
0
                return nullptr;
787
0
            }
788
0
            poTemplateDS = poTmpDS.get();
789
0
        }
790
791
0
        out.nX = poTemplateDS->GetRasterXSize();
792
0
        out.nY = poTemplateDS->GetRasterYSize();
793
0
        out.nBands = 1;
794
0
        out.srs = OGRSpatialReferenceRefCountedPtr::makeClone(
795
0
            poTemplateDS->GetSpatialRef());
796
0
        out.hasGT = poTemplateDS->GetGeoTransform(out.gt) == CE_None;
797
798
0
        maxSourceBands = 0;
799
800
0
        if (pipelineInputDS)
801
0
        {
802
0
            sources[DEFAULT_SOURCE_NAME] = PIPELINE_INPUT_DSN;
803
0
            if (auto props =
804
0
                    UpdateSourceProperties(out, pipelineInputDS, options))
805
0
            {
806
0
                sourceProps[DEFAULT_SOURCE_NAME] = props.value();
807
0
                maxSourceBands = props.value().nBands;
808
0
            }
809
0
            else
810
0
            {
811
0
                return nullptr;  // error message emitted from UpdateSourceProperties
812
0
            }
813
0
        }
814
0
        else
815
0
        {
816
            // Collect properties of the different sources, and verify them for
817
            // consistency.
818
0
            for (const auto &[source_name, dsn] : sources)
819
0
            {
820
                // TODO avoid opening the first source twice.
821
0
                std::unique_ptr<GDALDataset> ds(
822
0
                    GDALDataset::Open(dsn.c_str(), GDAL_OF_RASTER));
823
824
0
                if (!ds)
825
0
                {
826
0
                    CPLError(CE_Failure, CPLE_AppDefined, "Failed to open %s",
827
0
                             dsn.c_str());
828
0
                    return nullptr;
829
0
                }
830
831
0
                auto props = UpdateSourceProperties(out, ds.get(), options);
832
0
                if (props.has_value())
833
0
                {
834
0
                    maxSourceBands = std::max(maxSourceBands, props->nBands);
835
0
                    sourceProps[source_name] = std::move(props.value());
836
0
                }
837
0
                else
838
0
                {
839
0
                    return nullptr;  // error message emitted from UpdateSourceProperties
840
0
                }
841
0
            }
842
0
        }
843
0
    }
844
845
0
    size_t iExpr = 0;
846
847
0
    auto poDS = VRTDataset::CreateVRTDataset("", out.nX, out.nY, 0,
848
0
                                             options.dstType, nullptr);
849
850
0
    for (const auto &origExpression : expressions)
851
0
    {
852
0
        GDALDataType bandType = options.dstType;
853
854
        // If output band type has not been specified, set it equal to the
855
        // input band type for certain pixel functions, if the inputs have
856
        // a consistent band type.
857
0
        if (bandType == GDT_Unknown && dialect == "builtin" &&
858
0
            (origExpression == "min" || origExpression == "max" ||
859
0
             origExpression == "mode"))
860
0
        {
861
0
            for (const auto &[_, props] : sourceProps)
862
0
            {
863
0
                if (bandType == GDT_Unknown)
864
0
                {
865
0
                    bandType = props.eDT;
866
0
                }
867
0
                else if (props.eDT == GDT_Unknown || props.eDT != bandType)
868
0
                {
869
0
                    bandType = GDT_Unknown;
870
0
                    break;
871
0
                }
872
0
            }
873
0
        }
874
875
0
        if (!CreateVRTDerivedBand(
876
0
                poDS.get(), bandType, origExpression, dialect, flatten, noData,
877
0
                pixelFunctionArguments[iExpr], sources, sourceProps,
878
0
                fakeSourceFilename, pipelineInputDS))
879
0
        {
880
0
            return nullptr;
881
0
        }
882
0
        ++iExpr;
883
0
    }
884
885
0
    if (out.hasGT)
886
0
    {
887
0
        poDS->SetGeoTransform(out.gt);
888
0
    }
889
0
    if (out.srs)
890
0
    {
891
0
        poDS->SetSpatialRef(out.srs.get());
892
0
    }
893
894
0
    return poDS;
895
0
}
896
897
/************************************************************************/
898
/*          GDALRasterCalcAlgorithm::GDALRasterCalcAlgorithm()          */
899
/************************************************************************/
900
901
GDALRasterCalcAlgorithm::GDALRasterCalcAlgorithm(bool standaloneStep) noexcept
902
0
    : GDALRasterPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
903
0
                                      ConstructorOptions()
904
0
                                          .SetStandaloneStep(standaloneStep)
905
0
                                          .SetAddDefaultArguments(false)
906
0
                                          .SetAutoOpenInputDatasets(false)
907
0
                                          .SetInputDatasetMetaVar("INPUTS")
908
0
                                          .SetInputDatasetMaxCount(INT_MAX))
909
0
{
910
0
    AddRasterInputArgs(false, false);
911
0
    if (standaloneStep)
912
0
    {
913
0
        AddProgressArg();
914
0
        AddRasterOutputArgs(false);
915
0
    }
916
917
0
    AddOutputDataTypeArg(&m_type);
918
919
0
    AddArg("no-check-crs", 0,
920
0
           _("Do not check consistency of input coordinate reference systems"),
921
0
           &m_noCheckCRS)
922
0
        .AddHiddenAlias("no-check-srs");
923
0
    AddArg("no-check-extent", 0, _("Do not check consistency of input extents"),
924
0
           &m_noCheckExtent);
925
926
0
    AddArg("propagate-nodata", 0,
927
0
           _("Whether to set pixels to the output NoData value if any of the "
928
0
             "input pixels is NoData"),
929
0
           &m_propagateNoData);
930
931
0
    AddArg("calc", 0, _("Expression(s) to evaluate"), &m_expr)
932
0
        .SetRequired()
933
0
        .SetPackedValuesAllowed(false)
934
0
        .SetMinCount(1)
935
0
        .SetAutoCompleteFunction(
936
0
            [this](const std::string &currentValue)
937
0
            {
938
0
                std::vector<std::string> ret;
939
0
                if (m_dialect == "builtin")
940
0
                {
941
0
                    if (currentValue.find('(') == std::string::npos)
942
0
                        return VRTDerivedRasterBand::GetPixelFunctionNames();
943
0
                }
944
0
                return ret;
945
0
            });
946
947
0
    AddArg("dialect", 0, _("Expression dialect"), &m_dialect)
948
0
        .SetDefault(m_dialect)
949
0
        .SetChoices("muparser", "builtin");
950
951
0
    AddArg("flatten", 0,
952
0
           _("Generate a single band output raster per expression, even if "
953
0
             "input datasets are multiband"),
954
0
           &m_flatten);
955
956
0
    AddNodataArg(&m_nodata, true);
957
958
    // This is a hidden option only used by test_gdalalg_raster_calc_expression_rewriting()
959
    // for now
960
0
    AddArg("no-check-expression", 0,
961
0
           _("Whether to skip expression validity checks for virtual format "
962
0
             "output"),
963
0
           &m_noCheckExpression)
964
0
        .SetHidden();
965
966
0
    AddValidationAction(
967
0
        [this]()
968
0
        {
969
0
            GDALPipelineStepRunContext ctxt;
970
0
            return m_noCheckExpression || !IsGDALGOutput() || RunStep(ctxt);
971
0
        });
972
0
}
973
974
/************************************************************************/
975
/*                  GDALRasterCalcAlgorithm::RunImpl()                  */
976
/************************************************************************/
977
978
bool GDALRasterCalcAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
979
                                      void *pProgressData)
980
0
{
981
0
    GDALPipelineStepRunContext stepCtxt;
982
0
    stepCtxt.m_pfnProgress = pfnProgress;
983
0
    stepCtxt.m_pProgressData = pProgressData;
984
0
    return RunPreStepPipelineValidations() && RunStep(stepCtxt);
985
0
}
986
987
/************************************************************************/
988
/*                  GDALRasterCalcAlgorithm::RunStep()                  */
989
/************************************************************************/
990
991
bool GDALRasterCalcAlgorithm::RunStep(GDALPipelineStepRunContext &ctxt)
992
0
{
993
0
    CPLAssert(!m_outputDataset.GetDatasetRef());
994
995
0
    GDALCalcOptions options;
996
0
    options.checkExtent = !m_noCheckExtent;
997
0
    options.checkCRS = !m_noCheckCRS;
998
0
    if (!m_type.empty())
999
0
    {
1000
0
        options.dstType = GDALGetDataTypeByName(m_type.c_str());
1001
0
    }
1002
1003
0
    GDALDataset *poPipelineInput = nullptr;
1004
0
    std::vector<std::string> inputFilenames;
1005
0
    if (m_inputDataset.size() == 1 && m_inputDataset[0].GetDatasetRef())
1006
0
    {
1007
0
        poPipelineInput = m_inputDataset[0].GetDatasetRef();
1008
0
    }
1009
0
    else
1010
0
    {
1011
0
        if (!ReadFileLists(m_inputDataset, inputFilenames))
1012
0
        {
1013
0
            return false;
1014
0
        }
1015
0
    }
1016
1017
0
    std::vector<std::vector<std::string>> pixelFunctionArgs;
1018
0
    if (m_dialect == "builtin")
1019
0
    {
1020
0
        for (std::string &expr : m_expr)
1021
0
        {
1022
0
            const CPLStringList aosTokens(
1023
0
                CSLTokenizeString2(expr.c_str(), "()",
1024
0
                                   CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
1025
0
            const char *pszFunction = aosTokens[0];
1026
0
            const auto *pair =
1027
0
                VRTDerivedRasterBand::GetPixelFunction(pszFunction);
1028
0
            if (!pair)
1029
0
            {
1030
0
                ReportError(CE_Failure, CPLE_NotSupported,
1031
0
                            "'%s' is an unknown builtin function", pszFunction);
1032
0
                return false;
1033
0
            }
1034
0
            if (aosTokens.size() == 2)
1035
0
            {
1036
0
                std::vector<std::string> validArguments;
1037
0
                AddOptionsSuggestions(pair->second.c_str(), 0, std::string(),
1038
0
                                      validArguments);
1039
0
                for (std::string &s : validArguments)
1040
0
                {
1041
0
                    if (!s.empty() && s.back() == '=')
1042
0
                        s.pop_back();
1043
0
                }
1044
1045
0
                const CPLStringList aosTokensArgs(CSLTokenizeString2(
1046
0
                    aosTokens[1], ",",
1047
0
                    CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
1048
0
                for (const auto &[key, value] :
1049
0
                     cpl::IterateNameValue(aosTokensArgs))
1050
0
                {
1051
0
                    if (std::find(validArguments.begin(), validArguments.end(),
1052
0
                                  key) == validArguments.end())
1053
0
                    {
1054
0
                        if (validArguments.empty())
1055
0
                        {
1056
0
                            ReportError(
1057
0
                                CE_Failure, CPLE_IllegalArg,
1058
0
                                "'%s' is a unrecognized argument for builtin "
1059
0
                                "function '%s'. It does not accept any "
1060
0
                                "argument",
1061
0
                                key, pszFunction);
1062
0
                        }
1063
0
                        else
1064
0
                        {
1065
0
                            std::string validArgumentsStr;
1066
0
                            for (const std::string &s : validArguments)
1067
0
                            {
1068
0
                                if (!validArgumentsStr.empty())
1069
0
                                    validArgumentsStr += ", ";
1070
0
                                validArgumentsStr += '\'';
1071
0
                                validArgumentsStr += s;
1072
0
                                validArgumentsStr += '\'';
1073
0
                            }
1074
0
                            ReportError(
1075
0
                                CE_Failure, CPLE_IllegalArg,
1076
0
                                "'%s' is a unrecognized argument for builtin "
1077
0
                                "function '%s'. Only %s %s supported",
1078
0
                                key, pszFunction,
1079
0
                                validArguments.size() == 1 ? "is" : "are",
1080
0
                                validArgumentsStr.c_str());
1081
0
                        }
1082
0
                        return false;
1083
0
                    }
1084
0
                    CPL_IGNORE_RET_VAL(value);
1085
0
                }
1086
0
                pixelFunctionArgs.emplace_back(aosTokensArgs);
1087
0
            }
1088
0
            else
1089
0
            {
1090
0
                pixelFunctionArgs.push_back(std::vector<std::string>());
1091
0
            }
1092
0
            expr = pszFunction;
1093
0
        }
1094
0
    }
1095
0
    else
1096
0
    {
1097
0
        pixelFunctionArgs.resize(m_expr.size());
1098
0
    }
1099
1100
0
    if (m_propagateNoData)
1101
0
    {
1102
0
        if (m_nodata == "none")
1103
0
        {
1104
0
            ReportError(CE_Failure, CPLE_AppDefined,
1105
0
                        "Output NoData value must be specified to use "
1106
0
                        "--propagate-nodata");
1107
0
            return false;
1108
0
        }
1109
0
        for (auto &args : pixelFunctionArgs)
1110
0
        {
1111
0
            args.push_back("propagateNoData=1");
1112
0
        }
1113
0
    }
1114
1115
0
    int maxSourceBands = 0;
1116
0
    const bool bIsVRT =
1117
0
        m_format == "VRT" ||
1118
0
        (m_format.empty() &&
1119
0
         EQUAL(CPLGetExtensionSafe(m_outputDataset.GetName().c_str()).c_str(),
1120
0
               "VRT"));
1121
1122
0
    std::variant<GDALDataset *, const std::vector<std::string> *> inputs;
1123
0
    if (poPipelineInput)
1124
0
    {
1125
0
        inputs = poPipelineInput;
1126
0
    }
1127
0
    else
1128
0
    {
1129
0
        inputs = &inputFilenames;
1130
0
    }
1131
1132
0
    auto vrt =
1133
0
        GDALCalcCreateVRTDerived(inputs, m_expr, m_dialect, m_flatten, m_nodata,
1134
0
                                 pixelFunctionArgs, options, maxSourceBands);
1135
0
    if (vrt == nullptr)
1136
0
    {
1137
0
        return false;
1138
0
    }
1139
1140
0
    if (!m_noCheckExpression)
1141
0
    {
1142
0
        const bool bIsGDALG =
1143
0
            m_format == "GDALG" ||
1144
0
            (m_format.empty() &&
1145
0
             cpl::ends_with(m_outputDataset.GetName(), ".gdalg.json"));
1146
0
        if (!m_standaloneStep || m_format == "stream" || bIsVRT || bIsGDALG)
1147
0
        {
1148
            // Try reading a single pixel to check formulas are valid.
1149
0
            std::vector<GByte> dummyData(vrt->GetRasterCount());
1150
1151
0
            auto poGTIFFDrv = GetGDALDriverManager()->GetDriverByName("GTiff");
1152
0
            std::string osTmpFilename;
1153
0
            if (poGTIFFDrv)
1154
0
            {
1155
0
                std::string osFilename =
1156
0
                    VSIMemGenerateHiddenFilename("tmp.tif");
1157
0
                auto poDS = std::unique_ptr<GDALDataset>(
1158
0
                    poGTIFFDrv->Create(osFilename.c_str(), 1, 1, maxSourceBands,
1159
0
                                       GDT_UInt8, nullptr));
1160
0
                if (poDS)
1161
0
                    osTmpFilename = std::move(osFilename);
1162
0
            }
1163
0
            if (!osTmpFilename.empty())
1164
0
            {
1165
0
                auto fakeVRT = GDALCalcCreateVRTDerived(
1166
0
                    &inputFilenames, m_expr, m_dialect, m_flatten, m_nodata,
1167
0
                    pixelFunctionArgs, options, maxSourceBands, osTmpFilename);
1168
0
                if (fakeVRT &&
1169
0
                    fakeVRT->RasterIO(GF_Read, 0, 0, 1, 1, dummyData.data(), 1,
1170
0
                                      1, GDT_UInt8, vrt->GetRasterCount(),
1171
0
                                      nullptr, 0, 0, 0, nullptr) != CE_None)
1172
0
                {
1173
0
                    return false;
1174
0
                }
1175
0
            }
1176
0
            if (bIsGDALG)
1177
0
            {
1178
0
                return true;
1179
0
            }
1180
0
        }
1181
0
    }
1182
1183
0
    if (m_format == "stream" || !m_standaloneStep)
1184
0
    {
1185
0
        m_outputDataset.Set(std::move(vrt));
1186
0
        return true;
1187
0
    }
1188
1189
0
    CPLStringList translateArgs;
1190
0
    if (!m_format.empty())
1191
0
    {
1192
0
        translateArgs.AddString("-of");
1193
0
        translateArgs.AddString(m_format.c_str());
1194
0
    }
1195
0
    for (const auto &co : m_creationOptions)
1196
0
    {
1197
0
        translateArgs.AddString("-co");
1198
0
        translateArgs.AddString(co.c_str());
1199
0
    }
1200
1201
0
    bool bOK = false;
1202
0
    GDALTranslateOptions *translateOptions =
1203
0
        GDALTranslateOptionsNew(translateArgs.List(), nullptr);
1204
0
    if (translateOptions)
1205
0
    {
1206
0
        GDALTranslateOptionsSetProgress(translateOptions, ctxt.m_pfnProgress,
1207
0
                                        ctxt.m_pProgressData);
1208
1209
0
        auto poOutDS =
1210
0
            std::unique_ptr<GDALDataset>(GDALDataset::FromHandle(GDALTranslate(
1211
0
                m_outputDataset.GetName().c_str(),
1212
0
                GDALDataset::ToHandle(vrt.get()), translateOptions, nullptr)));
1213
0
        GDALTranslateOptionsFree(translateOptions);
1214
1215
0
        bOK = poOutDS != nullptr;
1216
0
        m_outputDataset.Set(std::move(poOutDS));
1217
0
    }
1218
1219
0
    return bOK;
1220
0
}
1221
1222
0
GDALRasterCalcAlgorithmStandalone::~GDALRasterCalcAlgorithmStandalone() =
1223
    default;
1224
1225
//! @endcond