Coverage Report

Created: 2026-09-14 07:15

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libavif/src/gainmap.c
Line
Count
Source
1
// Copyright 2023 Google LLC
2
// SPDX-License-Identifier: BSD-2-Clause
3
4
#include "avif/internal.h"
5
#include <assert.h>
6
#include <float.h>
7
#include <math.h>
8
#include <string.h>
9
10
// NaN-safe clamp to [0, 1]. AVIF_CLAMP passes NaN through because IEEE 754
11
// comparisons with NaN always return false. fmaxf/fminf return the non-NaN
12
// argument per C99 ยง7.12.12, so this clamps NaN to 0.
13
static float avifNanSafeClamp(float val)
14
0
{
15
0
    return fminf(1.0f, fmaxf(0.0f, val));
16
0
}
17
18
static void avifGainMapSetEncodingDefaults(avifGainMap * gainMap)
19
0
{
20
0
    for (int i = 0; i < 3; ++i) {
21
0
        gainMap->gainMapMin[i] = (avifSignedFraction) { 1, 1 };
22
0
        gainMap->gainMapMax[i] = (avifSignedFraction) { 1, 1 };
23
0
        gainMap->baseOffset[i] = (avifSignedFraction) { 1, 64 };
24
0
        gainMap->alternateOffset[i] = (avifSignedFraction) { 1, 64 };
25
0
        gainMap->gainMapGamma[i] = (avifUnsignedFraction) { 1, 1 };
26
0
    }
27
0
    gainMap->baseHdrHeadroom = (avifUnsignedFraction) { 0, 1 };
28
0
    gainMap->alternateHdrHeadroom = (avifUnsignedFraction) { 1, 1 };
29
0
    gainMap->useBaseColorSpace = AVIF_TRUE;
30
0
}
31
32
static float avifSignedFractionToFloat(avifSignedFraction f)
33
0
{
34
0
    if (f.d == 0) {
35
0
        return 0.0f;
36
0
    }
37
0
    return (float)f.n / f.d;
38
0
}
39
40
static float avifUnsignedFractionToFloat(avifUnsignedFraction f)
41
0
{
42
0
    if (f.d == 0) {
43
0
        return 0.0f;
44
0
    }
45
0
    return (float)f.n / f.d;
46
0
}
47
48
// ---------------------------------------------------------------------------
49
// Apply a gain map.
50
51
// Returns a weight in [-1.0, 1.0] that represents how much the gain map should be applied.
52
static float avifGetGainMapWeight(float hdrHeadroom, const avifGainMap * gainMap)
53
0
{
54
0
    const float baseHdrHeadroom = avifUnsignedFractionToFloat(gainMap->baseHdrHeadroom);
55
0
    const float alternateHdrHeadroom = avifUnsignedFractionToFloat(gainMap->alternateHdrHeadroom);
56
0
    if (baseHdrHeadroom == alternateHdrHeadroom) {
57
        // Do not apply the gain map if the HDR headroom is the same.
58
        // This case is not handled in the specification and does not make practical sense.
59
0
        return 0.0f;
60
0
    }
61
0
    const float w = AVIF_CLAMP((hdrHeadroom - baseHdrHeadroom) / (alternateHdrHeadroom - baseHdrHeadroom), 0.0f, 1.0f);
62
0
    return (alternateHdrHeadroom < baseHdrHeadroom) ? -w : w;
63
0
}
64
65
// Linear interpolation between 'a' and 'b' (returns 'a' if w == 0.0f, returns 'b' if w == 1.0f).
66
static inline float lerp(float a, float b, float w)
67
0
{
68
0
    return (1.0f - w) * a + w * b;
69
0
}
70
71
#define SDR_WHITE_NITS 203.0f
72
73
avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage,
74
                                    avifColorPrimaries baseColorPrimaries,
75
                                    avifTransferCharacteristics baseTransferCharacteristics,
76
                                    const avifGainMap * gainMap,
77
                                    float hdrHeadroom,
78
                                    avifColorPrimaries outputColorPrimaries,
79
                                    avifTransferCharacteristics outputTransferCharacteristics,
80
                                    avifRGBImage * toneMappedImage,
81
                                    avifContentLightLevelInformationBox * clli,
82
                                    avifDiagnostics * diag)
