Extract Exif to avifImage in avifPNGRead()

Use png_get_eXIf_1().
Add ignoreICC, ignoreExif, ignoreXMP to avifReadImage().
Add --ignore-exif and --ignore-xmp to avifenc.c.

Test in avifmetadatatest.cc and test_cmd.sh.
Add paris_exif_xmp_icc.jpg, paris_exif_xmp_icc.png and
paris_exif_at_end.png. I took and strongly compressed these
pictures. I redacted the GPS coordinates from the Exif entries.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24573f9..510475f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@
 * avifenc: Add --sharpyuv, which enables "sharp" RGB to YUV420 conversion, which
   reduces artifacts caused by 420 chroma downsampling. Needs libsharpyuv (part
   of the libwebp repository) at compile time.
+* avifenc: Add --ignore-exif and --ignore-xmp flags.
 
 ## [0.10.1] - 2022-04-11
 
diff --git a/apps/avifenc.c b/apps/avifenc.c
index 2e72a1c..c263a56 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -91,14 +91,16 @@
            AVIF_SPEED_SLOWEST,
            AVIF_SPEED_FASTEST);
     printf("    -c,--codec C                      : AV1 codec to use (choose from versions list below)\n");
-    printf("    --exif FILENAME                   : Provide an Exif metadata payload to be associated with the primary item\n");
-    printf("    --xmp FILENAME                    : Provide an XMP metadata payload to be associated with the primary item\n");
-    printf("    --icc FILENAME                    : Provide an ICC profile payload to be associated with the primary item\n");
+    printf("    --exif FILENAME                   : Provide an Exif metadata payload to be associated with the primary item (implies --ignore-exif)\n");
+    printf("    --xmp FILENAME                    : Provide an XMP metadata payload to be associated with the primary item (implies --ignore-xmp)\n");
+    printf("    --icc FILENAME                    : Provide an ICC profile payload to be associated with the primary item (implies --ignore-icc)\n");
     printf("    -a,--advanced KEY[=VALUE]         : Pass an advanced, codec-specific key/value string pair directly to the codec. avifenc will warn on any not used by the codec.\n");
     printf("    --duration D                      : Set all following frame durations (in timescales) to D; default 1. Can be set multiple times (before supplying each filename)\n");
     printf("    --timescale,--fps V               : Set the timescale to V. If all frames are 1 timescale in length, this is equivalent to frames per second (Default: 30)\n");
     printf("                                        If neither duration nor timescale are set, avifenc will attempt to use the framerate stored in a y4m header, if present.\n");
     printf("    -k,--keyframe INTERVAL            : Set the forced keyframe interval (maximum frames between keyframes). Set to 0 to disable (default).\n");
+    printf("    --ignore-exif                     : If the input file contains embedded Exif metadata, ignore it (no-op if absent)\n");
+    printf("    --ignore-xmp                      : If the input file contains embedded XMP metadata, ignore it (no-op if absent)\n");
     printf("    --ignore-icc                      : If the input file contains an embedded ICC profile, ignore it (no-op if absent)\n");
     printf("    --pasp H,V                        : Add pasp property (aspect ratio). H=horizontal spacing, V=vertical spacing\n");
     printf("    --crop CROPX,CROPY,CROPW,CROPH    : Add clap property (clean aperture), but calculated from a crop rectangle\n");
@@ -257,6 +259,9 @@
 }
 
 static avifAppFileFormat avifInputReadImage(avifInput * input,
+                                            avifBool ignoreICC,
+                                            avifBool ignoreExif,
+                                            avifBool ignoreXMP,
                                             avifImage * image,
                                             uint32_t * outDepth,
                                             avifAppSourceTiming * sourceTiming,
@@ -288,6 +293,9 @@
                                                             input->requestedFormat,
                                                             input->requestedDepth,
                                                             flags,
+                                                            ignoreICC,
+                                                            ignoreExif,
+                                                            ignoreXMP,
                                                             image,
                                                             outDepth,
                                                             sourceTiming,
@@ -441,6 +449,8 @@
     avifCodecChoice codecChoice = AVIF_CODEC_CHOICE_AUTO;
     avifRange requestedRange = AVIF_RANGE_FULL;
     avifBool lossless = AVIF_FALSE;
+    avifBool ignoreExif = AVIF_FALSE;
+    avifBool ignoreXMP = AVIF_FALSE;
     avifBool ignoreICC = AVIF_FALSE;
     avifEncoder * encoder = avifEncoderCreate();
     avifImage * image = NULL;
@@ -646,6 +656,7 @@
                 returnCode = 1;
                 goto cleanup;
             }
