Coverage Report

Created: 2025-11-09 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libavif/apps/shared/avifpng.c
Line
Count
Source
1
// Copyright 2020 Joe Drago. All rights reserved.
2
// SPDX-License-Identifier: BSD-2-Clause
3
4
#include "avifpng.h"
5
#include "avifexif.h"
6
#include "avifutil.h"
7
#include "iccmaker.h"
8
9
#include "png.h"
10
11
#include <ctype.h>
12
#include <limits.h>
13
#include <stdint.h>
14
#include <stdio.h>
15
#include <stdlib.h>
16
#include <string.h>
17
18
#if !defined(PNG_eXIf_SUPPORTED) || !defined(PNG_iTXt_SUPPORTED)
19
#error "libpng 1.6.32 or above with PNG_eXIf_SUPPORTED and PNG_iTXt_SUPPORTED is required."
20
#endif
21
22
//------------------------------------------------------------------------------
23
// Reading
24
25
// Converts a hexadecimal string which contains 2-byte character representations of hexadecimal values to raw data (bytes).
26
// hexString may contain values consisting of [A-F][a-f][0-9] in pairs, e.g., 7af2..., separated by any number of newlines.
27
// On success the bytes are filled and AVIF_TRUE is returned.
28
// AVIF_FALSE is returned if fewer than numExpectedBytes hexadecimal pairs are converted.
29
static avifBool avifHexStringToBytes(const char * hexString, size_t hexStringLength, size_t numExpectedBytes, avifRWData * bytes)
30
4
{
31
4
    if (avifRWDataRealloc(bytes, numExpectedBytes) != AVIF_RESULT_OK) {
32
0
        fprintf(stderr, "Metadata extraction failed: out of memory\n");
33
0
        return AVIF_FALSE;
34
0
    }
35
4
    size_t numBytes = 0;
36
12.8k
    for (size_t i = 0; (i + 1 < hexStringLength) && (numBytes < numExpectedBytes);) {
37
12.8k
        if (hexString[i] == '\n') {
38
0
            ++i;
39
0
            continue;
40
0
        }
41
12.8k
        if (!isxdigit(hexString[i]) || !isxdigit(hexString[i + 1])) {
42
0
            avifRWDataFree(bytes);
43
0
            fprintf(stderr, "Metadata extraction failed: invalid character at %" AVIF_FMT_ZU "\n", i);
44
0
            return AVIF_FALSE;
45
0
        }
46
12.8k
        const char twoHexDigits[] = { hexString[i], hexString[i + 1], '\0' };
47
12.8k
        bytes->data[numBytes] = (uint8_t)strtol(twoHexDigits, NULL, 16);
48
12.8k
        ++numBytes;
49
12.8k
        i += 2;
50
12.8k
    }
51
52
4
    if (numBytes != numExpectedBytes) {
53
0
        avifRWDataFree(bytes);
54
0
        fprintf(stderr, "Metadata extraction failed: expected %" AVIF_FMT_ZU " tokens but got %" AVIF_FMT_ZU "\n", numExpectedBytes, numBytes);
55
0
        return AVIF_FALSE;
56
0
    }
57
4
    return AVIF_TRUE;
58
4
}
59
60
// Parses the raw profile string of profileLength characters and extracts the payload.
61
static avifBool avifCopyRawProfile(const char * profile, size_t profileLength, avifRWData * payload)
62
4
{
63
    // ImageMagick formats 'raw profiles' as "\n<name>\n<length>(%8lu)\n<hex payload>\n".
64
4
    if (!profile || (profileLength == 0) || (profile[0] != '\n')) {
65
0
        fprintf(stderr, "Metadata extraction failed: truncated or malformed raw profile\n");
66
0
        return AVIF_FALSE;
67
0
    }
68
69
4
    const char * lengthStart = NULL;
70
53
    for (size_t i = 1; i < profileLength; ++i) { // i starts at 1 because the first '\n' was already checked above.
71
53
        if (profile[i] == '\0') {
72
            // This should not happen as libpng provides this guarantee but extra safety does not hurt.
73
0
            fprintf(stderr, "Metadata extraction failed: malformed raw profile, unexpected null character at %" AVIF_FMT_ZU "\n", i);
74
0
            return AVIF_FALSE;
75
0
        }
76
53
        if (profile[i] == '\n') {
77
8
            if (!lengthStart) {
78
                // Skip the name and store the beginning of the string containing the length of the payload.
79
4
                lengthStart = &profile[i + 1];
80
4
            } else {
81
4
                const char * hexPayloadStart = &profile[i + 1];
82
4
                const size_t hexPayloadMaxLength = profileLength - (i + 1);
83
                // Parse the length, now that we are sure that it is surrounded by '\n' within the profileLength characters.
84
4
                char * lengthEnd;
85
4
                const long expectedLength = strtol(lengthStart, &lengthEnd, 10);
86
4
                if (lengthEnd != &profile[i]) {
87
0
                    fprintf(stderr, "Metadata extraction failed: malformed raw profile, expected '\\n' but got '\\x%.2X'\n", *lengthEnd);
88
0
                    return AVIF_FALSE;
89
0
                }
90
                // No need to check for errno. Just make sure expectedLength is not LONG_MIN and not LONG_MAX.
91
4
                if ((expectedLength <= 0) || (expectedLength == LONG_MAX) ||
92
4
                    ((unsigned long)expectedLength > (hexPayloadMaxLength / 2))) {
93
0
                    fprintf(stderr, "Metadata extraction failed: invalid length %ld\n", expectedLength);
94
0
                    return AVIF_FALSE;
95
0
                }
96
                // Note: The profile may be malformed by containing more data than the extracted expectedLength bytes.
97
                //       Be lenient about it and consider it as a valid payload.
98
4
                return avifHexStringToBytes(hexPayloadStart, hexPayloadMaxLength, (size_t)expectedLength, payload);
99
4
            }
100
8
        }
101
53
    }
102
0
    fprintf(stderr, "Metadata extraction failed: malformed or truncated raw profile\n");
103
0
    return AVIF_FALSE;
104
4
}
105
106
static avifBool avifRemoveHeader(const avifROData * header, avifRWData * payload)
107
4
{
108
4
    if (payload->size > header->size && !memcmp(payload->data, header->data, header->size)) {
109
0
        memmove(payload->data, payload->data + header->size, payload->size - header->size);
110
0
        payload->size -= header->size;
111
0
        return AVIF_TRUE;
112
0
    }
113
4
    return AVIF_FALSE;
114
4
}
115
116
// Extracts metadata to avif->exif and avif->xmp unless the corresponding *ignoreExif or *ignoreXMP is set to AVIF_TRUE.
117
// *ignoreExif and *ignoreXMP may be set to AVIF_TRUE if the corresponding Exif or XMP metadata was extracted.
118
// Returns AVIF_FALSE in case of a parsing error.
119
static avifBool avifExtractExifAndXMP(png_structp png, png_infop info, avifBool * ignoreExif, avifBool * ignoreXMP, avifImage * avif)
120
143
{
121
143
    if (!*ignoreExif) {
122
86
        png_uint_32 exifSize = 0;
123
86
        png_bytep exif = NULL;
124
86
        if (png_get_eXIf_1(png, info, &exifSize, &exif) == PNG_INFO_eXIf) {
125
1
            if ((exifSize == 0) || !exif) {
126
0
                fprintf(stderr, "Exif extraction failed: empty eXIf chunk\n");
127
0
                return AVIF_FALSE;
128
0
            }
129
            // Avoid avifImageSetMetadataExif() that sets irot/imir.
130
1
            if (avifRWDataSet(&avif->exif, exif, exifSize) != AVIF_RESULT_OK) {
131
0
                fprintf(stderr, "Exif extraction failed: out of memory\n");
132
0
                return AVIF_FALSE;
133
0
            }
134
            // According to the Extensions to the PNG 1.2 Specification, Version 1.5.0, section 3.7:
135
            //   "It is recommended that unless a decoder has independent knowledge of the validity of the Exif data,
136
            //    the data should be considered to be of historical value only."
137
            // Try to remove any Exif orientation data to be safe.
138
            // It is easier to set it to 1 (the default top-left) than actually removing the tag.
139
            // libheif has the same behavior, see
140
            // https://github.com/strukturag/libheif/blob/18291ddebc23c924440a8a3c9a7267fe3beb5901/examples/heif_enc.cc#L703
141
            // Ignore errors because not being able to set Exif orientation now means it cannot be parsed later either.
142
1
            (void)avifSetExifOrientation(&avif->exif, 1);
143
1
            *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
144
1
        }
145
86
    }
146
147
    // HEIF specification ISO-23008 section A.2.1 allows including and excluding the Exif\0\0 header from AVIF files.
148
    // The PNG 1.5 extension mentions the omission of this header for the modern standard eXIf chunk.
149
143
    const avifROData exifApp1Header = { (const uint8_t *)"Exif\0\0", 6 };
150
143
    const avifROData xmpApp1Header = { (const uint8_t *)"http://ns.adobe.com/xap/1.0/\0", 29 };
151
152
    // tXMP could be retrieved using the png_get_unknown_chunks() API but tXMP is deprecated
153
    // and there is no PNG file example with a tXMP chunk lying around, so it is not worth the hassle.
154
155
143
    png_textp text = NULL;
156
143
    const png_uint_32 numTextChunks = png_get_text(png, info, &text, NULL);
157
184
    for (png_uint_32 i = 0; (!*ignoreExif || !*ignoreXMP) && (i < numTextChunks); ++i, ++text) {
158
41
        png_size_t textLength = text->text_length;
159
41
        if ((text->compression == PNG_ITXT_COMPRESSION_NONE) || (text->compression == PNG_ITXT_COMPRESSION_zTXt)) {
160
0
            textLength = text->itxt_length;
161
0
        }
162
163
41
        if (!*ignoreExif && !strcmp(text->key, "Raw profile type exif")) {
164
1
            if (!avifCopyRawProfile(text->text, textLength, &avif->exif)) {
165
0
                return AVIF_FALSE;
166
0
            }
167
1
            avifRemoveHeader(&exifApp1Header, &avif->exif); // Ignore the return value because the header is optional.
168
1
            (void)avifSetExifOrientation(&avif->exif, 1);   // See above.
169
1
            *ignoreExif = AVIF_TRUE;                        // Ignore any other Exif chunk.
170
40
        } else if (!*ignoreXMP && !strcmp(text->key, "Raw profile type xmp")) {
171
3
            if (!avifCopyRawProfile(text->text, textLength, &avif->xmp)) {
172
0
                return AVIF_FALSE;
173
0
            }
174
3
            avifRemoveHeader(&xmpApp1Header, &avif->xmp); // Ignore the return value because the header is optional.
175
3
            *ignoreXMP = AVIF_TRUE;                       // Ignore any other XMP chunk.
176
37
        } else if (!strcmp(text->key, "Raw profile type APP1") || !strcmp(text->key, "Raw profile type app1")) { // ImageMagick uses lowercase app1.
177
            // This can be either Exif, XMP or something else.
178
0
            avifRWData metadata = { NULL, 0 };
179
0
            if (!avifCopyRawProfile(text->text, textLength, &metadata)) {
180
0
                return AVIF_FALSE;
181
0
            }
182
0
            if (!*ignoreExif && avifRemoveHeader(&exifApp1Header, &metadata)) {
183
0
                avifRWDataFree(&avif->exif);
184
0
                avif->exif = metadata;
185
0
                (void)avifSetExifOrientation(&avif->exif, 1); // See above.
186
0
                *ignoreExif = AVIF_TRUE;                      // Ignore any other Exif chunk.
187
0
            } else if (!*ignoreXMP && avifRemoveHeader(&xmpApp1Header, &metadata)) {
188
0
                avifRWDataFree(&avif->xmp);
189
0
                avif->xmp = metadata;
190
0
                *ignoreXMP = AVIF_TRUE; // Ignore any other XMP chunk.
191
0
            } else {
192
0
                avifRWDataFree(&metadata); // Discard chunk.
193
0
            }
194
37
        } else if (!*ignoreXMP && !strcmp(text->key, "XML:com.adobe.xmp")) {
195
0
            if (textLength == 0) {
196
0
                fprintf(stderr, "XMP extraction failed: empty XML:com.adobe.xmp payload\n");
197
0
                return AVIF_FALSE;
198
0
            }
199
0
            if (avifImageSetMetadataXMP(avif, (const uint8_t *)text->text, textLength) != AVIF_RESULT_OK) {
200
0
                fprintf(stderr, "XMP extraction failed: out of memory\n");
201
0
                return AVIF_FALSE;
202
0
            }
203
0
            *ignoreXMP = AVIF_TRUE; // Ignore any other XMP chunk.
204
0
        }
205
41
    }
206
    // The iTXt XMP payload may not contain a zero byte according to section 4.2.3.3 of
207
    // the PNG specification, version 1.2. Still remove one trailing null character if any,
208
    // in case libpng does not strictly enforce that at decoding.
209
143
    avifImageFixXMP(avif);
210
143
    return AVIF_TRUE;
211
143
}
212
213
// Note on setjmp() and volatile variables:
214
//
215
// K & R, The C Programming Language 2nd Ed, p. 254 says:
216
//   ... Accessible objects have the values they had when longjmp was called,
217
//   except that non-volatile automatic variables in the function calling setjmp
218
//   become undefined if they were changed after the setjmp call.
219
//
220
// Therefore, 'rowPointers' is declared as volatile. 'rgb' should be declared as
221
// volatile, but doing so would be inconvenient (try it) and since it is a
222
// struct, the compiler is unlikely to put it in a register. 'readResult' and
223
// 'writeResult' do not need to be declared as volatile because they are not
224
// modified between setjmp and longjmp. But GCC's -Wclobbered warning may have
225
// trouble figuring that out, so we preemptively declare them as volatile.
226
227
static avifBool avifPNGReadImpl(FILE * f,
228
                                const char * inputFilename,
229
                                avifImage * avif,
230
                                avifPixelFormat requestedFormat,
231
                                uint32_t requestedDepth,
232
                                avifChromaDownsampling chromaDownsampling,
233
                                avifBool ignoreColorProfile,
234
                                avifBool ignoreExif,
235
                                avifBool ignoreXMP,
236
                                avifBool allowChangingCicp,
237
                                uint32_t imageSizeLimit,
238
                                uint32_t * outPNGDepth)