83
0
{
84
0
    avifDiagnosticsClearError(diag);
85
86
0
    if (hdrHeadroom < 0.0f) {
87
0
        avifDiagnosticsPrintf(diag, "hdrHeadroom should be >= 0, got %f", hdrHeadroom);
88
0
        return AVIF_RESULT_INVALID_ARGUMENT;
89
0
    }
90
0
    if (baseImage == NULL || gainMap == NULL || toneMappedImage == NULL) {
91
0
        avifDiagnosticsPrintf(diag, "NULL input image");
92
0
        return AVIF_RESULT_INVALID_ARGUMENT;
93
0
    }
94
0
    AVIF_CHECKRES(avifGainMapValidateMetadata(gainMap, diag));
95
96
0
    const uint32_t width = baseImage->width;
97
0
    const uint32_t height = baseImage->height;
98
99
0
    const avifBool useBaseColorSpace = gainMap->useBaseColorSpace;
100
0
    const avifColorPrimaries gainMapMathPrimaries =
101
0
        (useBaseColorSpace || (gainMap->altColorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED)) ? baseColorPrimaries
102
0
                                                                                                : gainMap->altColorPrimaries;
103
0
    const avifBool needsInputColorConversion = (baseColorPrimaries != gainMapMathPrimaries);
104
0
    const avifBool needsOutputColorConversion = (gainMapMathPrimaries != outputColorPrimaries);
105
106
0
    avifImage * rescaledGainMap = NULL;
107
0
    avifRGBImage rgbGainMap;
108
    // Basic zero-initialization for now, avifRGBImageSetDefaults() is called later on.
109
0
    memset(&rgbGainMap, 0, sizeof(rgbGainMap));
110
111
0
    avifResult res = AVIF_RESULT_OK;
112
0
    toneMappedImage->width = width;
113
0
    toneMappedImage->height = height;
114
0
    AVIF_CHECKRES(avifRGBImageAllocatePixels(toneMappedImage));
115
116
    // --- After this point, the function should exit with 'goto cleanup' to free allocated pixels.
117
118
0
    const float weight = avifGetGainMapWeight(hdrHeadroom, gainMap);
119
120
    // Early exit if the gain map does not need to be applied and the pixel format is the same.
121
0
    if (weight == 0.0f && outputTransferCharacteristics == baseTransferCharacteristics &&
122
0
        outputColorPrimaries == baseColorPrimaries && baseImage->format == toneMappedImage->format &&
123
0
        baseImage->depth == toneMappedImage->depth && baseImage->isFloat == toneMappedImage->isFloat) {
124
0
        assert(baseImage->rowBytes == toneMappedImage->rowBytes);
125
0
        assert(baseImage->height == toneMappedImage->height);
126
        // Copy the base image.
127
0
        memcpy(toneMappedImage->pixels, baseImage->pixels, (size_t)baseImage->rowBytes * baseImage->height);
128
0
        goto cleanup;
129
0
    }
130
131
0
    avifRGBColorSpaceInfo baseRGBInfo;
132
0
    avifRGBColorSpaceInfo toneMappedPixelRGBInfo;
133
0
    if (!avifGetRGBColorSpaceInfo(baseImage, &baseRGBInfo) || !avifGetRGBColorSpaceInfo(toneMappedImage, &toneMappedPixelRGBInfo)) {
134
0
        avifDiagnosticsPrintf(diag, "Unsupported RGB color space");
135
0
        res = AVIF_RESULT_NOT_IMPLEMENTED;
136
0
        goto cleanup;
137
0
    }
138
139
0
    const avifTransferFunction gammaToLinear = avifTransferCharacteristicsGetGammaToLinearFunction(baseTransferCharacteristics);
140
0
    const avifTransferFunction linearToGamma = avifTransferCharacteristicsGetLinearToGammaFunction(outputTransferCharacteristics);
141
142
    // Early exit if the gain map does not need to be applied.
143
0
    if (weight == 0.0f) {
144
0
        const avifBool primariesDiffer = (baseColorPrimaries != outputColorPrimaries);
145
0
        double conversionCoeffs[3][3];
146
0
        if (primariesDiffer && !avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, outputColorPrimaries, conversionCoeffs)) {
147
0
            avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion");
148
0
            res = AVIF_RESULT_NOT_IMPLEMENTED;
149
0
            goto cleanup;
150
0
        }
151
        // Just convert from one rgb format to another.
152
0
        for (uint32_t j = 0; j < height; ++j) {
153
0
            for (uint32_t i = 0; i < width; ++i) {
154
0
                float basePixelRGBA[4];
155
0
                avifGetRGBAPixel(baseImage, i, j, &baseRGBInfo, basePixelRGBA);
156
0
                if (outputTransferCharacteristics != baseTransferCharacteristics || primariesDiffer) {
157
0
                    for (int c = 0; c < 3; ++c) {
158
0
                        basePixelRGBA[c] = gammaToLinear(basePixelRGBA[c]);
159
0
                    }
160
0
                    if (primariesDiffer) {
161
0
                        avifLinearRGBConvertColorSpace(basePixelRGBA, conversionCoeffs);
162
0
                    }
163
0
                    for (int c = 0; c < 3; ++c) {
164
0
                        basePixelRGBA[c] = avifNanSafeClamp(linearToGamma(basePixelRGBA[c]));
165
0
                    }
166
0
                }
167
0
                avifSetRGBAPixel(toneMappedImage, i, j, &toneMappedPixelRGBInfo, basePixelRGBA);
168
0
            }
169
0
        }
170
0
        goto cleanup;
171
0
    }
172
173
0
    double inputConversionCoeffs[3][3];
174
0
    double outputConversionCoeffs[3][3];
175
0
    if (needsInputColorConversion &&
176
0
        !avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, gainMapMathPrimaries, inputConversionCoeffs)) {
177
0
        avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion");
178
0
        res = AVIF_RESULT_NOT_IMPLEMENTED;
179
0
        goto cleanup;
180
0
    }
181
0
    if (needsOutputColorConversion &&
182
0
        !avifColorPrimariesComputeRGBToRGBMatrix(gainMapMathPrimaries, outputColorPrimaries, outputConversionCoeffs)) {
183
0
        avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion");
184
0
        res = AVIF_RESULT_NOT_IMPLEMENTED;
185
0
        goto cleanup;
186
0
    }
187
188
0
    if (gainMap->image->width != width || gainMap->image->height != height) {
189
0
        rescaledGainMap = avifImageCreateEmpty();
190
0
        if (rescaledGainMap == NULL) {
191
0
            res = AVIF_RESULT_OUT_OF_MEMORY;
192
0
            goto cleanup;
193
0
        }
194
0
        const avifCropRect rect = { 0, 0, gainMap->image->width, gainMap->image->height };
195
0
        res = avifImageSetViewRect(rescaledGainMap, gainMap->image, &rect);
196
0
        if (res != AVIF_RESULT_OK) {
197
0
            goto cleanup;
198
0
        }
199
0
        res = avifImageScale(rescaledGainMap, width, height, diag);
200
0
        if (res != AVIF_RESULT_OK) {
201
0
            goto cleanup;
202
0
        }
203
0
    }
204
0
    const avifImage * const gainMapImage = (rescaledGainMap != NULL) ? rescaledGainMap : gainMap->image;
205
206
0
    avifRGBImageSetDefaults(&rgbGainMap, gainMapImage);
207
0
    res = avifRGBImageAllocatePixels(&rgbGainMap);
208
0
    if (res != AVIF_RESULT_OK) {
209
0
        goto cleanup;
210
0
    }
211
0
    res = avifImageYUVToRGB(gainMapImage, &rgbGainMap);
212
0
    if (res != AVIF_RESULT_OK) {
213
0
        goto cleanup;
214
0
    }
215
216
0
    avifRGBColorSpaceInfo gainMapRGBInfo;
217
0
    if (!avifGetRGBColorSpaceInfo(&rgbGainMap, &gainMapRGBInfo)) {
218
0
        avifDiagnosticsPrintf(diag, "Unsupported RGB color space");
219
0
        res = AVIF_RESULT_NOT_IMPLEMENTED;
220
0
        goto cleanup;
221
0
    }
222
223
0
    float rgbMaxLinear = 0; // Max tone mapped pixel value across R, G and B channels.
224
0
    float rgbSumLinear = 0; // Sum of max(r, g, b) for mapped pixels.
225
    // The gain map metadata contains the encoding gamma, and 1/gamma should be used for decoding.
226
0
    const float gammaInv[3] = { 1.0f / avifUnsignedFractionToFloat(gainMap->gainMapGamma[0]),
227
0
                                1.0f / avifUnsignedFractionToFloat(gainMap->gainMapGamma[1]),
228
0
                                1.0f / avifUnsignedFractionToFloat(gainMap->gainMapGamma[2]) };
229
0
    const float gainMapMin[3] = { avifSignedFractionToFloat(gainMap->gainMapMin[0]),
230
0
                                  avifSignedFractionToFloat(gainMap->gainMapMin[1]),
231
0
                                  avifSignedFractionToFloat(gainMap->gainMapMin[2]) };
