Add Exif/XMP support in PNG/JPEG export

Change avifGetExifTiffHeaderOffset()'s signature.
Add avifImageGetExifOrientationFromIrotImir() and
avifSetExifOrientation() in internal.h.
Link the CMake target avif_apps to avif_internal.
Drop and warn about long XMP when exporting to JPEG.
Refactor defines in avifjpeg.c.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a6a112..d3532ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@
   from internal.h when BUILD_SHARED_LIBS is ON.
 
 ### Changed
+* Exif and XMP metadata is exported to PNG and JPEG files by default,
+  except XMP payloads larger than 65502 bytes in JPEG.
 * The --grid flag in avifenc can be used for images that are not evenly divided
   into cells.
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7648d3b..3832320 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -511,7 +511,7 @@
         avif_apps STATIC apps/shared/avifjpeg.c apps/shared/iccjpeg.c apps/shared/avifpng.c apps/shared/avifutil.c
                          apps/shared/y4m.c
     )
-    target_link_libraries(avif_apps avif ${AVIF_PLATFORM_LIBRARIES} ${PNG_LIBRARY} ${ZLIB_LIBRARY} ${JPEG_LIBRARY})
+    target_link_libraries(avif_apps avif_internal ${AVIF_PLATFORM_LIBRARIES} ${PNG_LIBRARY} ${ZLIB_LIBRARY} ${JPEG_LIBRARY})
     # In GitHub CI's macos-latest os image, /usr/local/include has not only the headers of libpng
     # libjpeg but also the headers of an older version of libavif. Put the avif include directory
     # before ${PNG_PNG_INCLUDE_DIR} ${JPEG_INCLUDE_DIR} to prevent picking up old libavif headers
diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index 8463b23..b83eaa6 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: BSD-2-Clause
 
 #include "avifjpeg.h"
+#include "avif/internal.h"
 #include "avifutil.h"
 
 #include <assert.h>
@@ -243,23 +244,29 @@
     return NULL;
 }
 
+#define AVIF_JPEG_MAX_MARKER_DATA_LENGTH 65533
+
+// Exif tag
+#define AVIF_JPEG_EXIF_HEADER "Exif\0\0"
+#define AVIF_JPEG_EXIF_HEADER_LENGTH 6
+
 // XMP tags
-#define AVIF_STANDARD_XMP_TAG "http://ns.adobe.com/xap/1.0/\0"
-#define AVIF_STANDARD_XMP_TAG_LENGTH 29
-#define AVIF_EXTENDED_XMP_TAG "http://ns.adobe.com/xmp/extension/\0"
-#define AVIF_EXTENDED_XMP_TAG_LENGTH 35
+#define AVIF_JPEG_STANDARD_XMP_TAG "http://ns.adobe.com/xap/1.0/\0"
+#define AVIF_JPEG_STANDARD_XMP_TAG_LENGTH 29
+#define AVIF_JPEG_EXTENDED_XMP_TAG "http://ns.adobe.com/xmp/extension/\0"
+#define AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH 35
 
 // One way of storing the Extended XMP GUID (generated by a camera for example).
-#define AVIF_XMP_NOTE_TAG "xmpNote:HasExtendedXMP=\""
-#define AVIF_XMP_NOTE_TAG_LENGTH 24
+#define AVIF_JPEG_XMP_NOTE_TAG "xmpNote:HasExtendedXMP=\""
+#define AVIF_JPEG_XMP_NOTE_TAG_LENGTH 24
 // Another way of storing the Extended XMP GUID (generated by exiftool for example).
-#define AVIF_ALTERNATIVE_XMP_NOTE_TAG "<xmpNote:HasExtendedXMP>"
-#define AVIF_ALTERNATIVE_XMP_NOTE_TAG_LENGTH 24
+#define AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG "<xmpNote:HasExtendedXMP>"
+#define AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH 24
 
-#define AVIF_EXTENDED_XMP_GUID_LENGTH 32
+#define AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH 32
 
 // Offset in APP1 segment (skip tag + guid + size + offset).
-#define AVIF_OFFSET_TILL_EXTENDED_XMP (AVIF_EXTENDED_XMP_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH + 4 + 4)
+#define AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP (AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH + 4 + 4)
 
 // Note on setjmp() and volatile variables:
 //
@@ -377,7 +384,7 @@
     }
 
     if (!ignoreExif) {
-        const avifROData tagExif = { (const uint8_t *)"Exif\0\0", 6 };
+        const avifROData tagExif = { (const uint8_t *)AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH };
         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) &&
@@ -387,6 +394,9 @@
                     fprintf(stderr, "Exif extraction failed: unsupported Exif split into multiple segments or invalid multiple Exif segments\n");
                     goto cleanup;
                 }
+                // Exif orientation, if any, is imported to avif->irot/imir and kept in avif->exif.
+                // libheif has the same behavior, see
+                // https://github.com/strukturag/libheif/blob/ea78603d8e47096606813d221725621306789ff2/examples/heif_enc.cc#L403
                 avifImageSetMetadataExif(avif, marker->data + tagExif.size, marker->data_length - tagExif.size);
                 found = AVIF_TRUE;
             }
@@ -396,33 +406,33 @@
         const uint8_t * standardXMPData = NULL;
         uint32_t standardXMPSize = 0; // At most 64kB as defined by Adobe XMP Specification Part 3.
         for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
-            if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_STANDARD_XMP_TAG_LENGTH) &&
-                !memcmp(marker->data, AVIF_STANDARD_XMP_TAG, AVIF_STANDARD_XMP_TAG_LENGTH)) {
+            if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_STANDARD_XMP_TAG_LENGTH) &&
+                !memcmp(marker->data, AVIF_JPEG_STANDARD_XMP_TAG, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH)) {
                 if (standardXMPData) {
                     fprintf(stderr, "XMP extraction failed: invalid multiple standard XMP segments\n");
                     goto cleanup;
                 }
-                standardXMPData = marker->data + AVIF_STANDARD_XMP_TAG_LENGTH;
-                standardXMPSize = (uint32_t)(marker->data_length - AVIF_STANDARD_XMP_TAG_LENGTH);
+                standardXMPData = marker->data + AVIF_JPEG_STANDARD_XMP_TAG_LENGTH;
+                standardXMPSize = (uint32_t)(marker->data_length - AVIF_JPEG_STANDARD_XMP_TAG_LENGTH);
             }
         }
 
         avifBool foundExtendedXMP = AVIF_FALSE;
