Add avifImageExtractExifOrientationToIrotImir()

Move avifGetExifTiffHeaderOffset() from write.c to exif.c.
Add tests in avifmetadatatest.
Add avifenc change to CHANGELOG.md.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff86177..773127b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@
   and avifRGBImage::avoidLibYUV to AVIF_TRUE.
 * avifRGBImage::chromaUpsampling now only applies to conversions that need
   upsampling chroma from 4:2:0 or 4:2:2 and has no impact on the use of libyuv.
+* Exif and XMP metadata is imported from PNG and JPEG files.
 
 ### Removed
 * alphaRange field was removed from the avifImage struct. It it presumed that
@@ -38,6 +39,7 @@
 * Add imageDimensionLimit field to avifDecoder struct
 * Add autoTiling field to avifEncoder struct
 * Add AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV value to avifChromaDownsampling enum
+* Add avifImageExtractExifOrientationToIrotImir()
 * avifdec: Add --dimension-limit, which specifies the image dimension limit
   (width or height) that should be tolerated
 * avifenc: Add --sharpyuv, which enables "sharp" RGB to YUV420 conversion, which
@@ -45,7 +47,8 @@
   of the libwebp repository) at compile time.
 * avifenc: Add --ignore-exif and --ignore-xmp flags.
 * avifenc: Add --autotiling, which sets --tilerowslog2 and --tilecolslog2
-  automatically
+  automatically.
+* avifenc: Input Exif orientation is converted to irot/imir by default.
 
 ## [0.10.1] - 2022-04-11
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 37e22ee..b20794c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -195,6 +195,7 @@
     src/avif.c
     src/colr.c
     src/diag.c
+    src/exif.c
     src/io.c
     src/mem.c
     src/obu.c
diff --git a/apps/avifenc.c b/apps/avifenc.c
index 918d521..84fbd8b 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -891,6 +891,8 @@
     }
     if (exifOverride.size) {
         avifImageSetMetadataExif(image, exifOverride.data, exifOverride.size);
+        // Ignore any Exif parsing failure.
+        (void)avifImageExtractExifOrientationToIrotImir(image);
     }
     if (xmpOverride.size) {
         avifImageSetMetadataXMP(image, xmpOverride.data, xmpOverride.size);
diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index 2e2bd60..b20399e 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -347,6 +347,8 @@
                     goto cleanup;
                 }
                 avifImageSetMetadataExif(avif, marker->data + tagExif.size, marker->data_length - tagExif.size);
+                // Ignore any Exif parsing failure.
+                (void)avifImageExtractExifOrientationToIrotImir(avif);
                 found = AVIF_TRUE;
             }
         }
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index d40ab1a..9512585 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -123,6 +123,8 @@
                 return AVIF_FALSE;
             }
             avifImageSetMetadataExif(avif, exif, exifSize);
+            // Ignore any Exif parsing failure.
+            (void)avifImageExtractExifOrientationToIrotImir(avif);
             *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
         }
     }
diff --git a/examples/avif_example_encode.c b/examples/avif_example_encode.c
index 34b6cdc..443aed8 100644
--- a/examples/avif_example_encode.c
+++ b/examples/avif_example_encode.c
@@ -31,7 +31,7 @@
     // * transferCharacteristics
     // * matrixCoefficients
     // * avifImageSetProfileICC()
-    // * avifImageSetMetadataExif()
+    // * avifImageSetMetadataExif() + avifImageExtractExifOrientationToIrotImir()
     // * avifImageSetMetadataXMP()
     // * yuvRange
     // * alphaPremultiplied
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 9e0203d..eead360 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -482,10 +482,17 @@
 
 AVIF_API void avifImageSetProfileICC(avifImage * image, const uint8_t * icc, size_t iccSize);
 
-// Warning: If the Exif payload is set and invalid, avifEncoderWrite() may return AVIF_RESULT_INVALID_EXIF_PAYLOAD
+// Set Exif metadata. It is highly recommended to match image->transformFlags, image->irot and image->imir to any Exif
+// orientation contained in image->exif before encoding it to AVIF. See avifImageExtractExifOrientationToIrotImir().
+// Warning: If the Exif payload is set and invalid, avifEncoderWrite() may return AVIF_RESULT_INVALID_EXIF_PAYLOAD.
 AVIF_API void avifImageSetMetadataExif(avifImage * image, const uint8_t * exif, size_t exifSize);