232
0
    const float gainMapMax[3] = { avifSignedFractionToFloat(gainMap->gainMapMax[0]),
233
0
                                  avifSignedFractionToFloat(gainMap->gainMapMax[1]),
234
0
                                  avifSignedFractionToFloat(gainMap->gainMapMax[2]) };
235
0
    const float baseOffset[3] = { avifSignedFractionToFloat(gainMap->baseOffset[0]),
236
0
                                  avifSignedFractionToFloat(gainMap->baseOffset[1]),
237
0
                                  avifSignedFractionToFloat(gainMap->baseOffset[2]) };
238
0
    const float alternateOffset[3] = { avifSignedFractionToFloat(gainMap->alternateOffset[0]),
239
0
                                       avifSignedFractionToFloat(gainMap->alternateOffset[1]),
240
0
                                       avifSignedFractionToFloat(gainMap->alternateOffset[2]) };
241
0
    for (uint32_t j = 0; j < height; ++j) {
242
0
        for (uint32_t i = 0; i < width; ++i) {
243
0
            float basePixelRGBA[4];
244
0
            avifGetRGBAPixel(baseImage, i, j, &baseRGBInfo, basePixelRGBA);
245
0
            float gainMapRGBA[4];
246
0
            avifGetRGBAPixel(&rgbGainMap, i, j, &gainMapRGBInfo, gainMapRGBA);
247
248
            // Apply gain map.
249
0
            float toneMappedPixelRGBA[4];
250
0
            float pixelRgbMaxLinear = 0.0f; //  = max(r, g, b) for this pixel
251
252
0
            for (int c = 0; c < 3; ++c) {
253
0
                basePixelRGBA[c] = gammaToLinear(basePixelRGBA[c]);
254
0
            }
255
256
0
            if (needsInputColorConversion) {
257
                // Convert basePixelRGBA to gainMapMathPrimaries.
258
0
                avifLinearRGBConvertColorSpace(basePixelRGBA, inputConversionCoeffs);
259
0
            }
260
261
0
            for (int c = 0; c < 3; ++c) {
262
0
                const float baseLinear = basePixelRGBA[c];
263
0
                const float gainMapValue = gainMapRGBA[c];
264
265
                // Undo gamma & affine transform; the result is in log2 space.
266
0
                const float gainMapLog2 = lerp(gainMapMin[c], gainMapMax[c], powf(gainMapValue, gammaInv[c]));
267
0
                const float toneMappedLinear = (baseLinear + baseOffset[c]) * exp2f(gainMapLog2 * weight) - alternateOffset[c];
268
269
0
                if (toneMappedLinear > rgbMaxLinear) {
270
0
                    rgbMaxLinear = toneMappedLinear;
271
0
                }
272
0
                if (toneMappedLinear > pixelRgbMaxLinear) {
273
0
                    pixelRgbMaxLinear = toneMappedLinear;
274
0
                }
275
276
0
                toneMappedPixelRGBA[c] = toneMappedLinear;
277
0
            }
278
279
0
            if (needsOutputColorConversion) {
280
                // Convert toneMappedPixelRGBA to outputColorPrimaries.
281
0
                avifLinearRGBConvertColorSpace(toneMappedPixelRGBA, outputConversionCoeffs);
282
0
            }
283
284
0
            for (int c = 0; c < 3; ++c) {
285
0
                if (isnan(toneMappedPixelRGBA[c])) {
286
0
                    avifDiagnosticsPrintf(diag, "Degenerate gain map parameters produce NaN at pixel (%u, %u)", i, j);
287
0
                    res = AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE;
288
0
                    goto cleanup;
289
0
                }
290
0
                toneMappedPixelRGBA[c] = avifNanSafeClamp(linearToGamma(toneMappedPixelRGBA[c]));
291
0
            }
292
293
0
            toneMappedPixelRGBA[3] = basePixelRGBA[3]; // Alpha is unaffected by tone mapping.
294
0
            rgbSumLinear += pixelRgbMaxLinear;
295
0
            avifSetRGBAPixel(toneMappedImage, i, j, &toneMappedPixelRGBInfo, toneMappedPixelRGBA);
296
0
        }
297
0
    }
298
0
    if (clli != NULL) {
299
        // For exact CLLI value definitions, see ISO/IEC 23008-2 section D.3.35
300
        // at https://standards.iso.org/ittf/PubliclyAvailableStandards/index.html
301
        // See also discussion in https://github.com/AOMediaCodec/libavif/issues/1727
302
303
        // Convert extended SDR (where 1.0 is SDR white) to nits.
304
0
        clli->maxCLL = (uint16_t)AVIF_CLAMP(avifRoundf(rgbMaxLinear * SDR_WHITE_NITS), 0.0f, (float)UINT16_MAX);
305
0
        const float rgbAverageLinear = rgbSumLinear / ((size_t)width * height);
306
0
        clli->maxPALL = (uint16_t)AVIF_CLAMP(avifRoundf(rgbAverageLinear * SDR_WHITE_NITS), 0.0f, (float)UINT16_MAX);
307
0
    }
308
309
0
cleanup:
310
0
    avifRGBImageFreePixels(&rgbGainMap);
311
0
    if (rescaledGainMap != NULL) {
312
0
        avifImageDestroy(rescaledGainMap);
313
0
    }
314
315
0
    return res;
316
0
}
317
318
avifResult avifImageApplyGainMap(const avifImage * baseImage,
319
                                 const avifGainMap * gainMap,
320
                                 float hdrHeadroom,
321
                                 avifColorPrimaries outputColorPrimaries,
322
                                 avifTransferCharacteristics outputTransferCharacteristics,
323
                                 avifRGBImage * toneMappedImage,
324
                                 avifContentLightLevelInformationBox * clli,
325
                                 avifDiagnostics * diag)
