/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 | } |