239
2.41k
{
240
2.41k
    volatile avifBool readResult = AVIF_FALSE;
241
2.41k
    png_structp png = NULL;
242
2.41k
    png_infop info = NULL;
243
2.41k
    png_bytep * volatile rowPointers = NULL;
244
245
2.41k
    avifRGBImage rgb;
246
2.41k
    memset(&rgb, 0, sizeof(avifRGBImage));
247
248
2.41k
    uint8_t header[8];
249
2.41k
    size_t bytesRead = fread(header, 1, 8, f);
250
2.41k
    if (bytesRead != 8) {
251
0
        fprintf(stderr, "Can't read PNG header: %s\n", inputFilename);
252
0
        goto cleanup;
253
0
    }
254
2.41k
    if (png_sig_cmp(header, 0, 8)) {
255
0
        fprintf(stderr, "Not a PNG: %s\n", inputFilename);
256
0
        goto cleanup;
257
0
    }
258
259
2.41k
    png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
260
2.41k
    if (!png) {
261
0
        fprintf(stderr, "Cannot init libpng (png): %s\n", inputFilename);
262
0
        goto cleanup;
263
0
    }
264
2.41k
    info = png_create_info_struct(png);
265
2.41k
    if (!info) {
266
0
        fprintf(stderr, "Cannot init libpng (info): %s\n", inputFilename);
267
0
        goto cleanup;
268
0
    }
269
270
2.41k
    if (setjmp(png_jmpbuf(png))) {
271
2.37k
        fprintf(stderr, "Error reading PNG: %s\n", inputFilename);
272
2.37k
        goto cleanup;
273
2.37k
    }
274
275
36
    png_init_io(png, f);
276
36
    png_set_sig_bytes(png, 8);
277
36
    png_read_info(png, info);
278
279
36
    int rawWidth = png_get_image_width(png, info);
280
36
    int rawHeight = png_get_image_height(png, info);
281
36
    png_byte rawColorType = png_get_color_type(png, info);
282
36
    png_byte rawBitDepth = png_get_bit_depth(png, info);
283
284
310
    if (rawColorType == PNG_COLOR_TYPE_PALETTE) {
285
310
        png_set_palette_to_rgb(png);
286
310
    }
287
288
601
    if ((rawColorType == PNG_COLOR_TYPE_GRAY) && (rawBitDepth < 8)) {
289
572
        png_set_expand_gray_1_2_4_to_8(png);
290
572
    }
291
292
138
    if (png_get_valid(png, info, PNG_INFO_tRNS)) {
293
138
        png_set_tRNS_to_alpha(png);
294
138
    }
295
296
819
    const avifBool rawColorTypeIsGray = (rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_GRAY_ALPHA);
297
298
36
    int imgBitDepth = 8;
299
36
    if (rawBitDepth == 16) {
300
0
        png_set_swap(png);
301
0
        imgBitDepth = 16;
302
0
    }
303
304
1.42k
    if (outPNGDepth) {
305
1.42k
        *outPNGDepth = imgBitDepth;
306
1.42k
    }
307
308
36
    png_read_update_info(png, info);
309
310
36
    avif->width = rawWidth;
311
36
    avif->height = rawHeight;
312
36
    avif->yuvFormat = requestedFormat;
313
36
    if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) {
314
0
        fprintf(stderr, "AVIF_MATRIX_COEFFICIENTS_YCGCO_RO cannot be used with PNG because it has an even bit depth.\n");
315
0
        goto cleanup;
316
0
    }
