This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository ffmpeg.

commit 784aa09fa8222f6704d953b5035f8d0f0bec1623
Author:     Leo Izen <[email protected]>
AuthorDate: Sun Nov 30 06:55:16 2025 -0500
Commit:     Leo Izen <[email protected]>
CommitDate: Sat Dec 20 11:53:23 2025 -0500

    avcodec/exif: parse additional EXIF IFDs
    
    Most EXIF metadata is in IFD0 and most EXIF payloads only contain
    one IFD, but it is possible for there to be more IFDs after the
    existing trailing one. exiftool and similar software report these IFDs
    as IFD1, IFD2, etc. This commit reads those additional IFDs and attaches
    them as dummy entries in the top-level IFD ranging from 0xFFFC down to
    0xFFED, which are unused by the EXIF spec. The EXIF API is only able to
    return and work with a single IFD, so by attaching it as a subdirectory
    this metadata can be preserved.
    
    This is done transparently through the read/write process. Upon parsing
    an additional IFD1, it will be attached, but it will be written with
    av_exif_write after IFD0 rather than as a subdirectory, as intended.
    
    Existing files without more than one IFD, i.e. most files, will be 
unaffected
    by this change, as well as API clients looking to parse specific fields, but
    now more metadata is parsed and written, rather than simply being discarded
    as trailing data.
    
    Signed-off-by: Leo Izen <[email protected]>
---
 libavcodec/exif.c                                  | 111 +++++++++++++++++++--
 tests/ref/fate/exif-image-jpg                      |   8 +-
 tests/ref/fate/exif-image-webp                     |   8 +-
 .../fate/mov-heic-demux-still-image-multiple-thumb |  22 +++-
 4 files changed, 137 insertions(+), 12 deletions(-)

diff --git a/libavcodec/exif.c b/libavcodec/exif.c
index 38438661cc..0de543e35a 100644
--- a/libavcodec/exif.c
+++ b/libavcodec/exif.c
@@ -192,6 +192,24 @@ static const struct exif_tag tag_list[] = { // JEITA 
CP-3451 EXIF specification:
     {"InteropIFD",                 0xA005}, // <- Table 13 Interoperability 
IFD Attribute Information
     {"GlobalParametersIFD",        0x0190},
     {"ProfileIFD",                 0xc6f5},
+
+    /* Extra FFmpeg tags */
+    { "IFD1",                      0xFFFC},
+    { "IFD2",                      0xFFFB},
+    { "IFD3",                      0xFFFA},
+    { "IFD4",                      0xFFF9},
+    { "IFD5",                      0xFFF8},
+    { "IFD6",                      0xFFF7},
+    { "IFD7",                      0xFFF6},
+    { "IFD8",                      0xFFF5},
+    { "IFD9",                      0xFFF4},
+    { "IFD10",                     0xFFF3},
+    { "IFD11",                     0xFFF2},
+    { "IFD12",                     0xFFF1},
+    { "IFD13",                     0xFFF0},
+    { "IFD14",                     0xFFEF},
+    { "IFD15",                     0xFFEE},
+    { "IFD16",                     0xFFED},
 };
 
 /* same as type_sizes but with string == 1 */