+            ignoreExif = AVIF_TRUE;
         } else if (!strcmp(arg, "--xmp")) {
             NEXTARG();
             if (!readEntireFile(arg, &xmpOverride)) {
@@ -653,6 +664,7 @@
                 returnCode = 1;
                 goto cleanup;
             }
+            ignoreXMP = AVIF_TRUE;
         } else if (!strcmp(arg, "--icc")) {
             NEXTARG();
             if (!readEntireFile(arg, &iccOverride)) {
@@ -660,6 +672,7 @@
                 returnCode = 1;
                 goto cleanup;
             }
+            ignoreICC = AVIF_TRUE;
         } else if (!strcmp(arg, "--duration")) {
             NEXTARG();
             int durationInt = atoi(arg);
@@ -707,6 +720,10 @@
             }
             avifEncoderSetCodecSpecificOption(encoder, tempBuffer, value);
             free(tempBuffer);
+        } else if (!strcmp(arg, "--ignore-exif")) {
+            ignoreExif = AVIF_TRUE;
+        } else if (!strcmp(arg, "--ignore-xmp")) {
+            ignoreXMP = AVIF_TRUE;
         } else if (!strcmp(arg, "--ignore-icc")) {
             ignoreICC = AVIF_TRUE;
         } else if (!strcmp(arg, "--pasp")) {
@@ -832,7 +849,8 @@
     avifInputFile * firstFile = avifInputGetNextFile(&input);
     uint32_t sourceDepth = 0;
     avifAppSourceTiming firstSourceTiming;
-    avifAppFileFormat inputFormat = avifInputReadImage(&input, image, &sourceDepth, &firstSourceTiming, flags);
+    avifAppFileFormat inputFormat =
+        avifInputReadImage(&input, ignoreICC, ignoreExif, ignoreXMP, image, &sourceDepth, &firstSourceTiming, flags);
     if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
         fprintf(stderr, "Cannot determine input file format: %s\n", firstFile->filename);
         returnCode = 1;
@@ -864,9 +882,6 @@
         }
     }
 
-    if (ignoreICC) {
-        avifImageSetProfileICC(image, NULL, 0);
-    }
     if (iccOverride.size) {
         avifImageSetProfileICC(image, iccOverride.data, iccOverride.size);
     }
@@ -1040,7 +1055,8 @@
             cellImage->alphaPremultiplied = image->alphaPremultiplied;
             gridCells[gridCellIndex] = cellImage;
 