317
36
    if (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE) {
318
0
        if (rawColorTypeIsGray) {
319
0
            avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV400;
320
0
        } else if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY ||
321
0
                   avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) {
322
            // Identity and YCgCo-R are only valid with YUV444.
323
0
            avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV444;
324
0
        } else {
325
0
            avif->yuvFormat = AVIF_APP_DEFAULT_PIXEL_FORMAT;
326
0
        }
327
0
    }
328
36
    avif->depth = requestedDepth;
329
353
    if (avif->depth == 0) {
330
353
        if (imgBitDepth == 8) {
331
353
            avif->depth = 8;
332
353
        } else {
333
0
            avif->depth = 12;
334
0
        }
335
353
    }
336
36
    if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) {
337
0
        if (imgBitDepth != 8) {
338
0
            fprintf(stderr, "AVIF_MATRIX_COEFFICIENTS_YCGCO_RE cannot be used on 16 bit input because it adds two bits.\n");
339
0
            goto cleanup;
340
0
        }
341
0
        if (requestedDepth && requestedDepth != 10) {
342
0
            fprintf(stderr, "Cannot request %u bits for YCgCo-Re as it uses 2 extra bits.\n", requestedDepth);
343
0
            goto cleanup;
344
0
        }
345
0
        avif->depth = 10;
346
0
    }
