Fix reading gray/color ICC profile from color/gray image in apps (#2675)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0350b30..3bea607 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,8 +19,9 @@
* 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 88b58cd..bd2f272 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -909,12 +909,19 @@
unsigned int iccDataLen;
if (read_icc_profile(&cinfo, &iccDataTmp, &iccDataLen)) {
iccData = iccDataTmp;
- if (requestedFormat == AVIF_PIXEL_FORMAT_YUV400) {
+ const avifBool isGray = (cinfo.jpeg_color_space == JCS_GRAYSCALE);
+ if (!isGray && (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 (isGray && 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 @@
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 isGray = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400;
+ cinfo.input_components = isGray ? 1 : 3;
+ cinfo.in_color_space = isGray ? 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 6abd16b..3ff21b9 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -298,7 +298,8 @@
png_set_tRNS_to_alpha(png);
}
- if ((rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_GRAY_ALPHA)) {
+ const avifBool rawColorTypeIsGray = (rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_GRAY_ALPHA);
+ if (rawColorTypeIsGray) {
png_set_gray_to_rgb(png);
}
@@ -322,7 +323,7 @@
goto cleanup;
}
if (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE) {
- if ((rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_GRAY_ALPHA)) {
+ if (rawColorTypeIsGray) {
avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV400;
} else if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY ||
avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) {
@@ -365,13 +366,18 @@
// 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 (!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;
diff --git a/tests/gtest/avifreadimagetest.cc b/tests/gtest/avifreadimagetest.cc
index 68e1285..62f56d5 100644
--- a/tests/gtest/avifreadimagetest.cc
+++ b/tests/gtest/avifreadimagetest.cc
@@ -4,6 +4,8 @@
#include <string>
#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 @@
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 new_path =
+ testing::TempDir() + "tmp_RGB2Gray2RGB." + ext;
+ if (ext == "png") {
+ ASSERT_EQ(
+ avifPNGWrite(new_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(new_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 kept.
+ ASSERT_EQ(new_file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
+ }
+ }
}
}
}