diff mbox series

[FFmpeg-devel,v2,1/3] various: change EXIF metadata into AVFrameSideData

Message ID 20240916144344.390716-2-leo.izen@gmail.com
State New
Headers show
Series Exif Overhaul | expand

Checks

Context Check Description
yinshiyou/make_loongarch64 success Make finished
yinshiyou/make_fate_loongarch64 success Make fate finished
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Leo Izen Sept. 16, 2024, 2:43 p.m. UTC
This patch centralizes much of the EXIF parsing and handling code for
libavcodec, and delegates its own AVFrameSideData type to containing
the buffer that holds EXIF metadata. This patch also adds exposes the
exif parsing routing so it can be called by ffprobe, and updates the
corresponding FATE tests to read the keys from the side data instead of
from the main frame metadata.

This commit also deprecates an avpriv_ function in exif.h, exposing the
parsing functionality as a public API in the exported libavcodec/exif.h
header file.

Signed-off-by: Leo Izen <leo.izen@gmail.com>
---
 fftools/ffprobe.c                  |  27 +-
 libavcodec/Makefile                |   1 +
 libavcodec/exif.c                  | 394 +++++++++++++++++++++++++++--
 libavcodec/exif.h                  |  32 ++-
 libavcodec/exif_internal.h         |  55 ++++
 libavcodec/mjpegdec.c              |  91 +------
 libavcodec/mjpegdec.h              |   2 +-
 libavcodec/tiff.c                  |  19 +-
 libavcodec/tiff.h                  |   1 +
 libavcodec/version.h               |   2 +-
 libavcodec/webp.c                  |  38 +--
 libavformat/avidec.c               |   4 +-
 libavutil/frame.c                  |   2 +
 libavutil/frame.h                  |   6 +
 tests/ref/fate/exif-image-embedded |   5 +-
 tests/ref/fate/exif-image-jpg      |  91 +++----
 tests/ref/fate/exif-image-webp     |  91 +++----
 17 files changed, 631 insertions(+), 230 deletions(-)
 create mode 100644 libavcodec/exif_internal.h
diff mbox series

Patch

diff --git a/fftools/ffprobe.c b/fftools/ffprobe.c
index bf5ebe3ce0..cdd7af6e82 100644
--- a/fftools/ffprobe.c
+++ b/fftools/ffprobe.c
@@ -32,6 +32,7 @@ 
 #include "libavformat/avformat.h"
 #include "libavformat/version.h"
 #include "libavcodec/avcodec.h"
+#include "libavcodec/exif.h"
 #include "libavcodec/version.h"
 #include "libavutil/ambient_viewing_environment.h"
 #include "libavutil/avassert.h"
@@ -2040,19 +2041,30 @@  static void writer_register_all(void)
     memset( (ptr) + (cur_n), 0, ((new_n) - (cur_n)) * sizeof(*(ptr)) ); \
 }
 
-static inline int show_tags(WriterContext *w, AVDictionary *tags, int section_id)
+static inline int show_dict(WriterContext *w, const AVDictionary *tags)
 {
     const AVDictionaryEntry *tag = NULL;
     int ret = 0;
-
     if (!tags)
         return 0;
-    writer_print_section_header(w, NULL, section_id);
-
     while ((tag = av_dict_iterate(tags, tag))) {
-        if ((ret = print_str_validate(tag->key, tag->value)) < 0)
+        ret = print_str_validate(tag->key, tag->value);
+        if (ret < 0)
             break;
     }
+    return ret;
+}
+
+static inline int show_tags(WriterContext *w, const AVDictionary *tags, int section_id)
+{
+    int ret;
+
+    if (!tags)
+        return 0;
+    writer_print_section_header(w, NULL, section_id);
+
+    ret = show_dict(w, tags);
+
     writer_print_section_footer(w);
 
     return ret;
@@ -2920,6 +2932,11 @@  static void print_frame_side_data(WriterContext *w,
         } else if (sd->type == AV_FRAME_DATA_FILM_GRAIN_PARAMS) {
             AVFilmGrainParams *fgp = (AVFilmGrainParams *)sd->data;
             print_film_grain_params(w, fgp);
+        } else if (sd->type == AV_FRAME_DATA_EXIF) {
+            AVDictionary *dict = NULL;
+            int ret = av_exif_parse_buffer(NULL, sd->data, sd->size, &dict, AV_EXIF_PARSE_TIFF_HEADER);
+            if (ret >= 0)
+                show_dict(w, dict);
         }
         writer_print_section_footer(w);
     }
diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 502be8b09b..4736126b75 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -16,6 +16,7 @@  HEADERS = ac3_parser.h                                                  \
           dirac.h                                                       \
           dv_profile.h                                                  \
           dxva2.h                                                       \
+          exif.h                                                        \
           jni.h                                                         \
           mediacodec.h                                                  \
           packet.h                                                      \
diff --git a/libavcodec/exif.c b/libavcodec/exif.c
index 959d114d09..8442e9f174 100644
--- a/libavcodec/exif.c
+++ b/libavcodec/exif.c
@@ -1,6 +1,7 @@ 
 /*
  * EXIF metadata parser
  * Copyright (c) 2013 Thilo Borgmann <thilo.borgmann _at_ mail.de>
+ * Copyright (c) 2024 Leo Izen <leo.izen@gmail.com>
  *
  * This file is part of FFmpeg.
  *
@@ -23,12 +24,17 @@ 
  * @file
  * EXIF metadata parser
  * @author Thilo Borgmann <thilo.borgmann _at_ mail.de>
+ * @author Leo Izen <leo.izen@gmail.com>
  */
 
-#include "exif.h"
+#include "libavutil/attributes.h"
+#include "libavutil/display.h"
+
+#include "exif_internal.h"
 #include "tiff_common.h"
 
 #define EXIF_TAG_NAME_LENGTH   32
