Add --icc flag to avifdec (#2042)

Same meaning as in avifenc.
Move readEntireFile() from avifenc to avifutil.

Add CHANGELOG.md entry.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa146dc..16c0c4c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,7 @@
 * Add avifResult enum entry AVIF_RESULT_INTERNAL_ERROR.
 * Require libyuv by default (but it can still be disabled with
   -DAVIF_LIBYUV=OFF).
+* Add avifdec --icc flag to override the output color profile.
 
 ### Changed since 1.0.0
 * Update aom.cmd: v3.8.1
diff --git a/apps/avifdec.c b/apps/avifdec.c
index 705db3c..1c1788b 100644
--- a/apps/avifdec.c
+++ b/apps/avifdec.c
@@ -43,6 +43,7 @@
     printf("                        avifdec will use --index to choose which layer to decode (in progressive order).\n");
     printf("    --no-strict       : Disable strict decoding, which disables strict validation checks and errors\n");
     printf("    -i,--info         : Decode all frames and display all image information instead of saving to disk\n");
+    printf("    --icc FILENAME    : Provide an ICC profile payload (implies --ignore-icc)\n");
     printf("    --ignore-icc      : If the input file contains an embedded ICC profile, ignore it (no-op if absent)\n");
     printf("    --size-limit C    : Specifies the image size limit (in total pixels) that should be tolerated.\n");
     printf("                        Default: %u, set to a smaller value to further restrict.\n", AVIF_DEFAULT_IMAGE_SIZE_LIMIT);
@@ -64,6 +65,7 @@
     avifCodecChoice codecChoice = AVIF_CODEC_CHOICE_AUTO;
     avifBool infoOnly = AVIF_FALSE;
     avifChromaUpsampling chromaUpsampling = AVIF_CHROMA_UPSAMPLING_AUTOMATIC;
+    const char * iccOverrideFilename = NULL;
     avifBool ignoreICC = AVIF_FALSE;
     avifBool rawColor = AVIF_FALSE;
     avifBool allowProgressive = AVIF_FALSE;
@@ -178,6 +180,10 @@
             strictFlags = AVIF_STRICT_DISABLED;
         } else if (!strcmp(arg, "-i") || !strcmp(arg, "--info")) {
             infoOnly = AVIF_TRUE;
+        } else if (!strcmp(arg, "--icc")) {
+            NEXTARG();
+            iccOverrideFilename = arg;
+            ignoreICC = AVIF_TRUE;
         } else if (!strcmp(arg, "--ignore-icc")) {
             ignoreICC = AVIF_TRUE;
         } else if (!strcmp(arg, "--size-limit")) {
@@ -355,11 +361,30 @@
         assert(result == AVIF_RESULT_OK);
     }
 
+    if (iccOverrideFilename) {
+        avifRWData iccOverride = AVIF_DATA_EMPTY;
+        if (!avifReadEntireFile(iccOverrideFilename, &iccOverride)) {
+            fprintf(stderr, "ERROR: Unable to read ICC: %s\n", iccOverrideFilename);
+            avifRWDataFree(&iccOverride);
+            goto cleanup;
+        }
+        printf("[--icc] Setting ICC profile: %s\n", iccOverrideFilename);
+        result = avifImageSetProfileICC(decoder->image, iccOverride.data, iccOverride.size);
+        avifRWDataFree(&iccOverride);
+        if (result != AVIF_RESULT_OK) {
+            fprintf(stderr, "ERROR: Failed to set ICC: %s\n", avifResultToString(result));
+            goto cleanup;
+        }
+    }
+
     avifAppFileFormat outputFormat = avifGuessFileFormat(outputFilename);
     if (outputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
         fprintf(stderr, "Cannot determine output file extension: %s\n", outputFilename);
         goto cleanup;
     } else if (outputFormat == AVIF_APP_FILE_FORMAT_Y4M) {
+        if (decoder->image->icc.size || decoder->image->exif.size || decoder->image->xmp.size) {
+            printf("Warning: metadata dropped when saving to y4m.\n");
+        }
         if (!y4mWrite(outputFilename, decoder->image)) {
             goto cleanup;
         }
diff --git a/apps/avifenc.c b/apps/avifenc.c
index 980711d..7abbf4b 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -653,36 +653,6 @@
     return AVIF_TRUE;
 }
 
