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);
+          }
+        }
       }
     }
   }