+// Set XMP metadata.
 AVIF_API void avifImageSetMetadataXMP(avifImage * image, const uint8_t * xmp, size_t xmpSize);
 
+// 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);
+
 AVIF_API avifResult avifImageAllocatePlanes(avifImage * image, avifPlanesFlags planes); // Ignores any pre-existing planes
 AVIF_API void avifImageFreePlanes(avifImage * image, avifPlanesFlags planes);           // Ignores already-freed planes
 AVIF_API void avifImageStealPlanes(avifImage * dstImage, avifImage * srcImage, avifPlanesFlags planes);
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 5cc5aab..ff01e45 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -42,10 +42,16 @@
 
 float avifRoundf(float v);
 
+// H (host) is platform-dependent. Could be little- or big-endian.
+// N (network) is big-endian: most- to least-significant bytes.
+// C (custom) is little-endian: least- to most-significant bytes.
+// Never read N or C values; only access after casting to uint8_t*.
 uint16_t avifHTONS(uint16_t s);
 uint16_t avifNTOHS(uint16_t s);
+uint16_t avifCTOHS(uint16_t s);
 uint32_t avifHTONL(uint32_t l);
 uint32_t avifNTOHL(uint32_t l);
+uint32_t avifCTOHL(uint32_t l);
 uint64_t avifHTON64(uint64_t l);
 uint64_t avifNTOH64(uint64_t l);
 
@@ -199,6 +205,12 @@
 avifBool avifAreGridDimensionsValid(avifPixelFormat yuvFormat, uint32_t imageW, uint32_t imageH, uint32_t tileW, uint32_t tileH, avifDiagnostics * diag);
 
 // ---------------------------------------------------------------------------
+// Metadata
+
+// Validates the first bytes of the Exif payload and finds the TIFF header offset.
+avifResult avifGetExifTiffHeaderOffset(const avifRWData * exif, uint32_t * offset);
+
+// ---------------------------------------------------------------------------
 // avifCodecDecodeInput
 
 // Legal spatial_id values are [0,1,2,3], so this serves as a sentinel value for "do not filter by spatial_id"
@@ -354,7 +366,7 @@
 void avifDiagnosticsPrintf(avifDiagnostics * diag, const char * format, ...);
 
 // ---------------------------------------------------------------------------
-// avifStream
+// avifStream (network byte order; big-endian unless specified)
 
 typedef size_t avifBoxMarker;
 
@@ -382,7 +394,9 @@
 avifBool avifROStreamSkip(avifROStream * stream, size_t byteCount);
 avifBool avifROStreamRead(avifROStream * stream, uint8_t * data, size_t size);
 avifBool avifROStreamReadU16(avifROStream * stream, uint16_t * v);
+avifBool avifROStreamReadU16Endianness(avifROStream * stream, uint16_t * v, avifBool littleEndian);
 avifBool avifROStreamReadU32(avifROStream * stream, uint32_t * v);
+avifBool avifROStreamReadU32Endianness(avifROStream * stream, uint32_t * v, avifBool littleEndian);
 avifBool avifROStreamReadUX8(avifROStream * stream, uint64_t * v, uint64_t factor); // Reads a factor*8 sized uint, saves in v
 avifBool avifROStreamReadU64(avifROStream * stream, uint64_t * v);
 avifBool avifROStreamReadString(avifROStream * stream, char * output, size_t outputSize);