-static avifBool readEntireFile(const char * filename, avifRWData * raw)
-{
-    FILE * f = fopen(filename, "rb");
-    if (!f) {
-        return AVIF_FALSE;
-    }
-
-    fseek(f, 0, SEEK_END);
-    long pos = ftell(f);
-    if (pos <= 0) {
-        fclose(f);
-        return AVIF_FALSE;
-    }
-    size_t fileSize = (size_t)pos;
-    fseek(f, 0, SEEK_SET);
-
-    if (avifRWDataRealloc(raw, fileSize) != AVIF_RESULT_OK) {
-        fclose(f);
-        return AVIF_FALSE;
-    }
-    size_t bytesRead = fread(raw->data, 1, fileSize, f);
-    fclose(f);
-
-    if (bytesRead != fileSize) {
-        avifRWDataFree(raw);
-        return AVIF_FALSE;
-    }
-    return AVIF_TRUE;
-}
-
 // Returns NULL if a memory allocation failed. The return value should be freed with free().
 static char * avifStrdup(const char * str)
 {
@@ -1773,21 +1743,21 @@
             }
         } else if (!strcmp(arg, "--exif")) {
             NEXTARG();
-            if (!readEntireFile(arg, &exifOverride)) {
+            if (!avifReadEntireFile(arg, &exifOverride)) {
                 fprintf(stderr, "ERROR: Unable to read Exif metadata: %s\n", arg);
                 goto cleanup;
             }
             settings.ignoreExif = AVIF_TRUE;
         } else if (!strcmp(arg, "--xmp")) {
             NEXTARG();
-            if (!readEntireFile(arg, &xmpOverride)) {
+            if (!avifReadEntireFile(arg, &xmpOverride)) {
                 fprintf(stderr, "ERROR: Unable to read XMP metadata: %s\n", arg);
                 goto cleanup;
             }
             settings.ignoreXMP = AVIF_TRUE;
         } else if (!strcmp(arg, "--icc")) {
             NEXTARG();
-            if (!readEntireFile(arg, &iccOverride)) {
+            if (!avifReadEntireFile(arg, &iccOverride)) {
                 fprintf(stderr, "ERROR: Unable to read ICC profile: %s\n", arg);
                 goto cleanup;
             }
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index 20f54ea..3af0cdf 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -348,6 +348,36 @@
     return format;
 }
 
+avifBool avifReadEntireFile(const char * filename, avifRWData * raw)
+{
+    FILE * f = fopen(filename, "rb");
+    if (!f) {
+        return AVIF_FALSE;
+    }
+
+    fseek(f, 0, SEEK_END);
+    long pos = ftell(f);
+    if (pos <= 0) {
+        fclose(f);
+        return AVIF_FALSE;
+    }
+    size_t fileSize = (size_t)pos;
+    fseek(f, 0, SEEK_SET);
+
+    if (avifRWDataRealloc(raw, fileSize) != AVIF_RESULT_OK) {
+        fclose(f);
+        return AVIF_FALSE;
+    }
+    size_t bytesRead = fread(raw->data, 1, fileSize, f);
+    fclose(f);
+
+    if (bytesRead != fileSize) {
+        avifRWDataFree(raw);
+        return AVIF_FALSE;
+    }
+    return AVIF_TRUE;
+}
+
 void avifImageFixXMP(avifImage * image)
 {
     // Zero bytes are forbidden in UTF-8 XML: https://en.wikipedia.org/wiki/Valid_characters_in_XML
diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h
index 55edd52..1870097 100644
--- a/apps/shared/avifutil.h
+++ b/apps/shared/avifutil.h
@@ -84,6 +84,9 @@
                                 avifAppSourceTiming * sourceTiming,
                                 struct y4mFrameIterator ** frameIter);
 
+// Copies all the bytes from the file at filename to a newly allocated memory chunk.
+avifBool avifReadEntireFile(const char * filename, avifRWData * raw);
+
 // Removes a single trailing null character from the image->xmp, if there is exactly one.
 void avifImageFixXMP(avifImage * image);
 
diff --git a/tests/test_cmd_metadata.sh b/tests/test_cmd_metadata.sh
index 5e8ebd9..cfbd9fc 100755
--- a/tests/test_cmd_metadata.sh
+++ b/tests/test_cmd_metadata.sh
@@ -38,19 +38,24 @@
 fi
 
 AVIFENC="${BINARY_DIR}/avifenc"
+AVIFDEC="${BINARY_DIR}/avifdec"
 
 # Input file paths.
 INPUT_PNG="${TESTDATA_DIR}/paris_icc_exif_xmp.png"
 INPUT_JPG="${TESTDATA_DIR}/paris_exif_xmp_icc.jpg"
+INPUT_ICC="${TESTDATA_DIR}/sRGB2014.icc"
 # Output file names.
 ENCODED_FILE="avif_test_cmd_metadata_encoded.avif"
 ENCODED_FILE_NO_METADATA="avif_test_cmd_metadata_encoded_no_metadata.avif"
 ENCODED_FILE_MORE_METADATA="avif_test_cmd_metadata_encoded_more_metadata.avif"
+DECODED_FILE="avif_test_cmd_metadata_decoded.png"
+DECODED_FILE_CHANGED_ICC="avif_test_cmd_metadata_decoded_changed_icc.png"
 
 # Cleanup
 cleanup() {
   pushd ${TMP_DIR}
-    rm -- "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" "${ENCODED_FILE_MORE_METADATA}"
+    rm -- "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" "${ENCODED_FILE_MORE_METADATA}" \
+          "${DECODED_FILE}" "${DECODED_FILE_CHANGED_ICC}"
   popd
 }
 trap cleanup EXIT
@@ -60,14 +65,24 @@
   echo "Testing metadata enc"
   for INPUT in "${INPUT_PNG}" "${INPUT_JPG}"; do
     "${AVIFENC}" "${INPUT}" -o "${ENCODED_FILE}"
+
     # Ignoring a metadata chunk should produce a different output file.
+
     "${AVIFENC}" "${INPUT}" -o "${ENCODED_FILE_NO_METADATA}" --ignore-icc
     cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
     "${AVIFENC}" "${INPUT}" -o "${ENCODED_FILE_NO_METADATA}" --ignore-exif
     cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
     "${AVIFENC}" "${INPUT}" -o "${ENCODED_FILE_NO_METADATA}" --ignore-xmp
     cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
+
+    "${AVIFDEC}" "${ENCODED_FILE}" "${DECODED_FILE}"
+    "${AVIFDEC}" "${ENCODED_FILE}" --ignore-icc "${DECODED_FILE_CHANGED_ICC}"
+    cmp "${DECODED_FILE}" "${DECODED_FILE_CHANGED_ICC}" && exit 1
+    "${AVIFDEC}" "${ENCODED_FILE}" --icc "${INPUT_ICC}" "${DECODED_FILE_CHANGED_ICC}"
+    cmp "${DECODED_FILE}" "${DECODED_FILE_CHANGED_ICC}" && exit 1
+
     # As should adding metadata.
+
     "${AVIFENC}" "${INPUT}" -o "${ENCODED_FILE_MORE_METADATA}" --clli 1000,50
     cmp "${ENCODED_FILE}" "${ENCODED_FILE_MORE_METADATA}" && exit 1
   done