-        uint8_t extendedXMPGUID[AVIF_EXTENDED_XMP_GUID_LENGTH]; // The value is common to all extended XMP segments.
+        uint8_t extendedXMPGUID[AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]; // The value is common to all extended XMP segments.
         for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
-            if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_EXTENDED_XMP_TAG_LENGTH) &&
-                !memcmp(marker->data, AVIF_EXTENDED_XMP_TAG, AVIF_EXTENDED_XMP_TAG_LENGTH)) {
+            if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH) &&
+                !memcmp(marker->data, AVIF_JPEG_EXTENDED_XMP_TAG, AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH)) {
                 if (!standardXMPData) {
                     fprintf(stderr, "XMP extraction failed: extended XMP segment found, missing standard XMP segment\n");
                     goto cleanup;
                 }
 
-                if (marker->data_length < AVIF_OFFSET_TILL_EXTENDED_XMP) {
+                if (marker->data_length < AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP) {
                     fprintf(stderr, "XMP extraction failed: truncated extended XMP segment\n");
                     goto cleanup;
                 }
-                const uint8_t * guid = &marker->data[AVIF_EXTENDED_XMP_TAG_LENGTH];
-                for (size_t c = 0; c < AVIF_EXTENDED_XMP_GUID_LENGTH; ++c) {
+                const uint8_t * guid = &marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH];
+                for (size_t c = 0; c < AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH; ++c) {
                     // According to Adobe XMP Specification Part 3 section 1.1.3.1:
                     //   "128-bit GUID stored as a 32-byte ASCII hex string, capital A-F, no null termination"
                     if (((guid[c] < '0') || (guid[c] > '9')) && ((guid[c] < 'A') || (guid[c] > 'F'))) {
@@ -431,17 +441,17 @@
                     }
                 }
                 // Size of the current extended segment.
-                const size_t extendedXMPSize = marker->data_length - AVIF_OFFSET_TILL_EXTENDED_XMP;
+                const size_t extendedXMPSize = marker->data_length - AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP;
                 // Expected size of the sum of all extended segments.
                 // According to Adobe XMP Specification Part 3 section 1.1.3.1:
                 //   "full length of the ExtendedXMP serialization as a 32-bit unsigned integer"
                 const uint32_t totalExtendedXMPSize =
-                    avifJPEGReadUint32BigEndian(&marker->data[AVIF_EXTENDED_XMP_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH]);
+                    avifJPEGReadUint32BigEndian(&marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]);
                 // Offset in totalXMP after standardXMP.
                 // According to Adobe XMP Specification Part 3 section 1.1.3.1:
                 //   "offset of this portion as a 32-bit unsigned integer"
-                const uint32_t extendedXMPOffset =
-                    avifJPEGReadUint32BigEndian(&marker->data[AVIF_EXTENDED_XMP_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH + 4]);
+                const uint32_t extendedXMPOffset = avifJPEGReadUint32BigEndian(
+                    &marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH + 4]);
                 if (((uint64_t)standardXMPSize + totalExtendedXMPSize) > SIZE_MAX) {
                     fprintf(stderr, "XMP extraction failed: total XMP size is too large\n");
                     goto cleanup;
@@ -451,7 +461,7 @@
                     goto cleanup;
                 }
                 if (foundExtendedXMP) {
-                    if (memcmp(guid, extendedXMPGUID, AVIF_EXTENDED_XMP_GUID_LENGTH)) {
+                    if (memcmp(guid, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH)) {
                         fprintf(stderr, "XMP extraction failed: extended XMP segment GUID mismatch\n");
                         goto cleanup;
                     }
@@ -460,7 +470,7 @@
                         goto cleanup;
                     }
                 } else {
-                    memcpy(extendedXMPGUID, guid, AVIF_EXTENDED_XMP_GUID_LENGTH);
+                    memcpy(extendedXMPGUID, guid, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH);
 
                     avifRWDataRealloc(&totalXMP, (size_t)standardXMPSize + totalExtendedXMPSize);
                     memcpy(totalXMP.data, standardXMPData, standardXMPSize);
@@ -473,7 +483,7 @@
                 }
                 // According to Adobe XMP Specification Part 3 section 1.1.3.1:
                 //   "A robust JPEG reader should tolerate the marker segments in any order."