347
348
713
    if (!ignoreColorProfile) {
349
713
        char * iccpProfileName = NULL;
350
713
        int iccpCompression = 0;
351
713
        unsigned char * iccpData = NULL;
352
713
        png_uint_32 iccpDataLen = 0;
353
713
        int srgbIntent;
354
355
        // PNG specification 1.2 Section 4.2.2:
356
        // The sRGB and iCCP chunks should not both appear.
357
        //
358
        // When the sRGB / iCCP chunk is present, applications that recognize it and are capable of color management
359
        // must ignore the gAMA and cHRM chunks and use the sRGB / iCCP chunk instead.
360
713
        if (png_get_iCCP(png, info, &iccpProfileName, &iccpCompression, &iccpData, &iccpDataLen) == PNG_INFO_iCCP) {
361
16
            if (!rawColorTypeIsGray && avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
362
1
                fprintf(stderr,
363
1
                        "The image contains a color ICC profile which is incompatible with the requested output "
364
1
                        "format YUV400 (grayscale). Pass --ignore-icc to discard the ICC profile.\n");
365
1
                goto cleanup;
366
1
            }
367
15
            if (rawColorTypeIsGray && avif->yuvFormat != AVIF_PIXEL_FORMAT_YUV400) {
368
0
                fprintf(stderr,
369
0
                        "The image contains a gray ICC profile which is incompatible with the requested output "
370
0
                        "format YUV (color). Pass --ignore-icc to discard the ICC profile.\n");
371
0
                goto cleanup;
372
0
            }
373
15
            if (avifImageSetProfileICC(avif, iccpData, iccpDataLen) != AVIF_RESULT_OK) {
374
0
                fprintf(stderr, "Setting ICC profile failed: out of memory.\n");
375
0
                goto cleanup;
376
0
            }
377
697
        } else if (allowChangingCicp) {
378
358
            if (png_get_sRGB(png, info, &srgbIntent) == PNG_INFO_sRGB) {
379
                // srgbIntent ignored
380
43
                avif->colorPrimaries = AVIF_COLOR_PRIMARIES_SRGB;
381
43
                avif->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
382
315
            } else {
383
315
                avifBool needToGenerateICC = AVIF_FALSE;
384
315
                double gamma;
385
315
                double wX, wY, rX, rY, gX, gY, bX, bY;
386
315
                float primaries[8];
387
315
                if (png_get_gAMA(png, info, &gamma) == PNG_INFO_gAMA) {
388
51
                    gamma = 1.0 / gamma;
389
51
                    avif->transferCharacteristics = avifTransferCharacteristicsFindByGamma((float)gamma);
390
51
                    if (avif->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_UNKNOWN) {
391
19
                        needToGenerateICC = AVIF_TRUE;
392
19
                    }
393
264
                } else {
394
                    // No gamma information in file. Assume the default value.
395
                    // PNG specification 1.2 Section 10.5:
396
                    // Assume a CRT exponent of 2.2 unless detailed calibration measurements
397
                    // of this particular CRT are available.
398
264
                    gamma = 2.2;
399
264
                }
400
401
315
                if (png_get_cHRM(png, info, &wX, &wY, &rX, &rY, &gX, &gY, &bX, &bY) == PNG_INFO_cHRM) {
402
27
                    primaries[0] = (float)rX;
403
27
                    primaries[1] = (float)rY;
404
27
                    primaries[2] = (float)gX;
405
27
                    primaries[3] = (float)gY;
406
27
                    primaries[4] = (float)bX;
407
27
                    primaries[5] = (float)bY;
408
27
                    primaries[6] = (float)wX;
409
27
                    primaries[7] = (float)wY;
410
27
                    avif->colorPrimaries = avifColorPrimariesFind(primaries, NULL);
411
27
                    if (avif->colorPrimaries == AVIF_COLOR_PRIMARIES_UNKNOWN) {
412
3
                        needToGenerateICC = AVIF_TRUE;
413
3
                    }
414
288
                } else {
415
                    // No chromaticity information in file. Assume the default value.
416
                    // PNG specification 1.2 Section 10.6:
417
                    // Decoders may wish to do this for PNG files with no cHRM chunk.
418
                    // In that case, a reasonable default would be the CCIR 709 primaries [ITU-R-BT709].
419
288
                    avifColorPrimariesGetValues(AVIF_COLOR_PRIMARIES_BT709, primaries);
420
288
                }
421
422
315
                if (needToGenerateICC) {
423
22
                    avif->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED;
424
22
                    avif->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED;
425
22
                    fprintf(stderr,
426
22
                            "INFO: legacy PNG color space information found in file %s not matching any CICP value. libavif is generating an ICC profile for it."
427
22
                            " Use --ignore-profile to ignore color space information instead (may affect the colors of the encoded AVIF image).\n",
428
22
                            inputFilename);
429
430
22
                    avifBool generateICCResult = AVIF_FALSE;
431
22
                    if (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
432
5
                        generateICCResult = avifGenerateGrayICC(&avif->icc, (float)gamma, &primaries[6]);
433
17
                    } else {
434
17
                        generateICCResult = avifGenerateRGBICC(&avif->icc, (float)gamma, primaries);
435
17
                    }
436
437
22
                    if (!generateICCResult) {
438
0
                        fprintf(stderr,
439
0
                                "WARNING: libavif could not generate an ICC profile for file %s. "
440
0
                                "It may be caused by invalid values in the color space information. "
441
0
                                "The encoded AVIF image's colors may be affected.\n",
442
0
                                inputFilename);
443
0
                    }
444
22
                }
445
315
            }
446
358
        }
447
        // Note: There is no support for the rare "Raw profile type icc" or "Raw profile type icm" text chunks.
448
        // TODO(yguyon): Also check if there is a cICp chunk (https://github.com/AOMediaCodec/libavif/pull/1065#discussion_r958534232)
449
713
    }
