Read XMP from PNG and Exif,XMP from Jpeg

diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index fcaa4ce..4cb54ba 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -239,7 +239,14 @@
 // longjmp. But GCC's -Wclobbered warning may have trouble figuring that out, so
 // we preemptively declare it as volatile.
 
-avifBool avifJPEGRead(const char * inputFilename, avifImage * avif, avifPixelFormat requestedFormat, uint32_t requestedDepth, avifRGBToYUVFlags flags)
+avifBool avifJPEGRead(const char * inputFilename,
+                      avifImage * avif,
+                      avifPixelFormat requestedFormat,
+                      uint32_t requestedDepth,
+                      avifRGBToYUVFlags flags,
+                      avifBool ignoreICC,
+                      avifBool ignoreExif,
+                      avifBool ignoreXMP)
 {
     volatile avifBool ret = AVIF_FALSE;
     uint8_t * volatile iccData = NULL;
@@ -263,15 +270,22 @@
 
     jpeg_create_decompress(&cinfo);
 
-    setup_read_icc_profile(&cinfo);
+    if (!ignoreExif || !ignoreXMP) {
+        jpeg_save_markers(&cinfo, JPEG_APP0 + 1, /*length_limit=*/0xFFFF); // Exif/XMP
+    }
+    if (!ignoreICC) {
+        setup_read_icc_profile(&cinfo);
+    }
     jpeg_stdio_src(&cinfo, f);
     jpeg_read_header(&cinfo, TRUE);
 
-    uint8_t * iccDataTmp;
-    unsigned int iccDataLen;
-    if (read_icc_profile(&cinfo, &iccDataTmp, &iccDataLen)) {
-        iccData = iccDataTmp;
-        avifImageSetProfileICC(avif, iccDataTmp, (size_t)iccDataLen);
+    if (!ignoreICC) {
+        uint8_t * iccDataTmp;
+        unsigned int iccDataLen;
+        if (read_icc_profile(&cinfo, &iccDataTmp, &iccDataLen)) {
+            iccData = iccDataTmp;
+            avifImageSetProfileICC(avif, iccDataTmp, (size_t)iccDataLen);
+        }
     }
 
     avif->yuvFormat = requestedFormat; // This may be AVIF_PIXEL_FORMAT_NONE, which is "auto" to avifJPEGReadCopy()
@@ -320,13 +334,48 @@
         }
     }
 
+    if (!ignoreExif) {
+        const avifROData tagExif = { (const uint8_t *)"Exif\0\0", 6 };
+        avifBool found = AVIF_FALSE;
+        for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
+            if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > tagExif.size) &&
+                !memcmp(marker->data, tagExif.data, tagExif.size)) {
+                if (found) {
+                    // TODO(yguyon): Implement instead of outputting an error.
+                    fprintf(stderr, "Exif extraction failed: unsupported Exif split into multiple chunks or invalid multiple Exif chunks\n");
+                    goto cleanup;
+                }
+                avifImageSetMetadataExif(avif, marker->data + tagExif.size, marker->data_length - tagExif.size);
+                found = AVIF_TRUE;
+            }
+        }
+    }
+    if (!ignoreXMP) {
+        const avifROData tagStandardXmp = { (const uint8_t *)"http://ns.adobe.com/xap/1.0/\0", 29 };
+        const avifROData tagExtendedXmp = { (const uint8_t *)"http://ns.adobe.com/xmp/extension/\0", 35 };
+        avifBool found = AVIF_FALSE;
+        for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
+            if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > tagStandardXmp.size) &&
+                !memcmp(marker->data, tagStandardXmp.data, tagStandardXmp.size)) {
+                if (found) {
+                    fprintf(stderr, "XMP extraction failed: invalid multiple XMP chunks\n");
+                    goto cleanup;
+                }
+                avifImageSetMetadataXMP(avif, marker->data + tagStandardXmp.size, marker->data_length - tagStandardXmp.size);
+                found = AVIF_TRUE;
+            } else if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > tagExtendedXmp.size) &&
+                       !memcmp(marker->data, tagExtendedXmp.data, tagExtendedXmp.size)) {
+                // TODO(yguyon): Implement instead of outputting an error.
+                fprintf(stderr, "XMP extraction failed: extended XMP is unsupported\n");
+                goto cleanup;
+            }
+        }
+    }
     jpeg_finish_decompress(&cinfo);
     ret = AVIF_TRUE;
 cleanup:
     jpeg_destroy_decompress(&cinfo);
