Coverage Report

Created: 2025-10-13 06:28

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