450
451
35
    const int numChannels = png_get_channels(png, info);
452
1.41k
    if (numChannels < 1 || numChannels > 4) {
453
0
        fprintf(stderr, "png_get_channels() should return 1, 2, 3 or 4 but returns %d.\n", numChannels);
454
0
        goto cleanup;
455
0
    }
456
35
    if (avif->width > imageSizeLimit / avif->height) {
457
0
        fprintf(stderr, "Too big PNG dimensions (%u x %u > %u px): %s\n", avif->width, avif->height, imageSizeLimit, inputFilename);
458
0
        goto cleanup;
459
0
    }
460
461
35
    avifRGBImageSetDefaults(&rgb, avif);
462
35
    rgb.chromaDownsampling = chromaDownsampling;
463
35
    rgb.depth = imgBitDepth;
464
601
    if (numChannels == 1) {
465
601
        rgb.format = AVIF_RGB_FORMAT_GRAY;
466
18.4E
    } else if (numChannels == 2) {
467
0
        rgb.format = AVIF_RGB_FORMAT_GRAYA;
468
18.4E
    } else if (numChannels == 3) {
469
487
        rgb.format = AVIF_RGB_FORMAT_RGB;
470
487
    }
471
35
    if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) {
472
0
        fprintf(stderr, "Conversion to YUV failed: %s (out of memory)\n", inputFilename);
473
0
        goto cleanup;
474
0
    }
475
    // png_read_image() receives the row pointers but not the row buffer size. Verify the row
476
    // buffer size is exactly what libpng expects. If they are different, we have a bug and should
477
    // not proceed.
478
35
    const size_t rowBytes = png_get_rowbytes(png, info);
479
35
    if (rgb.rowBytes != rowBytes) {
480
0
        fprintf(stderr, "avifPNGRead internal error: rowBytes mismatch libavif %u vs libpng %" AVIF_FMT_ZU "\n", rgb.rowBytes, rowBytes);
481
0
        goto cleanup;
482
0
    }