-                memcpy(&totalXMP.data[standardXMPSize + extendedXMPOffset], &marker->data[AVIF_OFFSET_TILL_EXTENDED_XMP], extendedXMPSize);
+                memcpy(&totalXMP.data[standardXMPSize + extendedXMPOffset], &marker->data[AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP], extendedXMPSize);
 
                 // Make sure no previously read data was overwritten by the current segment.
                 if (memchr(&extendedXMPReadBytes.data[extendedXMPOffset], 1, extendedXMPSize)) {
@@ -494,14 +504,14 @@
 
             // According to Adobe XMP Specification Part 3 section 1.1.3.1:
             //   "A reader must incorporate only ExtendedXMP blocks whose GUID matches the value of xmpNote:HasExtendedXMP."
-            uint8_t xmpNote[AVIF_XMP_NOTE_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH];
-            memcpy(xmpNote, AVIF_XMP_NOTE_TAG, AVIF_XMP_NOTE_TAG_LENGTH);
-            memcpy(xmpNote + AVIF_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_EXTENDED_XMP_GUID_LENGTH);
+            uint8_t xmpNote[AVIF_JPEG_XMP_NOTE_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH];
+            memcpy(xmpNote, AVIF_JPEG_XMP_NOTE_TAG, AVIF_JPEG_XMP_NOTE_TAG_LENGTH);
+            memcpy(xmpNote + AVIF_JPEG_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH);
             if (!avifJPEGFindSubstr(standardXMPData, standardXMPSize, xmpNote, sizeof(xmpNote))) {
                 // Try the alternative before returning an error.
-                uint8_t alternativeXmpNote[AVIF_ALTERNATIVE_XMP_NOTE_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH];
-                memcpy(alternativeXmpNote, AVIF_ALTERNATIVE_XMP_NOTE_TAG, AVIF_ALTERNATIVE_XMP_NOTE_TAG_LENGTH);
-                memcpy(alternativeXmpNote + AVIF_ALTERNATIVE_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_EXTENDED_XMP_GUID_LENGTH);
+                uint8_t alternativeXmpNote[AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH];
+                memcpy(alternativeXmpNote, AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG, AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH);
+                memcpy(alternativeXmpNote + AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH);
                 if (!avifJPEGFindSubstr(standardXMPData, standardXMPSize, alternativeXmpNote, sizeof(alternativeXmpNote))) {
                     fprintf(stderr, "XMP extraction failed: standard and extended XMP GUID mismatch\n");
                     goto cleanup;
@@ -571,9 +581,72 @@
     jpeg_start_compress(&cinfo, TRUE);
 
     if (avif->icc.data && (avif->icc.size > 0)) {
+        // TODO(yguyon): Use jpeg_write_icc_profile() instead?
         write_icc_profile(&cinfo, avif->icc.data, (unsigned int)avif->icc.size);
     }
 
+    if (avif->exif.data && (avif->exif.size > 0)) {
+        size_t exifTiffHeaderOffset;
+        avifResult result = avifGetExifTiffHeaderOffset(avif->exif.data, avif->exif.size, &exifTiffHeaderOffset);
+        if (result != AVIF_RESULT_OK) {
+            fprintf(stderr, "Error writing JPEG metadata: %s\n", avifResultToString(result));
+            goto cleanup;
+        }
+
+        avifRWData exif = { NULL, 0 };
+        avifRWDataRealloc(&exif, AVIF_JPEG_EXIF_HEADER_LENGTH + avif->exif.size - exifTiffHeaderOffset);
+        memcpy(exif.data, AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH);
+        memcpy(exif.data + AVIF_JPEG_EXIF_HEADER_LENGTH, avif->exif.data + exifTiffHeaderOffset, avif->exif.size - exifTiffHeaderOffset);
+        // Make sure the Exif orientation matches the irot/imir values.
+        // libheif does not have the same behavior. The orientation is applied to samples and orientation data is discarded there,
+        // see https://github.com/strukturag/libheif/blob/ea78603d8e47096606813d221725621306789ff2/examples/encoder_jpeg.cc#L187
+        result = avifSetExifOrientation(&exif, avifImageGetExifOrientationFromIrotImir(avif));
+        if (result != AVIF_RESULT_OK) {
+            // Ignore errors if the orientation is the default one because not being able to set Exif orientation now
+            // means a reader will not be able to parse it later either.
+            if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
+                fprintf(stderr, "Error writing JPEG metadata: %s\n", avifResultToString(result));
+                avifRWDataFree(&exif);
+                goto cleanup;
+            }
+        }
+
+        avifROData remainingExif = { exif.data, exif.size };
+        while (remainingExif.size > AVIF_JPEG_MAX_MARKER_DATA_LENGTH) {
+            jpeg_write_marker(&cinfo, JPEG_APP0 + 1, remainingExif.data, AVIF_JPEG_MAX_MARKER_DATA_LENGTH);
+            remainingExif.data += AVIF_JPEG_MAX_MARKER_DATA_LENGTH;
+            remainingExif.size -= AVIF_JPEG_MAX_MARKER_DATA_LENGTH;
+        }
+        jpeg_write_marker(&cinfo, JPEG_APP0 + 1, remainingExif.data, (unsigned int)remainingExif.size);
+        avifRWDataFree(&exif);
+    } else if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
+        // There is no Exif yet, but we need to store the orientation.
+        // TODO(yguyon): Add a valid Exif payload or rotate the samples.
+    }
+
+    if (avif->xmp.data && (avif->xmp.size > 0)) {
+        // See XMP specification part 3.
+        if (avif->xmp.size > 65502) {
+            // libheif just refuses to export JPEG with long XMP, see
+            // https://github.com/strukturag/libheif/blob/18291ddebc23c924440a8a3c9a7267fe3beb5901/examples/encoder_jpeg.cc#L227
+            // But libheif also ignores extended XMP at reading, so converting a JPEG with extended XMP to HEIC and back to JPEG
+            // works, with the extended XMP part dropped, even if it had fit into a single JPEG marker.
+
+            // In libavif the whole XMP payload is dropped if it exceeds a single JPEG marker size limit, with a warning.
+            // The advantage is that it keeps the whole XMP payload, including the extended part, if it fits into a single JPEG
+            // marker. This is acceptable because section 1.1.3.1 of XMP specification part 3 says
+            //   "It is unusual for XMP to exceed 65502 bytes; typically, it is around 2 KB."
+            fprintf(stderr, "Warning writing JPEG metadata: XMP payload is too big and was dropped\n");
+        } else {
+            avifRWData xmp = { NULL, 0 };
+            avifRWDataRealloc(&xmp, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH + avif->xmp.size);
+            memcpy(xmp.data, AVIF_JPEG_STANDARD_XMP_TAG, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH);
+            memcpy(xmp.data + AVIF_JPEG_STANDARD_XMP_TAG_LENGTH, avif->xmp.data, avif->xmp.size);
+            jpeg_write_marker(&cinfo, JPEG_APP0 + 1, xmp.data, (unsigned int)xmp.size);
+            avifRWDataFree(&xmp);
+        }
+    }
+
     while (cinfo.next_scanline < cinfo.image_height) {
         row_pointer[0] = &rgb.pixels[cinfo.next_scanline * rgb.rowBytes];
         (void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index bf8a8e8..b4e0283 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: BSD-2-Clause
 
 #include "avifpng.h"
+#include "avif/internal.h"
 #include "avifutil.h"
 
 #include "png.h"
@@ -20,6 +21,9 @@
 typedef png_charp png_iccp_datap;
 #endif
 
+//------------------------------------------------------------------------------
+// Reading
+
 // 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.
@@ -122,7 +126,17 @@
                 fprintf(stderr, "Exif extraction failed: empty eXIf chunk\n");
                 return AVIF_FALSE;
             }
-            avifImageSetMetadataExif(avif, exif, exifSize);
+            // Avoid avifImageSetMetadataExif() that sets irot/imir.
+            avifRWDataSet(&avif->exif, exif, exifSize);
+            // According to the Extensions to the PNG 1.2 Specification, Version 1.5.0, section 3.7:
+            //   "It is recommended that unless a decoder has independent knowledge of the validity of the Exif data,
+            //    the data should be considered to be of historical value only."
+            // Try to remove any Exif orientation data to be safe.
+            // It is easier to set it to 1 (the default top-left) than actually removing the tag.
+            // libheif has the same behavior, see
+            // https://github.com/strukturag/libheif/blob/18291ddebc23c924440a8a3c9a7267fe3beb5901/examples/heif_enc.cc#L703
+            // Ignore errors because not being able to set Exif orientation now means it cannot be parsed later either.
+            (void)avifSetExifOrientation(&avif->exif, 1);
             *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
         }
     }
