diff --git a/CHANGELOG.md b/CHANGELOG.md index 0350b30315..3bea607dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,9 @@ The changes are relative to the previous release, unless the baseline is specifi * Fix local libargparse dependency patch step on macOS 10.15 and earlier. * Patch local libyuv dependency for compatibility with gcc 10. * Use stricter C99 syntax to avoid related compilation issues. -* Reject the conversion in avifenc of non-monochrome input to monochrome when an - ICC profile is present and not explicitly discarded. +* Reject the conversion in avifenc from non-monochrome/monochrome to + monochrome/non-monochrome when an ICC profile is present and not explicitly + discarded. ## [1.2.0] - 2025-02-25 diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c index 88b58cd912..563d54d0e1 100644 --- a/apps/shared/avifjpeg.c +++ b/apps/shared/avifjpeg.c @@ -909,12 +909,19 @@ static avifBool avifJPEGReadInternal(FILE * f, unsigned int iccDataLen; if (read_icc_profile(&cinfo, &iccDataTmp, &iccDataLen)) { iccData = iccDataTmp; - if (requestedFormat == AVIF_PIXEL_FORMAT_YUV400) { + const avifBool is_gray = (cinfo.jpeg_color_space == JCS_GRAYSCALE); + if (!is_gray && (requestedFormat == 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 (is_gray && requestedFormat != 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, iccDataTmp, (size_t)iccDataLen) != AVIF_RESULT_OK) { fprintf(stderr, "Setting ICC profile failed: %s (out of memory)\n", inputFilename); goto cleanup; @@ -1283,8 +1290,9 @@ avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int jpeg_stdio_dest(&cinfo, f); cinfo.image_width = avif->width; cinfo.image_height = avif->height; - cinfo.input_components = 3; - cinfo.in_color_space = JCS_RGB; + const avifBool is_gray = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400; + cinfo.input_components = is_gray ? 1 : 3; + cinfo.in_color_space = is_gray ? JCS_GRAYSCALE : JCS_RGB; jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, jpegQuality, TRUE); jpeg_start_compress(&cinfo, TRUE); diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c index 6abd16b8cd..388ac2b124 100644 --- a/apps/shared/avifpng.c +++ b/apps/shared/avifpng.c @@ -298,7 +298,8 @@ avifBool avifPNGRead(const char * inputFilename, png_set_tRNS_to_alpha(png); } - if ((rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_GRAY_ALPHA)) { + const avifBool raw_color_type_is_gray = (rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_GRAY_ALPHA); + if (raw_color_type_is_gray) { png_set_gray_to_rgb(png); } @@ -322,7 +323,7 @@ avifBool avifPNGRead(const char * inputFilename, goto cleanup; } if (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE) { - if ((rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_GRAY_ALPHA)) { + if (raw_color_type_is_gray) { avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; } else if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY || avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) { @@ -365,8 +366,13 @@ avifBool avifPNGRead(const char * inputFilename, // When the sRGB / iCCP chunk is present, applications that recognize it and are capable of color management // must ignore the gAMA and cHRM chunks and use the sRGB / iCCP chunk instead. if (png_get_iCCP(png, info, &iccpProfileName, &iccpCompression, &iccpData, &iccpDataLen) == PNG_INFO_iCCP) { - if (rawColorType != PNG_COLOR_TYPE_GRAY && rawColorType != PNG_COLOR_TYPE_GRAY_ALPHA && - avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) { + if (!raw_color_type_is_gray && 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 (raw_color_type_is_gray && 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"); diff --git a/tests/gtest/avifreadimagetest.cc b/tests/gtest/avifreadimagetest.cc index 68e128583d..72b8e50f60 100644 --- a/tests/gtest/avifreadimagetest.cc +++ b/tests/gtest/avifreadimagetest.cc @@ -4,6 +4,8 @@ #include #include "avif/avif.h" +#include "avifjpeg.h" +#include "avifpng.h" #include "aviftest_helpers.h" #include "avifutil.h" #include "gtest/gtest.h" @@ -289,29 +291,80 @@ TEST(ICCTest, GeneratedICCHash) { 0); } -// Verify the invalidity of keeping the ICC profile for a gray image read from -// an RGB image. -TEST(ICCTest, RGB2Gray) { - for (const auto& file_name : - {"paris_icc_exif_xmp.png", "paris_exif_xmp_icc.jpg"}) { - const std::string file_path = std::string(data_path) + file_name; - for (bool ignore_icc : {false, true}) { - ImagePtr image(avifImageCreateEmpty()); - // Read the image. - const avifAppFileFormat file_format = avifReadImage( - file_path.c_str(), - /*requestedFormat=*/AVIF_PIXEL_FORMAT_YUV400, - /*requestedDepth=*/0, - /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, - /*ignoreColorProfile=*/ignore_icc, /*ignoreExif=*/false, - /*ignoreXMP=*/false, /*allowChangingCicp=*/true, - /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), - /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, - /*frameIter=*/nullptr); - if (ignore_icc) { - ASSERT_NE(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN); +// Simpler function to read an image. +static avifAppFileFormat avifReadImageForRGB2Gray2RGB(const std::string& path, + avifPixelFormat format, + bool ignore_icc, + ImagePtr& image) { + return avifReadImage( + path.c_str(), format, /*requestedDepth=*/0, + /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, + /*ignoreColorProfile=*/ignore_icc, /*ignoreExif=*/false, + /*ignoreXMP=*/false, /*allowChangingCicp=*/true, + /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), + /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, + /*frameIter=*/nullptr); +} + +// Verify the invalidity of keeping the ICC profile for a gray/color image read +// from a color/gray image. +TEST(ICCTest, RGB2Gray2RGB) { + constexpr char file_name[] = "paris_icc_exif_xmp.png"; + const std::string file_path = std::string(data_path) + file_name; + + for (auto format : {AVIF_PIXEL_FORMAT_YUV400, AVIF_PIXEL_FORMAT_YUV444}) { + // Read the ground truth image in the appropriate format. + ImagePtr image(avifImageCreateEmpty()); + ASSERT_NE(image, nullptr); + ASSERT_NE(avifReadImageForRGB2Gray2RGB(file_path, format, + /*ignore_icc=*/true, image), + AVIF_APP_FILE_FORMAT_UNKNOWN); + + // Add an ICC profile. + float primariesCoords[8]; + avifColorPrimariesGetValues(AVIF_COLOR_PRIMARIES_BT709, primariesCoords); + + testutil::AvifRwData icc; + if (format == AVIF_PIXEL_FORMAT_YUV400) { + EXPECT_EQ(avifGenerateGrayICC(&icc, 2.2f, primariesCoords), AVIF_TRUE); + } else { + EXPECT_EQ(avifGenerateRGBICC(&icc, 2.2f, primariesCoords), AVIF_TRUE); + } + ASSERT_EQ(avifImageSetProfileICC(image.get(), icc.data, icc.size), + AVIF_RESULT_OK); + + for (const std::string ext : {"png", "jpg"}) { + // Write the image with the appropriate codec. + const std::string tmp_path = + testing::TempDir() + "tmp_RGB2Gray2RGB." + ext; + if (ext == "png") { + ASSERT_EQ( + avifPNGWrite(tmp_path.c_str(), image.get(), /*requestedDepth=*/0, + AVIF_CHROMA_UPSAMPLING_BEST_QUALITY, + /*compressionLevel=*/0), + AVIF_TRUE); } else { - ASSERT_EQ(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN); + ASSERT_EQ( + avifJPEGWrite(tmp_path.c_str(), image.get(), /*jpegQuality=*/75, + AVIF_CHROMA_UPSAMPLING_BEST_QUALITY), + AVIF_TRUE); + } + + for (bool ignore_icc : {false, true}) { + for (auto new_format : + {AVIF_PIXEL_FORMAT_YUV400, AVIF_PIXEL_FORMAT_YUV444}) { + ImagePtr new_image(avifImageCreateEmpty()); + ASSERT_NE(new_image, nullptr); + const avifAppFileFormat new_file_format = + avifReadImageForRGB2Gray2RGB(new_path, new_format, ignore_icc, + new_image); + if (format == new_format || ignore_icc) { + ASSERT_NE(new_file_format, AVIF_APP_FILE_FORMAT_UNKNOWN); + } else { + // When formats are different, the ICC cannot be kpet. + ASSERT_EQ(new_file_format, AVIF_APP_FILE_FORMAT_UNKNOWN); + } + } } } }