diff --git a/src/exif.c b/src/exif.c
new file mode 100644
index 0000000..fc73f8c
--- /dev/null
+++ b/src/exif.c
@@ -0,0 +1,119 @@
+// Copyright 2022 Google LLC. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif/internal.h"
+
+#include <stdint.h>
+#include <string.h>
+
+avifResult avifGetExifTiffHeaderOffset(const avifRWData * exif, uint32_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)) {
+            return AVIF_RESULT_OK;
+        }
+    }
+    // Couldn't find the TIFF header
+    return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+}
+
+avifResult avifImageExtractExifOrientationToIrotImir(avifImage * image)
+{
+    const avifTransformFlags otherFlags = image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
+    uint32_t offset;
+    const avifResult result = avifGetExifTiffHeaderOffset(&image->exif, &offset);
+    if (result != AVIF_RESULT_OK) {
+        // Couldn't find the TIFF header
+        return result;
+    }
+
+    avifROData raw = { image->exif.data + offset, image->exif.size - offset };
+    const avifBool littleEndian = (raw.data[0] == 'I');
+    avifROStream stream;
+    avifROStreamStart(&stream, &raw, NULL, NULL);
+
+    // TIFF Header
+    uint32_t offsetTo0thIfd;
+    if (!avifROStreamSkip(&stream, 4) || // Skip tiffHeaderBE or tiffHeaderLE.
+        !avifROStreamReadU32Endianness(&stream, &offsetTo0thIfd, littleEndian)) {
+        return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+    }
+
+    avifROStreamSetOffset(&stream, offsetTo0thIfd);
+    uint16_t fieldCount;
+    if (!avifROStreamReadU16Endianness(&stream, &fieldCount, littleEndian)) {
+        return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+    }
+    for (uint16_t field = 0; field < fieldCount; ++field) { // for each field interoperability array
+        uint16_t tag;
+        uint16_t type;
+        uint32_t count;
+        uint16_t firstHalfOfValueOffset;
+        if (!avifROStreamReadU16Endianness(&stream, &tag, littleEndian) || !avifROStreamReadU16Endianness(&stream, &type, littleEndian) ||
+            !avifROStreamReadU32Endianness(&stream, &count, littleEndian) ||
+            !avifROStreamReadU16Endianness(&stream, &firstHalfOfValueOffset, littleEndian) || !avifROStreamSkip(&stream, 2)) {
+            return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+        }
+        // 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;
+            }
+        }
+    }
+    // Orientation is in the 0th IFD, so no need to parse the following ones.
+
+    // 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.
+    image->transformFlags = otherFlags;
+    image->irot.angle = 0; // ignored
+    image->imir.mode = 0;  // ignored
+    return AVIF_RESULT_OK;
+}
diff --git a/src/stream.c b/src/stream.c
index a0cf021..a5935b4 100644
--- a/src/stream.c
+++ b/src/stream.c
@@ -108,6 +108,13 @@
     return AVIF_TRUE;
 }
 
+avifBool avifROStreamReadU16Endianness(avifROStream * stream, uint16_t * v, avifBool littleEndian)
+{
+    AVIF_CHECK(avifROStreamRead(stream, (uint8_t *)v, sizeof(uint16_t)));
+    *v = littleEndian ? avifCTOHS(*v) : avifNTOHS(*v);
+    return AVIF_TRUE;
+}
+
 avifBool avifROStreamReadU32(avifROStream * stream, uint32_t * v)
 {
     AVIF_CHECK(avifROStreamRead(stream, (uint8_t *)v, sizeof(uint32_t)));
@@ -115,6 +122,13 @@
     return AVIF_TRUE;
 }
 
+avifBool avifROStreamReadU32Endianness(avifROStream * stream, uint32_t * v, avifBool littleEndian)
+{
+    AVIF_CHECK(avifROStreamRead(stream, (uint8_t *)v, sizeof(uint32_t)));
+    *v = littleEndian ? avifCTOHL(*v) : avifNTOHL(*v);
+    return AVIF_TRUE;
+}
+
 avifBool avifROStreamReadU64(avifROStream * stream, uint64_t * v)
 {
     AVIF_CHECK(avifROStreamRead(stream, (uint8_t *)v, sizeof(uint64_t)));
diff --git a/src/utils.c b/src/utils.c
index 2e5cb7c..697cb0e 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -29,6 +29,12 @@
     return (uint16_t)((data[1] << 0) | (data[0] << 8));
 }
 