-    if (f) {
-        fclose(f);
-    }
+    fclose(f);
     free(iccData);
     avifRGBImageFreePixels(&rgb);
     return ret;
diff --git a/apps/shared/avifjpeg.h b/apps/shared/avifjpeg.h
index 5e2b981..214bcca 100644
--- a/apps/shared/avifjpeg.h
+++ b/apps/shared/avifjpeg.h
@@ -10,7 +10,14 @@
 extern "C" {
 #endif
 
-avifBool avifJPEGRead(const char * inputFilename, avifImage * avif, avifPixelFormat requestedFormat, uint32_t requestedDepth, avifRGBToYUVFlags flags);
+avifBool avifJPEGRead(const char * inputFilename,
+                      avifImage * avif,
+                      avifPixelFormat requestedFormat,
+                      uint32_t requestedDepth,
+                      avifRGBToYUVFlags flags,
+                      avifBool ignoreICC,
+                      avifBool ignoreExif,
+                      avifBool ignoreXMP);
 avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int jpegQuality, avifYUVToRGBFlags conversionFlags);
 
 #ifdef __cplusplus
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index 07882fe..0e9ed2c 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -26,31 +26,6 @@
 // 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);) {
@@ -60,7 +35,7 @@
         }
         if (!isxdigit(hexString[i]) || !isxdigit(hexString[i + 1])) {
             avifRWDataFree(bytes);
-            fprintf(stderr, "Exif extraction failed: invalid character at " AVIF_FMT_ZU "\n", i);
+            fprintf(stderr, "Metadata extraction failed: invalid character at " AVIF_FMT_ZU "\n", i);
             return AVIF_FALSE;
         }
         const char twoHexDigits[] = { hexString[i], hexString[i + 1], '\0' };
@@ -71,7 +46,7 @@
 
     if (numBytes != numExpectedBytes) {
         avifRWDataFree(bytes);
-        fprintf(stderr, "Exif extraction failed: expected " AVIF_FMT_ZU " tokens but got " AVIF_FMT_ZU "\n", numExpectedBytes, numBytes);
+        fprintf(stderr, "Metadata extraction failed: expected " AVIF_FMT_ZU " tokens but got " AVIF_FMT_ZU "\n", numExpectedBytes, numBytes);
         return AVIF_FALSE;
     }
     return AVIF_TRUE;
@@ -82,7 +57,7 @@
 {
     // 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");
+        fprintf(stderr, "Metadata extraction failed: truncated or malformed raw profile\n");
         return AVIF_FALSE;
     }
 
@@ -90,7 +65,7 @@
     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);
+            fprintf(stderr, "Metadata extraction failed: malformed raw profile, unexpected null character at " AVIF_FMT_ZU "\n", i);
             return AVIF_FALSE;
         }
         if (profile[i] == '\n') {
@@ -104,13 +79,13 @@
                 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);
+                    fprintf(stderr, "Metadata 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);
+                    fprintf(stderr, "Metadata 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.
@@ -119,46 +94,94 @@
             }
         }
     }