326
0
{
327
0
    avifDiagnosticsClearError(diag);
328
329
0
    if (baseImage->icc.size > 0 || gainMap->altICC.size > 0) {
330
0
        avifDiagnosticsPrintf(diag, "Tone mapping for images with ICC profiles is not supported");
331
0
        return AVIF_RESULT_NOT_IMPLEMENTED;
332
0
    }
333
334
0
    avifRGBImage baseImageRgb;
335
0
    avifRGBImageSetDefaults(&baseImageRgb, baseImage);
336
0
    AVIF_CHECKRES(avifRGBImageAllocatePixels(&baseImageRgb));
337
0
    avifResult res = avifImageYUVToRGB(baseImage, &baseImageRgb);
338
0
    if (res != AVIF_RESULT_OK) {
339
0
        goto cleanup;
340
0
    }
341
342
0
    res = avifRGBImageApplyGainMap(&baseImageRgb,
343
0
                                   baseImage->colorPrimaries,
344
0
                                   baseImage->transferCharacteristics,
345
0
                                   gainMap,
346
0
                                   hdrHeadroom,
347
0
                                   outputColorPrimaries,
348
0
                                   outputTransferCharacteristics,
349
0
                                   toneMappedImage,
350
0
                                   clli,
351
0
                                   diag);
352
353
0
cleanup:
354
0
    avifRGBImageFreePixels(&baseImageRgb);
355
356
0
    return res;
357
0
}
358
359
// ---------------------------------------------------------------------------
360
// Create a gain map.
361
362
// Returns the index of the histogram bucket for a given value, for a histogram with 'numBuckets' buckets,
363
// and values ranging in [bucketMin, bucketMax]ย (values outside of the range are added to the first/last buckets).
364
static int avifValueToBucketIdx(float v, float bucketMin, float bucketMax, int numBuckets)
365
0
{
366
0
    v = AVIF_CLAMP(v, bucketMin, bucketMax);
367
0
    return AVIF_MIN((int)avifRoundf((v - bucketMin) / (bucketMax - bucketMin) * numBuckets), numBuckets - 1);
368
0
}
369
// Returns the lower end of the value range belonging to the given histogram bucket.
370
static float avifBucketIdxToValue(int idx, float bucketMin, float bucketMax, int numBuckets)
371
0
{
372
0
    return idx * (bucketMax - bucketMin) / numBuckets + bucketMin;
373
0
}
374
375
avifResult avifFindMinMaxWithoutOutliers(const float * gainMapF, size_t numPixels, float * rangeMin, float * rangeMax)
376
0
{
377
0
    const float bucketSize = 0.01f;        // Size of one bucket. Empirical value.
378
0
    const float maxOutliersRatio = 0.001f; // 0.1%
379
0
    const int maxOutliersOnEachSide = (int)avifRoundf(numPixels * maxOutliersRatio / 2.0f);
380
381
0
    float min = gainMapF[0];
382
0
    float max = gainMapF[0];
383
0
    for (size_t i = 1; i < numPixels; ++i) {
384
0
        min = AVIF_MIN(min, gainMapF[i]);
385
0
        max = AVIF_MAX(max, gainMapF[i]);
386
0
    }
387
388
0
    *rangeMin = min;
389
0
    *rangeMax = max;
390
0
    if ((max - min) <= (bucketSize * 2) || maxOutliersOnEachSide == 0) {
391
0
        return AVIF_RESULT_OK;
392
0
    }
393
394
0
    const int maxNumBuckets = 10000;
395
0
    const int numBuckets = AVIF_MIN((int)ceilf((max - min) / bucketSize), maxNumBuckets);
396
0
    int * histogram = avifAlloc(sizeof(int) * numBuckets);
397
0
    if (histogram == NULL) {
398
0
        return AVIF_RESULT_OUT_OF_MEMORY;
399
0
    }
400
0
    memset(histogram, 0, sizeof(int) * numBuckets);
401
0
    for (size_t i = 0; i < numPixels; ++i) {
402
0
        ++(histogram[avifValueToBucketIdx(gainMapF[i], min, max, numBuckets)]);
403
0
    }
404
405
0
    int leftOutliers = 0;
406
0
    for (int i = 0; i < numBuckets; ++i) {
407
0
        leftOutliers += histogram[i];
408
0
        if (leftOutliers > maxOutliersOnEachSide) {
409
0
            break;
410
0
        }
411
0
        if (histogram[i] == 0) {
412
            // +1 to get the higher end of the bucket.
413
0
            *rangeMin = avifBucketIdxToValue(i + 1, min, max, numBuckets);
414
0
        }
415
0
    }
416
417
0
    int rightOutliers = 0;
418
0
    for (int i = numBuckets - 1; i >= 0; --i) {
419
0
        rightOutliers += histogram[i];
420
0
        if (rightOutliers > maxOutliersOnEachSide) {
421
0
            break;
422
0
        }
423
0
        if (histogram[i] == 0) {
424
0
            *rangeMax = avifBucketIdxToValue(i, min, max, numBuckets);
425
0
        }
426
0
    }
427
428
0
    avifFree(histogram);
429
0
    return AVIF_RESULT_OK;
430
0
}
431
432
avifResult avifGainMapValidateMetadata(const avifGainMap * gainMap, avifDiagnostics * diag)
433
0
{
434
0
    for (int i = 0; i < 3; ++i) {
435
0
        if (gainMap->gainMapMin[i].d == 0 || gainMap->gainMapMax[i].d == 0 || gainMap->gainMapGamma[i].d == 0 ||
436
0
            gainMap->baseOffset[i].d == 0 || gainMap->alternateOffset[i].d == 0) {
437
0
            avifDiagnosticsPrintf(diag, "Per-channel denominator is 0 in gain map metadata");
438
0
            return AVIF_RESULT_INVALID_ARGUMENT;
439
0
        }
440
0
        if ((int64_t)gainMap->gainMapMax[i].n * gainMap->gainMapMin[i].d <
441
0
            (int64_t)gainMap->gainMapMin[i].n * gainMap->gainMapMax[i].d) {
442
0
            avifDiagnosticsPrintf(diag, "Per-channel max is less than per-channel min in gain map metadata");
443
0
            return AVIF_RESULT_INVALID_ARGUMENT;
444
0
        }
445
0
        if (gainMap->gainMapGamma[i].n == 0) {
446
0
            avifDiagnosticsPrintf(diag, "Per-channel gamma is 0 in gain map metadata");
447
0
            return AVIF_RESULT_INVALID_ARGUMENT;
448
0
        }
449
0
    }
450
0
    if (gainMap->baseHdrHeadroom.d == 0 || gainMap->alternateHdrHeadroom.d == 0) {
451
0
        avifDiagnosticsPrintf(diag, "Headroom denominator is 0 in gain map metadata");
452
0
        return AVIF_RESULT_INVALID_ARGUMENT;
453
0
    }
454
0
    if (gainMap->useBaseColorSpace != 0 && gainMap->useBaseColorSpace != 1) {
455
0
        avifDiagnosticsPrintf(diag, "useBaseColorSpace is %d in gain map metadata", gainMap->useBaseColorSpace);
456
0
        return AVIF_RESULT_INVALID_ARGUMENT;
457
0
    }
458
0
    return AVIF_RESULT_OK;
459
0
}
460
461
avifBool avifSameGainMapMetadata(const avifGainMap * a, const avifGainMap * b)
462
0
{
463
0
    if (a->baseHdrHeadroom.n != b->baseHdrHeadroom.n || a->baseHdrHeadroom.d != b->baseHdrHeadroom.d ||
464
0
        a->alternateHdrHeadroom.n != b->alternateHdrHeadroom.n || a->alternateHdrHeadroom.d != b->alternateHdrHeadroom.d) {
465
0
        return AVIF_FALSE;
466
0
    }
467
0
    for (int c = 0; c < 3; ++c) {
468
0
        if (a->gainMapMin[c].n != b->gainMapMin[c].n || a->gainMapMin[c].d != b->gainMapMin[c].d ||
469
0
            a->gainMapMax[c].n != b->gainMapMax[c].n || a->gainMapMax[c].d != b->gainMapMax[c].d ||
470
0
            a->gainMapGamma[c].n != b->gainMapGamma[c].n || a->gainMapGamma[c].d != b->gainMapGamma[c].d ||
471
0
            a->baseOffset[c].n != b->baseOffset[c].n || a->baseOffset[c].d != b->baseOffset[c].d ||
472
0
            a->alternateOffset[c].n != b->alternateOffset[c].n || a->alternateOffset[c].d != b->alternateOffset[c].d) {
473
0
            return AVIF_FALSE;
474
0
        }
475
0
    }
476
0
    return AVIF_TRUE;
477
0
}
478
479
avifBool avifSameGainMapAltMetadata(const avifGainMap * a, const avifGainMap * b)
480
0
{
481
0
    if (a->altICC.size != b->altICC.size || memcmp(a->altICC.data, b->altICC.data, a->altICC.size) != 0 ||
482
0
        a->altColorPrimaries != b->altColorPrimaries || a->altTransferCharacteristics != b->altTransferCharacteristics ||
483
0
        a->altMatrixCoefficients != b->altMatrixCoefficients || a->altYUVRange != b->altYUVRange || a->altDepth != b->altDepth ||
484
0
        a->altPlaneCount != b->altPlaneCount || a->altCLLI.maxCLL != b->altCLLI.maxCLL || a->altCLLI.maxPALL != b->altCLLI.maxPALL) {
485
0
        return AVIF_FALSE;
486
0
    }
487
0
    return AVIF_TRUE;
488
0
}
489
490
static const float kEpsilon = 1e-10f;
491
492
// Decides which of 'basePrimaries' or 'altPrimaries' should be used for doing gain map math when creating a gain map.
493
// The other image (base or alternate) will be converted to this color space before computing
494
// the ratio between the two images.
495
// If a pixel color is outside of the target color space, some of the converted channel values will be negative.
496
// This should be avoided, as the negative values must either be clamped or offset before computing the log2()
497
// (since log2 only works on > 0 values). But a large offset causes artefacts when partially applying the gain map.
498
// Therefore we want to do gain map math in the larger of the two color spaces.
499
static avifResult avifChooseColorSpaceForGainMapMath(avifColorPrimaries basePrimaries,
500
                                                     avifColorPrimaries altPrimaries,
501
                                                     avifColorPrimaries * gainMapMathColorSpace)
