Commit 00249664de for ffmpeg
commit 00249664deb532efeedfd25a300f030c7bcec982
Author: James Almer <jamrial@gmail.com>
Date: Mon Oct 5 16:13:51 2026 -0300
avformat/mov: support exporting derived images of type tmap as Gain Map Stream Groups
Signed-off-by: James Almer <jamrial@gmail.com>
diff --git a/libavformat/isom.h b/libavformat/isom.h
index 2516d65d78..dad5acf961 100644
--- a/libavformat/isom.h
+++ b/libavformat/isom.h
@@ -297,8 +297,10 @@ typedef struct HEIFItemRef {
typedef struct HEIFItem {
AVStream *st;
+ AVStreamGroup *stg;
HEIFItemRef *iref_list;
int nb_iref_list;
+ int flags;
char *name;
int item_id;
int64_t extent_length;
@@ -435,6 +437,8 @@ void ff_mp4_parse_es_descr(AVIOContext *pb, int *es_id);
#define MOV_TKHD_FLAG_IN_PREVIEW 0x0004
#define MOV_TKHD_FLAG_IN_POSTER 0x0008
+#define MOV_INFE_FLAG_HIDDEN 0x01
+
#define MOV_SAMPLE_DEPENDENCY_UNKNOWN 0x0
#define MOV_SAMPLE_DEPENDENCY_YES 0x1
#define MOV_SAMPLE_DEPENDENCY_NO 0x2
diff --git a/libavformat/mov.c b/libavformat/mov.c
index f88c139ba9..6d2f34ce00 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -46,6 +46,7 @@
#include "libavutil/opt.h"
#include "libavutil/aes.h"
#include "libavutil/aes_ctr.h"
+#include "libavutil/gain_map.h"
#include "libavutil/pixdesc.h"
#include "libavutil/sha.h"
#include "libavutil/spherical.h"
@@ -9539,10 +9540,10 @@ static int mov_read_infe(MOVContext *c, AVIOContext *pb, MOVAtom atom)
int64_t size = atom.size;
uint32_t item_type;
int item_id;
- int i, version, ret;
+ int i, version, flags, ret;
version = avio_r8(pb);
- avio_rb24(pb); // flags.
+ flags = avio_rb24(pb);
size -= 4;
if (size < 0)
return AVERROR_INVALIDDATA;
@@ -9595,6 +9596,7 @@ static int mov_read_infe(MOVContext *c, AVIOContext *pb, MOVAtom atom)
av_bprint_finalize(&item_name, ret ? &item->name : NULL);
item->item_id = item_id;
item->type = item_type;
+ item->flags = flags;
switch (item_type) {
case MKTAG('a','v','0','1'):
@@ -9679,13 +9681,104 @@ fail:
return ret;
}
+static int mov_read_iref_dimg_tmap(MOVContext *c, AVIOContext *pb,
+ HEIFItem *item, int version, int entries)
+{
+ HEIFGroup *tmap = av_realloc_array(c->heif_group, c->nb_heif_group + 1U,
+ sizeof(*c->heif_group));
+ int ret;
+
+ if (!tmap)
+ return AVERROR(ENOMEM);
+ c->heif_group = tmap;
+
+ if (entries != 2) {
+ avpriv_report_missing_feature(c->fc, "Derived image item of type %s with an "
+ "item reference count other than 2",
+ av_fourcc2str(item->type));
+ return 0;
+ }
+
+ tmap = &tmap[c->nb_heif_group];
+
+ tmap->item_idx_list = NULL; // Not needed
+ tmap->item_id_list = av_malloc_array(entries, sizeof(*tmap->item_id_list));
+ tmap->item_list = av_calloc(entries, sizeof(*tmap->item_list));
+ if (!tmap->item_id_list || !tmap->item_list) {
+ ret = AVERROR(ENOMEM);
+ goto fail;
+ }
+
+ /* 'to' item ids */
+ for (int i = 0; i < entries; i++) {
+ tmap->item_id_list[i] = version ? avio_rb32(pb) : avio_rb16(pb);
+
+ if (avio_feof(pb)) {
+ ret = AVERROR_INVALIDDATA;
+ goto fail;
+ }
+ }
+
+ tmap->nb_items = entries;
+ tmap->item = item;
+ ++c->nb_heif_group;
+
+ return 0;
+fail:
+ av_freep(&tmap->item_id_list);
+ av_freep(&tmap->item_list);
+
+ return ret;
+}
+
+static int mov_read_iref_dimg_grid(MOVContext *c, AVIOContext *pb,
+ HEIFItem *item, int version, int entries)
+{
+ HEIFGroup *grid = av_realloc_array(c->heif_group, c->nb_heif_group + 1U,
+ sizeof(*c->heif_group));
+ int ret;
+
+ if (!grid)
+ return AVERROR(ENOMEM);
+ c->heif_group = grid;
+ grid = &grid[c->nb_heif_group];
+
+ grid->item_id_list = av_malloc_array(entries, sizeof(*grid->item_id_list));
+ grid->item_idx_list = av_calloc(entries, sizeof(*grid->item_idx_list));
+ grid->item_list = av_calloc(entries, sizeof(*grid->item_list));
+ if (!grid->item_id_list || !grid->item_list || !grid->item_idx_list) {
+ ret = AVERROR(ENOMEM);
+ goto fail;
+ }
+ /* 'to' item ids */
+ for (int i = 0; i < entries; i++) {
+ grid->item_id_list[i] = version ? avio_rb32(pb) : avio_rb16(pb);
+
+ if (avio_feof(pb)) {
+ ret = AVERROR_INVALIDDATA;
+ goto fail;
+ }
+ }
+
+ grid->nb_items = entries;
+ grid->item = item;
+ ++c->nb_heif_group;
+
+ return 0;
+fail:
+ av_freep(&grid->item_id_list);
+ av_freep(&grid->item_idx_list);
+ av_freep(&grid->item_list);
+
+ return ret;
+}
+
static int mov_read_iref_dimg(MOVContext *c, AVIOContext *pb, int version)
{
HEIFItem *item = NULL;
- HEIFGroup *grid;
- int entries, i;
+ int entries;
int from_item_id = version ? avio_rb32(pb) : avio_rb16(pb);
- int ret = 0;
+ int ret;
for (int i = 0; i < c->nb_heif_group; i++) {
if (c->heif_group[i].item->item_id == from_item_id) {
@@ -9702,6 +9795,7 @@ static int mov_read_iref_dimg(MOVContext *c, AVIOContext *pb, int version)
switch (item->type) {
case MKTAG('g','r','i','d'):
case MKTAG('i','o','v','l'):
+ case MKTAG('t','m','a','p'):
break;
default:
avpriv_report_missing_feature(c->fc, "Derived Image item of type %s",
@@ -9711,7 +9805,7 @@ static int mov_read_iref_dimg(MOVContext *c, AVIOContext *pb, int version)
break;
}
if (!item) {
- av_log(c->fc, AV_LOG_ERROR, "Missing grid information\n");
+ av_log(c->fc, AV_LOG_ERROR, "Missing derived image information\n");
return AVERROR_INVALIDDATA;
}
@@ -9722,42 +9816,20 @@ static int mov_read_iref_dimg(MOVContext *c, AVIOContext *pb, int version)
return AVERROR_INVALIDDATA;
}
- grid = av_realloc_array(c->heif_group, c->nb_heif_group + 1U,
- sizeof(*c->heif_group));
- if (!grid)
- return AVERROR(ENOMEM);
- c->heif_group = grid;
- grid = &grid[c->nb_heif_group];
-
- grid->item_id_list = av_malloc_array(entries, sizeof(*grid->item_id_list));
- grid->item_idx_list = av_calloc(entries, sizeof(*grid->item_idx_list));
- grid->item_list = av_calloc(entries, sizeof(*grid->item_list));
- if (!grid->item_id_list || !grid->item_list || !grid->item_idx_list) {
- ret = AVERROR(ENOMEM);
- goto fail;
- }
- /* 'to' item ids */
- for (i = 0; i < entries; i++) {
- grid->item_id_list[i] = version ? avio_rb32(pb) : avio_rb16(pb);
-
- if (avio_feof(pb)) {
- ret = AVERROR_INVALIDDATA;
- goto fail;
- }
- }
-
- grid->nb_items = entries;
- grid->item = item;
- ++c->nb_heif_group;
-
av_log(c->fc, AV_LOG_TRACE, "dimg: from_item_id %d, entries %d\n",
from_item_id, entries);
- return 0;
-fail:
- av_freep(&grid->item_id_list);
- av_freep(&grid->item_idx_list);
- av_freep(&grid->item_list);
+ switch (item->type) {
+ case MKTAG('g','r','i','d'):
+ case MKTAG('i','o','v','l'):
+ ret = mov_read_iref_dimg_grid(c, pb, item, version, entries);
+ break;
+ case MKTAG('t','m','a','p'):
+ ret = mov_read_iref_dimg_tmap(c, pb, item, version, entries);
+ break;
+ default:
+ av_unreachable("item->type should have been checked above");
+ }
return ret;
}
@@ -11332,6 +11404,203 @@ static int mov_parse_tiles(AVFormatContext *s)
return 0;
}
+static int mov_parse_heif_layered_video_tmap(AVFormatContext *s, const HEIFGroup *tmap)
+{
+ MOVContext *mov = s->priv_data;
+ AVStreamGroup *stg;
+ AVStreamGroupLayeredVideo *layered_video;
+ AVGainMapParams *gain = NULL;
+ uint8_t *payload = NULL;
+ int64_t pos, offset = 0;
+ size_t gain_size;
+ int64_t err64;
+ int err;
+
+ if (!(s->pb->seekable & AVIO_SEEKABLE_NORMAL)) {
+ av_log(s, AV_LOG_WARNING, "tmap box with non seekable input\n");
+ return 0;
+ }
+
+ stg = avformat_stream_group_create(s, AV_STREAM_GROUP_PARAMS_GAIN_MAP, NULL);
+ if (!stg)
+ return AVERROR(ENOMEM);
+
+ stg->id = tmap->item->item_id;
+ layered_video = stg->params.layered_video;
+
+ for (int j = 0; j < tmap->nb_items; j++) {
+ int item_id = tmap->item_id_list[j];
+ HEIFItem *item = get_heif_item(mov, item_id);
+ AVStreamGroup *child_stg;
+ AVStream *st;
+
+ if (!item) {
+ av_log(s, AV_LOG_WARNING, "HEIF item id %d referenced by tmap id %d doesn't "
+ "exist\n",
+ item_id, tmap->item->item_id);
+ ff_remove_stream_group(s, stg);
+ return 0;
+ }
+
+ st = item->st;
+ child_stg = item->stg;
+ if (!st && !child_stg) {
+ av_log(s, AV_LOG_WARNING, "HEIF item id %d from tmap id %d doesn't "
+ "reference a stream or tmap\n",
+ item_id, tmap->item->item_id);
+ ff_remove_stream_group(s, stg);
+ return 0;
+ }
+ av_assert0(!st != !child_stg);
+
+ tmap->item_list[j] = item;
+
+ if (st)
+ err = avformat_stream_group_add_stream(stg, st);
+ else
+ err = avformat_stream_group_add_stream_group(stg, child_stg);
+ if (err < 0) {
+ if (err == AVERROR(EEXIST))
+ err = AVERROR_INVALIDDATA;
+
+ return err;
+ }
+
+ if ((item->flags & MOV_INFE_FLAG_HIDDEN)) {
+ if (st)
+ st->disposition |= AV_DISPOSITION_DEPENDENT;
+ else
+ child_stg->disposition |= AV_DISPOSITION_DEPENDENT;
+ }
+ }
+
+ layered_video->width = tmap->item->width;
+ layered_video->height = tmap->item->height;
+
+ if (tmap->item->is_idat_relative) {
+ if (!mov->idat_offset) {
+ av_log(s, AV_LOG_ERROR, "Missing idat box required by the tmap\n");
+ return AVERROR_INVALIDDATA;
+ }
+ offset = mov->idat_offset;
+ }
+
+ if (offset > INT64_MAX - tmap->item->extent_offset)
+ return AVERROR_INVALIDDATA;
+
+ gain = av_gain_map_params_alloc(&gain_size);
+ if (!gain)
+ return AVERROR(ENOMEM);
+
+ payload = av_malloc(tmap->item->extent_length);
+ if (!payload) {
+ return AVERROR(ENOMEM);
+ goto fail;
+ }
+
+ pos = avio_tell(s->pb);
+ err64 = avio_seek(s->pb, tmap->item->extent_offset + offset, SEEK_SET);
+ if (err64 != tmap->item->extent_offset + offset) {
+ err64 = err64 < 0 ? err64 : AVERROR_INVALIDDATA;
+ goto fail;
+ }
+ avio_r8(s->pb); /* version */
+
+ err = avio_read(s->pb, payload, tmap->item->extent_length);
+ if (err == tmap->item->extent_length)
+ err64 = avio_seek(s->pb, pos, SEEK_SET);
+ else {
+ err = err < 0 ? err : AVERROR_INVALIDDATA;
+ goto fail;
+ }
+ if (err64 != pos) {
+ err = err64 < 0 ? err64 : AVERROR_INVALIDDATA;
+ goto fail;
+ }
+
+ err = av_gain_map_params_from_iso21496(gain, payload, tmap->item->extent_length);
+ av_freep(&payload);
+ if (err < 0)
+ goto fail;
+
+ for (int j = 0; j < tmap->nb_items; j++) {
+ HEIFItem *item = tmap->item_list[j];
+ AVPacketSideData *sd;
+ AVPacketSideData **coded_side_data;
+ int *nb_coded_side_data;
+
+ if (!(item->flags & MOV_INFE_FLAG_HIDDEN))
+ continue;
+
+ layered_video->el_index = j;
+
+ if (item->st) {
+ coded_side_data = &item->st->codecpar->coded_side_data;
+ nb_coded_side_data = &item->st->codecpar->nb_coded_side_data;
+ } else {
+ av_assert0(item->stg);
+ coded_side_data = &item->stg->coded_side_data;
+ nb_coded_side_data = &item->stg->nb_coded_side_data;
+ }
+
+ sd = av_packet_side_data_add(coded_side_data, nb_coded_side_data,
+ AV_PKT_DATA_GAIN_MAP_PARAMS,
+ (uint8_t *)gain, gain_size, 0);
+ if (!sd) {
+ av_free(gain);
+ return AVERROR(ENOMEM);
+ }
+ gain = NULL;
+
+ break;
+ }
+ av_free(gain);
+
+ err = mov_parse_heif_item_references(s, tmap->item, &stg->coded_side_data,
+ &stg->nb_coded_side_data);
+ if (err < 0)
+ return err;
+
+ if (tmap->item->name)
+ av_dict_set(&stg->metadata, "title", tmap->item->name, 0);
+ if (tmap->item->item_id == mov->primary_item_id)
+ stg->disposition |= AV_DISPOSITION_DEFAULT;
+
+ tmap->item->stg = stg;
+
+ return 0;
+fail:
+ av_free(gain);
+ av_free(payload);
+
+ return err;
+}
+
+static int mov_parse_heif_layered_video(AVFormatContext *s)
+{
+ MOVContext *mov = s->priv_data;
+ int err;
+
+ for (int i = 0; i < mov->nb_heif_group; i++) {
+ const HEIFGroup *tmap = &mov->heif_group[i];
+
+ if (!tmap->item)
+ continue;
+
+ switch (tmap->item->type) {
+ case MKTAG('t','m','a','p'):
+ err = mov_parse_heif_layered_video_tmap(s, tmap);
+ if (err < 0)
+ return err;
+ break;
+ default:
+ break;
+ }
+ }
+
+ return 0;
+}
+
static int mov_parse_heif_items(AVFormatContext *s)
{
MOVContext *mov = s->priv_data;
@@ -11390,6 +11659,10 @@ static int mov_parse_heif_items(AVFormatContext *s)
err = mov_parse_tiles(s);
if (err < 0)
return err;
+
+ err = mov_parse_heif_layered_video(s);
+ if (err < 0)
+ return err;
}
return 0;