-    fprintf(stderr, "Exif extraction failed: malformed or truncated raw profile\n");
+    fprintf(stderr, "Metadata 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)
+static avifBool avifRemoveHeader(const avifROData * header, avifRWData * payload)
+{
+    if (payload->size > header->size && !memcmp(payload->data, header->data, header->size)) {
+        memmove(payload->data, payload->data + header->size, payload->size - header->size);
+        payload->size -= header->size;
+        return AVIF_TRUE;
+    }
+    return AVIF_FALSE;
+}
+
+// Extracts metadata to avif->exif and avif->xmp unless the corresponding *ignoreExif or *ignoreXMP is set to AVIF_TRUE.
+// *ignoreExif and *ignoreXMP may be set to AVIF_TRUE if the corresponding Exif or XMP metadata was extracted.
+// Returns AVIF_FALSE in case of a parsing error.
+static avifBool avifExtractExifAndXMP(png_structp png, png_infop info, avifBool * ignoreExif, avifBool * ignoreXMP, 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;
+    if (!*ignoreExif) {
+        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);
+            *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
         }
-        avifImageSetMetadataExif(avif, exif, exifSize);
-        return AVIF_TRUE;
     }
 #endif // PNG_eXIf_SUPPORTED
 
+    // HEIF specification ISO-23008 section A.2.1 allows including and excluding the Exif\0\0 header from AVIF files.
+    // The PNG 1.5 extension mentions the omission of this header for the modern standard eXIf chunk.
+    const avifROData exifApp1Header = { (const uint8_t *)"Exif\0\0", 6 };
+    const avifROData xmpApp1Header = { (const uint8_t *)"http://ns.adobe.com/xap/1.0/\0", 29 };
+
+    // tXMP could be retrieved using the png_get_unknown_chunks() API but tXMP is deprecated
+    // and there is no PNG file example with a tXMP chunk lying around, so it is not worth the hassle.
+
     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) {
+    for (png_uint_32 i = 0; (!*ignoreExif || !*ignoreXMP) && (i < numTextChunks); ++i, ++text) {
+        png_size_t textLength = text->text_length;
 #ifdef PNG_iTXt_SUPPORTED
-                case PNG_ITXT_COMPRESSION_NONE:
-                case PNG_ITXT_COMPRESSION_zTXt:
-                    textLength = text->itxt_length;
-                    break;
+        if ((text->compression == PNG_ITXT_COMPRESSION_NONE) || (text->compression == PNG_ITXT_COMPRESSION_zTXt)) {
+            textLength = text->itxt_length;
+        }
 #endif
-                case PNG_TEXT_COMPRESSION_NONE:
-                case PNG_TEXT_COMPRESSION_zTXt:
-                default:
-                    textLength = text->text_length;
-                    break;
+
+        if (!*ignoreExif && !strcmp(text->key, "Raw profile type exif")) {
+            if (!avifCopyRawProfile(text->text, textLength, &avif->exif)) {
+                return AVIF_FALSE;
             }
-            return avifCopyRawProfile(text->text, textLength, &avif->exif);
+            avifRemoveHeader(&exifApp1Header, &avif->exif); // Ignore the return value because the header is optional.
+            *ignoreExif = AVIF_TRUE;                        // Ignore any other Exif chunk.
+        } else if (!*ignoreXMP && !strcmp(text->key, "Raw profile type xmp")) {
+            if (!avifCopyRawProfile(text->text, textLength, &avif->xmp)) {
+                return AVIF_FALSE;
+            }
+            avifRemoveHeader(&xmpApp1Header, &avif->xmp); // Ignore the return value because the header is optional.
+            *ignoreXMP = AVIF_TRUE;                       // Ignore any other XMP chunk.
+        } else if (!strcmp(text->key, "Raw profile type APP1")) {
+            // This can be either Exif, XMP or something else.
+            avifRWData metadata = { NULL, 0 };
+            if (!avifCopyRawProfile(text->text, textLength, &metadata)) {
+                return AVIF_FALSE;
+            }
+            if (!*ignoreExif && avifRemoveHeader(&exifApp1Header, &metadata)) {
+                avifRWDataFree(&avif->exif);
+                avif->exif = metadata;
+                *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
+            } else if (!*ignoreXMP && avifRemoveHeader(&xmpApp1Header, &metadata)) {
+                avifRWDataFree(&avif->xmp);
+                avif->xmp = metadata;
+                *ignoreXMP = AVIF_TRUE; // Ignore any other XMP chunk.
+            } else {
+                avifRWDataFree(&metadata); // Discard chunk.
+            }
+        } else if (!*ignoreXMP && !strcmp(text->key, "XML:com.adobe.xmp")) {
+            if (textLength == 0) {
+                fprintf(stderr, "XMP extraction failed: empty XML:com.adobe.xmp payload\n");
+                return AVIF_FALSE;
+            }
+            avifImageSetMetadataXMP(avif, (const uint8_t *)text->text, textLength);
+            *ignoreXMP = AVIF_TRUE; // Ignore any other XMP chunk.
         }
     }
     return AVIF_TRUE;
@@ -183,7 +206,9 @@
                      avifPixelFormat requestedFormat,
                      uint32_t requestedDepth,
                      avifRGBToYUVFlags flags,
+                     avifBool ignoreICC,
                      avifBool ignoreExif,
+                     avifBool ignoreXMP,
                      uint32_t * outPNGDepth)
 {
     volatile avifBool readResult = AVIF_FALSE;
@@ -232,14 +257,17 @@
     png_set_sig_bytes(png, 8);
     png_read_info(png, info);
 
-    char * iccpProfileName = NULL;
-    int iccpCompression = 0;
-    unsigned char * iccpData = NULL;
-    png_uint_32 iccpDataLen = 0;
-    if (png_get_iCCP(png, info, &iccpProfileName, &iccpCompression, (png_iccp_datap *)&iccpData, &iccpDataLen) == PNG_INFO_iCCP) {
-        avifImageSetProfileICC(avif, iccpData, iccpDataLen);
+    if (!ignoreICC) {
+        char * iccpProfileName = NULL;
+        int iccpCompression = 0;
+        unsigned char * iccpData = NULL;
+        png_uint_32 iccpDataLen = 0;
+        if (png_get_iCCP(png, info, &iccpProfileName, &iccpCompression, (png_iccp_datap *)&iccpData, &iccpDataLen) == PNG_INFO_iCCP) {
+            avifImageSetProfileICC(avif, iccpData, iccpDataLen);
+        }
+        // Note: There is no support for the rare "Raw profile type icc" or "Raw profile type icm" text chunks.
+        // TODO(yguyon): Also check if there is a cICp chunk (https://github.com/AOMediaCodec/libavif/pull/1065#discussion_r958534232)
     }
-    // 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);
@@ -313,25 +341,22 @@
         goto cleanup;
     }
 
-    if (!ignoreExif) {
-        // Read Exif metadata at the beginning of the file.
-        if (!avifExtractExif(png, info, avif)) {
+    // Read Exif metadata at the beginning of the file.
+    if (!avifExtractExifAndXMP(png, info, &ignoreExif, &ignoreXMP, avif)) {
+        goto cleanup;
+    }
+    // Read Exif or XMP metadata at the end of the file if there was none at the beginning.
+    if (!ignoreExif || !ignoreXMP) {
+        infoEnd = png_create_info_struct(png);
+        if (!infoEnd) {
+            fprintf(stderr, "Cannot init libpng (infoEnd): %s\n", inputFilename);
             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;
-            }
+        png_read_end(png, infoEnd);
+        if (!avifExtractExifAndXMP(png, infoEnd, &ignoreExif, &ignoreXMP, avif)) {
+            goto cleanup;
         }
     }
-    // TODO(yguyon): Extract XMP to avif->xmp, if any.
     readResult = AVIF_TRUE;
 
 cleanup:
diff --git a/apps/shared/avifpng.h b/apps/shared/avifpng.h
index c15d7a1..9597211 100644
--- a/apps/shared/avifpng.h
+++ b/apps/shared/avifpng.h
@@ -16,7 +16,9 @@
                      avifPixelFormat requestedFormat,
                      uint32_t requestedDepth,
                      avifRGBToYUVFlags flags,
+                     avifBool ignoreICC,
                      avifBool ignoreExif,
+                     avifBool ignoreXMP,
                      uint32_t * outPNGDepth);
 avifBool avifPNGWrite(const char * outputFilename,
                       const avifImage * avif,
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index 0752dbf..c0c4470 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -245,15 +245,14 @@
             *outDepth = image->depth;
         }
     } else if (format == AVIF_APP_FILE_FORMAT_JPEG) {
-        if (!avifJPEGRead(filename, image, requestedFormat, requestedDepth, flags)) {
+        if (!avifJPEGRead(filename, image, requestedFormat, requestedDepth, flags, ignoreICC, ignoreExif, ignoreXMP)) {
             return AVIF_APP_FILE_FORMAT_UNKNOWN;
         }
         if (outDepth) {
             *outDepth = 8;
         }
     } else if (format == AVIF_APP_FILE_FORMAT_PNG) {
-        (void)ignoreICC, (void)ignoreXMP; // TODO(yguyon): Implement
-        if (!avifPNGRead(filename, image, requestedFormat, requestedDepth, flags, ignoreExif, outDepth)) {
+        if (!avifPNGRead(filename, image, requestedFormat, requestedDepth, flags, ignoreICC, ignoreExif, ignoreXMP, outDepth)) {
             return AVIF_APP_FILE_FORMAT_UNKNOWN;
         }
     } else {
diff --git a/tests/data/paris_exif_at_end.png b/tests/data/paris_exif_at_end.png
deleted file mode 100644
index 53bfc08..0000000
--- a/tests/data/paris_exif_at_end.png
+++ /dev/null
Binary files differ
diff --git a/tests/data/paris_exif_xmp_icc.jpg b/tests/data/paris_exif_xmp_icc.jpg
index 805f9df..4b40002 100644
--- a/tests/data/paris_exif_xmp_icc.jpg
+++ 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
deleted file mode 100644
index df1f02e..0000000
--- a/tests/data/paris_exif_xmp_icc.png
+++ /dev/null
Binary files differ
diff --git a/tests/data/paris_icc_exif_xmp.png b/tests/data/paris_icc_exif_xmp.png
new file mode 100644
index 0000000..1bee17d
--- /dev/null
+++ b/tests/data/paris_icc_exif_xmp.png
Binary files differ
diff --git a/tests/data/paris_icc_exif_xmp_at_end.png b/tests/data/paris_icc_exif_xmp_at_end.png
new file mode 100644
index 0000000..7ad09e3
--- /dev/null
+++ b/tests/data/paris_icc_exif_xmp_at_end.png
Binary files differ
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index 690e83c..d70c983 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -155,40 +155,36 @@
 
 INSTANTIATE_TEST_SUITE_P(
     PngNone, MetadataTest,
-    Combine(Values("paris_exif_xmp_icc.png"),  // zTXt iCCP iTXt IDAT
+    Combine(Values("paris_icc_exif_xmp.png"),  // iCCP zTXt zTXt IDAT
             /*use_icc=*/Values(false), /*use_exif=*/Values(false),
-            /*use_xmp=*/Values(false),
-            // ignoreICC is not yet implemented.
-            /*expected_icc=*/Values(true),
+            /*use_xmp=*/Values(false), /*expected_icc=*/Values(false),
             /*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)));
+    Combine(Values("paris_icc_exif_xmp.png"),  // iCCP zTXt zTXt IDAT
+            /*use_icc=*/Values(true), /*use_exif=*/Values(true),
+            /*use_xmp=*/Values(true), /*expected_icc=*/Values(true),
+            /*expected_exif=*/Values(true), /*expected_xmp=*/Values(true)));
 
 INSTANTIATE_TEST_SUITE_P(
     PngExifAtEnd, MetadataTest,
-    Combine(Values("paris_exif_at_end.png"),  // iCCP IDAT eXIf
+    Combine(Values("paris_icc_exif_xmp_at_end.png"),  // iCCP IDAT eXIf tEXt
             /*use_icc=*/Values(true), /*use_exif=*/Values(true),
             /*use_xmp=*/Values(true), /*expected_icc=*/Values(true),
-            /*expected_exif=*/Values(true), /*expected_xmp=*/Values(false)));
+            /*expected_exif=*/Values(true), /*expected_xmp=*/Values(true)));
 
 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)));
+    Combine(Values("paris_exif_xmp_icc.jpg"),  // APP1-Exif, APP1-XMP, APP2-ICC
+            /*use_icc=*/Values(true), /*use_exif=*/Values(true),
+            /*use_xmp=*/Values(true), /*expected_icc=*/Values(true),
+            /*expected_exif=*/Values(true), /*expected_xmp=*/Values(true)));
 
 // Verify all parsers lead exactly to the same metadata bytes.
 TEST(MetadataTest, Compare) {
-  constexpr const char* kFileNames[] = {"paris_exif_at_end.png",
+  constexpr const char* kFileNames[] = {"paris_icc_exif_xmp.png",
                                         "paris_exif_xmp_icc.jpg",
-                                        "paris_exif_xmp_icc.png"};
+                                        "paris_icc_exif_xmp_at_end.png"};
   avifImage* images[sizeof(kFileNames) / sizeof(kFileNames[0])];
   avifImage** image_it = images;
   for (const char* file_name : kFileNames) {
@@ -205,13 +201,8 @@
   }
 
   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->exif, images[0]->exif));