502
0
{
503
0
    if (basePrimaries == altPrimaries) {
504
0
        *gainMapMathColorSpace = basePrimaries;
505
0
        return AVIF_RESULT_OK;
506
0
    }
507
    // Color convert pure red, pure green and pure blue in turn and see if they result in negative values.
508
0
    float rgba[4] = { 0 };
509
0
    double baseToAltCoeffs[3][3];
510
0
    double altToBaseCoeffs[3][3];
511
0
    if (!avifColorPrimariesComputeRGBToRGBMatrix(basePrimaries, altPrimaries, baseToAltCoeffs) ||
512
0
        !avifColorPrimariesComputeRGBToRGBMatrix(altPrimaries, basePrimaries, altToBaseCoeffs)) {
513
0
        return AVIF_RESULT_NOT_IMPLEMENTED;
514
0
    }
515
516
0
    float baseColorspaceChannelMin = 0;
517
0
    float altColorspaceChannelMin = 0;
518
0
    for (int c = 0; c < 3; ++c) {
519
0
        rgba[0] = rgba[1] = rgba[2] = 0;
520
0
        rgba[c] = 1.0f;
521
0
        avifLinearRGBConvertColorSpace(rgba, altToBaseCoeffs);
522
0
        for (int i = 0; i < 3; ++i) {
523
0
            baseColorspaceChannelMin = AVIF_MIN(baseColorspaceChannelMin, rgba[i]);
524
0
        }
525
0
        rgba[0] = rgba[1] = rgba[2] = 0;
526
0
        rgba[c] = 1.0f;
527
0
        avifLinearRGBConvertColorSpace(rgba, baseToAltCoeffs);
528
0
        for (int i = 0; i < 3; ++i) {
529
0
            altColorspaceChannelMin = AVIF_MIN(altColorspaceChannelMin, rgba[i]);
530
0
        }
531
0
    }
532
    // Pick the colorspace that has the largest min value (which is more or less the largest color space).
533
0
    *gainMapMathColorSpace = (altColorspaceChannelMin <= baseColorspaceChannelMin) ? basePrimaries : altPrimaries;
534
0
    return AVIF_RESULT_OK;
535
0
}
536
537
avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage,
538
                                      avifColorPrimaries baseColorPrimaries,
539
                                      avifTransferCharacteristics baseTransferCharacteristics,
540
                                      const avifRGBImage * altRgbImage,
541
                                      avifColorPrimaries altColorPrimaries,
542
                                      avifTransferCharacteristics altTransferCharacteristics,
543
                                      avifGainMap * gainMap,
544
                                      avifDiagnostics * diag)