+#define MAKERNOTE_TAG 0x927c
 
 struct exif_tag {
     char      name[EXIF_TAG_NAME_LENGTH];
@@ -175,11 +181,6 @@  static int exif_add_metadata(void *logctx, int count, int type,
                              AVDictionary **metadata)
 {
     switch(type) {
-    case 0:
-        av_log(logctx, AV_LOG_WARNING,
-               "Invalid TIFF tag type 0 found for %s with size %d\n",
-               name, count);
-        return 0;
     case TIFF_DOUBLE   : return ff_tadd_doubles_metadata(count, name, sep, gb, le, metadata);
     case TIFF_SSHORT   : return ff_tadd_shorts_metadata(count, name, sep, gb, le, 1, metadata);
     case TIFF_SHORT    : return ff_tadd_shorts_metadata(count, name, sep, gb, le, 0, metadata);
@@ -192,12 +193,20 @@  static int exif_add_metadata(void *logctx, int count, int type,
     case TIFF_SLONG    :
     case TIFF_LONG     : return ff_tadd_long_metadata(count, name, sep, gb, le, metadata);
     default:
-        avpriv_request_sample(logctx, "TIFF tag type (%u)", type);
+        av_log(logctx, AV_LOG_WARNING,
+            "Invalid TIFF tag type %d found for %s with size %d\n", type, name, count);
         return 0;
     };
 }
 
+static int exif_parse_ifd_list(void *logctx, GetByteContext *gb, int le,
+                               int depth, AVDictionary **metadata);
 
+/**
+ * Decodes a tag and stores it in the dictionary **metadata.
+ * GetByteContext *gbytes is seeked to the next tag.
+ * If the tag is an IFD, it is recursively decoded.
+ */
 static int exif_decode_tag(void *logctx, GetByteContext *gbytes, int le,
                            int depth, AVDictionary **metadata)
 {
@@ -220,7 +229,7 @@  static int exif_decode_tag(void *logctx, GetByteContext *gbytes, int le,
     // store metadata or proceed with next IFD
     ret = ff_tis_ifd(id);
     if (ret) {
-        ret = ff_exif_decode_ifd(logctx, gbytes, le, depth + 1, metadata);
+        ret = exif_parse_ifd_list(logctx, gbytes, le, depth + 1, metadata);
     } else {
         const char *name = exif_get_tag_name(id);
         char buf[7];
@@ -239,35 +248,374 @@  static int exif_decode_tag(void *logctx, GetByteContext *gbytes, int le,
     return ret;
 }
 
+static const uint8_t casio_header[] = {
+    'Q', 'V', 'C', 0, 0, 0,
+};
+
+static const uint8_t fuji_header[] = {
+    'F', 'U', 'J', 'I',
+};
+
+static const uint8_t nikon_header[] = {
+    'N', 'i', 'k', 'o', 'n', 0,
+};
 
-int ff_exif_decode_ifd(void *logctx, GetByteContext *gbytes,
-                       int le, int depth, AVDictionary **metadata)
+static const uint8_t olympus1_header[] = {
+    'O', 'L', 'Y', 'M', 'P', 0,
+};
+
+static const uint8_t olympus2_header[] = {
+    'O', 'L', 'Y', 'M', 'P', 'U', 'S', 0, 'I', 'I',
+};
+
+static const uint8_t panosonic_header[] = {
+    'P', 'a', 'n', 'o', 's', 'o', 'n',  'i', 'c', 0, 0, 0,
+};
+
+static const uint8_t aoc_header[] = {
+    'A', 'O', 'C', 0,
+};
+
+static const uint8_t sigma_header[] = {
+    'S', 'I', 'G', 'M', 'A', 0, 0, 0,
+};
+
+static const uint8_t foveon_header[] = {
+    'F', 'O', 'V', 'E', 'O', 'N', 0, 0,
+};
+
+static const uint8_t sony_header[] = {
+    'S', 'O', 'N', 'Y', ' ', 'D', 'S', 'C', ' ', 0, 0, 0,
+};
+
+/**
+ * Get the offset for IFD inside MakerNote.
+ *
+ * Returns -1 if this makernote isn't an IFD or if it contains
+ * and IFD that's relative to the start of the MakerNote, otherwise
+ * returns the offset.
+ *
+ * Most manufacturers use IFDs for these with no header (e.g. Canon)
+ * so the default is to assume the MakerNote is an IFD with offset 0.
+ */
+static int exif_get_makernote_offset(GetByteContext *gb) {
+    if (!memcmp(gb->buffer, casio_header, sizeof(casio_header))) {
+        return -1;
+    } else if (!memcmp(gb->buffer, fuji_header, sizeof(fuji_header))) {
+        return -1;
+    } else if (!memcmp(gb->buffer, olympus2_header, sizeof(olympus2_header))) {
+        return -1;
+    } else if (!memcmp(gb->buffer, olympus1_header, sizeof(olympus1_header)))  {
+        return 8;
+    } else if (!memcmp(gb->buffer, nikon_header, sizeof(nikon_header))) {
+        if (bytestream2_get_bytes_left(gb) < 14)
+            return -1;
+        else if (AV_RB32(gb->buffer + 10) == 0x49492a00 || AV_RB32(gb->buffer + 10) == 0x4d4d002a)
+            return -1;
+        return 8;
+    } else if (!memcmp(gb->buffer, panosonic_header, sizeof(panosonic_header))) {
+        return 12;
+    } else if (!memcmp(gb->buffer, aoc_header, sizeof(aoc_header))) {
+        return 6;
+    } else if (!memcmp(gb->buffer, sigma_header, sizeof(sigma_header))) {
+        return 10;
+    } else if (!memcmp(gb->buffer, foveon_header, sizeof(foveon_header))) {
+        return 10;
+    } else if (!memcmp(gb->buffer, sony_header, sizeof(sony_header))) {
+        return 12;
+    }
+
+    return 0;
+}
+
+/**
+ * Calculate the size of an IFD, so exif_collect_ifd_list knows how big of a space it has
+ * to allocate. It does so by adding the tag sizes, and recursively adding the sizes of
+ * IFD tags encounted.
+ */
+static int exif_get_collect_size(void *logctx, GetByteContext *gb, int le, int depth)
 {
-    int i, ret;
-    int entries;
+    int entries, total_size = 2;
+    GetByteContext gbytes;
 
-    entries = ff_tget_short(gbytes, le);
+    if (depth > 2)
+        return 0;
 
-    if (bytestream2_get_bytes_left(gbytes) < entries * 12) {
+    gbytes = *gb;
+    entries = ff_tget_short(&gbytes, le);
+    if (bytestream2_get_bytes_left(&gbytes) < entries * 12)
         return AVERROR_INVALIDDATA;
+
+    for (int i = 0; i < entries; i++) {
+        int cur_pos, makernote_ifd = -1;
+        unsigned id, count;
+        enum TiffTypes type;
+        ff_tread_tag(&gbytes, le, &id, &type, &count, &cur_pos);
+        if (!bytestream2_tell(&gbytes)) {
+            bytestream2_seek(&gbytes, cur_pos, SEEK_SET);
+            continue;
+        }
+        if (id == MAKERNOTE_TAG)
+            makernote_ifd = exif_get_makernote_offset(&gbytes);
+        if (id != MAKERNOTE_TAG && ff_tis_ifd(id) || makernote_ifd >= 0) {
+            int ret, makernote_off = makernote_ifd >= 0 ? makernote_ifd : 0;
+            bytestream2_seek(&gbytes, makernote_off, SEEK_CUR);
+            ret = exif_get_collect_size(logctx, &gbytes, le, depth + 1);
+            if (ret < 0)
+                return ret;
+            total_size += ret + 12 + makernote_off;
+        } else {
+            int payload_size = type == TIFF_STRING ? count : count * type_sizes[type];
+            if (payload_size > 4)
+                total_size += 12 + payload_size;
+            else
+                total_size += 12;
+        }
+        bytestream2_seek(&gbytes, cur_pos, SEEK_SET);
     }
 
-    for (i = 0; i < entries; i++) {
-        if ((ret = exif_decode_tag(logctx, gbytes, le, depth, metadata)) < 0) {
-            return ret;
+    return total_size;
+}
+
+static inline void tput16(PutByteContext *pb, const int le, const uint16_t value)
+{
+    le ? bytestream2_put_le16(pb, value) : bytestream2_put_be16(pb, value);
+}
+
+static inline void tput32(PutByteContext *pb, const int le, const uint32_t value)
+{
+    le ? bytestream2_put_le32(pb, value) : bytestream2_put_be32(pb, value);
+}
+
+/**
+ * Takes an IFD and collects it into a compact buffer with initial offset of 0.
+ * This is useful if ExifTag occurs inside TIFF, or another file with initial
+ * nonzero offsets.
+ */
+static int exif_collect_ifd_list(void *logctx, GetByteContext *gb, int le, int depth, PutByteContext *pb)
+{
+    int entries, ret = 0, offset;
+    GetByteContext gbytes;
+
+    if (depth > 2)
+        return 0;
+
+    gbytes = *gb;
+    entries = ff_tget_short(&gbytes, le);
+    if (bytestream2_get_bytes_left(&gbytes) < entries * 12)
+        return AVERROR_INVALIDDATA;
+
+    tput16(pb, le, entries);
+    offset = bytestream2_tell_p(pb) + entries * 12;
+    for (int i = 0; i < entries; i++) {
+        int cur_pos, makernote_ifd = -1;
+        unsigned id, count;
+        enum TiffTypes type;
+        ff_tread_tag(&gbytes, le, &id, &type, &count, &cur_pos);
+        if (!bytestream2_tell(&gbytes)) {
+            bytestream2_seek(&gbytes, cur_pos, SEEK_SET);
+            continue;
+        }
+        if (bytestream2_get_bytes_left_p(pb) < 12)
+            return AVERROR_BUFFER_TOO_SMALL;
+        tput16(pb, le, id);
+        tput16(pb, le, type);
+        tput32(pb, le, count);
+        if (id == MAKERNOTE_TAG)
+            makernote_ifd = exif_get_makernote_offset(&gbytes);
+        if (id != MAKERNOTE_TAG && ff_tis_ifd(id) || makernote_ifd >= 0) {
+            int tell = bytestream2_tell_p(pb);
+            int makernote_off = makernote_ifd >= 0 ? makernote_ifd : 0;
+            tput32(pb, le, offset);
+            bytestream2_seek_p(pb, offset, SEEK_SET);
+            if (makernote_off)
+                bytestream2_copy_buffer(pb, &gbytes, makernote_off);
+            ret = exif_collect_ifd_list(logctx, &gbytes, le, depth + 1, pb);
+            if (ret < 0)
+                return ret;
+            offset += ret + makernote_off;
+            bytestream2_seek_p(pb, tell + 4, SEEK_SET);
+        } else  {
+            int payload_size = type == TIFF_STRING ? count : count * type_sizes[type];
+            if (payload_size > 4) {
+                int tell = bytestream2_tell_p(pb);
+                tput32(pb, le, offset);
+                bytestream2_seek_p(pb, offset, SEEK_SET);
+                if (bytestream2_get_bytes_left(&gbytes) < payload_size)
+                    return AVERROR_INVALIDDATA;
+                bytestream2_put_buffer(pb, gbytes.buffer, payload_size);
+                offset += payload_size;
+                bytestream2_seek_p(pb, tell + 4, SEEK_SET);
+            } else {
+                bytestream2_put_ne32(pb, bytestream2_get_ne32(&gbytes));
+            }
         }
+        bytestream2_seek(&gbytes, cur_pos, SEEK_SET);
+    }
+
+    return offset;
+}
+
+int ff_exif_collect_ifd(void *logctx, GetByteContext *gb, int le, AVBufferRef **buffer)
+{
+    AVBufferRef *ref = NULL;
+    int total_size, ret;
+    PutByteContext pb;
+    if (!buffer)
+        return 0;
+
+    total_size = exif_get_collect_size(logctx, gb, le, 0);
+    if (total_size <= 0)
+        return total_size;
+    total_size += 8;
+    ref = av_buffer_alloc(total_size);
+    if (!ref)
+        return AVERROR(ENOMEM);
+    bytestream2_init_writer(&pb, ref->data, total_size);
+    bytestream2_put_be32(&pb, le ? 0x49492a00 : 0x4d4d002a);
+    tput32(&pb, le, 8);
+
+    ret = exif_collect_ifd_list(logctx, gb, le, 0, &pb);
+    if (ret < 0)
+        av_buffer_unref(&ref);
+
+    *buffer = ref;
+    return ret;
+}
+
+/**
+ * Parse an IFD and read it into the dictonary **metadata.
+ */
+static int exif_parse_ifd_list(void *logctx, GetByteContext *gb, int le,
+                               int depth, AVDictionary **metadata)
+{
+    int entries = ff_tget_short(gb, le);
+    if (bytestream2_get_bytes_left(gb) < entries * 12)
+        return AVERROR_INVALIDDATA;
+
+    for (int i = 0; i < entries; i++) {
+        int ret = exif_decode_tag(logctx, gb, le, depth, metadata);
+        if (ret < 0)
+            return ret;
     }
 
     // return next IDF offset or 0x000000000 or a value < 0 for failure
-    return ff_tget_long(gbytes, le);
+    return ff_tget_long(gb, le);
 }
 
-int avpriv_exif_decode_ifd(void *logctx, const uint8_t *buf, int size,
-                           int le, int depth, AVDictionary **metadata)
+int av_exif_parse_buffer(void *logctx, const uint8_t *buf, size_t size,
+                         AVDictionary **metadata, enum AVExifParseMode parse_mode)
 {
-    GetByteContext gb;
+    int ret, le;
+    GetByteContext gbytes;
+    if (size > INT_MAX)
+        return AVERROR(EINVAL);
+    bytestream2_init(&gbytes, buf, size);
+    if (parse_mode == AV_EXIF_PARSE_TIFF_HEADER) {
+        int ifd_offset;
+        // read TIFF header
+        ret = ff_tdecode_header(&gbytes, &le, &ifd_offset);
+        if (ret < 0) {
+            av_log(logctx, AV_LOG_ERROR, "invalid TIFF header in EXIF data\n");
+            return ret;
+        }
+        bytestream2_seek(&gbytes, ifd_offset, SEEK_SET);
+    } else {
+        le = parse_mode == AV_EXIF_ASSUME_LE;
+    }
+
+    // read 0th IFD and store the metadata
+    // (return values > 0 indicate the presence of subimage metadata)
+    ret = exif_parse_ifd_list(logctx, &gbytes, le, 0, metadata);
+    if (ret < 0) {
+        av_log(logctx, AV_LOG_ERROR, "error decoding EXIF data\n");
+        return ret;
+    }
+
+    return bytestream2_tell(&gbytes);
+}
+
+/**
+ * Attach an AV_FRAME_DATA_DISPLAYMATRIX based on the orientation string value.
+ */
+static int attach_displaymatrix(void *logctx, AVFrame *frame, const char *value)
+{
+    char *endptr;
+    AVFrameSideData *sd;
+    long orientation = strtol(value, &endptr, 0);
+    int32_t *matrix;
+    /* invalid string */
+    if (*endptr || endptr == value)
+        return 0;
+    /* invalid orientation */
+    if (orientation < 2 || orientation > 8)
+        return 0;
+    sd = av_frame_new_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX, sizeof(int32_t) * 9);
+    if (!sd) {
+        av_log(logctx, AV_LOG_ERROR, "Could not allocate frame side data\n");
+        return AVERROR(ENOMEM);
+    }
+    matrix = (int32_t *) sd->data;
+
+    switch (orientation) {
+    case 2:
+        av_display_rotation_set(matrix, 0.0);
+        av_display_matrix_flip(matrix, 1, 0);
+        break;
+    case 3:
+        av_display_rotation_set(matrix, 180.0);
+        break;
+    case 4:
+        av_display_rotation_set(matrix, 180.0);
+        av_display_matrix_flip(matrix, 1, 0);
+        break;
+    case 5:
+        av_display_rotation_set(matrix, 90.0);
+        av_display_matrix_flip(matrix, 1, 0);
+        break;
+    case 6:
+        av_display_rotation_set(matrix, 90.0);
+        break;
+    case 7:
+        av_display_rotation_set(matrix, -90.0);
+        av_display_matrix_flip(matrix, 1, 0);
+        break;
+    case 8:
+        av_display_rotation_set(matrix, -90.0);
+        break;
+    default:
+        av_assert0(0);
+    }
 
-    bytestream2_init(&gb, buf, size);
+    return 0;
+}
 
-    return ff_exif_decode_ifd(logctx, &gb, le, depth, metadata);
+int ff_exif_attach(void *logctx, AVFrame *frame, AVBufferRef **data)
+{
+    const AVDictionaryEntry *e = NULL;
+    int ret;
+    AVDictionary *m = NULL;
+    AVBufferRef *buffer = *data;
+    AVFrameSideData *sd = av_frame_new_side_data_from_buf(frame, AV_FRAME_DATA_EXIF, buffer);
+    if (!sd)
+        return AVERROR(ENOMEM);
+    *data = NULL;
+    ret = av_exif_parse_buffer(logctx, buffer->data, buffer->size, &m, AV_EXIF_PARSE_TIFF_HEADER);
+    if (ret < 0)
+        return ret;
+
+    if ((e = av_dict_get(m, "Orientation", e, AV_DICT_IGNORE_SUFFIX))) {
+        ret = attach_displaymatrix(logctx, frame, e->value);
+        if (ret < 0)
+            return ret;
+    }
+
+    return 0;
+}
+
+attribute_deprecated
+int avpriv_exif_decode_ifd(void *logctx, const uint8_t *buf, int size,
+                           int le, int depth, AVDictionary **metadata)
+{
+    return av_exif_parse_buffer(logctx, buf, size, metadata, le ? AV_EXIF_ASSUME_LE : AV_EXIF_ASSUME_BE);
 }
diff --git a/libavcodec/exif.h b/libavcodec/exif.h
index f70d21391a..cf54be68ba 100644
--- a/libavcodec/exif.h
+++ b/libavcodec/exif.h
@@ -1,6 +1,7 @@ 
 /*
  * EXIF metadata parser
  * Copyright (c) 2013 Thilo Borgmann <thilo.borgmann _at_ mail.de>
+ * Copyright (c) 2024 Leo Izen <leo.izen@gmail.com>
  *
  * This file is part of FFmpeg.
  *
@@ -23,21 +24,40 @@ 
  * @file
  * EXIF metadata parser
  * @author Thilo Borgmann <thilo.borgmann _at_ mail.de>
+ * @author Leo Izen <leo.izen@gmail.com>
  */
 
 #ifndef AVCODEC_EXIF_H
 #define AVCODEC_EXIF_H
 
+#include <stddef.h>
 #include <stdint.h>
+
+#include "libavutil/attributes.h"
 #include "libavutil/dict.h"
-#include "bytestream.h"
 
-/** Recursively decodes all IFD's and
- *  adds included TAGS into the metadata dictionary. */
+enum AVExifParseMode {
+    AV_EXIF_PARSE_TIFF_HEADER,
+    AV_EXIF_ASSUME_LE,
+    AV_EXIF_ASSUME_BE,
+};
+
+/**
+ * Parse an EXIF metadata buffer into the AVDictionary **metadata.
+ *
+ * @param logctx A log context for error logging.
+ * @param buf This buffer contains an EXIF blob.
+ * @param size The size of the buffer.
+ * @param metadata A metadata dictionary, into which the key/value tags are written.
+ * @param parse_mode An enum indicating whether to parse the TIFF header,
+ *      or to assume it's LE/BE and skip it.
+ * @return negative upon failure, otherwise total bytes read from the buffer.
+ */
+int av_exif_parse_buffer(void *logctx, const uint8_t *data, size_t size,
+                         AVDictionary **metadata, enum AVExifParseMode parse_mode);
+
+attribute_deprecated
 int avpriv_exif_decode_ifd(void *logctx, const uint8_t *buf, int size,
                            int le, int depth, AVDictionary **metadata);
 
-int ff_exif_decode_ifd(void *logctx, GetByteContext *gbytes, int le,
-                       int depth, AVDictionary **metadata);
-
 #endif /* AVCODEC_EXIF_H */
diff --git a/libavcodec/exif_internal.h b/libavcodec/exif_internal.h
new file mode 100644
index 0000000000..2c788856d6
--- /dev/null
+++ b/libavcodec/exif_internal.h
@@ -0,0 +1,55 @@ 
+/*
+ * EXIF metadata parser - internal functions
+ * Copyright (c) 2013 Thilo Borgmann <thilo.borgmann _at_ mail.de>
+ * Copyright (c) 2024 Leo Izen <leo.izen@gmail.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * EXIF metadata parser - internal functions
+ * @author Thilo Borgmann <thilo.borgmann _at_ mail.de>
+ * @author Leo Izen <leo.izen@gmail.com>
+ */
+
+#ifndef AVCODEC_EXIF_INTERNAL_H
+#define AVCODEC_EXIF_INTERNAL_H
+
+#include "libavutil/buffer.h"
+#include "libavutil/frame.h"
+#include "bytestream.h"
+#include "exif.h"
+
+/**
+ * Attach an AVBufferRef containing EXIF metadata to a frame.
+ * This function allocates the necessary AVFrameSideData and attaches it,
+ * and also attaches any necessary other sidedata that can be read from the,
+ * EXIF metadata, such as a display matrix.
+ */
+int ff_exif_attach(void *logctx, AVFrame *frame, AVBufferRef **data);
+
+/**
+ * Collects an IFD into a buffer. **buffer points to an AVBufferRef *, which will
+ * be allocated by this function. The caller needs to unref the buffer when it is
+ * done with it. This function also writes the EXIF/TIFF header into the buffer
+ * based on the endianness, so encoders can pass the buffer as-is. This function can be
+ * used by TIFF or other codecs that have non-zero IFD offsets on their EXIF metadata.
+ */
+int ff_exif_collect_ifd(void *logctx, GetByteContext *gb, int le, AVBufferRef **buffer);
+
+#endif /* AVCODEC_EXIF_INTERNAL_H */
diff --git a/libavcodec/mjpegdec.c b/libavcodec/mjpegdec.c
index 86ec58713c..9ed71976df 100644
--- a/libavcodec/mjpegdec.c
+++ b/libavcodec/mjpegdec.c
@@ -43,6 +43,7 @@ 
 #include "codec_internal.h"
 #include "copy_block.h"
 #include "decode.h"
+#include "exif_internal.h"
 #include "hwaccel_internal.h"
 #include "hwconfig.h"
 #include "idctdsp.h"
@@ -2039,8 +2040,6 @@  static int mjpeg_decode_app(MJpegDecodeContext *s)
 
     /* EXIF metadata */
     if (s->start_code == APP1 && id == AV_RB32("Exif") && len >= 2) {
-        GetByteContext gbytes;
-        int ret, le, ifd_offset, bytes_read;
         const uint8_t *aligned;
 
         skip_bits(&s->gb, 16); // skip padding
@@ -2048,27 +2047,12 @@  static int mjpeg_decode_app(MJpegDecodeContext *s)
 
         // init byte wise reading
         aligned = align_get_bits(&s->gb);
-        bytestream2_init(&gbytes, aligned, len);
-
-        // read TIFF header
-        ret = ff_tdecode_header(&gbytes, &le, &ifd_offset);
-        if (ret) {
-            av_log(s->avctx, AV_LOG_ERROR, "mjpeg: invalid TIFF header in EXIF data\n");
-        } else {
-            bytestream2_seek(&gbytes, ifd_offset, SEEK_SET);
-
-            // read 0th IFD and store the metadata
-            // (return values > 0 indicate the presence of subimage metadata)
-            ret = ff_exif_decode_ifd(s->avctx, &gbytes, le, 0, &s->exif_metadata);
-            if (ret < 0) {
-                av_log(s->avctx, AV_LOG_ERROR, "mjpeg: error decoding EXIF data\n");
-            }
-        }
-
-        bytes_read = bytestream2_tell(&gbytes);
-        skip_bits(&s->gb, bytes_read << 3);
-        len -= bytes_read;
-
+        s->exif_buffer = av_buffer_alloc(len);
+        if (!s->exif_buffer)
+            return AVERROR(ENOMEM);
+        memcpy(s->exif_buffer->data, aligned, len);
+        skip_bits(&s->gb, len << 3);
+        len = 0;
         goto out;
     }
 
@@ -2380,13 +2364,12 @@  int ff_mjpeg_decode_frame_from_buf(AVCodecContext *avctx, AVFrame *frame,
     int index;
     int ret = 0;
     int is16bit;
-    AVDictionaryEntry *e = NULL;
 
     s->force_pal8 = 0;
 
     s->buf_size = buf_size;
 
-    av_dict_free(&s->exif_metadata);
+    av_buffer_unref(&s->exif_buffer);
     av_freep(&s->stereo3d);
     s->adobe_transform = -1;
 
@@ -2853,60 +2836,12 @@  the_end:
         }
     }
 
-    if (e = av_dict_get(s->exif_metadata, "Orientation", e, AV_DICT_IGNORE_SUFFIX)) {
-        char *value = e->value + strspn(e->value, " \n\t\r"), *endptr;
-        int orientation = strtol(value, &endptr, 0);
-
-        if (!*endptr) {
-            AVFrameSideData *sd = NULL;
-
-            if (orientation >= 2 && orientation <= 8) {
-                int32_t *matrix;
-
-                sd = av_frame_new_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX, sizeof(int32_t) * 9);
-                if (!sd) {
-                    av_log(avctx, AV_LOG_ERROR, "Could not allocate frame side data\n");
-                    return AVERROR(ENOMEM);
-                }
-
-                matrix = (int32_t *)sd->data;
-
-                switch (orientation) {
-                case 2:
-                    av_display_rotation_set(matrix, 0.0);
-                    av_display_matrix_flip(matrix, 1, 0);
-                    break;
-                case 3:
-                    av_display_rotation_set(matrix, 180.0);
-                    break;
-                case 4:
-                    av_display_rotation_set(matrix, 180.0);
-                    av_display_matrix_flip(matrix, 1, 0);
-                    break;
-                case 5:
-                    av_display_rotation_set(matrix, 90.0);
-                    av_display_matrix_flip(matrix, 1, 0);
-                    break;
-                case 6:
-                    av_display_rotation_set(matrix, 90.0);
-                    break;
-                case 7:
-                    av_display_rotation_set(matrix, -90.0);
-                    av_display_matrix_flip(matrix, 1, 0);
-                    break;
-                case 8:
-                    av_display_rotation_set(matrix, -90.0);
-                    break;
-                default:
-                    av_assert0(0);
-                }
-            }
-        }
+    if (s->exif_buffer) {
+        ret = ff_exif_attach(s->avctx, frame, &s->exif_buffer);
+        if (ret < 0)
+            return ret;
     }
 
-    av_dict_copy(&frame->metadata, s->exif_metadata, 0);
-    av_dict_free(&s->exif_metadata);
-
     if (avctx->codec_id != AV_CODEC_ID_SMVJPEG &&
         (avctx->codec_tag == MKTAG('A', 'V', 'R', 'n') ||
          avctx->codec_tag == MKTAG('A', 'V', 'D', 'J')) &&
@@ -2961,7 +2896,7 @@  av_cold int ff_mjpeg_decode_end(AVCodecContext *avctx)
         av_freep(&s->blocks[i]);
         av_freep(&s->last_nnz[i]);
     }
-    av_dict_free(&s->exif_metadata);
+    av_buffer_unref(&s->exif_buffer);
 
     reset_icc_profile(s);
 
diff --git a/libavcodec/mjpegdec.h b/libavcodec/mjpegdec.h
index 13c524d597..780381445f 100644
--- a/libavcodec/mjpegdec.h
+++ b/libavcodec/mjpegdec.h
@@ -138,7 +138,7 @@  typedef struct MJpegDecodeContext {
     unsigned int ljpeg_buffer_size;
 
     int extern_huff;
-    AVDictionary *exif_metadata;
+    AVBufferRef *exif_buffer;
 
     AVStereo3D *stereo3d; ///!< stereoscopic information (cached, since it is read before frame allocation)
 
diff --git a/libavcodec/tiff.c b/libavcodec/tiff.c
index 37b56e9757..45c3ae8c97 100644
--- a/libavcodec/tiff.c
+++ b/libavcodec/tiff.c
@@ -46,6 +46,7 @@ 
 #include "bytestream.h"
 #include "codec_internal.h"
 #include "decode.h"
+#include "exif_internal.h"
 #include "faxcompr.h"
 #include "lzw.h"
 #include "tiff.h"
@@ -1268,7 +1269,7 @@  static int tiff_decode_tag(TiffContext *s, AVFrame *frame)
         s->last_tag = tag;
 
     off = bytestream2_tell(&s->gb);
-    if (count == 1) {
+    if (count == 1 && tag != TIFF_EXIFTAG) {
         switch (type) {
         case TIFF_BYTE:
         case TIFF_SHORT:
@@ -1770,6 +1771,22 @@  static int tiff_decode_tag(TiffContext *s, AVFrame *frame)
     case TIFF_SOFTWARE_NAME:
         ADD_METADATA(count, "software", NULL);
         break;
+    case TIFF_EXIFTAG: {
+        AVBufferRef *exif = NULL;
+        int next;
+        gb_temp = s->gb;
+        next = ff_exif_collect_ifd(s->avctx, &gb_temp, s->le, &exif);
+        if (next < 0)
+            av_log(s->avctx, AV_LOG_ERROR, "Error parsing TIFF exif tags: %d\n", next);
+        else if (next)
+            bytestream2_seek(&s->gb, next, SEEK_SET);
+        if (exif) {
+            ret = ff_exif_attach(s->avctx, frame, &exif);
+            if (ret < 0)
+                return ret;
+        }
+        break;
+    }
     case DNG_VERSION:
         if (count == 4) {
             unsigned int ver[4];
diff --git a/libavcodec/tiff.h b/libavcodec/tiff.h
index 12afcfa6e5..2628f9885f 100644
--- a/libavcodec/tiff.h
+++ b/libavcodec/tiff.h
@@ -94,6 +94,7 @@  enum TiffTags {
     TIFF_GEO_KEY_DIRECTORY  = 0x87AF,
     TIFF_GEO_DOUBLE_PARAMS  = 0x87B0,
     TIFF_GEO_ASCII_PARAMS   = 0x87B1,
+    TIFF_EXIFTAG            = 0x8769,
 };
 
 /** abridged list of DNG tags */
diff --git a/libavcodec/version.h b/libavcodec/version.h
index 7531c6c42a..9b8c267529 100644
--- a/libavcodec/version.h
+++ b/libavcodec/version.h
@@ -29,7 +29,7 @@ 
 
 #include "version_major.h"
 
-#define LIBAVCODEC_VERSION_MINOR  14
+#define LIBAVCODEC_VERSION_MINOR  15
 #define LIBAVCODEC_VERSION_MICRO 100
 
 #define LIBAVCODEC_VERSION_INT  AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
diff --git a/libavcodec/webp.c b/libavcodec/webp.c
index 7c2a5f0111..659f641652 100644
--- a/libavcodec/webp.c
+++ b/libavcodec/webp.c
@@ -35,6 +35,9 @@ 
  * Exif metadata
  * ICC profile
  *
+ * @author Leo Izen <leo.izen@gmail.com>
+ * Exif metadata
+ *
  * Unimplemented:
  *   - Animation
  *   - XMP metadata
@@ -48,7 +51,7 @@ 
 #include "bytestream.h"
 #include "codec_internal.h"
 #include "decode.h"
-#include "exif.h"
+#include "exif_internal.h"
 #include "get_bits.h"
 #include "thread.h"
 #include "tiff_common.h"
@@ -1452,13 +1455,12 @@  static int webp_decode_frame(AVCodecContext *avctx, AVFrame *p,
             break;
         }
         case MKTAG('E', 'X', 'I', 'F'): {
-            int le, ifd_offset, exif_offset = bytestream2_tell(&gb);
-            AVDictionary *exif_metadata = NULL;
-            GetByteContext exif_gb;
+            AVBufferRef *buf;
 
             if (s->has_exif) {
                 av_log(avctx, AV_LOG_VERBOSE, "Ignoring extra EXIF chunk\n");
-                goto exif_end;
+                bytestream2_skip(&gb, chunk_size);
+                break;
             }
             if (!(vp8x_flags & VP8X_FLAG_EXIF_METADATA))
                 av_log(avctx, AV_LOG_WARNING,
@@ -1466,25 +1468,13 @@  static int webp_decode_frame(AVCodecContext *avctx, AVFrame *p,
                        "VP8X header\n");
 
             s->has_exif = 1;
-            bytestream2_init(&exif_gb, avpkt->data + exif_offset,
-                             avpkt->size - exif_offset);
-            if (ff_tdecode_header(&exif_gb, &le, &ifd_offset) < 0) {
-                av_log(avctx, AV_LOG_ERROR, "invalid TIFF header "
-                       "in Exif data\n");
-                goto exif_end;
-            }
-
-            bytestream2_seek(&exif_gb, ifd_offset, SEEK_SET);
-            if (ff_exif_decode_ifd(avctx, &exif_gb, le, 0, &exif_metadata) < 0) {
-                av_log(avctx, AV_LOG_ERROR, "error decoding Exif data\n");
-                goto exif_end;
-            }
-
-            av_dict_copy(&p->metadata, exif_metadata, 0);
-
-exif_end:
-            av_dict_free(&exif_metadata);
-            bytestream2_skip(&gb, chunk_size);
+            buf = av_buffer_alloc(chunk_size);
+            if (!buf)
+                return AVERROR(ENOMEM);
+            bytestream2_get_buffer(&gb, buf->data, chunk_size);
+            ret = ff_exif_attach(avctx, p, &buf);
+            if (ret < 0)
+                return ret;
             break;
         }
         case MKTAG('I', 'C', 'C', 'P'): {
diff --git a/libavformat/avidec.c b/libavformat/avidec.c
index 1ae09efc15..1cea72471f 100644
--- a/libavformat/avidec.c
+++ b/libavformat/avidec.c
@@ -433,8 +433,8 @@  static int avi_extract_stream_metadata(AVFormatContext *s, AVStream *st)
         offset = bytestream2_tell(&gb);
 
         // decode EXIF tags from IFD, AVI is always little-endian
-        return avpriv_exif_decode_ifd(s, data + offset, data_size - offset,
-                                      1, 0, &st->metadata);
+        return av_exif_parse_buffer(s, data + offset, data_size - offset,
+                                    &st->metadata, AV_EXIF_ASSUME_LE);
         break;
     case MKTAG('C', 'A', 'S', 'I'):
         avpriv_request_sample(s, "RIFF stream data tag type CASI (%u)", tag);
diff --git a/libavutil/frame.c b/libavutil/frame.c
index 5cbfc6a48b..f668ed87fd 100644
--- a/libavutil/frame.c
+++ b/libavutil/frame.c
@@ -56,6 +56,8 @@  static const AVSideDataDescriptor sd_props[] = {
     [AV_FRAME_DATA_SPHERICAL]                   = { "Spherical Mapping",                            AV_SIDE_DATA_PROP_GLOBAL },
     [AV_FRAME_DATA_ICC_PROFILE]                 = { "ICC profile",                                  AV_SIDE_DATA_PROP_GLOBAL },
     [AV_FRAME_DATA_SEI_UNREGISTERED]            = { "H.26[45] User Data Unregistered SEI message",  AV_SIDE_DATA_PROP_MULTI },
+    [AV_FRAME_DATA_EXIF]                        = { "EXIF metadata",
+    AV_SIDE_DATA_PROP_GLOBAL },
 };
 
 static void get_frame_defaults(AVFrame *frame)
diff --git a/libavutil/frame.h b/libavutil/frame.h
index 60bb966f8b..3614b412bb 100644
--- a/libavutil/frame.h
+++ b/libavutil/frame.h
@@ -228,6 +228,12 @@  enum AVFrameSideDataType {
      * encoding.
      */
     AV_FRAME_DATA_VIDEO_HINT,
+
+    /**
+     * Extensible image file format metadata. The payload is a buffer containing
+     * EXIF metadata, starting with either 49 49 2a 00, or 4d 4d 00 2a.
+     */
+    AV_FRAME_DATA_EXIF,
 };
 
 enum AVActiveFormatDescription {
diff --git a/tests/ref/fate/exif-image-embedded b/tests/ref/fate/exif-image-embedded
index 98b6ec5a44..1edda0fc92 100644
--- a/tests/ref/fate/exif-image-embedded
+++ b/tests/ref/fate/exif-image-embedded
@@ -29,8 +29,11 @@  color_space=bt470bg
 color_primaries=unknown
 color_transfer=unknown
 chroma_location=center
-TAG:UserComment=AppleMark
+[SIDE_DATA]
+side_data_type=EXIF metadata
+UserComment=AppleMark
 
+[/SIDE_DATA]
 [/FRAME]
 [FRAME]
 media_type=audio
diff --git a/tests/ref/fate/exif-image-jpg b/tests/ref/fate/exif-image-jpg
index 2e314078da..bcc6184085 100644
--- a/tests/ref/fate/exif-image-jpg
+++ b/tests/ref/fate/exif-image-jpg
@@ -29,31 +29,33 @@  color_space=bt470bg
 color_primaries=unknown
 color_transfer=unknown
 chroma_location=center
-TAG:ImageDescription=                               
-TAG:Make=Canon
-TAG:Model=Canon PowerShot SX200 IS
-TAG:Orientation=    1
-TAG:XResolution=    180:1      
-TAG:YResolution=    180:1      
-TAG:ResolutionUnit=    2
-TAG:DateTime=2013:07:18 13:12:03
-TAG:YCbCrPositioning=    2
-TAG:ExposureTime=      1:1250   
-TAG:FNumber=     40:10     
-TAG:ISOSpeedRatings=  160
-TAG:ExifVersion= 48,  50,  50,  49
-TAG:DateTimeOriginal=2013:07:18 13:12:03
-TAG:DateTimeDigitized=2013:07:18 13:12:03
-TAG:ComponentsConfiguration=  1,   2,   3,   0
-TAG:CompressedBitsPerPixel=      3:1      
-TAG:ShutterSpeedValue=    329:32     
-TAG:ApertureValue=    128:32     
-TAG:ExposureBiasValue=      0:3      
-TAG:MaxApertureValue=    113:32     
-TAG:MeteringMode=    5
-TAG:Flash=   16
-TAG:FocalLength=   5000:1000   
-TAG:MakerNote=
+[SIDE_DATA]
+side_data_type=EXIF metadata
+ImageDescription=                               
+Make=Canon
+Model=Canon PowerShot SX200 IS
+Orientation=    1
+XResolution=    180:1      
+YResolution=    180:1      
+ResolutionUnit=    2
+DateTime=2013:07:18 13:12:03
+YCbCrPositioning=    2
+ExposureTime=      1:1250   
+FNumber=     40:10     
+ISOSpeedRatings=  160
+ExifVersion= 48,  50,  50,  49
+DateTimeOriginal=2013:07:18 13:12:03
+DateTimeDigitized=2013:07:18 13:12:03
+ComponentsConfiguration=  1,   2,   3,   0
+CompressedBitsPerPixel=      3:1      
+ShutterSpeedValue=    329:32     
+ApertureValue=    128:32     
+ExposureBiasValue=      0:3      
+MaxApertureValue=    113:32     
+MeteringMode=    5
+Flash=   16
+FocalLength=   5000:1000   
+MakerNote=
  25,   0,   1,   0,   3,   0,  48,   0,   0,   0,  28,   4,   0,   0,   2,   0
   3,   0,   4,   0,   0,   0, 124,   4,   0,   0,   3,   0,   3,   0,   4,   0
   0,   0, 132,   4,   0,   0,   4,   0,   3,   0,  34,   0,   0,   0, 140,   4
@@ -195,7 +197,7 @@  TAG:MakerNote=
   0,   0,   0,   0,   8,   0,   0,   0,   0,   0,   0,   0,  10,   0,   0,   0
 255, 255,   0,   0,   0,   0, 239, 154, 237, 228, 191, 235,  20, 171,  30,   6
   2, 129,  88, 251,  56,  49,  73,  73,  42,   0, 222,   2,   0,   0
-TAG:UserComment=
+UserComment=
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
@@ -213,22 +215,23 @@  TAG:UserComment=
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
   0,   0,   0,   0,   0,   0,   0,   0
-TAG:FlashpixVersion= 48,  49,  48,  48
-TAG:ColorSpace=    1
-TAG:PixelXDimension= 4000
-TAG:PixelYDimension= 2248
-TAG:GPSLatitudeRef=R98
-TAG:GPSLatitude= 48,  49,  48,  48
-TAG:0x1001= 4000
-TAG:0x1002= 2248
-TAG:FocalPlaneXResolution=4000000:244    
-TAG:FocalPlaneYResolution=2248000:183    
-TAG:FocalPlaneResolutionUnit=    2
-TAG:SensingMethod=    2
-TAG:FileSource=  3
-TAG:CustomRendered=    0
-TAG:ExposureMode=    0
-TAG:WhiteBalance=    0
-TAG:DigitalZoomRatio=   4000:4000   
-TAG:SceneCaptureType=    0
+FlashpixVersion= 48,  49,  48,  48
+ColorSpace=    1
+PixelXDimension= 4000
+PixelYDimension= 2248
+GPSLatitudeRef=R98
+GPSLatitude= 48,  49,  48,  48
+0x1001= 4000
+0x1002= 2248
+FocalPlaneXResolution=4000000:244    
+FocalPlaneYResolution=2248000:183    
+FocalPlaneResolutionUnit=    2
+SensingMethod=    2
+FileSource=  3
+CustomRendered=    0
+ExposureMode=    0
+WhiteBalance=    0
+DigitalZoomRatio=   4000:4000   
+SceneCaptureType=    0
+[/SIDE_DATA]
 [/FRAME]
diff --git a/tests/ref/fate/exif-image-webp b/tests/ref/fate/exif-image-webp
index 73560e8ba0..d1fd7ea4e3 100644
--- a/tests/ref/fate/exif-image-webp
+++ b/tests/ref/fate/exif-image-webp
@@ -29,31 +29,33 @@  color_space=bt470bg
 color_primaries=unknown
 color_transfer=unknown
 chroma_location=unspecified
-TAG:ImageDescription=                               
-TAG:Make=Canon
-TAG:Model=Canon PowerShot SX200 IS
-TAG:Orientation=    1
-TAG:XResolution=    180:1      
-TAG:YResolution=    180:1      
-TAG:ResolutionUnit=    2
-TAG:DateTime=2013:07:18 13:12:03
-TAG:YCbCrPositioning=    2
-TAG:ExposureTime=      1:1250   
-TAG:FNumber=     40:10     
-TAG:ISOSpeedRatings=  160
-TAG:ExifVersion= 48,  50,  50,  49
-TAG:DateTimeOriginal=2013:07:18 13:12:03
-TAG:DateTimeDigitized=2013:07:18 13:12:03
-TAG:ComponentsConfiguration=  1,   2,   3,   0
-TAG:CompressedBitsPerPixel=      3:1      
-TAG:ShutterSpeedValue=    329:32     
-TAG:ApertureValue=    128:32     
-TAG:ExposureBiasValue=      0:3      
-TAG:MaxApertureValue=    113:32     
-TAG:MeteringMode=    5
-TAG:Flash=   16
-TAG:FocalLength=   5000:1000   
-TAG:MakerNote=
+[SIDE_DATA]
+side_data_type=EXIF metadata
+ImageDescription=                               
+Make=Canon
+Model=Canon PowerShot SX200 IS
+Orientation=    1
+XResolution=    180:1      
+YResolution=    180:1      
+ResolutionUnit=    2
+DateTime=2013:07:18 13:12:03
+YCbCrPositioning=    2
+ExposureTime=      1:1250   
+FNumber=     40:10     
+ISOSpeedRatings=  160
+ExifVersion= 48,  50,  50,  49
+DateTimeOriginal=2013:07:18 13:12:03
+DateTimeDigitized=2013:07:18 13:12:03
+ComponentsConfiguration=  1,   2,   3,   0
+CompressedBitsPerPixel=      3:1      
+ShutterSpeedValue=    329:32     
+ApertureValue=    128:32     
+ExposureBiasValue=      0:3      
+MaxApertureValue=    113:32     
+MeteringMode=    5
+Flash=   16
+FocalLength=   5000:1000   
+MakerNote=
  25,   0,   1,   0,   3,   0,  48,   0,   0,   0,  28,   4,   0,   0,   2,   0
   3,   0,   4,   0,   0,   0, 124,   4,   0,   0,   3,   0,   3,   0,   4,   0
   0,   0, 132,   4,   0,   0,   4,   0,   3,   0,  34,   0,   0,   0, 140,   4
@@ -195,7 +197,7 @@  TAG:MakerNote=
   0,   0,   0,   0,   8,   0,   0,   0,   0,   0,   0,   0,  10,   0,   0,   0
 255, 255,   0,   0,   0,   0, 239, 154, 237, 228, 191, 235,  20, 171,  30,   6
   2, 129,  88, 251,  56,  49,  73,  73,  42,   0, 222,   2,   0,   0
-TAG:UserComment=
+UserComment=
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
@@ -213,22 +215,23 @@  TAG:UserComment=
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0
   0,   0,   0,   0,   0,   0,   0,   0
-TAG:FlashpixVersion= 48,  49,  48,  48
-TAG:ColorSpace=    1
-TAG:PixelXDimension= 4000
-TAG:PixelYDimension= 2248
-TAG:GPSLatitudeRef=R98
-TAG:GPSLatitude= 48,  49,  48,  48
-TAG:0x1001= 4000
-TAG:0x1002= 2248
-TAG:FocalPlaneXResolution=4000000:244    
-TAG:FocalPlaneYResolution=2248000:183    
-TAG:FocalPlaneResolutionUnit=    2
-TAG:SensingMethod=    2
-TAG:FileSource=  3
-TAG:CustomRendered=    0
-TAG:ExposureMode=    0
-TAG:WhiteBalance=    0
-TAG:DigitalZoomRatio=   4000:4000   
-TAG:SceneCaptureType=    0
+FlashpixVersion= 48,  49,  48,  48
+ColorSpace=    1
+PixelXDimension= 4000
+PixelYDimension= 2248
+GPSLatitudeRef=R98
+GPSLatitude= 48,  49,  48,  48
+0x1001= 4000
+0x1002= 2248
+FocalPlaneXResolution=4000000:244    
+FocalPlaneYResolution=2248000:183    
+FocalPlaneResolutionUnit=    2
+SensingMethod=    2
+FileSource=  3
+CustomRendered=    0
+ExposureMode=    0
+WhiteBalance=    0
+DigitalZoomRatio=   4000:4000   
+SceneCaptureType=    0
+[/SIDE_DATA]
 [/FRAME]