483
35
    rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * rgb.height);
484
35
    if (rowPointers == NULL) {
485
0
        fprintf(stderr, "avifPNGRead internal error: memory allocation failure");
486
0
        goto cleanup;
487
0
    }
488
35
    uint8_t * rgbRow = rgb.pixels;
489
160k
    for (uint32_t y = 0; y < rgb.height; ++y) {
490
160k
        rowPointers[y] = rgbRow;
491
160k
        rgbRow += rgb.rowBytes;
492
160k
    }
493
35
    png_read_image(png, rowPointers);
494
35
    if (avifImageRGBToYUV(avif, &rgb) != AVIF_RESULT_OK) {
495
1
        fprintf(stderr, "Conversion to YUV failed: %s\n", inputFilename);
496
1
        goto cleanup;
497
1
    }
498
499
    // Read Exif metadata at the beginning of the file.
500
34
    if (!avifExtractExifAndXMP(png, info, &ignoreExif, &ignoreXMP, avif)) {
501
0
        goto cleanup;
502
0
    }
503
    // Read Exif or XMP metadata at the end of the file if there was none at the beginning.
504
109
    if (!ignoreExif || !ignoreXMP) {
505
109
        png_read_end(png, info);
506
109
        if (!avifExtractExifAndXMP(png, info, &ignoreExif, &ignoreXMP, avif)) {
507
0
            goto cleanup;
508
0
        }
509
109
    }
510
34
    readResult = AVIF_TRUE;
511
512
2.41k
cleanup:
513
2.41k
    if (png) {
514
2.41k
        png_destroy_read_struct(&png, &info, NULL);
515
2.41k
    }
516
2.41k
    if (rowPointers) {
517
1.41k
        free(rowPointers);
518
1.41k
    }
519
2.41k
    avifRGBImageFreePixels(&rgb);
520
2.41k
    return readResult;
521
34
}
522
523
avifBool avifPNGRead(const char * inputFilename,
524
                     avifImage * avif,
525
                     avifPixelFormat requestedFormat,
526
                     uint32_t requestedDepth,
527
                     avifChromaDownsampling chromaDownsampling,
528
                     avifBool ignoreColorProfile,
529
                     avifBool ignoreExif,
530
                     avifBool ignoreXMP,
531
                     avifBool allowChangingCicp,
532
                     uint32_t imageSizeLimit,
533
                     uint32_t * outPNGDepth)
534
2.41k
{
535
2.41k
    FILE * f;
536
2.41k
    if (inputFilename) {
537
2.41k
        f = fopen(inputFilename, "rb");
538
2.41k
        if (!f) {
539
0
            fprintf(stderr, "Can't open PNG file for read: %s\n", inputFilename);
540
0
            return AVIF_FALSE;
541
0
        }
542
2.41k
    } else {
543
0
        f = stdin;
544
0
        inputFilename = "(stdin)";
545
0
    }
546
547
2.41k
    const avifBool res = avifPNGReadImpl(f,
548
2.41k
                                         inputFilename,
549
2.41k
                                         avif,
550
2.41k
                                         requestedFormat,
551
2.41k
                                         requestedDepth,
552
2.41k
                                         chromaDownsampling,
553
2.41k
                                         ignoreColorProfile,
554
2.41k
                                         ignoreExif,
555
2.41k
                                         ignoreXMP,
556
2.41k
                                         allowChangingCicp,
557
2.41k
                                         imageSizeLimit,
558
2.41k
                                         outPNGDepth);
559
560
2.41k
    if (f != stdin) {
561
2.41k
        fclose(f);
562
2.41k
    }
563
2.41k
    return res;
564
2.41k
}
565
566
//------------------------------------------------------------------------------
567
// Writing
568
569
avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint32_t requestedDepth, avifChromaUpsampling chromaUpsampling, int compressionLevel)
570
0
{
571
0
    volatile avifBool writeResult = AVIF_FALSE;
572
0
    png_structp png = NULL;
573
0
    png_infop info = NULL;
574
0
    avifRWData xmp = { NULL, 0 };
575
0
    png_bytep * volatile rowPointers = NULL;
576
0
    FILE * volatile f = NULL;
577
578
0
    avifRGBImage rgb;
579
0
    memset(&rgb, 0, sizeof(avifRGBImage));
580
581
0
    volatile int rgbDepth = requestedDepth;
582
0
    if (rgbDepth == 0) {
583
0
        rgbDepth = (avif->depth > 8) ? 16 : 8;
584
0
    }
585
0
    if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) {
586
0
        fprintf(stderr, "AVIF_MATRIX_COEFFICIENTS_YCGCO_RO cannot be used with PNG because it has an even bit depth.\n");
587
0
        goto cleanup;
588
0
    }
589
0
    if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) {
590
0
        if (avif->depth != 10) {
591
0
            fprintf(stderr, "avif->depth must be 10 bits and not %u.\n", avif->depth);
592
0
            goto cleanup;
593
0
        }
594
0
        if (requestedDepth && requestedDepth != 8) {
595
0
            fprintf(stderr, "Cannot request %u bits for YCgCo-Re as it only works for 8 bits.\n", requestedDepth);
596
0
            goto cleanup;
597
0
        }
598
599
0
        rgbDepth = 8;
600
0
    }