@@ -151,6 +165,7 @@
                 return AVIF_FALSE;
             }
             avifRemoveHeader(&exifApp1Header, &avif->exif); // Ignore the return value because the header is optional.
+            (void)avifSetExifOrientation(&avif->exif, 1);   // See above.
             *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)) {
@@ -167,7 +182,8 @@
             if (!*ignoreExif && avifRemoveHeader(&exifApp1Header, &metadata)) {
                 avifRWDataFree(&avif->exif);
                 avif->exif = metadata;
-                *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
+                (void)avifSetExifOrientation(&avif->exif, 1); // See above.
+                *ignoreExif = AVIF_TRUE;                      // Ignore any other Exif chunk.
             } else if (!*ignoreXMP && avifRemoveHeader(&xmpApp1Header, &metadata)) {
                 avifRWDataFree(&avif->xmp);
                 avif->xmp = metadata;
@@ -368,11 +384,34 @@
     return readResult;
 }
 
+//------------------------------------------------------------------------------
+// Writing
+
+// Does the opposite of avifCopyRawProfile(): writes a payload as a raw profile string.
+static avifBool avifBytesToRawProfile(const avifRWData * bytes, const char * profileName, avifRWData * profile)
+{
+    // The width of the profile length is 8 characters.
+    if (bytes->size > 99999999) {
+        return AVIF_FALSE;
+    }
+    size_t position = 1 + strlen(profileName) + 1 + 8 + 1;
+    const size_t profileLength = position + bytes->size * 2 + 1;
+    avifRWDataRealloc(profile, profileLength);
+    snprintf((char *)profile->data, position + 1, "\n%s\n%08lu\n", profileName, (unsigned long)bytes->size);
+    for (size_t i = 0; i < bytes->size; ++i, position += 2) {
+        snprintf((char *)profile->data + position, 2 + 1, "%02x", bytes->data[i]);
+    }
+    profile->data[position] = '\n';
+    return AVIF_TRUE;
+}
+
 avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint32_t requestedDepth, avifChromaUpsampling chromaUpsampling, int compressionLevel)
 {
     volatile avifBool writeResult = AVIF_FALSE;
     png_structp png = NULL;
     png_infop info = NULL;
+    avifRWData exif = { NULL, 0 };
+    avifRWData xmp = { NULL, 0 };
     png_bytep * volatile rowPointers = NULL;
     FILE * volatile f = NULL;
 
@@ -448,6 +487,68 @@
     if (avif->icc.data && (avif->icc.size > 0)) {
         png_set_iCCP(png, info, "libavif", 0, (png_iccp_datap)avif->icc.data, (png_uint_32)avif->icc.size);
     }
+
+    png_text texts[2];
+    int numTextMetadataChunks = 0;
+    if (avif->exif.data && (avif->exif.size > 0)) {
+#ifdef PNG_eXIf_SUPPORTED
+        if (avif->exif.size > UINT32_MAX) {
+            fprintf(stderr, "Error writing PNG: Exif metadata is too big\n");
+            goto cleanup;
+        }
+        png_set_eXIf_1(png, info, (png_uint_32)avif->exif.size, avif->exif.data);
+#else
+        if (!avifBytesToRawProfile(&avif->exif, "Exif", &exif)) {
+            fprintf(stderr, "Error writing PNG: Exif metadata is too big\n");
+            goto cleanup;
+        }
+        png_text * text = &texts[numTextMetadataChunks++];
+        memset(text, 0, sizeof(*text));
+        text->compression = PNG_TEXT_COMPRESSION_zTXt;
+        text->key = "Raw profile type exif";
+        text->text = (char *)exif.data;
+        text->text_length = exif.size;
+#endif // PNG_eXIf_SUPPORTED
+    }
+    if (avif->xmp.data && (avif->xmp.size > 0)) {
+#ifdef PNG_iTXt_SUPPORTED
+        // The iTXt XMP payload may not contain a zero byte according to section 4.2.3.3 of
+        // the PNG specification, version 1.2. Otherwise, use a raw profile.
+        if (!memchr(avif->xmp.data, '\0', avif->xmp.size)) {
+            // Providing the length through png_text.itxt_length does not work.
+            // The given png_text.text string must end with a zero byte.
+            if (avif->xmp.size >= SIZE_MAX) {
+                fprintf(stderr, "Error writing PNG: XMP metadata is too big\n");
+                goto cleanup;
+            }
+            avifRWDataRealloc(&xmp, avif->xmp.size + 1);
+            memcpy(xmp.data, avif->xmp.data, avif->xmp.size);
+            xmp.data[avif->xmp.size] = '\0';
+            png_text * text = &texts[numTextMetadataChunks++];
+            memset(text, 0, sizeof(*text));
+            text->compression = PNG_ITXT_COMPRESSION_NONE;
+            text->key = "XML:com.adobe.xmp";
+            text->text = (char *)xmp.data;
+            text->itxt_length = xmp.size;
+        } else
+#endif // PNG_iTXt_SUPPORTED
+        {
+            if (!avifBytesToRawProfile(&avif->xmp, "XMP", &xmp)) {
+                fprintf(stderr, "Error writing PNG: XMP metadata is too big\n");
+                goto cleanup;
+            }
+            png_text * text = &texts[numTextMetadataChunks++];
+            memset(text, 0, sizeof(*text));
+            text->compression = PNG_TEXT_COMPRESSION_zTXt;
+            text->key = "Raw profile type xmp";
+            text->text = (char *)xmp.data;
+            text->text_length = xmp.size;
+        }
+    }
+    if (numTextMetadataChunks != 0) {
+        png_set_text(png, info, texts, numTextMetadataChunks);
+    }
+
     png_write_info(png, info);
 
     rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * avif->height);