+    EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, images[0]->xmp));
     EXPECT_TRUE(testutil::AreByteSequencesEqual(image->icc, images[0]->icc));
   }
 }
diff --git a/tests/test_cmd.sh b/tests/test_cmd.sh
index fde09a2..7f065d0 100755
--- a/tests/test_cmd.sh
+++ b/tests/test_cmd.sh
@@ -59,8 +59,21 @@
 
 # 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
+# PNG.
+"${AVIFENC}" "${TESTDATA_DIR}/paris_icc_exif_xmp.png" -o "${ENCODED_FILE}"
+"${AVIFENC}" "${TESTDATA_DIR}/paris_icc_exif_xmp.png" -o "${ENCODED_FILE_NO_METADATA}" --ignore-icc
+cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
+"${AVIFENC}" "${TESTDATA_DIR}/paris_icc_exif_xmp.png" -o "${ENCODED_FILE_NO_METADATA}" --ignore-exif
+cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
+"${AVIFENC}" "${TESTDATA_DIR}/paris_icc_exif_xmp.png" -o "${ENCODED_FILE_NO_METADATA}" --ignore-xmp
+cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
+# JPEG.
+"${AVIFENC}" "${TESTDATA_DIR}/paris_exif_xmp_icc.jpg" -o "${ENCODED_FILE}"
+"${AVIFENC}" "${TESTDATA_DIR}/paris_exif_xmp_icc.jpg" -o "${ENCODED_FILE_NO_METADATA}" --ignore-icc
+cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
+"${AVIFENC}" "${TESTDATA_DIR}/paris_exif_xmp_icc.jpg" -o "${ENCODED_FILE_NO_METADATA}" --ignore-exif
+cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
+"${AVIFENC}" "${TESTDATA_DIR}/paris_exif_xmp_icc.jpg" -o "${ENCODED_FILE_NO_METADATA}" --ignore-xmp
 cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
 
 # Argument parsing test with filenames starting with a dash.