| // Copyright 2020 Joe Drago. All rights reserved. | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| #include "avifpng.h" | |
| #include "avifexif.h" | |
| #include "avifutil.h" | |
| #include "iccmaker.h" | |
| #include "png.h" | |
| #include <ctype.h> | |
| #include <limits.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #if !defined(PNG_eXIf_SUPPORTED) || !defined(PNG_iTXt_SUPPORTED) | |
| #error "libpng 1.6.32 or above with PNG_eXIf_SUPPORTED and PNG_iTXt_SUPPORTED is required." | |
| #endif | |
| //------------------------------------------------------------------------------ | |
| // Reading | |
| // Converts a hexadecimal string which contains 2-byte character representations of hexadecimal values to raw data (bytes). | |
| // hexString may contain values consisting of [A-F][a-f][0-9] in pairs, e.g., 7af2..., separated by any number of newlines. | |
| // On success the bytes are filled and AVIF_TRUE is returned. | |
| // AVIF_FALSE is returned if fewer than numExpectedBytes hexadecimal pairs are converted. | |
| static avifBool avifHexStringToBytes(const char * hexString, size_t hexStringLength, size_t numExpectedBytes, avifRWData * bytes) | |
| { | |
| if (avifRWDataRealloc(bytes, numExpectedBytes) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Metadata extraction failed: out of memory\n"); | |
| return AVIF_FALSE; | |
| } | |
| size_t numBytes = 0; | |
| for (size_t i = 0; (i + 1 < hexStringLength) && (numBytes < numExpectedBytes);) { | |
| if (hexString[i] == '\n') { | |
| ++i; | |
| continue; | |
| } | |
| if (!isxdigit((unsigned char)hexString[i]) || !isxdigit((unsigned char)hexString[i + 1])) { | |
| avifRWDataFree(bytes); | |
| fprintf(stderr, "Metadata extraction failed: invalid character at %" AVIF_FMT_ZU "\n", i); | |
| return AVIF_FALSE; | |
| } | |
| const char twoHexDigits[] = { hexString[i], hexString[i + 1], '\0' }; | |
| bytes->data[numBytes] = (uint8_t)strtol(twoHexDigits, NULL, 16); | |
| ++numBytes; | |
| i += 2; | |
| } | |
| if (numBytes != numExpectedBytes) { | |
| avifRWDataFree(bytes); | |
| fprintf(stderr, "Metadata extraction failed: expected %" AVIF_FMT_ZU " tokens but got %" AVIF_FMT_ZU "\n", numExpectedBytes, numBytes); | |
| return AVIF_FALSE; | |
| } | |
| return AVIF_TRUE; | |
| } | |
| // Parses the raw profile string of profileLength characters and extracts the payload. | |
| static avifBool avifCopyRawProfile(const char * profile, size_t profileLength, avifRWData * payload) | |
| { | |
| // ImageMagick formats 'raw profiles' as "\n<name>\n<length>(%8lu)\n<hex payload>\n". | |
| if (!profile || (profileLength == 0) || (profile[0] != '\n')) { | |
| fprintf(stderr, "Metadata extraction failed: truncated or malformed raw profile\n"); | |
| return AVIF_FALSE; | |
| } | |
| const char * lengthStart = NULL; | |
| for (size_t i = 1; i < profileLength; ++i) { // i starts at 1 because the first '\n' was already checked above. | |
| if (profile[i] == '\0') { | |
| // This should not happen as libpng provides this guarantee but extra safety does not hurt. | |
| fprintf(stderr, "Metadata extraction failed: malformed raw profile, unexpected null character at %" AVIF_FMT_ZU "\n", i); | |
| return AVIF_FALSE; | |
| } | |
| if (profile[i] == '\n') { | |
| if (!lengthStart) { | |
| // Skip the name and store the beginning of the string containing the length of the payload. | |
| lengthStart = &profile[i + 1]; | |
| } else { | |
| const char * hexPayloadStart = &profile[i + 1]; | |
| const size_t hexPayloadMaxLength = profileLength - (i + 1); | |
| // Parse the length, now that we are sure that it is surrounded by '\n' within the profileLength characters. | |
| char * lengthEnd; | |
| const long expectedLength = strtol(lengthStart, &lengthEnd, 10); | |
| if (lengthEnd != &profile[i]) { | |
| fprintf(stderr, "Metadata extraction failed: malformed raw profile, expected '\\n' but got '\\x%.2X'\n", *lengthEnd); | |
| return AVIF_FALSE; | |
| } | |
| // No need to check for errno. Just make sure expectedLength is not LONG_MIN and not LONG_MAX. | |
| if ((expectedLength <= 0) || (expectedLength == LONG_MAX) || | |
| ((unsigned long)expectedLength > (hexPayloadMaxLength / 2))) { | |
| fprintf(stderr, "Metadata extraction failed: invalid length %ld\n", expectedLength); | |
| return AVIF_FALSE; | |
| } | |
| // Note: The profile may be malformed by containing more data than the extracted expectedLength bytes. | |
| // Be lenient about it and consider it as a valid payload. | |
| return avifHexStringToBytes(hexPayloadStart, hexPayloadMaxLength, (size_t)expectedLength, payload); | |
| } | |
| } | |
| } | |
| fprintf(stderr, "Metadata extraction failed: malformed or truncated raw profile\n"); | |
| return AVIF_FALSE; | |
| } | |
| static avifBool avifRemoveHeader(const avifROData * header, avifRWData * payload) | |
| { | |
| if (payload->size > header->size && !memcmp(payload->data, header->data, header->size)) { | |
| memmove(payload->data, payload->data + header->size, payload->size - header->size); | |
| payload->size -= header->size; | |
| return AVIF_TRUE; | |
| } | |
| return AVIF_FALSE; | |
| } | |
| // Extracts metadata to avif->exif and avif->xmp unless the corresponding *ignoreExif or *ignoreXMP is set to AVIF_TRUE. | |
| // *ignoreExif and *ignoreXMP may be set to AVIF_TRUE if the corresponding Exif or XMP metadata was extracted. | |
| // Returns AVIF_FALSE in case of a parsing error. | |
| static avifBool avifExtractExifAndXMP(png_structp png, png_infop info, avifBool * ignoreExif, avifBool * ignoreXMP, avifImage * avif) | |
| { | |
| if (!*ignoreExif) { | |
| png_uint_32 exifSize = 0; | |
| png_bytep exif = NULL; | |
| if (png_get_eXIf_1(png, info, &exifSize, &exif) == PNG_INFO_eXIf) { | |
| if ((exifSize == 0) || !exif) { | |
| fprintf(stderr, "Exif extraction failed: empty eXIf chunk\n"); | |
| return AVIF_FALSE; | |
| } | |
| // Avoid avifImageSetMetadataExif() that sets irot/imir. | |
| if (avifRWDataSet(&avif->exif, exif, exifSize) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Exif extraction failed: out of memory\n"); | |
| return AVIF_FALSE; | |
| } | |
| // According to the Extensions to the PNG 1.2 Specification, Version 1.5.0, section 3.7: | |
| // "It is recommended that unless a decoder has independent knowledge of the validity of the Exif data, | |
| // the data should be considered to be of historical value only." | |
| // Try to remove any Exif orientation data to be safe. | |
| // It is easier to set it to 1 (the default top-left) than actually removing the tag. | |
| // libheif has the same behavior, see | |
| // https://github.com/strukturag/libheif/blob/18291ddebc23c924440a8a3c9a7267fe3beb5901/examples/heif_enc.cc#L703 | |
| // However note that some software does use Exif orientation, e.g. Chrome, see also | |
| // https://github.com/AOMediaCodec/libavif/issues/3070 | |
| // Ignore errors because not being able to set Exif orientation now means it cannot be parsed later either. | |
| (void)avifSetExifOrientation(&avif->exif, 1); | |
| *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk. | |
| } | |
| } | |
| // HEIF specification ISO-23008 section A.2.1 allows including and excluding the Exif\0\0 header from AVIF files. | |
| // The PNG 1.5 extension mentions the omission of this header for the modern standard eXIf chunk. | |
| const avifROData exifApp1Header = { (const uint8_t *)"Exif\0\0", 6 }; | |
| const avifROData xmpApp1Header = { (const uint8_t *)"http://ns.adobe.com/xap/1.0/\0", 29 }; | |
| // tXMP could be retrieved using the png_get_unknown_chunks() API but tXMP is deprecated | |
| // and there is no PNG file example with a tXMP chunk lying around, so it is not worth the hassle. | |
| png_textp text = NULL; | |
| const png_uint_32 numTextChunks = png_get_text(png, info, &text, NULL); | |
| for (png_uint_32 i = 0; (!*ignoreExif || !*ignoreXMP) && (i < numTextChunks); ++i, ++text) { | |
| png_size_t textLength = text->text_length; | |
| if ((text->compression == PNG_ITXT_COMPRESSION_NONE) || (text->compression == PNG_ITXT_COMPRESSION_zTXt)) { | |
| textLength = text->itxt_length; | |
| } | |
| if (!*ignoreExif && !strcmp(text->key, "Raw profile type exif")) { | |
| if (!avifCopyRawProfile(text->text, textLength, &avif->exif)) { | |
| return AVIF_FALSE; | |
| } | |
| avifRemoveHeader(&exifApp1Header, &avif->exif); // Ignore the return value because the header is optional. | |
| (void)avifSetExifOrientation(&avif->exif, 1); // See above. | |
| *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk. | |
| } else if (!*ignoreXMP && !strcmp(text->key, "Raw profile type xmp")) { | |
| if (!avifCopyRawProfile(text->text, textLength, &avif->xmp)) { | |
| return AVIF_FALSE; | |
| } | |
| avifRemoveHeader(&xmpApp1Header, &avif->xmp); // Ignore the return value because the header is optional. | |
| *ignoreXMP = AVIF_TRUE; // Ignore any other XMP chunk. | |
| } else if (!strcmp(text->key, "Raw profile type APP1") || !strcmp(text->key, "Raw profile type app1")) { // ImageMagick uses lowercase app1. | |
| // This can be either Exif, XMP or something else. | |
| avifRWData metadata = { NULL, 0 }; | |
| if (!avifCopyRawProfile(text->text, textLength, &metadata)) { | |
| return AVIF_FALSE; | |
| } | |
| if (!*ignoreExif && avifRemoveHeader(&exifApp1Header, &metadata)) { | |
| avifRWDataFree(&avif->exif); | |
| avif->exif = metadata; | |
| (void)avifSetExifOrientation(&avif->exif, 1); // See above. | |
| *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk. | |
| } else if (!*ignoreXMP && avifRemoveHeader(&xmpApp1Header, &metadata)) { | |
| avifRWDataFree(&avif->xmp); | |
| avif->xmp = metadata; | |
| *ignoreXMP = AVIF_TRUE; // Ignore any other XMP chunk. | |
| } else { | |
| avifRWDataFree(&metadata); // Discard chunk. | |
| } | |
| } else if (!*ignoreXMP && !strcmp(text->key, "XML:com.adobe.xmp")) { | |
| if (textLength == 0) { | |
| fprintf(stderr, "XMP extraction failed: empty XML:com.adobe.xmp payload\n"); | |
| return AVIF_FALSE; | |
| } | |
| if (avifImageSetMetadataXMP(avif, (const uint8_t *)text->text, textLength) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "XMP extraction failed: out of memory\n"); | |
| return AVIF_FALSE; | |
| } | |
| *ignoreXMP = AVIF_TRUE; // Ignore any other XMP chunk. | |
| } | |
| } | |
| // The iTXt XMP payload may not contain a zero byte according to section 4.2.3.3 of | |
| // the PNG specification, version 1.2. Still remove one trailing null character if any, | |
| // in case libpng does not strictly enforce that at decoding. | |
| avifImageFixXMP(avif); | |
| return AVIF_TRUE; | |
| } | |
| typedef struct avifUnknownCicpChunkData | |
| { | |
| avifImage * avif; | |
| avifBool cicpChunkRead; | |
| } avifUnknownCicpChunkData; | |
| static void avifSetPNGCicp(avifImage * avif, | |
| png_byte cicpColorPrimaries, | |
| png_byte cicpTransferCharacteristics, | |
| png_byte cicpMatrixCoefficients, | |
| png_byte cicpVideoFullRangeFlag) | |
| { | |
| avif->colorPrimaries = cicpColorPrimaries; | |
| avif->transferCharacteristics = cicpTransferCharacteristics; | |
| // PNG specification Third Edition Section 4.3: | |
| // RGB is currently the only supported color model in PNG, and as such Matrix Coefficients shall be set to 0. | |
| // Note that we don't set avif->matrixCoefficients to this value as the Avif's YUV matrix is independent from the PNG's. | |
| if (cicpMatrixCoefficients != 0) { | |
| fprintf(stderr, "Warning: Unsupported PNG CICP matrix coefficients value %d. Expected to be 0.\n", cicpMatrixCoefficients); | |
| } | |
| // Limited range PNG files are uncommon and would require conversion to full range which we don't support for now for simplicity. | |
| // See also https://github.com/w3c/png/issues/312#issuecomment-2325281113 and https://svgees.us/blog/cICP.html#the-other-two-numbers | |
| // Similarly, we don't set avif->yuvRange to this value as the Avif's YUV range is independent from the PNG's. | |
| if (cicpVideoFullRangeFlag != 1) { | |
| fprintf(stderr, "Warning: Unsupported PNG CICP full range flag value %d. Expected to be 1.\n", cicpVideoFullRangeFlag); | |
| } | |
| } | |
| // Callback for unknown chunks. Used to detect/handle chunks that we are missing support for. | |
| static int avifPNGReadUnknownChunk(png_structp png_ptr, png_unknown_chunkp chunk) | |
| { | |
| avifUnknownCicpChunkData * data = (avifUnknownCicpChunkData *)png_get_user_chunk_ptr(png_ptr); | |
| avifImage * avif = data->avif; | |
| if (!memcmp(chunk->name, "cICP", 4)) { | |
| if (chunk->size >= 4) { | |
| data->cicpChunkRead = AVIF_TRUE; | |
| avifSetPNGCicp(avif, chunk->data[0], chunk->data[1], chunk->data[2], chunk->data[3]); | |
| } | |
| } | |
| return 1; | |
| } | |
| // Note on setjmp() and volatile variables: | |
| // | |
| // K & R, The C Programming Language 2nd Ed, p. 254 says: | |
| // ... Accessible objects have the values they had when longjmp was called, | |
| // except that non-volatile automatic variables in the function calling setjmp | |
| // become undefined if they were changed after the setjmp call. | |
| // | |
| // Therefore, 'rowPointers' is declared as volatile. 'rgb' should be declared as | |
| // volatile, but doing so would be inconvenient (try it) and since it is a | |
| // struct, the compiler is unlikely to put it in a register. 'readResult' and | |
| // 'writeResult' do not need to be declared as volatile because they are not | |
| // modified between setjmp and longjmp. But GCC's -Wclobbered warning may have | |
| // trouble figuring that out, so we preemptively declare them as volatile. | |
| static avifBool avifPNGReadImpl(FILE * f, | |
| const char * inputFilename, | |
| avifImage * avif, | |
| avifPixelFormat requestedFormat, | |
| uint32_t requestedDepth, | |
| avifChromaDownsampling chromaDownsampling, | |
| avifBool ignoreColorProfile, | |
| avifBool ignoreExif, | |
| avifBool ignoreXMP, | |
| avifBool ignoreAlpha, | |
| uint32_t imageSizeLimit, | |
| uint32_t * outPNGDepth, | |
| avifBool headerOnly) | |
| { | |
| volatile avifBool readResult = AVIF_FALSE; | |
| png_structp png = NULL; | |
| png_infop info = NULL; | |
| png_bytep * volatile rowPointers = NULL; | |
| avifRGBImage rgb; | |
| memset(&rgb, 0, sizeof(avifRGBImage)); | |
| uint8_t header[8]; | |
| size_t bytesRead = fread(header, 1, 8, f); | |
| if (bytesRead != 8) { | |
| fprintf(stderr, "Can't read PNG header: %s\n", inputFilename); | |
| goto cleanup; | |
| } | |
| if (png_sig_cmp(header, 0, 8)) { | |
| fprintf(stderr, "Not a PNG: %s\n", inputFilename); | |
| goto cleanup; | |
| } | |
| png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |
| if (!png) { | |
| fprintf(stderr, "Cannot init libpng (png): %s\n", inputFilename); | |
| goto cleanup; | |
| } | |
| info = png_create_info_struct(png); | |
| if (!info) { | |
| fprintf(stderr, "Cannot init libpng (info): %s\n", inputFilename); | |
| goto cleanup; | |
| } | |
| if (setjmp(png_jmpbuf(png))) { | |
| fprintf(stderr, "Error reading PNG: %s\n", inputFilename); | |
| goto cleanup; | |
| } | |
| avifUnknownCicpChunkData unknownChunkData = { avif, AVIF_FALSE }; | |
| png_set_read_user_chunk_fn(png, &unknownChunkData, &avifPNGReadUnknownChunk); | |
| png_init_io(png, f); | |
| png_set_sig_bytes(png, 8); | |
| png_read_info(png, info); | |
| int rawWidth = png_get_image_width(png, info); | |
| int rawHeight = png_get_image_height(png, info); | |
| png_byte rawColorType = png_get_color_type(png, info); | |
| png_byte rawBitDepth = png_get_bit_depth(png, info); | |
| if (rawColorType == PNG_COLOR_TYPE_PALETTE) { | |
| png_set_palette_to_rgb(png); | |
| } | |
| if ((rawColorType == PNG_COLOR_TYPE_GRAY) && (rawBitDepth < 8)) { | |
| png_set_expand_gray_1_2_4_to_8(png); | |
| } | |
| if (png_get_valid(png, info, PNG_INFO_tRNS)) { | |
| png_set_tRNS_to_alpha(png); | |
| } | |
| const avifBool rawColorTypeIsGray = (rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_GRAY_ALPHA); | |
| int imgBitDepth = 8; | |
| if (rawBitDepth == 16) { | |
| png_set_swap(png); | |
| imgBitDepth = 16; | |
| } | |
| if (outPNGDepth) { | |
| *outPNGDepth = imgBitDepth; | |
| } | |
| png_read_update_info(png, info); | |
| avif->width = rawWidth; | |
| avif->height = rawHeight; | |
| avif->yuvFormat = requestedFormat; | |
| if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) { | |
| fprintf(stderr, "AVIF_MATRIX_COEFFICIENTS_YCGCO_RO cannot be used with PNG because it has an even bit depth.\n"); | |
| goto cleanup; | |
| } | |
| if (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE) { | |
| if (rawColorTypeIsGray) { | |
| avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; | |
| } else if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY || | |
| avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) { | |
| // Identity and YCgCo-R are only valid with YUV444. | |
| avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; | |
| } else { | |
| avif->yuvFormat = AVIF_APP_DEFAULT_PIXEL_FORMAT; | |
| } | |
| } | |
| avif->depth = requestedDepth; | |
| if (avif->depth == 0) { | |
| if (imgBitDepth == 8) { | |
| avif->depth = 8; | |
| } else { | |
| avif->depth = 12; | |
| } | |
| } | |
| if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) { | |
| if (imgBitDepth != 8) { | |
| fprintf(stderr, "AVIF_MATRIX_COEFFICIENTS_YCGCO_RE cannot be used on 16 bit input because it adds two bits.\n"); | |
| goto cleanup; | |
| } | |
| if (requestedDepth && requestedDepth != 10) { | |
| fprintf(stderr, "Cannot request %u bits for YCgCo-Re as it uses 2 extra bits.\n", requestedDepth); | |
| goto cleanup; | |
| } | |
| avif->depth = 10; | |
| } | |
| if (!ignoreColorProfile) { | |
| char * iccpProfileName = NULL; | |
| int iccpCompression = 0; | |
| unsigned char * iccpData = NULL; | |
| png_uint_32 iccpDataLen = 0; | |
| int srgbIntent; | |
| // PNG specification Third Edition Section 4.3 lists color space information chunk types by priority: | |
| // Chunk Type Priority | |
| // cICP 1 | |
| // iCCP 2 | |
| // sRGB 3 | |
| // cHRM and gAMA 4 | |
| // If a single image contains more than one of these chunk types, the chunk with the lowest | |
| // Priority number should take precedence and any higher-numbered chunk types should be ignored. | |
| #if defined(PNG_cICP_SUPPORTED) | |
| png_byte cicpColorPrimaries; | |
| png_byte cicpTransferCharacteristics; | |
| png_byte cicpMatrixCoefficients; | |
| png_byte cicpVideoFullRangeFlag; | |
| if (png_get_cICP(png, info, &cicpColorPrimaries, &cicpTransferCharacteristics, &cicpMatrixCoefficients, &cicpVideoFullRangeFlag)) { | |
| avifSetPNGCicp(avif, cicpColorPrimaries, cicpTransferCharacteristics, cicpMatrixCoefficients, cicpVideoFullRangeFlag); | |
| } else | |
| #endif | |
| if (unknownChunkData.cicpChunkRead) { | |
| // Already handled in avifPNGReadUnknownChunk() | |
| } else if (png_get_iCCP(png, info, &iccpProfileName, &iccpCompression, &iccpData, &iccpDataLen) == PNG_INFO_iCCP) { | |
| if (!rawColorTypeIsGray && avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) { | |
| fprintf(stderr, | |
| "The image contains a color ICC profile which is incompatible with the requested output " | |
| "format YUV400 (grayscale). Pass --ignore-icc to discard the ICC profile.\n"); | |
| goto cleanup; | |
| } | |
| if (rawColorTypeIsGray && avif->yuvFormat != AVIF_PIXEL_FORMAT_YUV400) { | |
| fprintf(stderr, | |
| "The image contains a gray ICC profile which is incompatible with the requested output " | |
| "format YUV (color). Pass --ignore-icc to discard the ICC profile.\n"); | |
| goto cleanup; | |
| } | |
| if (avifImageSetProfileICC(avif, iccpData, iccpDataLen) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Setting ICC profile failed: out of memory.\n"); | |
| goto cleanup; | |
| } | |
| } else if (png_get_sRGB(png, info, &srgbIntent) == PNG_INFO_sRGB) { | |
| // srgbIntent ignored | |
| avif->colorPrimaries = AVIF_COLOR_PRIMARIES_SRGB; | |
| avif->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; | |
| } else { | |
| avifBool needToGenerateICC = AVIF_FALSE; | |
| double gamma; | |
| double wX, wY, rX, rY, gX, gY, bX, bY; | |
| float primaries[8]; | |
| if (png_get_gAMA(png, info, &gamma) == PNG_INFO_gAMA) { | |
| gamma = 1.0 / gamma; | |
| avif->transferCharacteristics = avifTransferCharacteristicsFindByGamma((float)gamma); | |
| if (avif->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_UNKNOWN) { | |
| needToGenerateICC = AVIF_TRUE; | |
| } | |
| } else { | |
| // No gamma information in file. Assume the default value. | |
| // PNG specification 1.2 Section 10.5: | |
| // Assume a CRT exponent of 2.2 unless detailed calibration measurements | |
| // of this particular CRT are available. | |
| gamma = 2.2; | |
| } | |
| if (png_get_cHRM(png, info, &wX, &wY, &rX, &rY, &gX, &gY, &bX, &bY) == PNG_INFO_cHRM) { | |
| primaries[0] = (float)rX; | |
| primaries[1] = (float)rY; | |
| primaries[2] = (float)gX; | |
| primaries[3] = (float)gY; | |
| primaries[4] = (float)bX; | |
| primaries[5] = (float)bY; | |
| primaries[6] = (float)wX; | |
| primaries[7] = (float)wY; | |
| avif->colorPrimaries = avifColorPrimariesFind(primaries, NULL); | |
| if (avif->colorPrimaries == AVIF_COLOR_PRIMARIES_UNKNOWN) { | |
| needToGenerateICC = AVIF_TRUE; | |
| } | |
| } else { | |
| // No chromaticity information in file. Assume the default value. | |
| // PNG specification 1.2 Section 10.6: | |
| // Decoders may wish to do this for PNG files with no cHRM chunk. | |
| // In that case, a reasonable default would be the CCIR 709 primaries [ITU-R-BT709]. | |
| avifColorPrimariesGetValues(AVIF_COLOR_PRIMARIES_BT709, primaries); | |
| } | |
| if (needToGenerateICC) { | |
| avif->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; | |
| avif->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; | |
| fprintf(stderr, | |
| "INFO: legacy PNG color space information found in file %s not matching any CICP value. libavif is generating an ICC profile for it." | |
| " Use --ignore-profile to ignore color space information instead (may affect the colors of the encoded AVIF image).\n", | |
| inputFilename); | |
| avifBool generateICCResult = AVIF_FALSE; | |
| if (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) { | |
| generateICCResult = avifGenerateGrayICC(&avif->icc, (float)gamma, &primaries[6]); | |
| } else { | |
| generateICCResult = avifGenerateRGBICC(&avif->icc, (float)gamma, primaries); | |
| } | |
| if (!generateICCResult) { | |
| fprintf(stderr, | |
| "WARNING: libavif could not generate an ICC profile for file %s. " | |
| "It may be caused by invalid values in the color space information. " | |
| "The encoded AVIF image's colors may be affected.\n", | |
| inputFilename); | |
| } | |
| } | |
| } | |
| // Note: There is no support for the rare "Raw profile type icc" or "Raw profile type icm" text chunks. | |
| } | |
| if (avif->width > imageSizeLimit / avif->height) { | |
| fprintf(stderr, "Too big PNG dimensions (%u x %u > %u px): %s\n", avif->width, avif->height, imageSizeLimit, inputFilename); | |
| goto cleanup; | |
| } | |
| if (headerOnly) { | |
| // All the metadata read so far (dimensions, depth, yuvFormat, ICC/color primaries if | |
| // requested) is already set on avif. Stop here instead of decoding any pixel data. | |
| readResult = AVIF_TRUE; | |
| goto cleanup; | |
| } | |
| const int numChannels = png_get_channels(png, info); | |
| if (numChannels < 1 || numChannels > 4) { | |
| fprintf(stderr, "png_get_channels() should return 1, 2, 3 or 4 but returns %d.\n", numChannels); | |
| goto cleanup; | |
| } | |
| avifRGBImageSetDefaults(&rgb, avif); | |
| rgb.chromaDownsampling = chromaDownsampling; | |
| rgb.depth = imgBitDepth; | |
| if (numChannels == 1) { | |
| rgb.format = AVIF_RGB_FORMAT_GRAY; | |
| } else if (numChannels == 2) { | |
| rgb.format = AVIF_RGB_FORMAT_GRAYA; | |
| } else if (numChannels == 3) { | |
| rgb.format = AVIF_RGB_FORMAT_RGB; | |
| } | |
| rgb.ignoreAlpha = ignoreAlpha; | |
| if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Conversion to YUV failed: %s (out of memory)\n", inputFilename); | |
| goto cleanup; | |
| } | |
| // png_read_image() receives the row pointers but not the row buffer size. Verify the row | |
| // buffer size is exactly what libpng expects. If they are different, we have a bug and should | |
| // not proceed. | |
| const size_t rowBytes = png_get_rowbytes(png, info); | |
| if (rgb.rowBytes != rowBytes) { | |
| fprintf(stderr, "avifPNGRead internal error: rowBytes mismatch libavif %u vs libpng %" AVIF_FMT_ZU "\n", rgb.rowBytes, rowBytes); | |
| goto cleanup; | |
| } | |
| rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * rgb.height); | |
| if (rowPointers == NULL) { | |
| fprintf(stderr, "avifPNGRead internal error: memory allocation failure"); | |
| goto cleanup; | |
| } | |
| uint8_t * rgbRow = rgb.pixels; | |
| for (uint32_t y = 0; y < rgb.height; ++y) { | |
| rowPointers[y] = rgbRow; | |
| rgbRow += rgb.rowBytes; | |
| } | |
| png_read_image(png, rowPointers); | |
| if (avifImageRGBToYUV(avif, &rgb) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Conversion to YUV failed: %s\n", inputFilename); | |
| goto cleanup; | |
| } | |
| // Read Exif metadata at the beginning of the file. | |
| if (!avifExtractExifAndXMP(png, info, &ignoreExif, &ignoreXMP, avif)) { | |
| goto cleanup; | |
| } | |
| // Read Exif or XMP metadata at the end of the file if there was none at the beginning. | |
| if (!ignoreExif || !ignoreXMP) { | |
| png_read_end(png, info); | |
| if (!avifExtractExifAndXMP(png, info, &ignoreExif, &ignoreXMP, avif)) { | |
| goto cleanup; | |
| } | |
| } | |
| readResult = AVIF_TRUE; | |
| cleanup: | |
| if (png) { | |
| png_destroy_read_struct(&png, &info, NULL); | |
| } | |
| if (rowPointers) { | |
| free(rowPointers); | |
| } | |
| avifRGBImageFreePixels(&rgb); | |
| return readResult; | |
| } | |
| avifBool avifPNGRead(const char * inputFilename, | |
| avifImage * avif, | |
| avifPixelFormat requestedFormat, | |
| uint32_t requestedDepth, | |
| avifChromaDownsampling chromaDownsampling, | |
| avifBool ignoreColorProfile, | |
| avifBool ignoreExif, | |
| avifBool ignoreXMP, | |
| avifBool ignoreAlpha, | |
| uint32_t imageSizeLimit, | |
| uint32_t * outPNGDepth) | |
| { | |
| FILE * f; | |
| if (inputFilename) { | |
| f = fopen(inputFilename, "rb"); | |
| if (!f) { | |
| fprintf(stderr, "Can't open PNG file for read: %s\n", inputFilename); | |
| return AVIF_FALSE; | |
| } | |
| } else { | |
| f = stdin; | |
| inputFilename = "(stdin)"; | |
| } | |
| const avifBool res = avifPNGReadImpl(f, | |
| inputFilename, | |
| avif, | |
| requestedFormat, | |
| requestedDepth, | |
| chromaDownsampling, | |
| ignoreColorProfile, | |
| ignoreExif, | |
| ignoreXMP, | |
| ignoreAlpha, | |
| imageSizeLimit, | |
| outPNGDepth, | |
| /*headerOnly=*/AVIF_FALSE); | |
| if (f != stdin) { | |
| fclose(f); | |
| } | |
| return res; | |
| } | |
| avifBool avifPNGPeek(const char * inputFilename, avifImage * avif) | |
| { | |
| FILE * f = fopen(inputFilename, "rb"); | |
| if (!f) { | |
| fprintf(stderr, "Can't open PNG file for read: %s\n", inputFilename); | |
| return AVIF_FALSE; | |
| } | |
| const avifBool res = avifPNGReadImpl(f, | |
| inputFilename, | |
| avif, | |
| /*requestedFormat=*/AVIF_PIXEL_FORMAT_NONE, | |
| /*requestedDepth=*/0, | |
| AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, | |
| /*ignoreColorProfile=*/AVIF_TRUE, | |
| /*ignoreExif=*/AVIF_TRUE, | |
| /*ignoreXMP=*/AVIF_TRUE, | |
| /*ignoreAlpha=*/AVIF_TRUE, | |
| /*imageSizeLimit=*/UINT32_MAX, | |
| /*outPNGDepth=*/NULL, | |
| /*headerOnly=*/AVIF_TRUE); | |
| fclose(f); | |
| return res; | |
| } | |
| //------------------------------------------------------------------------------ | |
| // Writing | |
| avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint32_t requestedDepth, avifChromaUpsampling chromaUpsampling, int compressionLevel) | |
| { | |
| volatile avifBool writeResult = AVIF_FALSE; | |
| png_structp png = NULL; | |
| png_infop info = NULL; | |
| avifRWData xmp = { NULL, 0 }; | |
| png_bytep * volatile rowPointers = NULL; | |
| FILE * volatile f = NULL; | |
| avifRGBImage rgbData; | |
| memset(&rgbData, 0, sizeof(avifRGBImage)); | |
| volatile int rgbDepth = requestedDepth; | |
| if (rgbDepth == 0) { | |
| rgbDepth = (avif->depth > 8) ? 16 : 8; | |
| } | |
| if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) { | |
| fprintf(stderr, "AVIF_MATRIX_COEFFICIENTS_YCGCO_RO cannot be used with PNG because it has an even bit depth.\n"); | |
| goto cleanup; | |
| } | |
| if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) { | |
| if (avif->depth != 10) { | |
| fprintf(stderr, "avif->depth must be 10 bits and not %u.\n", avif->depth); | |
| goto cleanup; | |
| } | |
| if (requestedDepth && requestedDepth != 8) { | |
| fprintf(stderr, "Cannot request %u bits for YCgCo-Re as it only works for 8 bits.\n", requestedDepth); | |
| goto cleanup; | |
| } | |
| rgbDepth = 8; | |
| } | |
| volatile avifBool hasTransforms = avif->transformFlags & (AVIF_TRANSFORM_CLAP | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR); | |
| volatile avifBool copyYPlane = (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) && !avif->alphaPlane && (avif->depth == 8) && | |
| (rgbDepth == 8) && !hasTransforms; | |
| volatile int colorType; | |
| if (copyYPlane) { | |
| colorType = PNG_COLOR_TYPE_GRAY; | |
| } else { | |
| avifRGBImageSetDefaults(&rgbData, avif); | |
| rgbData.depth = rgbDepth; | |
| if (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 && avif->alphaPlane) { | |
| colorType = PNG_COLOR_TYPE_GRAY_ALPHA; | |
| rgbData.format = AVIF_RGB_FORMAT_GRAYA; | |
| } else if (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 && !avif->alphaPlane) { | |
| colorType = PNG_COLOR_TYPE_GRAY; | |
| rgbData.format = AVIF_RGB_FORMAT_GRAY; | |
| } else { | |
| rgbData.chromaUpsampling = chromaUpsampling; | |
| colorType = PNG_COLOR_TYPE_RGBA; | |
| if (avifImageIsOpaque(avif)) { | |
| colorType = PNG_COLOR_TYPE_RGB; | |
| rgbData.format = AVIF_RGB_FORMAT_RGB; | |
| } | |
| } | |
| if (avifRGBImageAllocatePixels(&rgbData) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Conversion to RGB failed: %s (out of memory)\n", outputFilename); | |
| goto cleanup; | |
| } | |
| if (avifImageYUVToRGB(avif, &rgbData) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Conversion to RGB failed: %s\n", outputFilename); | |
| goto cleanup; | |
| } | |
| } | |
| // rgbView is a view on rgbData. avifApplyTransforms() may modify rgbData. | |
| avifRGBImage rgbView = rgbData; | |
| avifResult transformResult = avifApplyTransforms(&rgbView, &rgbData, avif); | |
| if (transformResult != AVIF_RESULT_OK) { | |
| if (transformResult == AVIF_RESULT_INVALID_ARGUMENT) { | |
| fprintf(stderr, "Warning, ignoring invalid transforms (clap/irot/imir)\n"); | |
| } else { | |
| fprintf(stderr, "Failed to apply transforms: %s\n", avifResultToString(transformResult)); | |
| goto cleanup; | |
| } | |
| } | |
| f = fopen(outputFilename, "wb"); | |
| if (!f) { | |
| fprintf(stderr, "Can't open PNG file for write: %s\n", outputFilename); | |
| goto cleanup; | |
| } | |
| png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |
| if (!png) { | |
| fprintf(stderr, "Cannot init libpng (png): %s\n", outputFilename); | |
| goto cleanup; | |
| } | |
| png_set_benign_errors(png, 1); // Treat benign errors as warnings. | |
| info = png_create_info_struct(png); | |
| if (!info) { | |
| fprintf(stderr, "Cannot init libpng (info): %s\n", outputFilename); | |
| goto cleanup; | |
| } | |
| if (setjmp(png_jmpbuf(png))) { | |
| fprintf(stderr, "Error writing PNG: %s\n", outputFilename); | |
| goto cleanup; | |
| } | |
| png_init_io(png, f); | |
| // Don't bother complaining about ICC profile's contents when transferring from AVIF to PNG. | |
| // It is up to the enduser to decide if they want to keep their ICC profiles or not. | |
| #if defined(PNG_SKIP_sRGB_CHECK_PROFILE) && defined(PNG_SET_OPTION_SUPPORTED) // See libpng-manual.txt, section XII. | |
| png_set_option(png, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); | |
| #endif | |
| if (compressionLevel >= 0) { | |
| png_set_compression_level(png, compressionLevel); | |
| } | |
| volatile uint32_t width = copyYPlane ? avif->width : rgbView.width; | |
| volatile uint32_t height = copyYPlane ? avif->height : rgbView.height; | |
| png_set_IHDR(png, info, width, height, rgbDepth, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); | |
| const avifBool hasIcc = avif->icc.data && (avif->icc.size > 0); | |
| if (hasIcc) { | |
| // If there is an ICC profile, the CICP values are irrelevant and only the ICC profile | |
| // is written. If we could extract the primaries/transfer curve from the ICC profile, | |
| // then they could be written in cHRM/gAMA chunks. | |
| png_set_iCCP(png, info, "libavif", 0, avif->icc.data, (png_uint_32)avif->icc.size); | |
| } else { | |
| const avifBool isSrgb = (avif->colorPrimaries == AVIF_COLOR_PRIMARIES_SRGB) && | |
| (avif->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_SRGB); | |
| if (isSrgb) { | |
| png_set_sRGB_gAMA_and_cHRM(png, info, PNG_sRGB_INTENT_PERCEPTUAL); | |
| } else { | |
| if (avif->colorPrimaries != AVIF_COLOR_PRIMARIES_UNKNOWN && avif->colorPrimaries != AVIF_COLOR_PRIMARIES_UNSPECIFIED) { | |
| float primariesCoords[8]; | |
| avifColorPrimariesGetValues(avif->colorPrimaries, primariesCoords); | |
| png_set_cHRM(png, | |
| info, | |
| primariesCoords[6], | |
| primariesCoords[7], | |
| primariesCoords[0], | |
| primariesCoords[1], | |
| primariesCoords[2], | |
| primariesCoords[3], | |
| primariesCoords[4], | |
| primariesCoords[5]); | |
| } | |
| float gamma; | |
| // Write the transfer characteristics IF it can be represented as a | |
| // simple gamma value. Most transfer characteristics cannot be | |
| // represented this way. Viewers that support the cICP chunk can use | |
| // that instead, but older viewers might show incorrect colors. | |
| if (avifTransferCharacteristicsGetGamma(avif->transferCharacteristics, &gamma) == AVIF_RESULT_OK) { | |
| png_set_gAMA(png, info, 1.0f / gamma); | |
| } | |
| } | |
| } | |
| png_text texts[2]; | |
| int numTextMetadataChunks = 0; | |
| if (avif->exif.data && (avif->exif.size > 0)) { | |
| if (avif->exif.size > UINT32_MAX) { | |
| fprintf(stderr, "Error writing PNG: Exif metadata is too big\n"); | |
| goto cleanup; | |
| } | |
| // Make a copy of the Exif data to avoid modifying the input image when setting orientation. | |
| avifRWData exif = { NULL, 0 }; | |
| if (avifRWDataRealloc(&exif, avif->exif.size) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Error writing PNG metadata: out of memory\n"); | |
| goto cleanup; | |
| } | |
| memcpy(exif.data, avif->exif.data, avif->exif.size); | |
| // We already rotated the pixels if necessary in avifApplyTransforms(), so we set the orientation to 1 (no rotation, no mirror). | |
| avifResult result = avifSetExifOrientation(&exif, 1); | |
| if (result != AVIF_RESULT_OK) { | |
| if (result == AVIF_RESULT_INVALID_EXIF_PAYLOAD || result == AVIF_RESULT_NOT_IMPLEMENTED) { | |
| // Either the Exif is invalid, or it doesn't have an orientation field. | |
| // If it's invalid, we can consider it as equivalent to not having an orientation. | |
| // In both cases, we can ignore the error. | |
| } else { | |
| fprintf(stderr, "Error writing PNG metadata: %s\n", avifResultToString(result)); | |
| avifRWDataFree(&exif); | |
| goto cleanup; | |
| } | |
| } | |
| png_set_eXIf_1(png, info, (png_uint_32)exif.size, exif.data); | |
| avifRWDataFree(&exif); | |
| } | |
| if (avif->xmp.data && (avif->xmp.size > 0)) { | |
| // The iTXt XMP payload may not contain a zero byte according to section 4.2.3.3 of | |
| // the PNG specification, version 1.2. | |
| // The chunk is given to libpng as is. Bytes after a zero byte may be stripped. | |
| // Providing the length through png_text.itxt_length does not work. | |
| // The given png_text.text string must end with a zero byte. | |
| if (avif->xmp.size >= SIZE_MAX) { | |
| fprintf(stderr, "Error writing PNG: XMP metadata is too big\n"); | |
| goto cleanup; | |
| } | |
| if (avifRWDataRealloc(&xmp, avif->xmp.size + 1) != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Error writing PNG: out of memory\n"); | |
| goto cleanup; | |
| } | |
| memcpy(xmp.data, avif->xmp.data, avif->xmp.size); | |
| xmp.data[avif->xmp.size] = '\0'; | |
| png_text * text = &texts[numTextMetadataChunks++]; | |
| memset(text, 0, sizeof(*text)); | |
| text->compression = PNG_ITXT_COMPRESSION_NONE; | |
| text->key = "XML:com.adobe.xmp"; | |
| text->text = (char *)xmp.data; | |
| text->itxt_length = xmp.size; | |
| } | |
| if (numTextMetadataChunks != 0) { | |
| png_set_text(png, info, texts, numTextMetadataChunks); | |
| } | |
| png_write_info(png, info); | |
| // Custom chunk writing, must appear after png_write_info. | |
| // With AVIF, an ICC profile takes priority over CICP, but with PNG files, CICP takes priority over ICC. | |
| // Therefore CICP should only be written if there is no ICC profile. | |
| if (!hasIcc) { | |
| const png_byte cicp[5] = "cICP"; | |
| const png_byte cicpData[4] = { (png_byte)avif->colorPrimaries, | |
| (png_byte)avif->transferCharacteristics, | |
| AVIF_MATRIX_COEFFICIENTS_IDENTITY, | |
| 1 /*full range*/ }; | |
| png_write_chunk(png, cicp, cicpData, 4); | |
| } | |
| rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * height); | |
| if (rowPointers == NULL) { | |
| fprintf(stderr, "Error writing PNG: memory allocation failure"); | |
| goto cleanup; | |
| } | |
| uint8_t * row; | |
| uint32_t rowBytes; | |
| if (copyYPlane) { | |
| row = avif->yuvPlanes[AVIF_CHAN_Y]; | |
| rowBytes = avif->yuvRowBytes[AVIF_CHAN_Y]; | |
| } else { | |
| row = rgbView.pixels; | |
| rowBytes = rgbView.rowBytes; | |
| } | |
| for (uint32_t y = 0; y < height; ++y) { | |
| rowPointers[y] = row; | |
| row += rowBytes; | |
| } | |
| if (rgbDepth > 8) { | |
| png_set_swap(png); | |
| } | |
| png_write_image(png, rowPointers); | |
| png_write_end(png, NULL); | |
| writeResult = AVIF_TRUE; | |
| printf("Wrote PNG: %s\n", outputFilename); | |
| cleanup: | |
| if (f) { | |
| fclose(f); | |
| } | |
| if (png) { | |
| png_destroy_write_struct(&png, &info); | |
| } | |
| avifRWDataFree(&xmp); | |
| if (rowPointers) { | |
| free(rowPointers); | |
| } | |
| avifRGBImageFreePixels(&rgbData); | |
| return writeResult; | |
| } |