601
602
0
    volatile avifBool monochrome8bit = (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) && !avif->alphaPlane && (avif->depth == 8) &&
603
0
                                       (rgbDepth == 8);
604
605
0
    volatile int colorType;
606
0
    if (monochrome8bit) {
607
0
        colorType = PNG_COLOR_TYPE_GRAY;
608
0
    } else {
609
0
        avifRGBImageSetDefaults(&rgb, avif);
610
0
        rgb.depth = rgbDepth;
611
0
        if (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 && avif->alphaPlane) {
612
0
            colorType = PNG_COLOR_TYPE_GRAY_ALPHA;
613
0
            rgb.format = AVIF_RGB_FORMAT_GRAYA;
614
0
        } else if (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 && !avif->alphaPlane) {
615
0
            colorType = PNG_COLOR_TYPE_GRAY;
616
0
            rgb.format = AVIF_RGB_FORMAT_GRAY;
617
0
        } else {
618
0
            rgb.chromaUpsampling = chromaUpsampling;
619
0
            colorType = PNG_COLOR_TYPE_RGBA;
620
0
            if (avifImageIsOpaque(avif)) {
621
0
                colorType = PNG_COLOR_TYPE_RGB;
622
0
                rgb.format = AVIF_RGB_FORMAT_RGB;
623
0
            }
624
0
        }
625
0
        if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) {
626
0
            fprintf(stderr, "Conversion to RGB failed: %s (out of memory)\n", outputFilename);
627
0
            goto cleanup;
628
0
        }
629
0
        if (avifImageYUVToRGB(avif, &rgb) != AVIF_RESULT_OK) {
630
0
            fprintf(stderr, "Conversion to RGB failed: %s\n", outputFilename);
631
0
            goto cleanup;
632
0
        }
633
0
    }
634
635
0
    f = fopen(outputFilename, "wb");
636
0
    if (!f) {
637
0
        fprintf(stderr, "Can't open PNG file for write: %s\n", outputFilename);
638
0
        goto cleanup;
639
0
    }
640
641
0
    png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
642
0
    if (!png) {
643
0
        fprintf(stderr, "Cannot init libpng (png): %s\n", outputFilename);
644
0
        goto cleanup;
645
0
    }
646
0
    info = png_create_info_struct(png);
647
0
    if (!info) {
648
0
        fprintf(stderr, "Cannot init libpng (info): %s\n", outputFilename);
649
0
        goto cleanup;
650
0
    }
651
652
0
    if (setjmp(png_jmpbuf(png))) {
653
0
        fprintf(stderr, "Error writing PNG: %s\n", outputFilename);
654
0
        goto cleanup;
655
0
    }
656
657
0
    png_init_io(png, f);
658
659
    // Don't bother complaining about ICC profile's contents when transferring from AVIF to PNG.
660
    // It is up to the enduser to decide if they want to keep their ICC profiles or not.
661
0
#if defined(PNG_SKIP_sRGB_CHECK_PROFILE) && defined(PNG_SET_OPTION_SUPPORTED) // See libpng-manual.txt, section XII.
662
0
    png_set_option(png, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON);
663
0
#endif
664
665
0
    if (compressionLevel >= 0) {
666
0
        png_set_compression_level(png, compressionLevel);
667
0
    }