+uint16_t avifCTOHS(uint16_t s)
+{
+    const uint8_t * data = (const uint8_t *)&s;
+    return (uint16_t)((data[0] << 0) | (data[1] << 8));
+}
+
 uint32_t avifHTONL(uint32_t l)
 {
     uint32_t result;
@@ -46,6 +52,12 @@
     return ((uint32_t)data[3] << 0) | ((uint32_t)data[2] << 8) | ((uint32_t)data[1] << 16) | ((uint32_t)data[0] << 24);
 }
 
+uint32_t avifCTOHL(uint32_t l)
+{
+    const uint8_t * data = (const uint8_t *)&l;
+    return ((uint32_t)data[0] << 0) | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16) | ((uint32_t)data[3] << 24);
+}
+
 uint64_t avifHTON64(uint64_t l)
 {
     uint64_t result;
diff --git a/src/write.c b/src/write.c
index 12438ac..8f0bb30 100644
--- a/src/write.c
+++ b/src/write.c
@@ -690,27 +690,11 @@
 
 static avifResult avifEncoderDataCreateExifItem(avifEncoderData * data, const avifRWData * exif)
 {
-    // Validate Exif payload (if any) and find TIFF header offset
-    uint32_t exifTiffHeaderOffset = 0;
-    if (exif->size < 4) {
-        // Can't even fit the TIFF header, something is wrong
-        return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
-    }
-
-    const uint8_t tiffHeaderBE[4] = { 'M', 'M', 0, 42 };
-    const uint8_t tiffHeaderLE[4] = { 'I', 'I', 42, 0 };
-    for (; exifTiffHeaderOffset < (exif->size - 4); ++exifTiffHeaderOffset) {
-        if (!memcmp(&exif->data[exifTiffHeaderOffset], tiffHeaderBE, sizeof(tiffHeaderBE))) {
-            break;
-        }
-        if (!memcmp(&exif->data[exifTiffHeaderOffset], tiffHeaderLE, sizeof(tiffHeaderLE))) {
-            break;
-        }
-    }
-
-    if (exifTiffHeaderOffset >= exif->size - 4) {
+    uint32_t exifTiffHeaderOffset;
+    const avifResult result = avifGetExifTiffHeaderOffset(exif, &exifTiffHeaderOffset);
+    if (result != AVIF_RESULT_OK) {
         // Couldn't find the TIFF header
-        return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+        return result;
     }
 
     avifEncoderItem * exifItem = avifEncoderDataCreateItem(data, "Exif", "Exif", 5, 0);
diff --git a/tests/data/README.md b/tests/data/README.md
index 5d5228c..4044f84 100644
--- a/tests/data/README.md
+++ b/tests/data/README.md
@@ -86,6 +86,21 @@
 |  152612 | tEXt   |   7832 | `Raw profile type xmp..XMP.0000` |
 |  160456 | IEND   |      0 |                                  |
 
+### File [paris_exif_orientation_5.jpg](paris_exif_orientation_5.jpg)
+
+![](paris_exif_orientation_5.jpg)
+
+License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE)
+
+Source: `paris_exif_xmp_icc.jpg` stripped from all metadata with `exiftool -all=` and Exif orientation added
+with `exiv2 -k -M "set Exif.Image.Orientation 5"`
+
+| address | marker      |  length | data                                 |
+|--------:|-------------|--------:|--------------------------------------|
+|       0 | 0xffd8 SOI  |         |                                      |
+|       2 | 0xffe1 APP1 |      34 | `Exif..II*......................`    |
+|         |             |         | ...                                  |
+
 ## Grid
 
 ### File [sofa_grid1x5_420.avif](sofa_grid1x5_420.avif)
diff --git a/tests/data/paris_exif_orientation_5.jpg b/tests/data/paris_exif_orientation_5.jpg
new file mode 100644
index 0000000..aed77d0
--- /dev/null
+++ b/tests/data/paris_exif_orientation_5.jpg
Binary files differ
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index 5017f13..7372b97 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -63,6 +63,9 @@
   if (use_exif) {
     avifImageSetMetadataExif(image.get(), kSampleExif.data(),
                              kSampleExif.size());
+    // kSampleExif is not a valid Exif payload, just some part of it.
+    ASSERT_EQ(avifImageExtractExifOrientationToIrotImir(image.get()),
+              AVIF_RESULT_INVALID_EXIF_PAYLOAD);
   }
   if (use_xmp) {
     avifImageSetMetadataXMP(image.get(), kSampleXmp.data(), kSampleXmp.size());
@@ -214,6 +217,99 @@
 
 //------------------------------------------------------------------------------
 
+TEST(MetadataTest, ExifButDefaultIrotImir) {
+  const testutil::AvifImagePtr image =
+      testutil::ReadImage(data_path, "paris_exif_xmp_icc.jpg");
+  ASSERT_NE(image, nullptr);
+  // The Exif metadata contains orientation information: 1.
+  // It is converted to no irot/imir.
+  EXPECT_GT(image->exif.size, 0u);
+  EXPECT_EQ(image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+            avifTransformFlags{AVIF_TRANSFORM_NONE});
+
+  const testutil::AvifRwData encoded =
+      testutil::Encode(image.get(), AVIF_SPEED_FASTEST);
+  const testutil::AvifImagePtr decoded =
+      testutil::Decode(encoded.data, encoded.size);
+  ASSERT_NE(decoded, nullptr);
+
+  // No irot/imir after decoding because 1 maps to default no irot/imir.
+  EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, decoded->exif));
+  EXPECT_EQ(
+      decoded->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+      avifTransformFlags{AVIF_TRANSFORM_NONE});
+}
+
+TEST(MetadataTest, ExifOrientation) {
+  const testutil::AvifImagePtr image =
+      testutil::ReadImage(data_path, "paris_exif_orientation_5.jpg");
+  ASSERT_NE(image, nullptr);
+  // The Exif metadata contains orientation information: 5.
+  EXPECT_GT(image->exif.size, 0u);
+  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);
+
+  const testutil::AvifRwData encoded =
+      testutil::Encode(image.get(), AVIF_SPEED_FASTEST);
+  const testutil::AvifImagePtr decoded =
+      testutil::Decode(encoded.data, encoded.size);
+  ASSERT_NE(decoded, nullptr);
+
+  // irot/imir are expected.
+  EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, decoded->exif));
+  EXPECT_EQ(
+      decoded->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+      avifTransformFlags{AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR});
+  EXPECT_EQ(decoded->irot.angle, 1u);
+  EXPECT_EQ(decoded->imir.mode, 0u);
+}
+
+TEST(MetadataTest, ExifOrientationAndForcedImir) {
+  const testutil::AvifImagePtr image =
+      testutil::ReadImage(data_path, "paris_exif_orientation_5.jpg");
+  ASSERT_NE(image, nullptr);
+  // The Exif metadata contains orientation information: 5.
+  // Force irot/imir to values that have a different meaning than 5.
+  // This is not recommended but for testing only.
+  EXPECT_GT(image->exif.size, 0u);
+  image->transformFlags = AVIF_TRANSFORM_IMIR;
+  image->imir.mode = 1;
+
+  const testutil::AvifRwData encoded =
+      testutil::Encode(image.get(), AVIF_SPEED_FASTEST);
+  const testutil::AvifImagePtr decoded =
+      testutil::Decode(encoded.data, encoded.size);
+  ASSERT_NE(decoded, nullptr);
+
+  // Exif orientation is still there but irot/imir do not match it.
+  EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, decoded->exif));
+  EXPECT_EQ(decoded->transformFlags, avifTransformFlags{AVIF_TRANSFORM_IMIR});
+  EXPECT_EQ(decoded->irot.angle, 0u);
+  EXPECT_EQ(decoded->imir.mode, image->imir.mode);
+}
+
+TEST(MetadataTest, ExifIfdOffsetLoopingTo8) {
+  const testutil::AvifImagePtr image(avifImageCreateEmpty(), avifImageDestroy);
+  ASSERT_NE(image, nullptr);
+  const uint8_t kBadExifPayload[128] = {
+      'M', 'M', 0, 42,                          // TIFF header
+      0,   0,   0, 8,                           // Offset to 0th IFD
+      0,   1,                                   // fieldCount
+      0,   0,   0, 0,  0, 0, 0, 0, 0, 0, 0, 0,  // tag, type, count, valueOffset
+      0,   0,   0, 8  // Invalid IFD offset, infinitely looping back to 0th IFD.
+  };
+  avifRWDataSet(&image->exif, kBadExifPayload,
+                sizeof(kBadExifPayload) / sizeof(kBadExifPayload[0]));
+  // avifImageExtractExifOrientationToIrotImir() does not verify  the whole
+  // payload, only the parts necessary to extract Exif orientation.
+  ASSERT_EQ(avifImageExtractExifOrientationToIrotImir(image.get()),
+            AVIF_RESULT_OK);
+}
+
+//------------------------------------------------------------------------------
+
 }  // namespace
 }  // namespace libavif
 
diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc
index 57d8929..701c46f 100644
--- a/tests/gtest/aviftest_helpers.cc
+++ b/tests/gtest/aviftest_helpers.cc
@@ -29,6 +29,11 @@
   avifRGBImageAllocatePixels(this);
 }
 
+AvifRwData::AvifRwData(AvifRwData&& other) : avifRWData{other} {
+  other.data = nullptr;
+  other.size = 0;
+}
+
 //------------------------------------------------------------------------------
 
 RgbChannelOffsets GetRgbChannelOffsets(avifRGBFormat format) {
@@ -242,11 +247,33 @@
                     ignore_exif, ignore_xmp, image.get(), /*outDepth=*/nullptr,
                     /*sourceTiming=*/nullptr,
                     /*frameIter=*/nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
-    return {nullptr, avifImageDestroy};
+    return {nullptr, nullptr};
   }
   return image;
 }
 
+AvifRwData Encode(const avifImage* image, int speed) {
+  testutil::AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy);
+  if (!encoder) return {};
+  encoder->speed = speed;
+  testutil::AvifRwData bytes;
+  if (avifEncoderWrite(encoder.get(), image, &bytes) != AVIF_RESULT_OK) {
+    return {};
+  }
+  return bytes;
+}
+
+AvifImagePtr Decode(const uint8_t* bytes, size_t num_bytes) {
+  testutil::AvifImagePtr decoded(avifImageCreateEmpty(), avifImageDestroy);
+  testutil::AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy);
+  if (!decoded || !decoder ||
+      (avifDecoderReadMemory(decoder.get(), decoded.get(), bytes, num_bytes) !=
+       AVIF_RESULT_OK)) {
+    return {nullptr, nullptr};
+  }
+  return decoded;
+}
+
 //------------------------------------------------------------------------------
 
 static avifResult avifIOLimitedReaderRead(avifIO* io, uint32_t readFlags,
diff --git a/tests/gtest/aviftest_helpers.h b/tests/gtest/aviftest_helpers.h
index a0b1c05..556b244 100644
--- a/tests/gtest/aviftest_helpers.h
+++ b/tests/gtest/aviftest_helpers.h
@@ -24,6 +24,8 @@
 class AvifRwData : public avifRWData {
  public:
   AvifRwData() : avifRWData{nullptr, 0} {}
+  AvifRwData(const AvifRwData&) = delete;
+  AvifRwData(AvifRwData&& other);
   ~AvifRwData() { avifRWDataFree(this); }
 };
 
@@ -68,7 +70,7 @@
                     bool ignore_alpha = false);
 
 //------------------------------------------------------------------------------
-// Shorter versions of avifutil.h functions
+// Shorter versions of libavif functions
 
 // Reads the image named file_name located in directory at folder_path.
 // Returns nullptr in case of error.
@@ -80,6 +82,14 @@
     avifBool ignore_icc = false, avifBool ignore_exif = false,
     avifBool ignore_xmp = false);
 
+// Encodes the image with default parameters.
+// Returns an empty payload in case of error.
+AvifRwData Encode(const avifImage* image, int speed = AVIF_SPEED_DEFAULT);
+
+// Decodes the bytes to an image with default parameters.
+// Returns nullptr in case of error.
+AvifImagePtr Decode(const uint8_t* bytes, size_t num_bytes);
+
 //------------------------------------------------------------------------------
 // avifIO overlay