545
0
{
546
0
    avifDiagnosticsClearError(diag);
547
548
0
    AVIF_CHECKERR(baseRgbImage != NULL && altRgbImage != NULL && gainMap != NULL && gainMap->image != NULL, AVIF_RESULT_INVALID_ARGUMENT);
549
0
    if (baseRgbImage->width != altRgbImage->width || baseRgbImage->height != altRgbImage->height) {
550
0
        avifDiagnosticsPrintf(diag, "Both images should have the same dimensions");
551
0
        return AVIF_RESULT_INVALID_ARGUMENT;
552
0
    }
553
0
    if (gainMap->image->width == 0 || gainMap->image->height == 0 || gainMap->image->depth == 0 ||
554
0
        gainMap->image->yuvFormat <= AVIF_PIXEL_FORMAT_NONE || gainMap->image->yuvFormat >= AVIF_PIXEL_FORMAT_COUNT) {
555
0
        avifDiagnosticsPrintf(diag, "gainMap->image should be non null with desired width, height, depth and yuvFormat set");
556
0
        return AVIF_RESULT_INVALID_ARGUMENT;
557
0
    }
558
0
    const avifBool colorSpacesDiffer = (baseColorPrimaries != altColorPrimaries);
559
0
    avifColorPrimaries gainMapMathPrimaries;
560
0
    AVIF_CHECKRES(avifChooseColorSpaceForGainMapMath(baseColorPrimaries, altColorPrimaries, &gainMapMathPrimaries));
561
0
    const uint32_t width = baseRgbImage->width;
562
0
    const uint32_t height = baseRgbImage->height;
563
564
0
    avifRGBColorSpaceInfo baseRGBInfo;
565
0
    avifRGBColorSpaceInfo altRGBInfo;
566
0
    if (!avifGetRGBColorSpaceInfo(baseRgbImage, &baseRGBInfo) || !avifGetRGBColorSpaceInfo(altRgbImage, &altRGBInfo)) {
567
0
        avifDiagnosticsPrintf(diag, "Unsupported RGB color space");
568
0
        return AVIF_RESULT_NOT_IMPLEMENTED;
569
0
    }
570
571
0
    float * gainMapF[3] = { 0 }; // Temporary buffers for the gain map as floating point values, one per RGB channel.
572
0
    avifRGBImage gainMapRGB;
573
0
    memset(&gainMapRGB, 0, sizeof(gainMapRGB));
574
0
    avifImage * gainMapImage = gainMap->image;
575
576
0
    avifResult res = AVIF_RESULT_OK;
577
    // --- After this point, the function should exit with 'goto cleanup' to free allocated resources.
578
579
0
    const size_t numPixels = (size_t)width * height;
580
0
    if (numPixels > SIZE_MAX / sizeof(float)) {
581
0
        res = AVIF_RESULT_INVALID_ARGUMENT;
582
0
        goto cleanup;
583
0
    }
584
0
    const size_t gainMapChannelSize = numPixels * sizeof(float);
585
0
    const avifBool singleChannel = (gainMap->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400);
586
0
    const int numGainMapChannels = singleChannel ? 1 : 3;
587
0
    for (int c = 0; c < numGainMapChannels; ++c) {
588
0
        gainMapF[c] = avifAlloc(gainMapChannelSize);
589
0
        if (gainMapF[c] == NULL) {
590
0
            res = AVIF_RESULT_OUT_OF_MEMORY;
591
0
            goto cleanup;
592
0
        }
593
0
    }
594
595
0
    avifGainMapSetEncodingDefaults(gainMap);
596
0
    gainMap->useBaseColorSpace = (gainMapMathPrimaries == baseColorPrimaries);
597
598
0
    float (*baseGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(baseTransferCharacteristics);
599
0
    float (*altGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(altTransferCharacteristics);
600
0
    float yCoeffs[3];
601
0
    avifColorPrimariesComputeYCoeffs(gainMapMathPrimaries, yCoeffs);
602
603
0
    double rgbConversionCoeffs[3][3];
604
0
    if (colorSpacesDiffer) {
605
0
        if (gainMap->useBaseColorSpace) {
606
0
            if (!avifColorPrimariesComputeRGBToRGBMatrix(altColorPrimaries, baseColorPrimaries, rgbConversionCoeffs)) {
607
0
                avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion");
608
0
                res = AVIF_RESULT_NOT_IMPLEMENTED;
609
0
                goto cleanup;
610
0
            }
611
0
        } else {
612
0
            if (!avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, altColorPrimaries, rgbConversionCoeffs)) {
613
0
                avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion");
614
0
                res = AVIF_RESULT_NOT_IMPLEMENTED;
615
0
                goto cleanup;
616
0
            }
617
0
        }
618
0
    }
619
620
0
    float baseOffset[3] = { avifSignedFractionToFloat(gainMap->baseOffset[0]),
621
0
                            avifSignedFractionToFloat(gainMap->baseOffset[1]),
622
0
                            avifSignedFractionToFloat(gainMap->baseOffset[2]) };
623
0
    float alternateOffset[3] = { avifSignedFractionToFloat(gainMap->alternateOffset[0]),
624
0
                                 avifSignedFractionToFloat(gainMap->alternateOffset[1]),
625
0
                                 avifSignedFractionToFloat(gainMap->alternateOffset[2]) };
626
627
    // If we are converting from one colorspace to another, some RGB values may be negative and an offset must be added to
628
    // avoid clamping (although the choice of color space to do the gain map computation with
629
    // avifChooseColorSpaceForGainMapMath() should mostly avoid this).
630
0
    if (colorSpacesDiffer) {
631
        // Color convert pure red, pure green and pure blue in turn and see if they result in negative values.
632
0
        float rgba[4] = { 0.0f };
633
0
        float channelMin[3] = { 0.0f };
634
0
        for (uint32_t j = 0; j < height; ++j) {
635
0
            for (uint32_t i = 0; i < width; ++i) {
636
0
                avifGetRGBAPixel(gainMap->useBaseColorSpace ? altRgbImage : baseRgbImage,
637
0
                                 i,
638
0
                                 j,
639
0
                                 gainMap->useBaseColorSpace ? &altRGBInfo : &baseRGBInfo,
640
0
                                 rgba);
641
642
                // Convert to linear.
643
0
                for (int c = 0; c < 3; ++c) {
644
0
                    if (gainMap->useBaseColorSpace) {
645
0
                        rgba[c] = altGammaToLinear(rgba[c]);
646
0
                    } else {
647
0
                        rgba[c] = baseGammaToLinear(rgba[c]);
648
0
                    }
649
0
                }
650
0
                avifLinearRGBConvertColorSpace(rgba, rgbConversionCoeffs);
651
0
                for (int c = 0; c < 3; ++c) {
652
0
                    channelMin[c] = AVIF_MIN(channelMin[c], rgba[c]);
653
0
                }
654
0
            }
655
0
        }
656
657
0
        for (int c = 0; c < 3; ++c) {
658
            // Large offsets cause artefacts when partially applying the gain map, so set a max (empirical) offset value.
659
            // If the offset is clamped, some gain map values will get clamped as well.
660
0
            const float maxOffset = 0.1f;
661
0
            if (channelMin[c] < -kEpsilon) {
662
                // Increase the offset to avoid negative values.
663
0
                if (gainMap->useBaseColorSpace) {
664
0
                    alternateOffset[c] = AVIF_MIN(alternateOffset[c] - channelMin[c], maxOffset);
665
0
                } else {
666
0
                    baseOffset[c] = AVIF_MIN(baseOffset[c] - channelMin[c], maxOffset);
667
0
                }
668
0
            }
669
0
        }
670
0
    }
671
672
    // Compute raw gain map values.
673
0
    float baseMax = 1.0f;
674
0
    float altMax = 1.0f;
675
0
    for (uint32_t j = 0; j < height; ++j) {
676
0
        for (uint32_t i = 0; i < width; ++i) {
677
0
            float baseRGBA[4];
678
0
            avifGetRGBAPixel(baseRgbImage, i, j, &baseRGBInfo, baseRGBA);
679
0
            float altRGBA[4];
680
0
            avifGetRGBAPixel(altRgbImage, i, j, &altRGBInfo, altRGBA);
681
682
            // Convert to linear.
683
0
            for (int c = 0; c < 3; ++c) {
684
0
                baseRGBA[c] = baseGammaToLinear(baseRGBA[c]);
685
0
                altRGBA[c] = altGammaToLinear(altRGBA[c]);
686
0
            }
687
688
0
            if (colorSpacesDiffer) {
689
0
                if (gainMap->useBaseColorSpace) {
690
                    // convert altRGBA to baseRGBA's color space
691
0
                    avifLinearRGBConvertColorSpace(altRGBA, rgbConversionCoeffs);
692
0
                } else {
693
                    // convert baseRGBA to altRGBA's color space
694
0
                    avifLinearRGBConvertColorSpace(baseRGBA, rgbConversionCoeffs);
695
0
                }
696
0
            }
697
698
0
            for (int c = 0; c < numGainMapChannels; ++c) {
699
0
                float base = baseRGBA[c];
700
0
                float alt = altRGBA[c];
701
0
                if (singleChannel) {
702
                    // Convert to grayscale.
703
0
                    base = yCoeffs[0] * baseRGBA[0] + yCoeffs[1] * baseRGBA[1] + yCoeffs[2] * baseRGBA[2];
704
0
                    alt = yCoeffs[0] * altRGBA[0] + yCoeffs[1] * altRGBA[1] + yCoeffs[2] * altRGBA[2];
705
0
                }
706
0
                if (base > baseMax) {
707
0
                    baseMax = base;
708
0
                }
709
0
                if (alt > altMax) {
710
0
                    altMax = alt;
711
0
                }
712
0
                const float ratio = (alt + alternateOffset[c]) / (base + baseOffset[c]);
713
0
                const float ratioLog2 = log2f(AVIF_MAX(ratio, kEpsilon));
714
0
                gainMapF[c][(size_t)j * width + i] = ratioLog2;
715
0
            }
716
0
        }
717
0
    }
718
719
    // Populate the gain map metadata's headrooms.
720
0
    const double baseHeadroom = log2f(AVIF_MAX(baseMax, kEpsilon));
721
0
    const double alternateHeadroom = log2f(AVIF_MAX(altMax, kEpsilon));
722
0
    if (!avifDoubleToUnsignedFraction(baseHeadroom, &gainMap->baseHdrHeadroom) ||
723
0
        !avifDoubleToUnsignedFraction(alternateHeadroom, &gainMap->alternateHdrHeadroom)) {
724
0
        res = AVIF_RESULT_INVALID_ARGUMENT;
725
0
        goto cleanup;
726
0
    }
727
728
    // Multiply the gainmap by sign(alternateHdrHeadroom - baseHdrHeadroom), to
729
    // ensure that it stores the log-ratio of the HDR representation to the SDR
730
    // representation.
731
0
    if (alternateHeadroom < baseHeadroom) {
732
0
        for (int c = 0; c < numGainMapChannels; ++c) {
733
0
            for (uint32_t j = 0; j < height; ++j) {
734
0
                for (uint32_t i = 0; i < width; ++i) {
735
0
                    gainMapF[c][(size_t)j * width + i] *= -1.f;
736
0
                }
737
0
            }
738
0
        }
739
0
    }
740
741
    // Find approximate min/max for each channel, discarding outliers.
742
0
    float gainMapMinLog2[3] = { 0.0f, 0.0f, 0.0f };
743
0
    float gainMapMaxLog2[3] = { 0.0f, 0.0f, 0.0f };
744
0
    for (int c = 0; c < numGainMapChannels; ++c) {
745
0
        res = avifFindMinMaxWithoutOutliers(gainMapF[c], numPixels, &gainMapMinLog2[c], &gainMapMaxLog2[c]);
746
0
        if (res != AVIF_RESULT_OK) {
747
0
            goto cleanup;
748
0
        }
749
0
    }
750
751
    // Populate the gain map metadata's min and max values.
752
0
    for (int c = 0; c < 3; ++c) {
753
0
        if (!avifDoubleToSignedFraction(gainMapMinLog2[singleChannel ? 0 : c], &gainMap->gainMapMin[c]) ||
754
0
            !avifDoubleToSignedFraction(gainMapMaxLog2[singleChannel ? 0 : c], &gainMap->gainMapMax[c]) ||
755
0
            !avifDoubleToSignedFraction(alternateOffset[c], &gainMap->alternateOffset[c]) ||
756
0
            !avifDoubleToSignedFraction(baseOffset[c], &gainMap->baseOffset[c])) {
757
0
            res = AVIF_RESULT_INVALID_ARGUMENT;
758
0
            goto cleanup;
759
0
        }
760
0
    }
761
762
    // Scale the gain map values to map [min, max] range to [0, 1].
763
0
    for (int c = 0; c < numGainMapChannels; ++c) {
764
0
        const float range = AVIF_MAX(gainMapMaxLog2[c] - gainMapMinLog2[c], 0.0f);
765
766
0
        if (range == 0.0f) {
767
0
            for (uint32_t j = 0; j < height; ++j) {
768
0
                for (uint32_t i = 0; i < width; ++i) {
769
                    // If the range is 0, the gain map values will be multiplied by zero when tonemapping so the values
770
                    // don't matter, but we still need to make sure that gainMapF is in [0,1].
771
0
                    gainMapF[c][(size_t)j * width + i] = 0.0f;
772
0
                }
773
0
            }
774
0
        } else {
775
            // Remap [min; max] range to [0; 1]
776
0
            const float gainMapGamma = avifUnsignedFractionToFloat(gainMap->gainMapGamma[c]);
777
0
            for (uint32_t j = 0; j < height; ++j) {
778
0
                for (uint32_t i = 0; i < width; ++i) {
779
0
                    float v = gainMapF[c][(size_t)j * width + i];
780
0
                    v = AVIF_CLAMP(v, gainMapMinLog2[c], gainMapMaxLog2[c]);
781
0
                    v = powf((v - gainMapMinLog2[c]) / range, gainMapGamma);
782
0
                    gainMapF[c][(size_t)j * width + i] = avifNanSafeClamp(v);
783
0
                }
784
0
            }
785
0
        }
786
0
    }
787
788
    // Convert the gain map to YUV.
789
0
    const uint32_t requestedWidth = gainMapImage->width;
790
0
    const uint32_t requestedHeight = gainMapImage->height;
791
0
    gainMapImage->width = width;
792
0
    gainMapImage->height = height;
793
794
0
    avifImageFreePlanes(gainMapImage, AVIF_PLANES_ALL); // Free planes in case they were already allocated.
795
0
    res = avifImageAllocatePlanes(gainMapImage, AVIF_PLANES_YUV);
796
0
    if (res != AVIF_RESULT_OK) {
797
0
        goto cleanup;
798
0
    }
799
800
0
    avifRGBImageSetDefaults(&gainMapRGB, gainMapImage);
801
0
    res = avifRGBImageAllocatePixels(&gainMapRGB);
802
0
    if (res != AVIF_RESULT_OK) {
803
0
        goto cleanup;
804
0
    }
805
806
0
    avifRGBColorSpaceInfo gainMapRGBInfo;
807
0
    if (!avifGetRGBColorSpaceInfo(&gainMapRGB, &gainMapRGBInfo)) {
808
0
        avifDiagnosticsPrintf(diag, "Unsupported RGB color space");
809
0
        return AVIF_RESULT_NOT_IMPLEMENTED;
810
0
    }
811
0
    for (uint32_t j = 0; j < height; ++j) {
812
0
        for (uint32_t i = 0; i < width; ++i) {
813
0
            const size_t offset = (size_t)j * width + i;
814
0
            const float r = gainMapF[0][offset];
815
0
            const float g = singleChannel ? r : gainMapF[1][offset];
816
0
            const float b = singleChannel ? r : gainMapF[2][offset];
817
0
            const float rgbaPixel[4] = { r, g, b, 1.0f };
818
0
            avifSetRGBAPixel(&gainMapRGB, i, j, &gainMapRGBInfo, rgbaPixel);
819
0
        }
820
0
    }
821
822
0
    res = avifImageRGBToYUV(gainMapImage, &gainMapRGB);
823
0
    if (res != AVIF_RESULT_OK) {
824
0
        goto cleanup;
825
0
    }
826
827
    // Scale down the gain map if requested.
828
    // Another way would be to scale the source images, but it seems to perform worse.
829
0
    if (requestedWidth != gainMapImage->width || requestedHeight != gainMapImage->height) {
830
0
        AVIF_CHECKRES(avifImageScale(gainMap->image, requestedWidth, requestedHeight, diag));
831
0
    }
832
833
0
cleanup:
834
0
    for (int c = 0; c < 3; ++c) {
835
0
        avifFree(gainMapF[c]);
836
0
    }
837
0
    avifRGBImageFreePixels(&gainMapRGB);
838
0
    if (res != AVIF_RESULT_OK) {
839
0
        avifImageFreePlanes(gainMapImage, AVIF_PLANES_ALL);
840
0
    }
841
842
0
    return res;
843
0
}
844
845
avifResult avifImageComputeGainMap(const avifImage * baseImage, const avifImage * altImage, avifGainMap * gainMap, avifDiagnostics * diag)
846
0
{
847
0
    avifDiagnosticsClearError(diag);
848
849
0
    if (baseImage == NULL || altImage == NULL || gainMap == NULL) {
850
0
        return AVIF_RESULT_INVALID_ARGUMENT;
851
0
    }
852
0
    if (baseImage->icc.size > 0 || altImage->icc.size > 0) {
853
0
        avifDiagnosticsPrintf(diag, "Computing gain maps for images with ICC profiles is not supported");
854
0
        return AVIF_RESULT_NOT_IMPLEMENTED;
855
0
    }
856
0
    if (baseImage->width != altImage->width || baseImage->height != altImage->height) {
857
0
        avifDiagnosticsPrintf(diag,
858
0
                              "Image dimensions don't match, got %dx%d and %dx%d",
859
0
                              baseImage->width,
860
0
                              baseImage->height,
861
0
                              altImage->width,
862
0
                              altImage->height);
863
0
        return AVIF_RESULT_INVALID_ARGUMENT;
864
0
    }
865
866
0
    avifResult res = AVIF_RESULT_OK;
867
868
0
    avifRGBImage baseImageRgb;
869
0
    avifRGBImageSetDefaults(&baseImageRgb, baseImage);
870
0
    avifRGBImage altImageRgb;
871
0
    avifRGBImageSetDefaults(&altImageRgb, altImage);
872
873
0
    AVIF_CHECKRES(avifRGBImageAllocatePixels(&baseImageRgb));
874
    // --- After this point, the function should exit with 'goto cleanup' to free allocated resources.
875
876
0
    res = avifImageYUVToRGB(baseImage, &baseImageRgb);
877
0
    if (res != AVIF_RESULT_OK) {
878
0
        goto cleanup;
879
0
    }
880
0
    res = avifRGBImageAllocatePixels(&altImageRgb);
881
0
    if (res != AVIF_RESULT_OK) {
882
0
        goto cleanup;
883
0
    }
884
0
    res = avifImageYUVToRGB(altImage, &altImageRgb);
885
0
    if (res != AVIF_RESULT_OK) {
886
0
        goto cleanup;
887
0
    }
888
889
0
    res = avifRGBImageComputeGainMap(&baseImageRgb,
890
0
                                     baseImage->colorPrimaries,
891
0
                                     baseImage->transferCharacteristics,
892
0
                                     &altImageRgb,
893
0
                                     altImage->colorPrimaries,
894
0
                                     altImage->transferCharacteristics,
895
0
                                     gainMap,
896
0
                                     diag);
897
898
0
    if (res != AVIF_RESULT_OK) {
899
0
        goto cleanup;
900
0
    }
901
902
0
    AVIF_CHECKRES(avifRWDataSet(&gainMap->altICC, altImage->icc.data, altImage->icc.size));
903
0
    gainMap->altColorPrimaries = altImage->colorPrimaries;
904
0
    gainMap->altTransferCharacteristics = altImage->transferCharacteristics;
905
0
    gainMap->altMatrixCoefficients = altImage->matrixCoefficients;
906
0
    gainMap->altDepth = altImage->depth;
907
0
    gainMap->altPlaneCount = (altImage->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1 : 3;
908
0
    gainMap->altCLLI = altImage->clli;
909
910
0
cleanup:
911
0
    avifRGBImageFreePixels(&baseImageRgb);
912
0
    avifRGBImageFreePixels(&altImageRgb);
913
0
    return res;
914
0
}