Commit 686b79d469 for ffmpeg
commit 686b79d469024397ae23e9ec0f6c2282dbaa6148
Author: James Almer <jamrial@gmail.com>
Date: Thu Oct 8 22:12:03 2026 -0300
avformat/mov: support exporting Apple gain maps as Stream Group
Signed-off-by: James Almer <jamrial@gmail.com>
diff --git a/libavformat/isom.h b/libavformat/isom.h
index 97d41b567f..42a124edb3 100644
--- a/libavformat/isom.h
+++ b/libavformat/isom.h
@@ -299,8 +299,10 @@ typedef struct HEIFItemRef {
typedef struct HEIFItem {
AVStream *st;
AVStreamGroup *stg;
- HEIFItemRef *iref_list;
- int nb_iref_list;
+ HEIFItemRef *iref_from_list;
+ int nb_iref_from_list;
+ HEIFItemRef *iref_to_list;
+ int nb_iref_to_list;
int flags;
char *name;
int item_id;
@@ -312,6 +314,7 @@ typedef struct HEIFItem {
int hflip;
int vflip;
int type;
+ int is_gainmap;
int is_idat_relative;
AVExifMetadata ifd;
uint8_t *icc_profile;
diff --git a/libavformat/mov.c b/libavformat/mov.c
index 9cec6255cd..0057ca7d92 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -9422,6 +9422,27 @@ static int rb_size(AVIOContext *pb, int64_t *value, int size)
return size;
}
+static int mov_read_auxc(MOVContext *c, AVIOContext *pb, MOVAtom atom)
+{
+ AVBPrint aux_type;
+ HEIFItem *item;
+
+ avio_r8(pb); /* version */
+ avio_rb24(pb); /* flags */
+
+ av_bprint_init(&aux_type, 0, AV_BPRINT_SIZE_AUTOMATIC);
+ ff_read_string_to_bprint_overwrite(pb, &aux_type, atom.size - 4);
+
+ av_log(c->fc, AV_LOG_TRACE, "auxC: item_id %d, aux_type %s\n",
+ c->cur_item_id, aux_type.str);
+
+ item = get_heif_item(c, c->cur_item_id);
+ if (item && (av_stristr(aux_type.str, "hdrgainmap")))
+ item->is_gainmap = 1;
+
+ return 0;
+}
+
static int mov_read_pitm(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
avio_rb32(pb); // version & flags.
@@ -9834,7 +9855,8 @@ static int mov_read_iref_dimg(MOVContext *c, AVIOContext *pb, int version)
return ret;
}
-static int mov_read_iref_cdsc(MOVContext *c, AVIOContext *pb, uint32_t type, int version, uint32_t size)
+static int mov_read_iref_cdsc(MOVContext *c, AVIOContext *pb, uint32_t type,
+ int version, uint32_t size)
{
HEIFItem *from_item = NULL;
int entries;
@@ -9844,7 +9866,8 @@ static int mov_read_iref_cdsc(MOVContext *c, AVIOContext *pb, uint32_t type, int
from_item = get_heif_item(c, from_item_id);
if (!from_item) {
- av_log(c->fc, AV_LOG_ERROR, "Missing stream referenced by thmb item\n");
+ av_log(c->fc, AV_LOG_ERROR, "Missing stream referenced by %s iref\n",
+ av_fourcc2str(type));
return AVERROR_INVALIDDATA;
}
@@ -9867,8 +9890,8 @@ static int mov_read_iref_cdsc(MOVContext *c, AVIOContext *pb, uint32_t type, int
continue;
}
- if (!av_dynarray2_add((void **)&item->iref_list, &item->nb_iref_list,
- sizeof(*item->iref_list), (const uint8_t *)&ref))
+ if (!av_dynarray2_add((void **)&item->iref_from_list, &item->nb_iref_from_list,
+ sizeof(*item->iref_from_list), (const uint8_t *)&ref))
return AVERROR(ENOMEM);
}
@@ -9878,6 +9901,54 @@ static int mov_read_iref_cdsc(MOVContext *c, AVIOContext *pb, uint32_t type, int
return 0;
}
+static int mov_read_iref_auxl(MOVContext *c, AVIOContext *pb, uint32_t type,
+ int version, uint32_t size)
+{
+ HEIFItem *from_item = NULL;
+ int entries;
+ unsigned from_item_id = version ? avio_rb32(pb) : avio_rb16(pb);
+
+ from_item = get_heif_item(c, from_item_id);
+ if (!from_item) {
+ av_log(c->fc, AV_LOG_ERROR, "Missing item referenced by %s iref\n",
+ av_fourcc2str(type));
+ return AVERROR_INVALIDDATA;
+ }
+
+ entries = avio_rb16(pb);
+ if (entries != 1) {
+ avpriv_report_missing_feature(c->fc, "%s iref with an item reference "
+ "count other than 1",
+ av_fourcc2str(type));
+ return 0;
+ }
+
+ /* 'to' item ids */
+ for (int i = 0; i < entries; i++) {
+ unsigned to_item_id = version ? avio_rb32(pb) : avio_rb16(pb);
+ const HEIFItemRef ref = { type, to_item_id };
+ HEIFItem *item = get_heif_item(c, to_item_id);
+
+ if (avio_feof(pb))
+ return AVERROR_INVALIDDATA;
+
+ if (!item) {
+ av_log(c->fc, AV_LOG_WARNING, "Missing item referenced by %s iref\n",
+ av_fourcc2str(type));
+ continue;
+ }
+
+ if (!av_dynarray2_add((void **)&from_item->iref_to_list, &from_item->nb_iref_to_list,
+ sizeof(*from_item->iref_to_list), (const uint8_t *)&ref))
+ return AVERROR(ENOMEM);
+ }
+
+ av_log(c->fc, AV_LOG_TRACE, "%s: from_item_id %d, entries %d\n",
+ av_fourcc2str(type), from_item_id, entries);
+
+ return 0;
+}
+
static int mov_read_iref(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
int version = avio_r8(pb);
@@ -9907,11 +9978,16 @@ static int mov_read_iref(MOVContext *c, AVIOContext *pb, MOVAtom atom)
return ret;
break;
case MKTAG('c','d','s','c'):
- case MKTAG('t','h','m','b'):
ret = mov_read_iref_cdsc(c, pb, type, version, size - 8);
if (ret < 0)
return ret;
break;
+ case MKTAG('a','u','x','l'):
+ case MKTAG('t','h','m','b'):
+ ret = mov_read_iref_auxl(c, pb, type, version, size - 8);
+ if (ret < 0)
+ return ret;
+ break;
default:
av_log(c->fc, AV_LOG_DEBUG, "Unknown iref type %s size %"PRIu32"\n",
av_fourcc2str(type), size);
@@ -10294,6 +10370,7 @@ static const MOVParseTableEntry mov_default_parse_table[] = {
{ MKTAG('p','c','m','C'), mov_read_pcmc }, /* PCM configuration box */
{ MKTAG('p','i','t','m'), mov_read_pitm },
{ MKTAG('e','v','c','C'), mov_read_glbl },
+{ MKTAG('a','u','x','C'), mov_read_auxc },
{ MKTAG('i','d','a','t'), mov_read_idat },
{ MKTAG('i','m','i','r'), mov_read_imir },
{ MKTAG('i','r','e','f'), mov_read_iref },
@@ -10834,7 +10911,8 @@ static int mov_read_close(AVFormatContext *s)
if (!mov->heif_item[i])
continue;
av_freep(&mov->heif_item[i]->name);
- av_freep(&mov->heif_item[i]->iref_list);
+ av_freep(&mov->heif_item[i]->iref_from_list);
+ av_freep(&mov->heif_item[i]->iref_to_list);
av_freep(&mov->heif_item[i]->icc_profile);
av_exif_free(&mov->heif_item[i]->ifd);
av_freep(&mov->heif_item[i]);
@@ -10991,12 +11069,18 @@ static int set_exif_from_item(AVFormatContext *s,
AVPacketSideData **coded_side_data, int *nb_coded_side_data,
const HEIFItem *item)
{
- AVPacketSideData *sd;
+ AVPacketSideData *sd =
+ (AVPacketSideData *)av_packet_side_data_get(*coded_side_data,
+ *nb_coded_side_data,
+ AV_PKT_DATA_EXIF);
const AVExifMetadata *ifd = &item->ifd;
AVBufferRef *buf = NULL;
int64_t offset;
int err;
+ if (sd)
+ return 0;
+
err = av_exif_write(s, ifd, &buf, AV_EXIF_T_OFF);
if (err < 0)
return err;
@@ -11022,9 +11106,17 @@ static int set_exif_from_item(AVFormatContext *s,
static int set_icc_profile_from_item(AVPacketSideData **coded_side_data, int *nb_coded_side_data,
const HEIFItem *item)
{
- AVPacketSideData *sd = av_packet_side_data_new(coded_side_data, nb_coded_side_data,
- AV_PKT_DATA_ICC_PROFILE,
- item->icc_profile_size, 0);
+ AVPacketSideData *sd =
+ (AVPacketSideData *)av_packet_side_data_get(*coded_side_data,
+ *nb_coded_side_data,
+ AV_PKT_DATA_ICC_PROFILE);
+
+ if (sd)
+ return 0;
+
+ sd = av_packet_side_data_new(coded_side_data, nb_coded_side_data,
+ AV_PKT_DATA_ICC_PROFILE,
+ item->icc_profile_size, 0);
if (!sd)
return AVERROR(ENOMEM);
@@ -11037,10 +11129,17 @@ static int set_display_matrix_from_item(AVPacketSideData **coded_side_data, int
const HEIFItem *item)
{
int32_t *matrix;
- AVPacketSideData *sd = av_packet_side_data_new(coded_side_data,
- nb_coded_side_data,
- AV_PKT_DATA_DISPLAYMATRIX,
- 9 * sizeof(*matrix), 0);
+ AVPacketSideData *sd =
+ (AVPacketSideData *)av_packet_side_data_get(*coded_side_data,
+ *nb_coded_side_data,
+ AV_PKT_DATA_DISPLAYMATRIX);
+
+ if (sd)
+ return 0;
+
+ sd = av_packet_side_data_new(coded_side_data, nb_coded_side_data,
+ AV_PKT_DATA_DISPLAYMATRIX,
+ 9 * sizeof(*matrix), 0);
if (!sd)
return AVERROR(ENOMEM);
@@ -11316,8 +11415,8 @@ static int mov_parse_heif_item_references(AVFormatContext *s, HEIFItem *item,
MOVContext *mov = s->priv_data;
int err;
- for (int j = 0; j < item->nb_iref_list; j++) {
- HEIFItem *ref = get_heif_item(mov, item->iref_list[j].item_id);
+ for (int j = 0; j < item->nb_iref_from_list; j++) {
+ HEIFItem *ref = get_heif_item(mov, item->iref_from_list[j].item_id);
av_assert0(ref);
switch(ref->type) {
@@ -11606,6 +11705,134 @@ fail:
return err;
}
+// Parse Apple style Gain Map metadata from Exif values
+// See https://developer.apple.com/documentation/appkit/applying-apple-hdr-effect-to-your-photos
+static int mov_parse_heif_layered_video_auxl(AVFormatContext *s,
+ HEIFItem *base, HEIFItem *gain_map)
+{
+ AVStreamGroupLayeredVideo *layered_video;
+ AVStreamGroup *stg;
+ AVGainMapParams *gain;
+ AVPacketSideData **coded_side_data;
+ int *nb_coded_side_data;
+ AVExifEntry *entry, *maker33 = NULL, *maker48 = NULL;
+ double maker_33 = 0.0, maker_48 = 0.0, headroom;
+ size_t gain_size;
+ int err;
+
+ if ((!gain_map->st && !gain_map->stg) ||
+ (!base->st && !base->stg)) {
+ av_log(s, AV_LOG_WARNING, "HEIF gain_map id %d or its base doesn't reference "
+ "a stream or group\n",
+ gain_map->item_id);
+ return 0;
+ }
+
+ if (!base->ifd.count)
+ return 0;
+
+ err = av_exif_get_entry(s, &base->ifd, 0x8769, 0, &entry);
+ if (err > 0)
+ err = av_exif_get_entry(s, &entry->value.ifd, 0x927c, 0, &entry);
+ if (err > 0) {
+ av_exif_get_entry(s, &entry->value.ifd, 0x21, 0, &maker33);
+ av_exif_get_entry(s, &entry->value.ifd, 0x30, 0, &maker48);
+ }
+ if ((!maker33 && !maker48) ||
+ (maker33 && maker33->type != AV_TIFF_SRATIONAL) ||
+ (maker48 && maker48->type != AV_TIFF_SRATIONAL))
+ return 0;
+
+ if (maker33)
+ maker_33 = av_q2d(*maker33->value.rat);
+ if (maker48)
+ maker_48 = av_q2d(*maker48->value.rat);
+
+ if (maker_33 < 1.0) {
+ if (maker_48 <= 0.01) {
+ headroom = -20.0 * maker_48 + 1.8;
+ } else {
+ headroom = -0.101 * maker_48 + 1.601;
+ }
+ } else {
+ if (maker_48 <= 0.01) {
+ headroom = -70.0 * maker_48 + 3.0;
+ } else {
+ headroom = -0.303 * maker_48 + 2.303;
+ }
+ }
+ if (headroom <= 0.0)
+ return 0;
+
+ stg = avformat_stream_group_create(s, AV_STREAM_GROUP_PARAMS_GAIN_MAP, NULL);
+ if (!stg)
+ return AVERROR(ENOMEM);
+
+ stg->id = gain_map->item_id;
+ layered_video = stg->params.layered_video;
+
+ layered_video->width = base->width;
+ layered_video->height = base->height;
+ layered_video->el_index = 1;
+
+ av_assert0(!base->st != !base->stg);
+ if (base->st)
+ err = avformat_stream_group_add_stream(stg, base->st);
+ else
+ err = avformat_stream_group_add_stream_group(stg, base->stg);
+ if (err < 0) {
+ if (err == AVERROR(EEXIST))
+ err = AVERROR_INVALIDDATA;
+ return err;
+ }
+
+ av_assert0(!gain_map->st != !gain_map->stg);
+ if (gain_map->st) {
+ coded_side_data = &gain_map->st->codecpar->coded_side_data;
+ nb_coded_side_data = &gain_map->st->codecpar->nb_coded_side_data;
+ err = avformat_stream_group_add_stream(stg, gain_map->st);
+ } else {
+ coded_side_data = &gain_map->stg->coded_side_data;
+ nb_coded_side_data = &gain_map->stg->nb_coded_side_data;
+ err = avformat_stream_group_add_stream_group(stg, gain_map->stg);
+ }
+ if (err < 0) {
+ if (err == AVERROR(EEXIST))
+ err = AVERROR_INVALIDDATA;
+ return err;
+ }
+
+ gain = av_gain_map_params_alloc(&gain_size);
+ if (!gain)
+ return AVERROR(ENOMEM);
+
+ gain->use_base_color_space = 1;
+ gain->alternate_hdr_headroom = av_d2q(headroom, INT_MAX);
+ gain->nb_channels = 1;
+ gain->channels[0].base_offset = (AVRational) { 0, 1 };
+ gain->channels[0].alternate_offset = (AVRational) { 0, 1 };
+ gain->channels[0].gain_map_max = gain->alternate_hdr_headroom;
+
+ AVPacketSideData *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);
+ }
+
+ // Apply all references from the base image except Exif to the Gain Map group
+ int count = base->ifd.count;
+ base->ifd.count = 0;
+ err = mov_apply_heif_item_references(s, base, &stg->coded_side_data,
+ &stg->nb_coded_side_data);
+ base->ifd.count = count;
+ if (err < 0)
+ return err;
+
+ return 0;
+}
+
static int mov_parse_heif_layered_video(AVFormatContext *s)
{
MOVContext *mov = s->priv_data;
@@ -11628,6 +11855,30 @@ static int mov_parse_heif_layered_video(AVFormatContext *s)
}
}
+ for (int i = 0; i < mov->nb_heif_item; i++) {
+ HEIFItem *item = mov->heif_item[i];
+ int err;
+
+ if (!item)
+ continue;
+
+ for (int j = 0; j < item->nb_iref_to_list; j++) {
+ HEIFItem *ref = get_heif_item(mov, item->iref_to_list[j].item_id);
+
+ av_assert0(ref);
+ switch(item->iref_to_list[j].type) {
+ case MKTAG('a','u','x','l'):
+ if (!item->is_gainmap)
+ break;
+
+ err = mov_parse_heif_layered_video_auxl(s, ref, item);
+ if (err < 0)
+ return err;
+ break;
+ }
+ }
+ }
+
return 0;
}