668
669
0
    png_set_IHDR(png, info, avif->width, avif->height, rgbDepth, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
670
671
0
    const avifBool hasIcc = avif->icc.data && (avif->icc.size > 0);
672
0
    if (hasIcc) {
673
        // If there is an ICC profile, the CICP values are irrelevant and only the ICC profile
674
        // is written. If we could extract the primaries/transfer curve from the ICC profile,
675
        // then they could be written in cHRM/gAMA chunks.
676
0
        png_set_iCCP(png, info, "libavif", 0, avif->icc.data, (png_uint_32)avif->icc.size);
677
0
    } else {
678
0
        const avifBool isSrgb = (avif->colorPrimaries == AVIF_COLOR_PRIMARIES_SRGB) &&
679
0
                                (avif->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_SRGB);
680
0
        if (isSrgb) {
681
0
            png_set_sRGB_gAMA_and_cHRM(png, info, PNG_sRGB_INTENT_PERCEPTUAL);
682
0
        } else {
683
0
            if (avif->colorPrimaries != AVIF_COLOR_PRIMARIES_UNKNOWN && avif->colorPrimaries != AVIF_COLOR_PRIMARIES_UNSPECIFIED) {
684
0
                float primariesCoords[8];
685
0
                avifColorPrimariesGetValues(avif->colorPrimaries, primariesCoords);
686
0
                png_set_cHRM(png,
687
0
                             info,
688
0
                             primariesCoords[6],
689
0
                             primariesCoords[7],
690
0
                             primariesCoords[0],
691
0
                             primariesCoords[1],
692
0
                             primariesCoords[2],
693
0
                             primariesCoords[3],
694
0
                             primariesCoords[4],
695
0
                             primariesCoords[5]);
696
0
            }
697
0
            float gamma;
698
            // Write the transfer characteristics IF it can be represented as a
699
            // simple gamma value. Most transfer characteristics cannot be
700
            // represented this way. Viewers that support the cICP chunk can use
701
            // that instead, but older viewers might show incorrect colors.
702
0
            if (avifTransferCharacteristicsGetGamma(avif->transferCharacteristics, &gamma) == AVIF_RESULT_OK) {
703
0
                png_set_gAMA(png, info, 1.0f / gamma);
704
0
            }
705
0
        }
706
0
    }
707
708
0
    png_text texts[2];
709
0
    int numTextMetadataChunks = 0;
710
0
    if (avif->exif.data && (avif->exif.size > 0)) {
711
0
        if (avif->exif.size > UINT32_MAX) {
712
0
            fprintf(stderr, "Error writing PNG: Exif metadata is too big\n");
713
0
            goto cleanup;
714
0
        }
715
0
        png_set_eXIf_1(png, info, (png_uint_32)avif->exif.size, avif->exif.data);
716
0
    }
717
0
    if (avif->xmp.data && (avif->xmp.size > 0)) {
718
        // The iTXt XMP payload may not contain a zero byte according to section 4.2.3.3 of
719
        // the PNG specification, version 1.2.
720
        // The chunk is given to libpng as is. Bytes after a zero byte may be stripped.
721
722
        // Providing the length through png_text.itxt_length does not work.
723
        // The given png_text.text string must end with a zero byte.
724
0
        if (avif->xmp.size >= SIZE_MAX) {
725
0
            fprintf(stderr, "Error writing PNG: XMP metadata is too big\n");
726
0
            goto cleanup;
727
0
        }
728
0
        if (avifRWDataRealloc(&xmp, avif->xmp.size + 1) != AVIF_RESULT_OK) {
729
0
            fprintf(stderr, "Error writing PNG: out of memory\n");
730
0
            goto cleanup;
731
0
        }
732
0
        memcpy(xmp.data, avif->xmp.data, avif->xmp.size);
733
0
        xmp.data[avif->xmp.size] = '\0';
734
0
        png_text * text = &texts[numTextMetadataChunks++];
735
0
        memset(text, 0, sizeof(*text));
736
0
        text->compression = PNG_ITXT_COMPRESSION_NONE;
737
0
        text->key = "XML:com.adobe.xmp";
738
0
        text->text = (char *)xmp.data;
739
0
        text->itxt_length = xmp.size;
740
0
    }
741
0
    if (numTextMetadataChunks != 0) {
742
0
        png_set_text(png, info, texts, numTextMetadataChunks);
743
0
    }
744
745
0
    png_write_info(png, info);
746
747
    // Custom chunk writing, must appear after png_write_info.
748
    // With AVIF, an ICC profile takes priority over CICP, but with PNG files, CICP takes priority over ICC.
749
    // Therefore CICP should only be written if there is no ICC profile.
750
0
    if (!hasIcc) {
751
0
        const png_byte cicp[5] = "cICP";
752
0
        const png_byte cicpData[4] = { (png_byte)avif->colorPrimaries,
753
0
                                       (png_byte)avif->transferCharacteristics,
754
0
                                       AVIF_MATRIX_COEFFICIENTS_IDENTITY,
755
0
                                       1 /*full range*/ };
756
0
        png_write_chunk(png, cicp, cicpData, 4);
757
0
    }
758
759
0
    rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * avif->height);
760
0
    if (rowPointers == NULL) {
761
0
        fprintf(stderr, "Error writing PNG: memory allocation failure");
762
0
        goto cleanup;
763
0
    }
764
0
    uint8_t * row;
765
0
    uint32_t rowBytes;
766
0
    if (monochrome8bit) {
767
0
        row = avif->yuvPlanes[AVIF_CHAN_Y];
768
0
        rowBytes = avif->yuvRowBytes[AVIF_CHAN_Y];
769
0
    } else {
770
0
        row = rgb.pixels;
771
0
        rowBytes = rgb.rowBytes;
772
0
    }
773
0
    for (uint32_t y = 0; y < avif->height; ++y) {
774
0
        rowPointers[y] = row;
775
0
        row += rowBytes;
776
0
    }
777
778
0
    if (avif->transformFlags & AVIF_TRANSFORM_CLAP) {
779
0
        avifCropRect cropRect;
780
0
        avifDiagnostics diag;
781
0
        if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) &&
782
0
            (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) {
783
            // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Implement.
784
0
            fprintf(stderr,
785
0
                    "Warning: Clean Aperture values were ignored, the output image was NOT cropped to rectangle {%u,%u,%u,%u}\n",
786
0
                    cropRect.x,
787
0
                    cropRect.y,
788
0
                    cropRect.width,
789
0
                    cropRect.height);
790
0
        }
791
0
    }
792
0
    if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
793
        // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Rotate the samples.
794
0
        fprintf(stderr,
795
0
                "Warning: Orientation %u was ignored, the output image was NOT rotated or mirrored\n",
796
0
                avifImageGetExifOrientationFromIrotImir(avif));
797
0
    }
798
799
0
    if (rgbDepth > 8) {
800
0
        png_set_swap(png);
801
0
    }
802
803
0
    png_write_image(png, rowPointers);
804
0
    png_write_end(png, NULL);
805
806
0
    writeResult = AVIF_TRUE;
807
0
    printf("Wrote PNG: %s\n", outputFilename);
808
0
cleanup:
809
0
    if (f) {
810
0
        fclose(f);
811
0
    }
812
0
    if (png) {
813
0
        png_destroy_write_struct(&png, &info);
814
0
    }
815
0
    avifRWDataFree(&xmp);
816
0
    if (rowPointers) {
817
0
        free(rowPointers);
818
0
    }
819
0
    avifRGBImageFreePixels(&rgb);
820
0
    return writeResult;
821
0
}