-            avifAppFileFormat nextInputFormat = avifInputReadImage(&input, cellImage, NULL, NULL, flags);
+            avifAppFileFormat nextInputFormat =
+                avifInputReadImage(&input, ignoreICC, ignoreExif, ignoreXMP, cellImage, NULL, NULL, flags);
             if (nextInputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
                 returnCode = 1;
                 goto cleanup;
@@ -1179,7 +1195,8 @@
             nextImage->yuvRange = image->yuvRange;
             nextImage->alphaPremultiplied = image->alphaPremultiplied;
 
-            avifAppFileFormat nextInputFormat = avifInputReadImage(&input, nextImage, NULL, NULL, flags);
+            avifAppFileFormat nextInputFormat =
+                avifInputReadImage(&input, ignoreICC, ignoreExif, ignoreXMP, nextImage, NULL, NULL, flags);
             if (nextInputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
                 returnCode = 1;
                 goto cleanup;
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index 14ce4c5..07882fe 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -6,6 +6,9 @@
 
 #include "png.h"
 
+#include <ctype.h>
+#include <limits.h>
+#include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -17,6 +20,150 @@
 typedef png_charp png_iccp_datap;
 #endif
 
+// 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)
+{
+    // Remove any leading new line. They are not part of the numExpectedBytes.
+    while ((hexStringLength != 0) && (hexString[0] == '\n')) {
+        ++hexString;
+        --hexStringLength;
+    }
+    if (hexStringLength < (numExpectedBytes * 2)) {
+        fprintf(stderr, "Exif extraction failed: " AVIF_FMT_ZU " missing characters\n", (numExpectedBytes * 2) - hexStringLength);
+        return AVIF_FALSE;
+    }
+
+    // Preprocess the input hexString by removing a tag commonly added at encoding, if present.
+    // HEIF specification ISO-23008 section A.2.1 allows including and excluding it from AVIF files.
+    // The PNG 1.5 extension mentions the omission of this header for the modern standard eXIf chunk.
+    const char tagExif00[] = "457869660000"; // "Exif\0\0" tag encoded as a hexadecimal string.
+    const size_t tagExif00Len = 6 * 2;
+    if ((numExpectedBytes >= (tagExif00Len / 2)) && !memcmp(hexString, tagExif00, tagExif00Len)) {
+        hexString += tagExif00Len;
+        hexStringLength -= tagExif00Len;
+        numExpectedBytes -= (tagExif00Len / 2);
+    }
+    if (numExpectedBytes == 0) {
+        fprintf(stderr, "Exif extraction failed: empty payload\n");
+        return AVIF_FALSE;
+    }
+
+    avifRWDataRealloc(bytes, numExpectedBytes);
+    size_t numBytes = 0;
+    for (size_t i = 0; (i + 1 < hexStringLength) && (numBytes < numExpectedBytes);) {
+        if (hexString[i] == '\n') {
+            ++i;
+            continue;
+        }
+        if (!isxdigit(hexString[i]) || !isxdigit(hexString[i + 1])) {
+            avifRWDataFree(bytes);
+            fprintf(stderr, "Exif 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, "Exif 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, "Exif 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, "Exif 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, "Exif 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, "Exif 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, "Exif extraction failed: malformed or truncated raw profile\n");
+    return AVIF_FALSE;
+}
+
+// Returns AVIF_TRUE if there was no Exif metadata located at info or if the Exif metadata located at info
+// was correctly parsed and imported to avif->exif. Returns AVIF_FALSE in case of error.
+static avifBool avifExtractExif(png_structp png, png_infop const info, avifImage * avif)
+{
+#ifdef PNG_eXIf_SUPPORTED
+    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;
+        }
+        avifImageSetMetadataExif(avif, exif, exifSize);
+        return AVIF_TRUE;
+    }
+#endif // PNG_eXIf_SUPPORTED
+
+    png_textp text = NULL;
+    const png_uint_32 numTextChunks = png_get_text(png, info, &text, NULL);
+    for (png_uint_32 i = 0; i < numTextChunks; ++i, ++text) {
+        if (!strcmp(text->key, "Raw profile type exif") || !strcmp(text->key, "Raw profile type APP1")) {
+            png_size_t textLength;
+            switch (text->compression) {
+#ifdef PNG_iTXt_SUPPORTED
+                case PNG_ITXT_COMPRESSION_NONE:
+                case PNG_ITXT_COMPRESSION_zTXt:
+                    textLength = text->itxt_length;
+                    break;
+#endif
+                case PNG_TEXT_COMPRESSION_NONE:
+                case PNG_TEXT_COMPRESSION_zTXt:
+                default:
+                    textLength = text->text_length;
+                    break;
+            }
+            return avifCopyRawProfile(text->text, textLength, &avif->exif);
+        }
+    }
+    return AVIF_TRUE;
+}
+
 // Note on setjmp() and volatile variables:
 //
 // K & R, The C Programming Language 2nd Ed, p. 254 says:
@@ -36,11 +183,13 @@
                      avifPixelFormat requestedFormat,
                      uint32_t requestedDepth,
                      avifRGBToYUVFlags flags,
+                     avifBool ignoreExif,
                      uint32_t * outPNGDepth)
 {
     volatile avifBool readResult = AVIF_FALSE;
     png_structp png = NULL;
     png_infop info = NULL;
+    png_infop infoEnd = NULL;
     png_bytep * volatile rowPointers = NULL;
 
     avifRGBImage rgb;
@@ -90,6 +239,7 @@
     if (png_get_iCCP(png, info, &iccpProfileName, &iccpCompression, (png_iccp_datap *)&iccpData, &iccpDataLen) == PNG_INFO_iCCP) {
         avifImageSetProfileICC(avif, iccpData, iccpDataLen);
     }
+    // TODO(yguyon): Also check if there is a cICp chunk (https://github.com/AOMediaCodec/libavif/pull/1065#discussion_r958534232)
 
     int rawWidth = png_get_image_width(png, info);
     int rawHeight = png_get_image_height(png, info);
@@ -162,6 +312,26 @@
         fprintf(stderr, "Conversion to YUV failed: %s\n", inputFilename);
         goto cleanup;
     }
+
+    if (!ignoreExif) {
+        // Read Exif metadata at the beginning of the file.
+        if (!avifExtractExif(png, info, avif)) {
+            goto cleanup;
+        }
+        // Read Exif metadata at the end of the file if there was none at the beginning.
+        if (!avif->exif.data) {
+            infoEnd = png_create_info_struct(png);
+            if (!infoEnd) {
+                fprintf(stderr, "Cannot init libpng (infoEnd): %s\n", inputFilename);
+                goto cleanup;
+            }
+            png_read_end(png, infoEnd);
+            if (!avifExtractExif(png, infoEnd, avif)) {
+                goto cleanup;
+            }
+        }
+    }
+    // TODO(yguyon): Extract XMP to avif->xmp, if any.
     readResult = AVIF_TRUE;
 
 cleanup:
@@ -170,6 +340,7 @@
     }
     if (png) {
         png_destroy_read_struct(&png, &info, NULL);
+        png_destroy_read_struct(&png, &infoEnd, NULL);
     }
     if (rowPointers) {
         free(rowPointers);
diff --git a/apps/shared/avifpng.h b/apps/shared/avifpng.h
index 7e946e4..c15d7a1 100644
--- a/apps/shared/avifpng.h
+++ b/apps/shared/avifpng.h
@@ -16,6 +16,7 @@
                      avifPixelFormat requestedFormat,
                      uint32_t requestedDepth,
                      avifRGBToYUVFlags flags,
+                     avifBool ignoreExif,
                      uint32_t * outPNGDepth);
 avifBool avifPNGWrite(const char * outputFilename,
                       const avifImage * avif,
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index b38082a..0752dbf 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -228,6 +228,9 @@
                                 avifPixelFormat requestedFormat,
                                 int requestedDepth,
                                 avifRGBToYUVFlags flags,
+                                avifBool ignoreICC,
+                                avifBool ignoreExif,
+                                avifBool ignoreXMP,
                                 avifImage * image,
                                 uint32_t * outDepth,
                                 avifAppSourceTiming * sourceTiming,
@@ -249,7 +252,8 @@
             *outDepth = 8;
         }
     } else if (format == AVIF_APP_FILE_FORMAT_PNG) {
-        if (!avifPNGRead(filename, image, requestedFormat, requestedDepth, flags, outDepth)) {
+        (void)ignoreICC, (void)ignoreXMP; // TODO(yguyon): Implement
+        if (!avifPNGRead(filename, image, requestedFormat, requestedDepth, flags, ignoreExif, outDepth)) {
             return AVIF_APP_FILE_FORMAT_UNKNOWN;
         }
     } else {
diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h
index 6543787..c1b77cd 100644
--- a/apps/shared/avifutil.h
+++ b/apps/shared/avifutil.h
@@ -64,6 +64,9 @@
                                 avifPixelFormat requestedFormat,
                                 int requestedDepth,
                                 avifRGBToYUVFlags flags,
+                                avifBool ignoreICC,
+                                avifBool ignoreExif,
+                                avifBool ignoreXMP,
                                 avifImage * image,
                                 uint32_t * outDepth,
                                 avifAppSourceTiming * sourceTiming,
diff --git a/doc/avifenc.1.md b/doc/avifenc.1.md
index bf381fb..95e450e 100644
--- a/doc/avifenc.1.md
+++ b/doc/avifenc.1.md
@@ -152,13 +152,16 @@
         - **svt**
 
 **\--exif** _FILENAME_
-:   Provide an Exif metadata payload to be associated with the primary item.
+:   Provide an Exif metadata payload to be associated with the primary item
+    (implies --ignore-exif).
 
 **\--xmp** _FILENAME_
-:   Provide an XMP metadata payload to be associated with the primary item.
+:   Provide an XMP metadata payload to be associated with the primary item
+    (implies --ignore-xmp).
 
 **\--icc** _FILENAME_
-:   Provide an ICC profile payload to be associated with the primary item.
+:   Provide an ICC profile payload to be associated with the primary item
+    (implies --ignore-icc).
 
 **-a**, **\--advanced** _KEY_[_=VALUE_]
 :   Pass an advanced, codec-specific key/value string pair directly to the
@@ -185,6 +188,14 @@
     Set to **0** to disable.
     Default is 0.
 
+**\--ignore-exif**
+:   If the input file contains embedded Exif metadata, ignore it (no-op if
+    absent).
+
+**\--ignore-xmp**
+:   If the input file contains embedded XMP metadata, ignore it (no-op if
+    absent).
+
 **\--ignore-icc**
 :   If the input file contains an embedded ICC profile, ignore it (no-op if
     absent).
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1bbe042..8636010 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -92,9 +92,9 @@
     add_test(NAME avifincrtest COMMAND avifincrtest ${CMAKE_CURRENT_SOURCE_DIR}/data)
 
     add_executable(avifmetadatatest gtest/avifmetadatatest.cc)
-    target_link_libraries(avifmetadatatest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
+    target_link_libraries(avifmetadatatest aviftest_helpers avif_apps ${GTEST_LIBRARIES})
     target_include_directories(avifmetadatatest PRIVATE ${GTEST_INCLUDE_DIRS})
-    add_test(NAME avifmetadatatest COMMAND avifmetadatatest)
+    add_test(NAME avifmetadatatest COMMAND avifmetadatatest ${CMAKE_CURRENT_SOURCE_DIR}/data)
 
     add_executable(avifrgbtoyuvtest gtest/avifrgbtoyuvtest.cc)
     target_link_libraries(avifrgbtoyuvtest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
@@ -119,6 +119,5 @@
     target_link_libraries(are_images_equal aviftest_helpers avif_apps)
     add_test(
         NAME test_cmd
-        COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd.sh ${CMAKE_BINARY_DIR}
-        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+        COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd.sh ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/data)
 endif()
diff --git a/tests/data/paris_exif_at_end.png b/tests/data/paris_exif_at_end.png
new file mode 100644
index 0000000..53bfc08
--- /dev/null
+++ b/tests/data/paris_exif_at_end.png
Binary files differ
diff --git a/tests/data/paris_exif_xmp_icc.jpg b/tests/data/paris_exif_xmp_icc.jpg
new file mode 100644
index 0000000..805f9df
--- /dev/null
+++ b/tests/data/paris_exif_xmp_icc.jpg
Binary files differ
diff --git a/tests/data/paris_exif_xmp_icc.png b/tests/data/paris_exif_xmp_icc.png
new file mode 100644
index 0000000..df1f02e
--- /dev/null
+++ b/tests/data/paris_exif_xmp_icc.png
Binary files differ
diff --git a/tests/gtest/are_images_equal.cc b/tests/gtest/are_images_equal.cc
index 1bfe65f..ac9c1d2 100644
--- a/tests/gtest/are_images_equal.cc
+++ b/tests/gtest/are_images_equal.cc
@@ -31,8 +31,10 @@
     // Make sure no color conversion happens.
     decoded[i]->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
     if (avifReadImage(argv[i + 1], requestedFormat, kRequestedDepth,
-                      AVIF_RGB_TO_YUV_DEFAULT, decoded[i].get(), &depth[i],
-                      nullptr, nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
+                      AVIF_RGB_TO_YUV_DEFAULT, /*ignoreICC=*/AVIF_FALSE,
+                      /*ignoreExif=*/AVIF_FALSE, /*ignoreXMP=*/AVIF_FALSE,
+                      decoded[i].get(), &depth[i], nullptr,
+                      nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
       std::cerr << "Image " << argv[i + 1] << " cannot be read." << std::endl;
       return 2;
     }
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index c4fd1be..690e83c 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -6,16 +6,21 @@
 
 #include "avif/avif.h"
 #include "aviftest_helpers.h"
+#include "avifutil.h"
 #include "gtest/gtest.h"
 
 using ::testing::Bool;
 using ::testing::Combine;
+using ::testing::Values;
 
 namespace libavif {
 namespace {
 
 //------------------------------------------------------------------------------
 
+// Used to pass the data folder path to the GoogleTest suites.
+const char* data_path = nullptr;
+
 // ICC color profiles are not checked by libavif so the content does not matter.
 // This is a truncated widespread ICC color profile.
 const std::array<uint8_t, 24> kSampleIcc = {
@@ -35,14 +40,15 @@
     0x67, 0x69, 0x6e, 0x3d, 0x22, 0xef, 0xbb, 0xbf, 0x22, 0x20, 0x69, 0x64};
 
 //------------------------------------------------------------------------------
+// AVIF encode/decode metadata tests
 
-class MetadataTest
+class AvifMetadataTest
     : public testing::TestWithParam<
           std::tuple</*use_icc=*/bool, /*use_exif=*/bool, /*use_xmp=*/bool>> {};
 
 // Encodes, decodes then verifies that the output metadata matches the input
 // metadata defined by the parameters.
-TEST_P(MetadataTest, EncodeDecode) {
+TEST_P(AvifMetadataTest, EncodeDecode) {
   const bool use_icc = std::get<0>(GetParam());
   const bool use_exif = std::get<1>(GetParam());
   const bool use_xmp = std::get<2>(GetParam());
@@ -81,32 +87,148 @@
             AVIF_RESULT_OK);
 
   // Compare input and output metadata.
-  if (use_icc) {
-    ASSERT_EQ(decoded->icc.size, kSampleIcc.size());
-    EXPECT_TRUE(
-        std::equal(kSampleIcc.begin(), kSampleIcc.end(), decoded->icc.data));
-  } else {
-    EXPECT_EQ(decoded->icc.size, 0u);
-  }
-  if (use_exif) {
-    ASSERT_EQ(decoded->exif.size, kSampleExif.size());
-    EXPECT_TRUE(
-        std::equal(kSampleExif.begin(), kSampleExif.end(), decoded->exif.data));
-  } else {
-    EXPECT_EQ(decoded->exif.size, 0u);
-  }
-  if (use_xmp) {
-    ASSERT_EQ(decoded->xmp.size, kSampleXmp.size());
-    EXPECT_TRUE(
-        std::equal(kSampleXmp.begin(), kSampleXmp.end(), decoded->xmp.data));
-  } else {
-    EXPECT_EQ(decoded->xmp.size, 0u);
-  }
+  EXPECT_TRUE(testutil::AreByteSequencesEqual(
+      decoded->icc.data, decoded->icc.size, kSampleIcc.data(),
+      use_icc ? kSampleIcc.size() : 0u));
+  EXPECT_TRUE(testutil::AreByteSequencesEqual(
+      decoded->exif.data, decoded->exif.size, kSampleExif.data(),
+      use_exif ? kSampleExif.size() : 0u));
+  EXPECT_TRUE(testutil::AreByteSequencesEqual(
+      decoded->xmp.data, decoded->xmp.size, kSampleXmp.data(),
+      use_xmp ? kSampleXmp.size() : 0u));
 }
 
-INSTANTIATE_TEST_SUITE_P(All, MetadataTest,
+INSTANTIATE_TEST_SUITE_P(All, AvifMetadataTest,
                          Combine(/*use_icc=*/Bool(), /*use_exif=*/Bool(),
                                  /*use_xmp=*/Bool()));
 
+//------------------------------------------------------------------------------
+// Jpeg and PNG metadata tests
+
+class MetadataTest
+    : public testing::TestWithParam<
+          std::tuple</*file_name=*/const char*, /*use_icc=*/bool,
+                     /*use_exif=*/bool, /*use_xmp=*/bool, /*expect_icc=*/bool,
+                     /*expect_exif=*/bool, /*expect_xmp=*/bool>> {};
+
+// zTXt "Raw profile type exif" at the beginning of a PNG file.
+TEST_P(MetadataTest, Read) {
+  const std::string file_path =
+      std::string(data_path) + "/" + std::get<0>(GetParam());
+  const bool use_icc = std::get<1>(GetParam());
+  const bool use_exif = std::get<2>(GetParam());
+  const bool use_xmp = std::get<3>(GetParam());
+  const bool expect_icc = std::get<4>(GetParam());
+  const bool expect_exif = std::get<5>(GetParam());
+  const bool expect_xmp = std::get<6>(GetParam());
+
+  avifImage* image = avifImageCreateEmpty();
+  ASSERT_NE(image, nullptr);
+  image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;  // lossless
+  ASSERT_NE(avifReadImage(file_path.c_str(), AVIF_PIXEL_FORMAT_NONE, 0,
+                          AVIF_RGB_TO_YUV_DEFAULT, !use_icc, !use_exif,
+                          !use_xmp, image, nullptr, nullptr, nullptr),
+            AVIF_APP_FILE_FORMAT_UNKNOWN);
+  EXPECT_NE(image->width * image->height, 0u);
+  if (expect_icc) {
+    EXPECT_NE(image->icc.size, 0u);
+    EXPECT_NE(image->icc.data, nullptr);
+  } else {
+    EXPECT_EQ(image->icc.size, 0u);
+    EXPECT_EQ(image->icc.data, nullptr);
+  }
+  if (expect_exif) {
+    EXPECT_NE(image->exif.size, 0u);
+    EXPECT_NE(image->exif.data, nullptr);
+  } else {
+    EXPECT_EQ(image->exif.size, 0u);
+    EXPECT_EQ(image->exif.data, nullptr);
+  }
+  if (expect_xmp) {
+    EXPECT_NE(image->xmp.size, 0u);
+    EXPECT_NE(image->xmp.data, nullptr);
+  } else {
+    EXPECT_EQ(image->xmp.size, 0u);
+    EXPECT_EQ(image->xmp.data, nullptr);
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    PngNone, MetadataTest,
+    Combine(Values("paris_exif_xmp_icc.png"),  // zTXt iCCP iTXt IDAT
+            /*use_icc=*/Values(false), /*use_exif=*/Values(false),
+            /*use_xmp=*/Values(false),
+            // ignoreICC is not yet implemented.
+            /*expected_icc=*/Values(true),
+            /*expected_exif=*/Values(false), /*expected_xmp=*/Values(false)));
+INSTANTIATE_TEST_SUITE_P(
+    PngAll, MetadataTest,
+    Combine(Values("paris_exif_xmp_icc.png"), /*use_icc=*/Values(true),
+            /*use_exif=*/Values(true), /*use_xmp=*/Values(true),
+            /*expected_icc=*/Values(true), /*expected_exif=*/Values(true),
+            // XMP extraction is not yet implemented.
+            /*expected_xmp=*/Values(false)));
+
+INSTANTIATE_TEST_SUITE_P(
+    PngExifAtEnd, MetadataTest,
+    Combine(Values("paris_exif_at_end.png"),  // iCCP IDAT eXIf
+            /*use_icc=*/Values(true), /*use_exif=*/Values(true),
+            /*use_xmp=*/Values(true), /*expected_icc=*/Values(true),
+            /*expected_exif=*/Values(true), /*expected_xmp=*/Values(false)));
+
+INSTANTIATE_TEST_SUITE_P(
+    Jpeg, MetadataTest,
+    Combine(Values("paris_exif_xmp_icc.jpg"), /*use_icc=*/Values(true),
+            /*use_exif=*/Values(true), /*use_xmp=*/Values(true),
+            /*expected_icc=*/Values(true),
+            // Exif and XMP are not yet implemented.
+            /*expected_exif=*/Values(false), /*expected_xmp=*/Values(false)));
+
+// Verify all parsers lead exactly to the same metadata bytes.
+TEST(MetadataTest, Compare) {
+  constexpr const char* kFileNames[] = {"paris_exif_at_end.png",
+                                        "paris_exif_xmp_icc.jpg",
+                                        "paris_exif_xmp_icc.png"};
+  avifImage* images[sizeof(kFileNames) / sizeof(kFileNames[0])];
+  avifImage** image_it = images;
+  for (const char* file_name : kFileNames) {
+    const std::string file_path = std::string(data_path) + "/" + file_name;
+
+    *image_it = avifImageCreateEmpty();
+    ASSERT_NE(*image_it, nullptr);
+    ASSERT_NE(avifReadImage(file_path.c_str(), AVIF_PIXEL_FORMAT_NONE, 0,
+                            AVIF_RGB_TO_YUV_DEFAULT, /*ignoreICC=*/false,
+                            /*ignoreExif=*/false, /*ignoreXMP=*/false,
+                            *image_it, nullptr, nullptr, nullptr),
+              AVIF_APP_FILE_FORMAT_UNKNOWN);
+    ++image_it;
+  }
+
+  for (avifImage* image : images) {
+    if (image->exif.size != 0) {  // Not implemented for JPEG.
+      EXPECT_TRUE(
+          testutil::AreByteSequencesEqual(image->exif, images[0]->exif));
+    }
+    if (image->xmp.size != 0) {  // Not implemented.
+      EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, images[0]->xmp));
+    }
+    EXPECT_TRUE(testutil::AreByteSequencesEqual(image->icc, images[0]->icc));
+  }
+}
+
+//------------------------------------------------------------------------------
+
 }  // namespace
 }  // namespace libavif
+
+int main(int argc, char** argv) {
+  ::testing::InitGoogleTest(&argc, argv);
+  if (argc != 2) {
+    std::cerr << "There must be exactly one argument containing the path to "
+                 "the test data folder"
+              << std::endl;
+    return 1;
+  }
+  libavif::data_path = argv[1];
+  return RUN_ALL_TESTS();
+}
diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc
index e108bc6..493512a 100644
--- a/tests/gtest/aviftest_helpers.cc
+++ b/tests/gtest/aviftest_helpers.cc
@@ -159,6 +159,16 @@
 
 //------------------------------------------------------------------------------
 
+bool AreByteSequencesEqual(const uint8_t data1[], size_t data1_length,
+                           const uint8_t data2[], size_t data2_length) {
+  if (data1_length != data2_length) return false;
+  return data1_length == 0 || std::equal(data1, data1 + data1_length, data2);
+}
+
+bool AreByteSequencesEqual(const avifRWData& data1, const avifRWData& data2) {
+  return AreByteSequencesEqual(data1.data, data1.size, data2.data, data2.size);
+}
+
 // Returns true if image1 and image2 are identical.
 bool AreImagesEqual(const avifImage& image1, const avifImage& image2,
                     bool ignore_alpha) {
@@ -212,7 +222,9 @@
       row2 += row_bytes2;
     }
   }
-  return true;
+  return AreByteSequencesEqual(image1.icc, image2.icc) &&
+         AreByteSequencesEqual(image1.exif, image2.exif) &&
+         AreByteSequencesEqual(image1.xmp, image2.xmp);
 }
 
 static avifResult avifIOLimitedReaderRead(avifIO* io, uint32_t readFlags,
diff --git a/tests/gtest/aviftest_helpers.h b/tests/gtest/aviftest_helpers.h
index 510e6b3..f7f2a2c 100644
--- a/tests/gtest/aviftest_helpers.h
+++ b/tests/gtest/aviftest_helpers.h
@@ -54,13 +54,21 @@
 void FillImageChannel(avifRGBImage* image, uint32_t channel_offset,
                       uint32_t value);
 
-// Returns true if both images have the same features and pixel values.
-// If ignore_alpha is true, the alpha channel is not taken into account in the
-// comparison.
+// Returns true if both arrays are empty or have the same length and bytes.
+// data1 may be null only when data1_length is 0.
+// data2 may be null only when data2_length is 0.
+bool AreByteSequencesEqual(const uint8_t data1[], size_t data1_length,
+                           const uint8_t data2[], size_t data2_length);
+bool AreByteSequencesEqual(const avifRWData& data1, const avifRWData& data2);
+
+// Returns true if both images have the same features, pixel values and
+// metadata. If ignore_alpha is true, the alpha channel is not taken into
+// account in the comparison.
 bool AreImagesEqual(const avifImage& image1, const avifImage& image2,
                     bool ignore_alpha = false);
 
 //------------------------------------------------------------------------------
+// avifIO overlay
 
 struct AvifIOLimitedReader {
   static constexpr uint64_t kNoClamp = std::numeric_limits<uint64_t>::max();
diff --git a/tests/test_cmd.sh b/tests/test_cmd.sh
index c280c0f..fde09a2 100755
--- a/tests/test_cmd.sh
+++ b/tests/test_cmd.sh
@@ -34,16 +34,17 @@
 AVIFENC="${BINARY_DIR}/avifenc"
 AVIFDEC="${BINARY_DIR}/avifdec"
 ARE_IMAGES_EQUAL="${BINARY_DIR}/tests/are_images_equal"
-TMP_ENCODED_FILE=/tmp/encoded.avif
-DECODED_FILE=/tmp/decoded.png
-PNG_FILE=/tmp/kodim03.png
-TMP_ENCODED_FILE_WTH_DASH=-encoded.avif
+ENCODED_FILE=/tmp/avif_test_cmd_encoded.avif
+ENCODED_FILE_NO_METADATA=/tmp/avif_test_cmd_encoded_no_metadata.avif
+ENCODED_FILE_WITH_DASH=-avif_test_cmd_encoded.avif
+DECODED_FILE=/tmp/avif_test_cmd_decoded.png
+PNG_FILE=/tmp/avif_test_cmd_kodim03.png
 
 # Prepare some extra data.
 set +x
 echo "Generating a color PNG"
-"${AVIFENC}" -s 10 "${TESTDATA_DIR}"/kodim03_yuv420_8bpc.y4m -o "${TMP_ENCODED_FILE}" > /dev/null
-"${AVIFDEC}" "${TMP_ENCODED_FILE}" "${PNG_FILE}" > /dev/null
+"${AVIFENC}" -s 10 "${TESTDATA_DIR}/kodim03_yuv420_8bpc.y4m" -o "${ENCODED_FILE}" > /dev/null
+"${AVIFDEC}" "${ENCODED_FILE}" "${PNG_FILE}" > /dev/null
 set -x
 
 # Basic calls.
@@ -52,35 +53,48 @@
 
 # Lossless test.
 echo "Testing basic lossless"
-"${AVIFENC}" -s 10 -l "${PNG_FILE}" -o "${TMP_ENCODED_FILE}"
-"${AVIFDEC}" "${TMP_ENCODED_FILE}" "${DECODED_FILE}"
+"${AVIFENC}" -s 10 -l "${PNG_FILE}" -o "${ENCODED_FILE}"
+"${AVIFDEC}" "${ENCODED_FILE}" "${DECODED_FILE}"
 "${ARE_IMAGES_EQUAL}" "${PNG_FILE}" "${DECODED_FILE}" 0
 
+# Metadata test.
+echo "Testing metadata enc/dec"
+"${AVIFENC}" "${TESTDATA_DIR}/paris_exif_xmp_icc.png" -o "${ENCODED_FILE}"
+"${AVIFENC}" "${TESTDATA_DIR}/paris_exif_xmp_icc.png" -o "${ENCODED_FILE_NO_METADATA}" --ignore-exif
+cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
+
 # Argument parsing test with filenames starting with a dash.
-"${AVIFENC}" -s 10 "${PNG_FILE}" -- "${TMP_ENCODED_FILE_WTH_DASH}"
-"${AVIFDEC}" --info  -- "${TMP_ENCODED_FILE_WTH_DASH}"
+"${AVIFENC}" -s 10 "${PNG_FILE}" -- "${ENCODED_FILE_WITH_DASH}"
+"${AVIFDEC}" --info  -- "${ENCODED_FILE_WITH_DASH}"
 # Passing a filename starting with a dash without using -- should fail.
 set +e
-"${AVIFENC}" -s 10 "${PNG_FILE}" "${TMP_ENCODED_FILE_WTH_DASH}"
+"${AVIFENC}" -s 10 "${PNG_FILE}" "${ENCODED_FILE_WITH_DASH}"
 if [[ $? -ne 1 ]]; then
   echo "Argument parsing should fail for avifenc"
   exit 1
 fi
-"${AVIFDEC}" --info "${TMP_ENCODED_FILE_WTH_DASH}"
+"${AVIFDEC}" --info "${ENCODED_FILE_WITH_DASH}"
 if [[ $? -ne 1 ]]; then
   echo "Argument parsing should fail for avifdec"
   exit 1
 fi
 set -e
-rm -- "${TMP_ENCODED_FILE_WTH_DASH}"
+rm -- "${ENCODED_FILE_WITH_DASH}"
 
 # Test code that should fail.
 set +e
-"${ARE_IMAGES_EQUAL}" "${TESTDATA_DIR}"/kodim23_yuv420_8bpc.y4m "${DECODED_FILE}" 0
+"${ARE_IMAGES_EQUAL}" "${TESTDATA_DIR}/kodim23_yuv420_8bpc.y4m" "${DECODED_FILE}" 0
 if [[ $? -ne 1 ]]; then
   echo "Image should be different"
+
+  # Cleanup
+  rm "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" "${DECODED_FILE}" "${PNG_FILE}"
   exit 1
 fi
 
 echo "TEST OK"
+
+# Cleanup
+rm "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" "${DECODED_FILE}" "${PNG_FILE}"
+
 exit 0