Coverage Report

Created: 2026-07-14 06:21

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