@@ -462,6 +563,9 @@
             rowPointers[y] = &rgb.pixels[y * rgb.rowBytes];
         }
     }
+    if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
+        // TODO(yguyon): Rotate the samples.
+    }
 
     if (rgbDepth > 8) {
         png_set_swap(png);
@@ -479,6 +583,8 @@
     if (png) {
         png_destroy_write_struct(&png, &info);
     }
+    avifRWDataFree(&exif);
+    avifRWDataFree(&xmp);
     if (rowPointers) {
         free(rowPointers);
     }
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 4fee2a1..04d2619 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -207,11 +207,15 @@
 // ---------------------------------------------------------------------------
 // Metadata
 
-// Validates the first bytes of the Exif payload and finds the TIFF header offset.
-avifResult avifGetExifTiffHeaderOffset(const avifRWData * exif, uint32_t * offset);
+// Validates the first bytes of the Exif payload and finds the TIFF header offset (up to UINT32_MAX).
+avifResult avifGetExifTiffHeaderOffset(const uint8_t * exif, size_t exifSize, size_t * offset);
 // Attempts to parse the image->exif payload for Exif orientation and sets image->transformFlags, image->irot and
 // image->imir on success. Returns AVIF_RESULT_INVALID_EXIF_PAYLOAD on failure.
 avifResult avifImageExtractExifOrientationToIrotImir(avifImage * image);
+// Converts image->transformFlags, image->irot and image->imir to the equivalent Exif orientation value in [1:8].
+uint8_t avifImageGetExifOrientationFromIrotImir(const avifImage * image);
+// Attempts to parse the image->exif payload until the Exif orientation is found, then sets it to the given value.
+avifResult avifSetExifOrientation(avifRWData * exif, uint8_t orientation);
 
 // ---------------------------------------------------------------------------
 // avifCodecDecodeInput
diff --git a/src/exif.c b/src/exif.c
index 4bdbd7a..2dbdfd8 100644
--- a/src/exif.c
+++ b/src/exif.c
@@ -6,12 +6,13 @@
 #include <stdint.h>
 #include <string.h>
 
-avifResult avifGetExifTiffHeaderOffset(const avifRWData * exif, uint32_t * offset)
+avifResult avifGetExifTiffHeaderOffset(const uint8_t * exif, size_t exifSize, size_t * offset)
 {
     const uint8_t tiffHeaderBE[4] = { 'M', 'M', 0, 42 };
     const uint8_t tiffHeaderLE[4] = { 'I', 'I', 42, 0 };
-    for (*offset = 0; *offset + 4 < exif->size; ++*offset) {
-        if (!memcmp(&exif->data[*offset], tiffHeaderBE, 4) || !memcmp(&exif->data[*offset], tiffHeaderLE, 4)) {
+    exifSize = AVIF_MIN(exifSize, UINT32_MAX);
+    for (*offset = 0; *offset + 4 < exifSize; ++*offset) {
+        if (!memcmp(&exif[*offset], tiffHeaderBE, 4) || !memcmp(&exif[*offset], tiffHeaderLE, 4)) {
             return AVIF_RESULT_OK;
         }
     }
@@ -19,17 +20,17 @@
     return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
 }
 
-avifResult avifImageExtractExifOrientationToIrotImir(avifImage * image)
+// Returns the offset to the Exif 8-bit orientation value and AVIF_RESULT_OK, or an error.
+// If the offset is set to exifSize, there was no parsing error but no orientation tag was found.
+static avifResult avifGetExifOrientationOffset(const uint8_t * exif, size_t exifSize, size_t * offset)
 {
-    const avifTransformFlags otherFlags = image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
-    uint32_t offset;
-    const avifResult result = avifGetExifTiffHeaderOffset(&image->exif, &offset);
+    const avifResult result = avifGetExifTiffHeaderOffset(exif, exifSize, offset);
     if (result != AVIF_RESULT_OK) {
         // Couldn't find the TIFF header
         return result;
     }
 
-    avifROData raw = { image->exif.data + offset, image->exif.size - offset };
+    avifROData raw = { exif + *offset, exifSize - *offset };
     const avifBool littleEndian = (raw.data[0] == 'I');
     avifROStream stream;
     avifROStreamStart(&stream, &raw, NULL, NULL);
@@ -59,56 +60,79 @@
         // Orientation attribute according to JEITA CP-3451C section 4.6.4 (TIFF Rev. 6.0 Attribute Information):
         const uint16_t shortType = 0x03;
         if (tag == 0x0112 && type == shortType && count == 0x01) {
-            // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A Orientation
-            // to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 sections 6.5.10 and 6.5.12.
-            switch (firstHalfOfValueOffset) {
-                case 1: // The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
-                    image->transformFlags = otherFlags;
-                    image->irot.angle = 0; // ignored
-                    image->imir.mode = 0;  // ignored
-                    return AVIF_RESULT_OK;
-                case 2: // The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
-                    image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
-                    image->irot.angle = 0; // ignored
-                    image->imir.mode = 1;
-                    return AVIF_RESULT_OK;
-                case 3: // The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
-                    image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
-                    image->irot.angle = 2;
-                    image->imir.mode = 0; // ignored
-                    return AVIF_RESULT_OK;
-                case 4: // The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
-                    image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
-                    image->irot.angle = 0; // ignored
-                    image->imir.mode = 0;
-                    return AVIF_RESULT_OK;
-                case 5: // The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
-                    image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
-                    image->irot.angle = 1; // applied before imir according to MIAF spec ISO/IEC 28002-12:2021 - section 7.3.6.7
-                    image->imir.mode = 0;
-                    return AVIF_RESULT_OK;
-                case 6: // The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
-                    image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
-                    image->irot.angle = 3;
-                    image->imir.mode = 0; // ignored
-                    return AVIF_RESULT_OK;
-                case 7: // The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
-                    image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
-                    image->irot.angle = 3; // applied before imir according to MIAF spec ISO/IEC 28002-12:2021 - section 7.3.6.7
-                    image->imir.mode = 0;
-                    return AVIF_RESULT_OK;
-                case 8: // The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
-                    image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
-                    image->irot.angle = 1;
-                    image->imir.mode = 0; // ignored
-                    return AVIF_RESULT_OK;
-                default: // reserved
-                    break;
+            // Only consider non-reserved orientation values, so that it is known that
+            // the most meaningful byte of firstHalfOfValueOffset is 0.
+            if (firstHalfOfValueOffset >= 1 && firstHalfOfValueOffset <= 8) {
+                // Offset to the least meaningful byte of firstHalfOfValueOffset.
+                *offset += avifROStreamOffset(&stream) - (littleEndian ? 4 : 3);
+                return AVIF_RESULT_OK;
             }
         }
     }
     // Orientation is in the 0th IFD, so no need to parse the following ones.
 
+    *offset = exifSize; // Signal missing orientation tag in valid Exif payload.
+    return AVIF_RESULT_OK;
+}
+
+avifResult avifImageExtractExifOrientationToIrotImir(avifImage * image)
+{
+    const avifTransformFlags otherFlags = image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
+    size_t offset;
+    const avifResult result = avifGetExifOrientationOffset(image->exif.data, image->exif.size, &offset);
+    if (result != AVIF_RESULT_OK) {
+        return result;
+    }
+    if (offset < image->exif.size) {
+        const uint8_t orientation = image->exif.data[offset];
+        // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A Orientation
+        // to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 sections 6.5.10 and 6.5.12.
+        switch (orientation) {
+            case 1: // The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
+                image->transformFlags = otherFlags;
+                image->irot.angle = 0; // ignored
+                image->imir.mode = 0;  // ignored
+                return AVIF_RESULT_OK;
+            case 2: // The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
+                image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
+                image->irot.angle = 0; // ignored
+                image->imir.mode = 1;
+                return AVIF_RESULT_OK;
+            case 3: // The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
+                image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
+                image->irot.angle = 2;
+                image->imir.mode = 0; // ignored
+                return AVIF_RESULT_OK;
+            case 4: // The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
+                image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
+                image->irot.angle = 0; // ignored
+                image->imir.mode = 0;
+                return AVIF_RESULT_OK;
+            case 5: // The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
+                image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
+                image->irot.angle = 1; // applied before imir according to MIAF spec ISO/IEC 28002-12:2021 - section 7.3.6.7
+                image->imir.mode = 0;
+                return AVIF_RESULT_OK;
+            case 6: // The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
+                image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
+                image->irot.angle = 3;
+                image->imir.mode = 0; // ignored
+                return AVIF_RESULT_OK;
+            case 7: // The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
+                image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
+                image->irot.angle = 3; // applied before imir according to MIAF spec ISO/IEC 28002-12:2021 - section 7.3.6.7
+                image->imir.mode = 0;
+                return AVIF_RESULT_OK;
+            case 8: // The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
+                image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
+                image->irot.angle = 1;
+                image->imir.mode = 0; // ignored
+                return AVIF_RESULT_OK;
+            default: // reserved
+                break;
+        }
+    }
+
     // The orientation tag is not mandatory (only recommended) according to JEITA CP-3451C section 4.6.8.A.
     // The default value is 1 if the orientation tag is missing, meaning:
     //   The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
@@ -118,6 +142,64 @@
     return AVIF_RESULT_OK;
 }
 
+uint8_t avifImageGetExifOrientationFromIrotImir(const avifImage * image)
+{
+    if ((image->transformFlags & AVIF_TRANSFORM_IROT) && (image->irot.angle == 1)) {
+        if (image->transformFlags & AVIF_TRANSFORM_IMIR) {
+            if (image->imir.mode) {
+                return 7; // 90 degrees anti-clockwise then swap left and right.
+            }
+            return 5; // 90 degrees anti-clockwise then swap top and bottom.
+        }
+        return 6; // 90 degrees anti-clockwise.
+    }
+    if ((image->transformFlags & AVIF_TRANSFORM_IROT) && (image->irot.angle == 2)) {
+        if (image->transformFlags & AVIF_TRANSFORM_IMIR) {
+            if (image->imir.mode) {
+                return 4; // 180 degrees anti-clockwise then swap left and right.
+            }
+            return 2; // 180 degrees anti-clockwise then swap top and bottom.
+        }
+        return 3; // 180 degrees anti-clockwise.
+    }
+    if ((image->transformFlags & AVIF_TRANSFORM_IROT) && (image->irot.angle == 3)) {
+        if (image->transformFlags & AVIF_TRANSFORM_IMIR) {
+            if (image->imir.mode) {
+                return 5; // 270 degrees anti-clockwise then swap left and right.
+            }
+            return 7; // 270 degrees anti-clockwise then swap top and bottom.
+        }
+        return 8; // 270 degrees anti-clockwise.
+    }
+    if (image->transformFlags & AVIF_TRANSFORM_IMIR) {
+        if (image->imir.mode) {
+            return 2; // Swap left and right.
+        }
+        return 4; // Swap top and bottom.
+    }
+    return 1; // Default orientation ("top-left", no-op).
+}
+
+avifResult avifSetExifOrientation(avifRWData * exif, uint8_t orientation)
+{
+    size_t offset;
+    const avifResult result = avifGetExifOrientationOffset(exif->data, exif->size, &offset);
+    if (result != AVIF_RESULT_OK) {
+        return result;
+    }
+    if (offset < exif->size) {
+        exif->data[offset] = orientation;
+        return AVIF_RESULT_OK;
+    }
+    // No Exif orientation was found.
+    if (orientation == 1) {
+        // The default orientation is 1, so if the given orientation is 1 too, do nothing.
+        return AVIF_RESULT_OK;
+    }
+    // Adding an orientation tag to an Exif payload is involved.
+    return AVIF_RESULT_NOT_IMPLEMENTED;
+}
+
 void avifImageSetMetadataExif(avifImage * image, const uint8_t * exif, size_t exifSize)
 {
     avifRWDataSet(&image->exif, exif, exifSize);
diff --git a/src/write.c b/src/write.c
index 8b33b0c..9576b77 100644
--- a/src/write.c
+++ b/src/write.c
@@ -692,8 +692,8 @@
 
 static avifResult avifEncoderDataCreateExifItem(avifEncoderData * data, const avifRWData * exif)
 {
-    uint32_t exifTiffHeaderOffset;
-    const avifResult result = avifGetExifTiffHeaderOffset(exif, &exifTiffHeaderOffset);
+    size_t exifTiffHeaderOffset;
+    const avifResult result = avifGetExifTiffHeaderOffset(exif->data, exif->size, &exifTiffHeaderOffset);
     if (result != AVIF_RESULT_OK) {
         // Couldn't find the TIFF header
         return result;
@@ -706,10 +706,10 @@
     exifItem->irefToID = data->primaryItemID;
     exifItem->irefType = "cdsc";
 
-    avifRWDataRealloc(&exifItem->metadataPayload, sizeof(uint32_t) + exif->size);
-    exifTiffHeaderOffset = avifHTONL(exifTiffHeaderOffset);
-    memcpy(exifItem->metadataPayload.data, &exifTiffHeaderOffset, sizeof(uint32_t));
-    memcpy(exifItem->metadataPayload.data + sizeof(uint32_t), exif->data, exif->size);
+    const uint32_t offset32bit = avifHTONL((uint32_t)exifTiffHeaderOffset);
+    avifRWDataRealloc(&exifItem->metadataPayload, sizeof(offset32bit) + exif->size);
+    memcpy(exifItem->metadataPayload.data, &offset32bit, sizeof(offset32bit));
+    memcpy(exifItem->metadataPayload.data + sizeof(offset32bit), exif->data, exif->size);
     return AVIF_RESULT_OK;
 }
 
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index 6b3300c..3bdc5a9 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -5,6 +5,8 @@
 #include <tuple>
 
 #include "avif/avif.h"
+#include "avifjpeg.h"
+#include "avifpng.h"
 #include "aviftest_helpers.h"
 #include "gtest/gtest.h"
 
@@ -112,21 +114,33 @@
 //------------------------------------------------------------------------------
 // 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>> {};
+testutil::AvifImagePtr WriteAndReadImage(const avifImage& image,
+                                         const std::string& file_name) {
+  const std::string file_path = testing::TempDir() + file_name;
+  if (file_name.substr(file_name.size() - 4) == ".png") {
+    if (!avifPNGWrite(file_path.c_str(), &image, /*requestedDepth=*/0,
+                      AVIF_CHROMA_UPSAMPLING_AUTOMATIC,
+                      /*compressionLevel=*/0)) {
+      return {nullptr, nullptr};
+    }
+  } else {
+    if (!avifJPEGWrite(file_path.c_str(), &image, /*jpegQuality=*/100,
+                       AVIF_CHROMA_UPSAMPLING_AUTOMATIC)) {
+      return {nullptr, nullptr};
+    }
+  }
+  return testutil::ReadImage(testing::TempDir().c_str(), file_name.c_str());
+}
 
-// zTXt "Raw profile type exif" at the beginning of a PNG file.
-TEST_P(MetadataTest, Read) {
+class MetadataTest : public testing::TestWithParam<
+                         std::tuple</*file_name=*/const char*, /*use_icc=*/bool,
+                                    /*use_exif=*/bool, /*use_xmp=*/bool>> {};
+
+TEST_P(MetadataTest, ReadWriteReadCompare) {
   const char* file_name = 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());
 
   const testutil::AvifImagePtr image = testutil::ReadImage(
       data_path, file_name, AVIF_PIXEL_FORMAT_NONE, 0,
@@ -134,55 +148,45 @@
   ASSERT_NE(image, nullptr);
   EXPECT_NE(image->width * image->height, 0u);
 
-  if (expect_icc) {
+  if (use_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) {
+  if (use_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) {
+  if (use_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);
   }
+
+  // Writing and reading that same metadata should give the same bytes.
+  for (const std::string extension : {".png", ".jpg"}) {
+    const testutil::AvifImagePtr temp_image =
+        WriteAndReadImage(*image, file_name + extension);
+    ASSERT_NE(temp_image, nullptr);
+    ASSERT_TRUE(testutil::AreByteSequencesEqual(image->icc, temp_image->icc));
+    ASSERT_TRUE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
+    ASSERT_TRUE(testutil::AreByteSequencesEqual(image->xmp, temp_image->xmp));
+  }
 }
 
 INSTANTIATE_TEST_SUITE_P(
-    PngNone, MetadataTest,
-    Combine(Values("paris_icc_exif_xmp.png"),  // iCCP zTXt zTXt IDAT
-            /*use_icc=*/Values(false), /*use_exif=*/Values(false),
-            /*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_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_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(true)));
-
-INSTANTIATE_TEST_SUITE_P(
-    Jpeg, MetadataTest,
-    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)));
+    PngJpeg, MetadataTest,
+    Combine(Values("paris_icc_exif_xmp.png",         // iCCP zTXt zTXt IDAT
+                   "paris_icc_exif_xmp_at_end.png",  // iCCP IDAT eXIf tEXt
+                   "paris_exif_xmp_icc.jpg"),  // APP1-Exif, APP1-XMP, APP2-ICC
+            /*use_icc=*/Bool(), /*use_exif=*/Bool(), /*use_xmp=*/Bool()));
 
 // Verify all parsers lead exactly to the same metadata bytes.
 TEST(MetadataTest, Compare) {
@@ -269,6 +273,26 @@
       avifTransformFlags{AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR});
   EXPECT_EQ(decoded->irot.angle, 1u);
   EXPECT_EQ(decoded->imir.mode, 0u);
+
+  // Exif orientation is kept in JPEG export.
+  testutil::AvifImagePtr temp_image =
+      WriteAndReadImage(*image, "paris_exif_orientation_5.jpg");
+  ASSERT_NE(temp_image, nullptr);
+  EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
+  EXPECT_EQ(image->transformFlags, temp_image->transformFlags);
+  EXPECT_EQ(image->irot.angle, temp_image->irot.angle);
+  EXPECT_EQ(image->imir.mode, temp_image->imir.mode);
+  EXPECT_EQ(image->width, temp_image->width);  // Samples are left untouched.
+
+  // Exif orientation in PNG export should be ignored or discarded.
+  temp_image = WriteAndReadImage(*image, "paris_exif_orientation_5.png");
+  ASSERT_NE(temp_image, nullptr);
+  EXPECT_FALSE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
+  EXPECT_EQ(
+      temp_image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+      avifTransformFlags{0});
+  // TODO(yguyon): Fix orientation not being applied to PNG samples.
+  EXPECT_EQ(image->width, temp_image->width /* should be height here */);
 }
 
 TEST(MetadataTest, ExifOrientationAndForcedImir) {
@@ -293,6 +317,39 @@
   EXPECT_EQ(decoded->transformFlags, avifTransformFlags{AVIF_TRANSFORM_IMIR});
   EXPECT_EQ(decoded->irot.angle, 0u);
   EXPECT_EQ(decoded->imir.mode, image->imir.mode);
+
+  // Exif orientation is set equivalent to irot/imir in JPEG export.
+  // Existing Exif orientation is overwritten.
+  const testutil::AvifImagePtr temp_image =
+      WriteAndReadImage(*image, "paris_exif_orientation_2.jpg");
+  ASSERT_NE(temp_image, nullptr);
+  EXPECT_FALSE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
+  EXPECT_EQ(image->transformFlags, temp_image->transformFlags);
+  EXPECT_EQ(image->imir.mode, temp_image->imir.mode);
+  EXPECT_EQ(image->width, temp_image->width);  // Samples are left untouched.
+}
+
+TEST(MetadataTest, RotatedJpegBecauseOfIrotImir) {
+  const testutil::AvifImagePtr image =
+      testutil::ReadImage(data_path, "paris_exif_orientation_5.jpg");
+  ASSERT_NE(image, nullptr);
+  avifImageSetMetadataExif(image.get(), nullptr, 0);  // Clear Exif.
+  // Orientation is kept in irot/imir.
+  EXPECT_EQ(image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+            avifTransformFlags{AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR});
+  EXPECT_EQ(image->irot.angle, 1u);
+  EXPECT_EQ(image->imir.mode, 0u);
+
+  // No Exif metadata to store the orientation: the samples should be rotated.
+  const testutil::AvifImagePtr temp_image =
+      WriteAndReadImage(*image, "paris_exif_orientation_5.jpg");
+  ASSERT_NE(temp_image, nullptr);
+  EXPECT_EQ(temp_image->exif.size, 0u);
+  EXPECT_EQ(
+      temp_image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+      avifTransformFlags{0});
+  // TODO(yguyon): Fix orientation not being applied to JPEG samples.
+  EXPECT_EQ(image->width, temp_image->width /* should be height here */);
 }
 
 TEST(MetadataTest, ExifIfdOffsetLoopingTo8) {
@@ -326,6 +383,15 @@
       testutil::ReadImage(data_path, "dog_exif_extended_xmp_icc.jpg");
   ASSERT_NE(image, nullptr);
   ASSERT_NE(image->xmp.size, 0u);
+  ASSERT_LT(image->xmp.size,
+            size_t{65503});  // Fits in a single JPEG APP1 marker.
+
+  for (const char* temp_file_name : {"dog.png", "dog.jpg"}) {
+    const testutil::AvifImagePtr temp_image =
+        WriteAndReadImage(*image, temp_file_name);
+    ASSERT_NE(temp_image, nullptr);
+    EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, temp_image->xmp));
+  }
 }
 
 TEST(MetadataTest, MultipleExtendedXMPAndAlternativeGUIDTag) {
@@ -333,6 +399,16 @@
       testutil::ReadImage(data_path, "paris_extended_xmp.jpg");
   ASSERT_NE(image, nullptr);
   ASSERT_GT(image->xmp.size, size_t{65536 * 2});
+
+  testutil::AvifImagePtr temp_image =
+      WriteAndReadImage(*image, "paris_extended_xmp.png");
+  ASSERT_NE(temp_image, nullptr);
+  EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, temp_image->xmp));
+
+  // Writing more than 65502 bytes of XMP in a JPEG is not supported.
+  temp_image = WriteAndReadImage(*image, "paris_extended_xmp.jpg");
+  ASSERT_NE(temp_image, nullptr);
+  ASSERT_EQ(temp_image->xmp.size, 0u);  // XMP was dropped.
 }
 
 //------------------------------------------------------------------------------