@@ -655,7 +673,9 @@ static size_t exif_get_ifd_size(const AVExifMetadata *ifd)
     for (size_t i = 0; i < ifd->count; i++) {
         const AVExifEntry *entry = &ifd->entries[i];
         if (entry->type == AV_TIFF_IFD) {
-            total_size += BASE_TAG_SIZE + exif_get_ifd_size(&entry->value.ifd) 
+ entry->ifd_offset;
+            /* this is an extra IFD, not an entry, so we don't need to add 
base tag size */
+            size_t base_size = entry->id > 0xFFECu && entry->id <= 0xFFFCu ? 0 
: BASE_TAG_SIZE;
+            total_size += base_size + exif_get_ifd_size(&entry->value.ifd) + 
entry->ifd_offset;
         } else {
             size_t payload_size = entry->count * exif_sizes[entry->type];
             total_size += BASE_TAG_SIZE + (payload_size > 4 ? payload_size : 
0);
@@ -728,12 +748,16 @@ int av_exif_write(void *logctx, const AVExifMetadata 
*ifd, AVBufferRef **buffer,
     AVBufferRef *buf = NULL;
     size_t size, headsize = 8;
     PutByteContext pb;
-    int ret, off = 0;
+    int ret = 0, off = 0, next;
+    AVExifMetadata *ifd_new = NULL;
+    AVExifMetadata extra_ifds[16] = { 0 };
 
     int le = 1;
 
-    if (*buffer)
-        return AVERROR(EINVAL);
+    if (*buffer) {
+        ret = AVERROR(EINVAL);
+        goto end;
+    }
 
     size = exif_get_ifd_size(ifd);
     switch (header_mode) {
@@ -753,8 +777,10 @@ int av_exif_write(void *logctx, const AVExifMetadata *ifd, 
AVBufferRef **buffer,
             break;
     }
     buf = av_buffer_alloc(size + off + headsize);
-    if (!buf)
-        return AVERROR(ENOMEM);
+    if (!buf) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
 
     if (header_mode == AV_EXIF_EXIF00) {
         AV_WL32(buf->data, MKTAG('E','x','i','f'));
@@ -772,16 +798,63 @@ int av_exif_write(void *logctx, const AVExifMetadata 
*ifd, AVBufferRef **buffer,
         tput32(&pb, le, 8);
     }
 
+    int extras;
+    for (extras = 0; extras < FF_ARRAY_ELEMS(extra_ifds); extras++) {
+        AVExifEntry *extra_entry = NULL;
+        uint16_t extra_tag = 0xFFFCu - extras;
+        ret = av_exif_get_entry(logctx, (AVExifMetadata *) ifd, extra_tag, 0, 
&extra_entry);
+        if (ret <= 0)
+            break;
+        av_log(logctx, AV_LOG_DEBUG, "found extra IFD tag: %04x\n", extra_tag);
+        if (!ifd_new) {
+            ifd_new = av_exif_clone_ifd(ifd);
+            if (!ifd_new)
+                break;
+            ifd = ifd_new;
+        }
+        /* calling remove_entry will call av_exif_free on the original */
+        AVExifMetadata *cloned = av_exif_clone_ifd(&extra_entry->value.ifd);
+        if (!cloned)
+            break;
+        extra_ifds[extras] = *cloned;
+        /* don't use av_exif_free here, we want to preserve internals */
+        av_free(cloned);
+        ret = av_exif_remove_entry(logctx, ifd_new, extra_tag, 0);
+        if (ret < 0)
+            break;
+    }
+
+    next = bytestream2_tell_p(&pb);
     ret = exif_write_ifd(logctx, &pb, le, 0, ifd);
     if (ret < 0) {
         av_buffer_unref(&buf);
         av_log(logctx, AV_LOG_ERROR, "error writing EXIF data: %s\n", 
av_err2str(ret));
         return ret;
     }
+    next += ret;
+
+    for (int i = 0; i < extras; i++) {
+        av_log(logctx, AV_LOG_DEBUG, "writing additional ifd at: %d\n", next);
+        /* exif_write_ifd always writes 0 i.e. last ifd so we overwrite that 
here */
+        bytestream2_seek_p(&pb, -4, SEEK_CUR);
+        tput32(&pb, le, next);
+        bytestream2_seek_p(&pb, next, SEEK_SET);
+        ret = exif_write_ifd(logctx, &pb, le, 0, &extra_ifds[i]);
+        if (ret < 0)
+            break;
+        next += ret;
+    }
 
     *buffer = buf;
+    ret = 0;
 
-    return 0;
+end:
+    av_exif_free(ifd_new);
+    av_freep(&ifd_new);
+    for (int i = 0; i < FF_ARRAY_ELEMS(extra_ifds); i++)
+        av_exif_free(&extra_ifds[i]);
+
+    return ret;
 }
 
 int av_exif_parse_buffer(void *logctx, const uint8_t *buf, size_t size,
@@ -839,8 +912,30 @@ int av_exif_parse_buffer(void *logctx, const uint8_t *buf, 
size_t size,
         av_log(logctx, AV_LOG_ERROR, "error decoding EXIF data: %s\n", 
av_err2str(ret));
         return ret;
     }
+    if (!ret)
+        goto finish;
+    int next = ret;
+    bytestream2_seek(&gbytes, next, SEEK_SET);
+
+    /* cap at 16 extra IFDs for sanity/parse security */
+    for (int extra_tag = 0xFFFCu; extra_tag > 0xFFECu; extra_tag--) {
+        AVExifMetadata extra_ifd = { 0 };
+        ret = exif_parse_ifd_list(logctx, &gbytes, le, 0, &extra_ifd, 1);
+        if (ret < 0) {
+            av_exif_free(&extra_ifd);
+            break;
+        }
+        next = ret;
+        av_log(logctx, AV_LOG_DEBUG, "found extra IFD: %04x with next=%d\n", 
extra_tag, ret);
+        bytestream2_seek(&gbytes, next, SEEK_SET);
+        ret = av_exif_set_entry(logctx, ifd, extra_tag, AV_TIFF_IFD, 1, NULL, 
0, &extra_ifd);
+        av_exif_free(&extra_ifd);
+        if (ret < 0 || !next || bytestream2_get_bytes_left(&gbytes) <= 0)
+            break;
+    }
 
-    return bytestream2_tell(&gbytes);
+finish:
+    return bytestream2_tell(&gbytes) + off;
 }
 
 #define COLUMN_SEP(i, c) ((i) ? ((i) % (c) ? ", " : "\n") : "")
diff --git a/tests/ref/fate/exif-image-jpg b/tests/ref/fate/exif-image-jpg
index 84136a9f85..f1a5ef00e8 100644
--- a/tests/ref/fate/exif-image-jpg
+++ b/tests/ref/fate/exif-image-jpg
@@ -197,6 +197,12 @@ TAG:ExifIFD/ExposureMode=      0
 TAG:ExifIFD/WhiteBalance=      0
 TAG:ExifIFD/DigitalZoomRatio=   4000:4000   
 TAG:ExifIFD/SceneCaptureType=      0
+TAG:IFD1/Compression=      6
+TAG:IFD1/XResolution=    180:1      
+TAG:IFD1/YResolution=    180:1      
+TAG:IFD1/ResolutionUnit=      2
+TAG:IFD1/JPEGInterchangeFormat=   5108
+TAG:IFD1/JPEGInterchangeFormatLength=   4214
 [SIDE_DATA]
 side_data_type=3x3 displaymatrix
 displaymatrix=
@@ -208,6 +214,6 @@ rotation=0
 [/SIDE_DATA]
 [SIDE_DATA]
 side_data_type=EXIF metadata
-size=3285
+size=3379
 [/SIDE_DATA]
 [/FRAME]
diff --git a/tests/ref/fate/exif-image-webp b/tests/ref/fate/exif-image-webp
index 7b695bc923..27b062d948 100644
--- a/tests/ref/fate/exif-image-webp
+++ b/tests/ref/fate/exif-image-webp
@@ -197,6 +197,12 @@ TAG:ExifIFD/ExposureMode=      0
 TAG:ExifIFD/WhiteBalance=      0
 TAG:ExifIFD/DigitalZoomRatio=   4000:4000   
 TAG:ExifIFD/SceneCaptureType=      0
+TAG:IFD1/Compression=      6
+TAG:IFD1/XResolution=    180:1      
+TAG:IFD1/YResolution=    180:1      
+TAG:IFD1/ResolutionUnit=      2
+TAG:IFD1/JPEGInterchangeFormat=   5108
+TAG:IFD1/JPEGInterchangeFormatLength=   4214
 [SIDE_DATA]
 side_data_type=3x3 displaymatrix
 displaymatrix=
@@ -208,6 +214,6 @@ rotation=0
 [/SIDE_DATA]
 [SIDE_DATA]
 side_data_type=EXIF metadata
-size=3285
+size=3379
 [/SIDE_DATA]
 [/FRAME]
diff --git a/tests/ref/fate/mov-heic-demux-still-image-multiple-thumb 
b/tests/ref/fate/mov-heic-demux-still-image-multiple-thumb
index 4e66fe978c..628976644e 100644
--- a/tests/ref/fate/mov-heic-demux-still-image-multiple-thumb
+++ b/tests/ref/fate/mov-heic-demux-still-image-multiple-thumb
@@ -1895,9 +1895,27 @@ TAG:ExifIFD/0xA434=LUMIX S 24-60/F2.8
 TAG:0x9006=  39936
 TAG:0x9007=  65536
 TAG:0x9008= 272401
+TAG:IFD1/Compression=      6
+TAG:IFD1/StripOffsets= 106496
+TAG:IFD1/Orientation=      1
+TAG:IFD1/XResolution=    180:1      
+TAG:IFD1/YResolution=    180:1      
+TAG:IFD1/ResolutionUnit=      2
+TAG:IFD1/JPEGInterchangeFormat= 106496
+TAG:IFD1/JPEGInterchangeFormatLength=  20480
+TAG:IFD1/YCbCrPositioning=      2
+TAG:IFD2/Compression=      6
+TAG:IFD2/StripOffsets= 126976
+TAG:IFD2/Orientation=      1
+TAG:IFD2/XResolution=    180:1      
+TAG:IFD2/YResolution=    180:1      
+TAG:IFD2/ResolutionUnit=      2
+TAG:IFD2/JPEGInterchangeFormat= 126976
+TAG:IFD2/JPEGInterchangeFormatLength= 858544
+TAG:IFD2/YCbCrPositioning=      2
 [SIDE_DATA]
 side_data_type=EXIF metadata
-size=30308
+size=30568
 [/SIDE_DATA]
 [SIDE_DATA]
 side_data_type=H.26[45] User Data Unregistered SEI message
@@ -1927,7 +1945,7 @@ DISPOSITION:still_image=0
 DISPOSITION:multilayer=0
 [SIDE_DATA]
 side_data_type=EXIF metadata
-size=30308
+size=30568
 [/SIDE_DATA]
 [/STREAM]
 [STREAM]

_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to