PR #25433 opened by James Almer (jamrial)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25433
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25433.patch

Currently supported:
- HEVC and jpeg streams
- Gain Map and Alpha channel derived images (Separate streams for color and
  gmap/alpha)
- Most common metadata types (Exif, ICC profiles, CLLI)

Not yet supported:
- VVC and AVC streams
- Animated HEIF
- grid/iovl derived images


>From 1f2c0da77d530f946f1777cf5c67924e653dc8bd Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Sat, 17 Oct 2026 21:28:33 -0300
Subject: [PATCH 1/6] avformat: add a HEIF muxer

Currently supported:
- HEVC and jpeg streams
- Gain Map and Alpha channel derived images (Separate streams for color and
  gmap/alpha)
- Most common metadata types (Exif, ICC profiles, CLLI)

Not yet supported:
- VVC and AVC streams
- Animated HEIF
- grid/iovl derived images

Signed-off-by: James Almer <[email protected]>
---
 configure                |    1 +
 libavformat/Makefile     |    1 +
 libavformat/allformats.c |    1 +
 libavformat/heifenc.c    | 1535 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 1538 insertions(+)
 create mode 100644 libavformat/heifenc.c

diff --git a/configure b/configure
index 083bd652c49..a2f507b8e9e 100755
--- a/configure
+++ b/configure
@@ -3916,6 +3916,7 @@ flac_demuxer_select="flac_parser"
 flv_muxer_select="aac_adtstoasc_bsf iso_writer"
 gxf_muxer_select="pcm_rechunk_bsf"
 hds_muxer_select="flv_muxer"
+heif_muxer_select="iso_writer"
 hls_demuxer_select="aac_demuxer ac3_demuxer adts_header ac3_parser 
eac3_demuxer mov_demuxer mpegts_demuxer"
 hls_muxer_select="mov_muxer mpegts_muxer webvtt_muxer"
 hxvs_demuxer_select="h264_parser hevc_parser"
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 92c09714fe7..e7c83dfc9f3 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -270,6 +270,7 @@ OBJS-$(CONFIG_HASH_MUXER)                += hashenc.o
 OBJS-$(CONFIG_HCA_DEMUXER)               += hca.o
 OBJS-$(CONFIG_HCOM_DEMUXER)              += hcom.o pcm.o
 OBJS-$(CONFIG_HDS_MUXER)                 += hdsenc.o
+OBJS-$(CONFIG_HEIF_MUXER)                += heifenc.o
 OBJS-$(CONFIG_HEVC_DEMUXER)              += hevcdec.o rawdec.o
 OBJS-$(CONFIG_HEVC_MUXER)                += rawenc.o
 OBJS-$(CONFIG_EVC_DEMUXER)               += evcdec.o rawdec.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index a4e859da333..0cd1997ef65 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -212,6 +212,7 @@ extern const FFOutputFormat ff_hash_muxer;
 extern const FFInputFormat  ff_hca_demuxer;
 extern const FFInputFormat  ff_hcom_demuxer;
 extern const FFOutputFormat ff_hds_muxer;
+extern const FFOutputFormat ff_heif_muxer;
 extern const FFInputFormat  ff_hevc_demuxer;
 extern const FFOutputFormat ff_hevc_muxer;
 extern const FFInputFormat  ff_hls_demuxer;
diff --git a/libavformat/heifenc.c b/libavformat/heifenc.c
new file mode 100644
index 00000000000..5b1e4856b1e
--- /dev/null
+++ b/libavformat/heifenc.c
@@ -0,0 +1,1535 @@
+/*
+ * HEIF muxer
+ * Copyright (c) 2026 James Almer
+ *
+ * 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
+ */
+
+#include "config_components.h"
+
+#include <stdint.h>
+#include <inttypes.h>
+
+#include "libavutil/avassert.h"
+#include "libavutil/gain_map.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/mastering_display_metadata.h"
+#include "libavutil/mem.h"
+#include "libavutil/pixdesc.h"
+#include "avformat.h"
+#include "avio.h"
+#include "avio_internal.h"
+#include "hevc.h"
+#include "internal.h"
+#include "mux.h"
+
+typedef struct ItemInfoEntry {
+    unsigned int flags;
+    unsigned int type;
+    unsigned int id;
+    uint8_t *idat_payload;
+    size_t idat_payload_size;
+    char *name;
+} ItemInfoEntry;
+
+typedef struct ItemLocationEntry {
+    int construction_method;
+    int extent_offset;
+    int extent_length;
+} ItemLocationEntry;
+
+typedef struct ItemPropertyAssociation {
+    int essential;
+    int ipco_id;
+} ItemPropertyAssociation;
+
+typedef struct ItemEntry {
+    ItemInfoEntry infe;
+    ItemLocationEntry iloc;
+    ItemPropertyAssociation *ipma;
+    unsigned int nb_ipma;
+    struct ItemReference **iref; // reverse reference
+    int nb_iref;
+    int seen_pkt;
+} ItemEntry;
+
+typedef struct HEVCConfiguration {
+    int id;
+    AVIOContext *pb;
+} HEVCConfiguration;
+
+typedef struct CleanApperture {
+    unsigned int cropped_width;
+    unsigned int cropped_height;
+    AVRational horiz_off;
+    AVRational vert_off;
+} CleanApperture;
+
+typedef struct ColorInformation {
+    unsigned int color_type;
+    enum AVColorPrimaries color_primaries;
+    enum AVColorTransferCharacteristic transfer_characteristics;
+    enum AVColorSpace matrix_coefficients;
+    enum AVColorRange color_range;
+    uint8_t *icc_profile;
+    size_t icc_profile_size;
+} ColorInformation;
+
+typedef struct ContentLightLevel {
+    unsigned int max_content_light_level;
+    unsigned int max_pic_average_light_level;
+} ContentLightLevel;
+
+typedef struct ImageSpatialExtents {
+    int width;
+    int height;
+} ImageSpatialExtents;
+
+typedef struct PixelInformation {
+    int nb_channels;
+    int bps[3];
+} PixelInformation;
+
+typedef struct ImageRotation {
+    int rotation;
+} ImageRotation;
+
+typedef struct AuxiliaryType {
+    char *aux_type;
+} AuxiliaryType;
+
+typedef struct ItemProperties {
+    unsigned int type;
+    void *ipco;
+} ItemProperties;
+
+typedef struct ItemReference {
+    int type;
+    int from_id;
+    int *to_id;
+    unsigned int nb_to_id;
+} ItemReference;
+
+typedef struct GroupList {
+    int type;
+    int id;
+    int *entities;
+    unsigned int nb_entities;
+} GroupList;
+
+typedef struct HEIFMuxContext {
+    AVIOContext *mdat_pb;
+    ItemEntry *item;
+    unsigned int item_alloc_size;
+    unsigned int nb_items;
+    ItemProperties *iprp;
+    unsigned int iprp_alloc_size;
+    unsigned int nb_iprp;
+    ItemReference *iref;
+    unsigned int iref_alloc_size;
+    unsigned int nb_iref;
+    GroupList *grpl;
+    unsigned int grpl_alloc_size;
+    unsigned int nb_grpl;
+    unsigned int primary_item;
+    enum AVCodecID codec_id;
+} HEIFMuxContext;
+
+/**
+ * Get the requested item.
+ */
+static ItemEntry *get_heif_item(HEIFMuxContext *heif, unsigned id)
+{
+    ItemEntry *item = NULL;
+
+    for (int i = 0; i < heif->nb_items; i++) {
+        if (heif->item[i].infe.id != id)
+            continue;
+
+        item = &heif->item[i];
+        break;
+    }
+
+    return item;
+}
+
+static ItemProperties *heif_add_iprp_entry(HEIFMuxContext *heif)
+{
+    ItemProperties *iprp = av_fast_realloc(heif->iprp, &heif->iprp_alloc_size,
+                                           (heif->nb_iprp + 1) * 
sizeof(*heif->iprp));
+    if (!iprp)
+        return NULL;
+
+    heif->iprp = iprp;
+    iprp = &heif->iprp[heif->nb_iprp];
+
+    return iprp;
+}
+
+static int heif_add_ipma_entry(ItemEntry *item, int essential, int id)
+{
+    ItemPropertyAssociation *ipma = av_dynarray2_add((void**)&item->ipma,
+                                                     &item->nb_ipma, 
sizeof(*ipma), NULL);
+    if (!ipma)
+        return AVERROR(ENOMEM);
+    *ipma = (ItemPropertyAssociation) { essential, id };
+
+    return 0;
+}
+
+#define IPCO_TYPE_CHECK(ipco_var, ipco_type) \
+    ItemProperties *iprp = &heif->iprp[j]; \
+    if (iprp->type != ipco_type) \
+        continue; \
+    ipco_var = iprp->ipco
+
+#define IPCO_TYPE_SET(ipco_var, ipco_type) \
+    ItemProperties *iprp = heif_add_iprp_entry(heif); \
+    if (!iprp) \
+        return AVERROR(ENOMEM); \
+    iprp->type = ipco_type; \
+    iprp->ipco = av_mallocz(sizeof(*ipco_var)); \
+    if (!iprp->ipco) \
+        return AVERROR(ENOMEM); \
+    ipco_var = iprp->ipco
+
+static int heif_build_hvcc(AVFormatContext *s, ItemEntry *item,
+                           const AVCodecParameters *par)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    HEVCConfiguration *hvcc;
+    AVIOContext *pb;
+    uint8_t *buf;
+    int buf_size;
+    int ret, j;
+
+    ret = avio_open_dyn_buf(&pb);
+    if (ret < 0)
+        return ret;
+
+    ret = ff_isom_write_hvcc(pb, par->extradata, par->extradata_size, 1, s);
+    if (ret < 0)
+        goto fail;
+
+    buf_size = avio_get_dyn_buf(pb, &buf);
+    if (!buf_size) {
+        ret = AVERROR(EINVAL);
+        goto fail;
+    }
+
+    for (j = 0; j < heif->nb_iprp; j++) {
+        uint8_t *ref;
+        int ref_size;
+
+        IPCO_TYPE_CHECK(hvcc, MKTAG('h','v','c','C'));
+        ref_size = avio_get_dyn_buf(hvcc->pb, &ref);
+        if (!ref_size || buf_size != ref_size)
+            continue;
+        if (!memcmp(buf, ref, buf_size))
+            break;
+    }
+    if (j == heif->nb_iprp) {
+        ItemProperties *iprp = heif_add_iprp_entry(heif);
+        if (!iprp) {
+            ret = AVERROR(ENOMEM);
+            goto fail;
+        }
+        iprp->type = MKTAG('h','v','c','C');
+        iprp->ipco = av_mallocz(sizeof(*hvcc));
+        if (!iprp->ipco) {
+            ret = AVERROR(ENOMEM);
+            goto fail;
+        }
+
+        hvcc = iprp->ipco;
+        hvcc->pb = pb;
+
+        heif->nb_iprp++;
+    } else
+        ffio_free_dyn_buf(&pb);
+
+    return heif_add_ipma_entry(item, 1, j + 1);
+fail:
+    ffio_free_dyn_buf(&pb);
+
+    return ret;
+}
+
+static int heif_build_ispe(AVFormatContext *s, ItemEntry *item, int width, int 
height)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    ImageSpatialExtents *ispe;
+    int j;
+
+    for (j = 0; j < heif->nb_iprp; j++) {
+        IPCO_TYPE_CHECK(ispe, MKTAG('i','s','p','e'));
+        if (ispe->width  == width &&
+            ispe->height == height)
+            break;
+    }
+    if (j == heif->nb_iprp) {
+        IPCO_TYPE_SET(ispe, MKTAG('i','s','p','e'));
+        ispe->width  = width;
+        ispe->height = height;
+
+        heif->nb_iprp++;
+    }
+
+    return heif_add_ipma_entry(item, 1, j + 1);
+}
+
+static int heif_build_clap(AVFormatContext *s, ItemEntry *item,
+                           const AVCodecParameters *par, const 
AVPacketSideData *sd)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    CleanApperture *clap;
+    int j;
+    unsigned int top    = AV_RL32(sd->data +  0);
+    unsigned int bottom = AV_RL32(sd->data +  4);
+    unsigned int left   = AV_RL32(sd->data +  8);
+    unsigned int right  = AV_RL32(sd->data + 12);
+    unsigned int cropped_width, cropped_height;
+    AVRational horiz_off, vert_off;
+
+    if ((left + (int64_t)right) >= par->width ||
+        (top + (int64_t)bottom) >= par->height) {
+        av_log(s, AV_LOG_ERROR, "Invalid cropping dimensions in stream side 
data\n");
+        return AVERROR(EINVAL);
+    }
+    if (!top && !bottom && !left && !right)
+        return 0;
+
+    cropped_width  = par->width - left - right;
+    cropped_height = par->height - top - bottom;
+    horiz_off =
+        av_sub_q((AVRational) { par->width - cropped_width, 2 },
+                 (AVRational) { left, 1 });
+    vert_off =
+        av_sub_q((AVRational) { par->height - cropped_height, 2 },
+                 (AVRational) { top, 1 });
+
+    for (j = 0; j < heif->nb_iprp; j++) {
+        IPCO_TYPE_CHECK(clap, MKTAG('c','l','a','p'));
+        if (clap->cropped_width  == cropped_width &&
+            clap->cropped_height == cropped_height &&
+            !av_cmp_q(clap->horiz_off, horiz_off) &&
+            !av_cmp_q(clap->vert_off, vert_off))
+            break;
+    }
+    if (j == heif->nb_iprp) {
+        IPCO_TYPE_SET(clap, MKTAG('c','l','a','p'));
+        clap->cropped_width  = cropped_width;
+        clap->cropped_height = cropped_height;
+        clap->horiz_off = horiz_off;
+        clap->vert_off = vert_off;
+
+        heif->nb_iprp++;
+    }
+
+    return heif_add_ipma_entry(item, 1, j + 1);
+}
+
+static int heif_build_colr(AVFormatContext *s, ItemEntry *item,
+                           const AVCodecParameters *par, const 
AVPacketSideData *sd)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    ColorInformation *colr = NULL;
+    int j;
+
+    av_assert0(par || sd);
+
+    for (j = 0; j < heif->nb_iprp; j++) {
+        IPCO_TYPE_CHECK(colr, MKTAG('c','o','l','r'));
+        switch (colr->color_type) {
+        case MKTAG('p','r','o','f'):
+            if (!sd || sd->size != colr->icc_profile_size ||
+                memcmp(sd->data, colr->icc_profile, sd->size))
+                continue;
+            break;
+        case MKTAG('n','c','l','x'):
+            if (!par ||
+                colr->color_primaries != par->color_primaries ||
+                colr->transfer_characteristics != par->color_trc ||
+                colr->matrix_coefficients != par->color_space ||
+                colr->color_range != par->color_range)
+                continue;
+            break;
+        }
+        break;
+    }
+    if (j == heif->nb_iprp) {
+        IPCO_TYPE_SET(colr, MKTAG('c','o','l','r'));
+        if (sd) {
+            colr->color_type = MKTAG('p','r','o','f');
+            colr->icc_profile = av_memdup(sd->data, sd->size);
+            if (!colr->icc_profile) {
+                av_freep(&iprp->ipco);
+                return AVERROR(ENOMEM);
+            }
+            colr->icc_profile_size = sd->size;
+        } else {
+            colr->color_type = MKTAG('n','c','l','x');
+            colr->color_primaries = par->color_primaries;
+            colr->transfer_characteristics = par->color_trc;
+            colr->matrix_coefficients = par->color_space;
+            colr->color_range = par->color_range;
+        }
+
+        heif->nb_iprp++;
+    }
+
+    return heif_add_ipma_entry(item, 0, j + 1);
+}
+
+static int heif_build_clli(AVFormatContext *s, ItemEntry *item,
+                           const AVPacketSideData *sd)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    ContentLightLevel *clli;
+    const AVContentLightMetadata *metadata =
+        (const AVContentLightMetadata *)sd->data;
+    int j;
+
+    for (j = 0; j < heif->nb_iprp; j++) {
+        IPCO_TYPE_CHECK(clli, MKTAG('c','l','l','i'));
+        if (clli->max_content_light_level     == metadata->MaxCLL &&
+            clli->max_pic_average_light_level == metadata->MaxFALL)
+            break;
+    }
+    if (j == heif->nb_iprp) {
+        IPCO_TYPE_SET(clli, MKTAG('c','l','l','i'));
+        clli->max_content_light_level     = metadata->MaxCLL;
+        clli->max_pic_average_light_level = metadata->MaxFALL;
+
+        heif->nb_iprp++;
+    }
+
+    return heif_add_ipma_entry(item, 1, j + 1);
+}
+
+static int heif_build_auxc(AVFormatContext *s, ItemEntry *item,
+                           enum AVStreamGroupParamsType type)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    AuxiliaryType *auxc;
+    const char alpha_hevc_urn[] = "urn:mpeg:hevc:2015:auxid:1";
+    const char alpha_mpegb_urn[] = 
"urn:mpeg:mpegB:cicp:systems:auxiliary:alpha";
+    const char *alpha_urn = heif->codec_id == AV_CODEC_ID_HEVC
+                            ? alpha_hevc_urn
+                            : alpha_mpegb_urn;
+    const char *urn = NULL;
+    int j;
+
+    for (j = 0; j < heif->nb_iprp; j++) {
+        IPCO_TYPE_CHECK(auxc, MKTAG('a','u','x','C'));
+        switch (type) {
+        case AV_STREAM_GROUP_PARAMS_ALPHA:
+            if (strstr(auxc->aux_type, alpha_urn))
+                urn = alpha_urn;
+            break;
+        default:
+            av_assert0(0);
+        }
+        if (urn)
+            break;
+    }
+    if (j == heif->nb_iprp) {
+        IPCO_TYPE_SET(auxc, MKTAG('a','u','x','C'));
+        switch (type) {
+        case AV_STREAM_GROUP_PARAMS_ALPHA:
+            urn = alpha_urn;
+            break;
+        default:
+            av_assert0(0);
+        }
+        auxc->aux_type = av_strdup(urn);
+        if (!auxc->aux_type)
+            return AVERROR(ENOMEM);
+
+        heif->nb_iprp++;
+    }
+
+    return heif_add_ipma_entry(item, 1, j + 1);
+}
+
+static int heif_build_pixi(AVFormatContext *s, ItemEntry *item,
+                           const AVCodecParameters *par)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    PixelInformation *pixi;
+    const AVPixFmtDescriptor *pixdesc = av_pix_fmt_desc_get(par->format);
+    int j;
+
+    for (j = 0; j < heif->nb_iprp; j++) {
+        int k;
+        IPCO_TYPE_CHECK(pixi, MKTAG('p','i','x','i'));
+        if (pixi->nb_channels != pixdesc->nb_components)
+            continue;
+        for (k = 0; k < pixdesc->nb_components; k++)
+            if (pixi->bps[k] != pixdesc->comp[k].depth)
+                break;
+        if (k == pixdesc->nb_components)
+            break;
+    }
+    if (j == heif->nb_iprp) {
+        IPCO_TYPE_SET(pixi, MKTAG('p','i','x','i'));
+        pixi->nb_channels = pixdesc->nb_components;
+        for (int j = 0; j < 3; j++)
+            pixi->bps[j] = pixdesc->comp[j].depth;
+
+        heif->nb_iprp++;
+    }
+
+    return heif_add_ipma_entry(item, 0, j + 1);
+}
+
+static int heif_build_iprp_entries(AVFormatContext *s, ItemEntry *item,
+                                   const AVCodecParameters *par,
+                                   AVPacketSideData *side_data, int 
nb_side_data)
+{
+    const AVPacketSideData *sd;
+    int ret;
+
+    sd = av_packet_side_data_get(side_data, nb_side_data,
+                                 AV_PKT_DATA_ICC_PROFILE);
+    ret = heif_build_colr(s, item, par, sd);
+    if (ret < 0)
+        return ret;
+
+    sd = av_packet_side_data_get(side_data, nb_side_data,
+                                 AV_PKT_DATA_CONTENT_LIGHT_LEVEL);
+    if (sd) {
+        ret = heif_build_clli(s, item, sd);
+        if (ret < 0)
+            return ret;
+    }
+
+    ret = heif_build_ispe(s, item, par->width, par->height);
+    if (ret < 0)
+        return ret;
+
+    ret = heif_build_pixi(s, item, par);
+    if (ret < 0)
+        return ret;
+
+    sd = av_packet_side_data_get(side_data, nb_side_data,
+                                 AV_PKT_DATA_FRAME_CROPPING);
+    if (sd && sd->size >= sizeof(uint32_t) * 4) {
+        ret = heif_build_clap(s, item, par, sd);
+        if (ret < 0)
+            return ret;
+    }
+
+    return 0;
+}
+
+static int heif_alloc_gain_map(AVFormatContext *s, const AVPacketSideData *sd, 
ItemEntry *item)
+{
+    int ret;
+
+    item->infe.idat_payload = av_mallocz(AV_GAIN_MAP_MAX_PAYLOAD_SIZE);
+    if (!item->infe.idat_payload)
+        return AVERROR(ENOMEM);
+
+    ret = av_gain_map_params_to_iso21496((AVGainMapParams *)sd->data, 
item->infe.idat_payload);
+    if (ret < 0) {
+        av_log(s, AV_LOG_ERROR, "Failed to parse Gain Map parameters\n");
+        return ret;
+    }
+
+    item->infe.idat_payload_size = ret;
+
+    return 0;
+}
+
+static int heif_init_gain_map(AVFormatContext *s, const AVStreamGroup *stg, 
ItemEntry *item)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    const AVStreamGroupLayeredVideo *layered_video = stg->params.layered_video;
+    const AVStream *base_st = stg->entries[!layered_video->el_index]->stream;
+    const AVStream *gain_st = stg->entries[ layered_video->el_index]->stream;
+    const AVPacketSideData *sd = 
av_packet_side_data_get(gain_st->codecpar->coded_side_data,
+                                                         
gain_st->codecpar->nb_coded_side_data,
+                                                         
AV_PKT_DATA_GAIN_MAP_PARAMS);
+    int ret;
+
+    if (!(gain_st->disposition & AV_DISPOSITION_DEPENDENT)) {
+        av_log(s, AV_LOG_ERROR, "Gain Map group #%d needs the gain map stream 
to have the "
+                                "dependent disposition\n", stg->index);
+        return AVERROR(EINVAL);
+    }
+
+    item->infe.type = MKTAG('t','m','a','p');
+    item->infe.id = heif->nb_items;
+    item->iloc.construction_method = 1;
+
+    if (sd) {
+        ret = heif_alloc_gain_map(s, sd, item);
+        if (ret < 0)
+            return ret;
+    }
+
+    ret = heif_build_iprp_entries(s, item, base_st->codecpar,
+                                  stg->coded_side_data,
+                                  stg->nb_coded_side_data);
+    if (ret < 0)
+        return ret;
+
+    return 0;
+}
+
+static int heif_init(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int ret;
+
+    heif->item = av_fast_realloc(NULL, &heif->item_alloc_size,
+                                 s->nb_streams * sizeof(*heif->item));
+    if (!heif->item)
+        return AVERROR(ENOMEM);
+
+    ret = avio_open_dyn_buf(&heif->mdat_pb);
+    if (ret < 0)
+        return ret;
+
+    heif->codec_id = AV_CODEC_ID_NONE;
+
+    // Build stream items
+    for (int i = 0; i < s->nb_streams; i++) {
+        AVStream *st = s->streams[i];
+        AVCodecParameters *par = st->codecpar;
+        ItemEntry *item = &heif->item[i];
+        const AVDictionaryEntry *title;
+
+        if (heif->codec_id != AV_CODEC_ID_NONE && heif->codec_id != 
par->codec_id) {
+            av_log(s, AV_LOG_ERROR, "All streams must use the same video 
codec\n");
+            return AVERROR_PATCHWELCOME;
+        }
+
+        memset(item, 0, sizeof(*item));
+        heif->nb_items++;
+
+        switch (par->codec_id) {
+        case AV_CODEC_ID_HEVC:
+            if (!par->extradata_size) {
+                av_log(s, AV_LOG_ERROR, "Missing extradata on stream #%d\n", 
i);
+                return AVERROR(EINVAL);
+            }
+
+            ret = heif_build_hvcc(s, item, par);
+            if (ret < 0)
+                return ret;
+            break;
+        case AV_CODEC_ID_MJPEG:
+            break;
+        default:
+            av_log(s, AV_LOG_ERROR, "Unsupported video codec\n");
+            return AVERROR(EINVAL);
+        }
+        heif->codec_id = par->codec_id;
+
+        ret = heif_build_iprp_entries(s, item, par,
+                                      par->coded_side_data,
+                                      par->nb_coded_side_data);
+        if (ret < 0)
+            return ret;
+
+        item->infe.type = par->codec_tag;
+        item->infe.id = heif->nb_items;
+
+        title = av_dict_get(st->metadata, "title", NULL, 0);
+        if (title) {
+            item->infe.name = av_strdup(title->value);
+            if (!item->infe.name)
+                return AVERROR(ENOMEM);
+        }
+
+        if (!heif->primary_item && (st->disposition & AV_DISPOSITION_DEFAULT))
+            heif->primary_item = item->infe.id;
+        else
+            item->infe.flags |= !!(st->disposition & AV_DISPOSITION_DEPENDENT);
+    }
+
+    // Build stream group items
+    for (int i = 0; i < s->nb_stream_groups; i++) {
+        const AVStreamGroup *stg = s->stream_groups[i];
+        const AVDictionaryEntry *title;
+        ItemEntry *item;
+        ItemReference *iref;
+
+        switch (stg->type) {
+        case AV_STREAM_GROUP_PARAMS_TILE_GRID:
+            av_log(s, AV_LOG_ERROR, "Tile Grid derived images are not 
supported\n");
+            return AVERROR_PATCHWELCOME;
+        case AV_STREAM_GROUP_PARAMS_GAIN_MAP:
+            if (stg->nb_entries != 2) {
+                av_log(s, AV_LOG_ERROR, "Invalid stream count in Gain Map 
group\n");
+                return AVERROR(EINVAL);
+            }
+            break;
+        default:
+            continue;
+        }
+
+        for (int j = 0; j < stg->nb_entries; j++) {
+            AVStreamGroupEntry *entry = stg->entries[j];
+
+            if (entry->type != AV_STREAM_GROUP_ENTRY_STREAM) {
+                av_log(s, AV_LOG_ERROR, "Derived images referencing other 
derived images is "
+                                        "not supported\n");
+                return AVERROR_PATCHWELCOME;
+            }
+        }
+
+        item = av_fast_realloc(heif->item, &heif->item_alloc_size,
+                               (heif->nb_items + 1) * sizeof(*heif->item));
+        if (!item)
+            return AVERROR(ENOMEM);
+
+        heif->item = item;
+        item = &heif->item[heif->nb_items++];
+        memset(item, 0, sizeof(*item));
+
+        switch (stg->type) {
+        case AV_STREAM_GROUP_PARAMS_GAIN_MAP:
+            ret = heif_init_gain_map(s, stg, item);
+            if (ret < 0)
+                return ret;
+            break;
+        default:
+            av_assert0(0);
+        }
+
+        iref = av_fast_realloc(heif->iref, &heif->iref_alloc_size,
+                               (heif->nb_iref + 1) * sizeof(*heif->iref));
+        if (!iref)
+            return AVERROR(ENOMEM);
+
+        heif->iref = iref;
+        iref = &heif->iref[heif->nb_iref];
+
+        iref->type = MKTAG('d','i','m','g');
+        iref->from_id = item->infe.id;
+        iref->to_id = av_calloc(stg->nb_entries, sizeof(*iref->to_id));
+        if (!iref->to_id)
+            return AVERROR(ENOMEM);
+        iref->nb_to_id = stg->nb_entries;
+
+        heif->nb_iref++;
+
+        for (int j = 0; j < stg->nb_entries; j++) {
+            AVStreamGroupEntry *entry = stg->entries[j];
+            ItemEntry *to_item = &heif->item[entry->stream->index];
+
+            av_dynarray_add(&to_item->iref, &to_item->nb_iref, iref);
+            if (!to_item->nb_iref)
+                return AVERROR(ENOMEM);
+
+            iref->to_id[j] = to_item->infe.id;
+        }
+
+        title = av_dict_get(stg->metadata, "title", NULL, 0);
+        if (title) {
+            item->infe.name = av_strdup(title->value);
+            if (!item->infe.name)
+                return AVERROR(ENOMEM);
+        }
+
+        if (!heif->primary_item && (stg->disposition & AV_DISPOSITION_DEFAULT))
+            heif->primary_item = item->infe.id;
+    }
+
+    // Build metadata items from streams
+    for (int i = 0; i < s->nb_streams; i++) {
+        AVStream *st = s->streams[i];
+        ItemEntry *item, *to_item = &heif->item[i];
+        ItemReference *iref;
+        const AVPacketSideData *sd;
+
+        sd = av_packet_side_data_get(st->codecpar->coded_side_data,
+                                     st->codecpar->nb_coded_side_data,
+                                     AV_PKT_DATA_EXIF);
+        if (!sd)
+            continue;
+
+        item = av_fast_realloc(heif->item, &heif->item_alloc_size,
+                               (heif->nb_items + 1) * sizeof(*heif->item));
+        if (!item)
+            return AVERROR(ENOMEM);
+
+        heif->item = item;
+        item = &heif->item[heif->nb_items++];
+        memset(item, 0, sizeof(*item));
+
+        item->infe.idat_payload = av_memdup(sd->data, sd->size);
+        if (!item->infe.idat_payload)
+            return AVERROR(ENOMEM);
+        item->infe.idat_payload_size = sd->size;
+        item->infe.type = MKTAG('E','x','i','f');
+        item->infe.id = heif->nb_items;
+        item->infe.flags |= 1;
+        item->iloc.construction_method = 1;
+
+        to_item = &heif->item[st->index];
+
+        iref = av_fast_realloc(heif->iref, &heif->iref_alloc_size,
+                               (heif->nb_iref + 1) * sizeof(*heif->iref));
+        if (!iref)
+            return AVERROR(ENOMEM);
+
+        heif->iref = iref;
+        iref = &heif->iref[heif->nb_iref];
+
+        iref->type = MKTAG('c','d','s','c');
+        iref->from_id = item->infe.id;
+        iref->to_id = av_mallocz(sizeof(*iref->to_id));
+        if (!iref->to_id)
+            return AVERROR(ENOMEM);
+        *iref->to_id = to_item->infe.id;
+        iref->nb_to_id = 1;
+
+        heif->nb_iref++;
+    }
+
+    // Build group list entries
+    for (int i = 0; i < heif->nb_iref; i++) {
+        const ItemReference *iref = &heif->iref[i];
+        const ItemEntry *from_item = &heif->item[iref->from_id - 1];
+        GroupList *grpl;
+        int base_id = 0;
+
+        if (iref->type != MKTAG('d','i','m','g') ||
+            from_item->infe.type != MKTAG('t','m','a','p'))
+            continue;
+
+        for (int j = 0; j < iref->nb_to_id; j++) {
+            const ItemEntry *to_item = &heif->item[iref->to_id[j] - 1];
+
+            if (to_item->infe.flags & 1)
+                continue;
+
+            base_id = to_item->infe.id;
+            break;
+        }
+
+        grpl = av_fast_realloc(heif->grpl, &heif->grpl_alloc_size,
+                               (heif->nb_grpl + 1) * sizeof(*heif->grpl));
+        if (!grpl)
+            return AVERROR(ENOMEM);
+
+        heif->grpl = grpl;
+        grpl = &heif->grpl[heif->nb_grpl];
+
+        grpl->type = MKTAG('a','l','t','r');
+        grpl->id = heif->nb_items + i + 1;
+        grpl->entities = av_calloc(2, sizeof(*grpl->entities));
+        if (!grpl->entities)
+            return AVERROR(ENOMEM);
+        grpl->nb_entities = 2;
+
+        heif->nb_grpl++;
+
+        av_assert0(base_id > 0);
+        grpl->entities[0] = iref->from_id;
+        grpl->entities[1] = base_id;
+    }
+
+    // Build stream group derived item references
+    for (int i = 0; i < s->nb_stream_groups; i++) {
+        const AVStreamGroup *stg = s->stream_groups[i];
+        const AVStreamGroupLayeredVideo *layered_video;
+        ItemEntry *from_item, *to_item;
+        ItemReference *iref;
+
+        if (stg->type != AV_STREAM_GROUP_PARAMS_ALPHA)
+            continue;
+
+        layered_video = stg->params.layered_video;
+        from_item = &heif->item[stg->entries[ 
layered_video->el_index]->stream->index];
+        to_item   = 
&heif->item[stg->entries[!layered_video->el_index]->stream->index];
+
+        iref = av_fast_realloc(heif->iref, &heif->iref_alloc_size,
+                               (heif->nb_iref + 1) * sizeof(*heif->iref));
+        if (!iref)
+            return AVERROR(ENOMEM);
+
+        heif->iref = iref;
+        iref = &heif->iref[heif->nb_iref];
+
+        iref->type = MKTAG('a','u','x','l');
+        iref->from_id = from_item->infe.id;
+        iref->to_id = av_malloc(sizeof(*iref->to_id));
+        if (!iref->to_id)
+            return AVERROR(ENOMEM);
+        *iref->to_id = to_item->infe.id;
+        iref->nb_to_id = 1;
+
+        heif->nb_iref++;
+
+        ret = heif_build_auxc(s, from_item, stg->type);
+        if (ret < 0)
+            return ret;
+    }
+
+    if (!heif->primary_item)
+        heif->primary_item = 1;
+
+    return 0;
+}
+
+static int64_t update_size(AVIOContext *pb, int64_t pos)
+{
+    int64_t curpos = avio_tell(pb);
+    int64_t ret = avio_seek(pb, pos, SEEK_SET);
+    if (ret < 0)
+        return ret;
+
+    avio_wb32(pb, curpos - pos); /* rewrite size */
+
+    ret = avio_seek(pb, curpos, SEEK_SET);
+    if (ret < 0)
+        return ret;
+
+    return curpos - pos;
+}
+
+static int heif_write_header(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t pos = avio_tell(s->pb);
+    char heic_brand[] = "heic", jpeg_brand[] = "jpeg";
+    char *brand = heif->codec_id == AV_CODEC_ID_HEVC
+                  ? heic_brand : jpeg_brand;
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "ftyp");
+    ffio_wfourcc(s->pb, brand);
+    avio_wb32(s->pb, 0);
+    ffio_wfourcc(s->pb, "mif1");
+    ffio_wfourcc(s->pb, brand);
+    for (int i = s->nb_streams; i < heif->nb_items; i++) {
+        ItemEntry * item = &heif->item[i];
+
+        if (item->infe.type != MKTAG('t','m','a','p'))
+            continue;
+        ffio_wfourcc(s->pb, "tmap");
+        break;
+    }
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    AVStream *st = s->streams[pkt->stream_index];
+    ItemEntry *item = &heif->item[pkt->stream_index];
+    int64_t extent_offset = avio_tell(heif->mdat_pb);
+    int size = pkt->size;
+
+    if (item->seen_pkt) {
+        av_log(s, AV_LOG_ERROR, "Only one packet per stream is supported\n");
+        return AVERROR(ENOSYS);
+    }
+
+    for (int i = 0; i < item->nb_iref; i++) {
+        ItemReference *iref = item->iref[i];
+        ItemEntry *from_item = get_heif_item(heif, iref->from_id);
+
+        if ((item->infe.flags & 1) &&
+            from_item->infe.type == MKTAG('t','m','a','p') &&
+            !from_item->infe.idat_payload) {
+            const AVPacketSideData *sd = 
av_packet_side_data_get(pkt->side_data,
+                                                                 
pkt->side_data_elems,
+                                                                 
AV_PKT_DATA_GAIN_MAP_PARAMS);
+            if (!sd)
+                break;
+
+            int ret = heif_alloc_gain_map(s, sd, from_item);
+            if (ret < 0)
+                return ret;
+        }
+    }
+
+    if (st->codecpar->extradata_size > 6 &&
+        (AV_RB24(st->codecpar->extradata) == 1 || 
AV_RB32(st->codecpar->extradata) == 1)) {
+        /* extradata is Annex B, assume the bitstream is too and convert it */
+        size = ff_hevc_annexb2mp4(heif->mdat_pb, pkt->data, pkt->size, 1, 
NULL);
+    } else
+        avio_write(heif->mdat_pb, pkt->data, pkt->size);
+
+    item->seen_pkt = 1;
+    item->iloc.extent_offset = extent_offset;
+    item->iloc.extent_length = size;
+
+    return 0;
+}
+
+static int heif_write_hdlr_tag(AVFormatContext *s)
+{
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "hdlr");
+    avio_wb32(s->pb, 0); /* Version & flags */
+    avio_wb32(s->pb, 0); /* handler */
+    ffio_wfourcc(s->pb, "pict"); /* handler type */
+    avio_wb32(s->pb, 0); /* reserved */
+    avio_wb32(s->pb, 0); /* reserved */
+    avio_wb32(s->pb, 0); /* reserved */
+    avio_w8(s->pb, 0);
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_iinf_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t ret, pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "iinf");
+    avio_wb32(s->pb, 0); /* Version & flags */
+    avio_wb16(s->pb, heif->nb_items); /* entry_count */
+
+    for (int i = 0; i < heif->nb_items; i++) {
+        ItemEntry *item = &heif->item[i];
+        int64_t infe_pos = avio_tell(s->pb);
+
+        avio_wb32(s->pb, 0); /* size */
+        ffio_wfourcc(s->pb, "infe");
+        avio_w8(s->pb, 2); /* Version */
+        avio_wb24(s->pb, item->infe.flags); /* flags */
+        avio_wb16(s->pb, item->infe.id); /* item_id */
+        avio_wb16(s->pb, 0); /* item_protection_index */
+        avio_write(s->pb, av_fourcc2str(item->infe.type), 4); /* item_type */
+        if (item->infe.name)
+            avio_write(s->pb, item->infe.name, strlen(item->infe.name)); /* 
item_name */
+        else
+            avio_w8(s->pb, 0); /* item_name */
+
+        ret = update_size(s->pb, infe_pos);
+        if (ret < 0)
+            return ret;
+    }
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_pitm_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "pitm");
+    avio_wb32(s->pb, 0); /* Version & flags */
+    avio_wb16(s->pb, heif->primary_item); /* item_id */
+
+    return update_size(s->pb, pos);
+}
+
+
+static int heif_write_ispe_tag(AVFormatContext *s, ImageSpatialExtents *ispe)
+{
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "ispe");
+    avio_wb32(s->pb, 0); /* Version & flags */
+    avio_wb32(s->pb, ispe->width); /* image_width */
+    avio_wb32(s->pb, ispe->height); /* image_height */
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_clap_tag(AVFormatContext *s, CleanApperture *clap)
+{
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0);
+    ffio_wfourcc(s->pb, "clap");
+    avio_wb32(s->pb, clap->cropped_width); /* apertureWidthN */
+    avio_wb32(s->pb, 1); /* apertureWidthD */
+    avio_wb32(s->pb, clap->cropped_height); /* apertureHeightN */
+    avio_wb32(s->pb, 1); /* apertureHeightD */
+
+    avio_wb32(s->pb, -clap->horiz_off.num);
+    avio_wb32(s->pb, clap->horiz_off.den);
+    avio_wb32(s->pb, -clap->vert_off.num);
+    avio_wb32(s->pb, clap->vert_off.den);
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_colr_tag(AVFormatContext *s, ColorInformation *colr)
+{
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "colr");
+    ffio_wfourcc(s->pb, av_fourcc2str(colr->color_type));
+    switch (colr->color_type) {
+    case MKTAG('n','c','l','x'):
+        avio_wb16(s->pb, colr->color_primaries);
+        avio_wb16(s->pb, colr->transfer_characteristics);
+        avio_wb16(s->pb, colr->matrix_coefficients);
+        avio_w8(s->pb, (colr->color_range == AVCOL_RANGE_JPEG) << 7);
+        break;
+    case MKTAG('p','r','o','f'):
+        av_assert0(colr->icc_profile_size);
+        avio_write(s->pb, colr->icc_profile, colr->icc_profile_size);
+        break;
+    }
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_clli_tag(AVFormatContext *s, ContentLightLevel *clli)
+{
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); // size
+    ffio_wfourcc(s->pb, "clli");
+    avio_wb16(s->pb, clli->max_content_light_level);
+    avio_wb16(s->pb, clli->max_pic_average_light_level);
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_auxc_tag(AVFormatContext *s, AuxiliaryType *auxc)
+{
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "auxC");
+    avio_wb32(s->pb, 0); /* Version & flags */
+    avio_put_str(s->pb, auxc->aux_type);
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_pixi_tag(AVFormatContext *s, PixelInformation *pixi)
+{
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "pixi");
+    avio_wb32(s->pb, 0); /* Version & flags */
+    avio_w8(s->pb, pixi->nb_channels); /* num_channels */
+    for (int i = 0; i < pixi->nb_channels; ++i)
+        avio_w8(s->pb, pixi->bps[i]); /* bits_per_channel */
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_hvcc_tag(AVFormatContext *s, HEVCConfiguration *hvcc)
+{
+    int64_t pos = avio_tell(s->pb);
+    uint8_t *buf;
+    int buf_size = avio_get_dyn_buf(hvcc->pb, &buf);
+
+    avio_wb32(s->pb, 0);
+    ffio_wfourcc(s->pb, "hvcC");
+    avio_write(s->pb, buf, buf_size);
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_ipco_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "ipco");
+    for (int i = 0; i < heif->nb_iprp; i++) {
+        ItemProperties *iprp = &heif->iprp[i];
+        switch (iprp->type) {
+        case MKTAG('h','v','c','C'): {
+            HEVCConfiguration *hvcc = iprp->ipco;
+            heif_write_hvcc_tag(s, hvcc);
+            break;
+        }
+        case MKTAG('i','s','p','e'): {
+            ImageSpatialExtents *ispe = iprp->ipco;
+            heif_write_ispe_tag(s, ispe);
+            break;
+        }
+        case MKTAG('c','l','a','p'): {
+            CleanApperture *clap = iprp->ipco;
+            heif_write_clap_tag(s, clap);
+            break;
+        }
+        case MKTAG('c','o','l','r'): {
+            ColorInformation *colr = iprp->ipco;
+            heif_write_colr_tag(s, colr);
+            break;
+        }
+        case MKTAG('c','l','l','i'): {
+            ContentLightLevel *clli = iprp->ipco;
+            heif_write_clli_tag(s, clli);
+            break;
+        }
+        case MKTAG('a','u','x','C'): {
+            AuxiliaryType *auxc = iprp->ipco;
+            heif_write_auxc_tag(s, auxc);
+            break;
+        }
+        case MKTAG('p','i','x','i'): {
+            PixelInformation *pixi = iprp->ipco;
+            heif_write_pixi_tag(s, pixi);
+            break;
+        }
+        }
+    }
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_ipma_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t pos = avio_tell(s->pb);
+    int nb_items = 0;
+
+    for (int i = 0; i < heif->nb_items; i++) {
+        ItemEntry *item = &heif->item[i];
+
+        if (!item->nb_ipma)
+            continue;
+        nb_items++;
+    }
+
+    if (!nb_items)
+        return 0;
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "ipma");
+    avio_wb32(s->pb, 0); /* Version & flags */
+    avio_wb32(s->pb, nb_items); /* entry_count */
+
+    for (int i = 0; i < heif->nb_items; i++) {
+        ItemEntry *item = &heif->item[i];
+
+        if (!item->nb_ipma)
+            continue;
+
+        avio_wb16(s->pb, item->infe.id); /* item_ID */
+        avio_w8(s->pb, item->nb_ipma); /* association_count */
+        for (int j = 0; j < item->nb_ipma; j++)
+            avio_w8(s->pb, (item->ipma[j].essential << 7) |
+                            item->ipma[j].ipco_id); /* association_count */
+    }
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_iprp_tag(AVFormatContext *s)
+{
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "iprp");
+    heif_write_ipco_tag(s);
+    heif_write_ipma_tag(s);
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_idat_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t pos = avio_tell(s->pb);
+    int i;
+
+    for (i = 0; i < heif->nb_items; i++) {
+        ItemEntry *item = &heif->item[i];
+
+        if (item->iloc.construction_method == 1)
+            break;
+    }
+
+    if (i == heif->nb_items)
+        return 0;
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "idat");
+    for (int i = 0; i < heif->nb_items; i++) {
+        ItemEntry *item = &heif->item[i];
+
+        if (item->iloc.construction_method != 1)
+            continue;
+
+        item->iloc.extent_offset = avio_tell(s->pb);
+
+        switch (item->infe.type) {
+        case MKTAG('t','m','a','p'):
+            if (!item->infe.idat_payload_size) {
+                av_log(s, AV_LOG_ERROR, "Missing Gain Map payload\n");
+                return AVERROR(EINVAL);
+            }
+            avio_w8(s->pb, 0); /* version */
+            break;
+        case MKTAG('E','x','i','f'):
+            avio_wb32(s->pb, 0); /* exif_tiff_header_offset */
+            break;
+        default:
+            av_assert0(0);
+        }
+        avio_write(s->pb, item->infe.idat_payload, 
item->infe.idat_payload_size);
+
+        item->iloc.extent_length  = avio_tell(s->pb) - 
item->iloc.extent_offset;
+        item->iloc.extent_offset -= (pos + 8);
+    }
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_iref_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t ret, pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "iref");
+    avio_wb32(s->pb, 0); /* version + flags */
+
+    for (int i = 0; i < heif->nb_iref; i++) {
+        ItemReference *iref = &heif->iref[i];
+        int64_t iref_pos = avio_tell(s->pb);
+
+        avio_wb32(s->pb, 0); /* size */
+        ffio_wfourcc(s->pb, av_fourcc2str(iref->type));
+        avio_wb16(s->pb, iref->from_id); /* from_item_ID */
+        avio_wb16(s->pb, iref->nb_to_id); /* reference_count */
+        for (int j = 0; j < iref->nb_to_id; j++)
+            avio_wb16(s->pb, iref->to_id[j]); /* to_item_ID */
+
+        ret = update_size(s->pb, iref_pos);
+        if (ret < 0)
+            return ret;
+    }
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_iloc_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t pos = avio_tell(s->pb);
+    unsigned int extra_offset;
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "iloc");
+    avio_w8(s->pb, 1); /* version */
+    avio_wb24(s->pb, 0); /* flags */
+    avio_w8(s->pb, (4 << 4) + 4); /* offset_size(4) and length_size(4) */
+    avio_w8(s->pb, 0); /* base_offset_size(4) and index_size(4) */
+    avio_wb16(s->pb, heif->nb_items); /* item_count */
+
+    extra_offset = avio_tell(s->pb) + heif->nb_items * 16 + 8 /* mdat + size 
*/;
+    for (int i = 0; i < heif->nb_items; i++) {
+        ItemEntry *item = &heif->item[i];
+
+        avio_wb16(s->pb, item->infe.id); /* item_id */
+        avio_wb16(s->pb, item->iloc.construction_method); /* 
construction_method */
+        avio_wb16(s->pb, 0); /* data_reference_index */
+        avio_wb16(s->pb, 1); /* extent_count */
+        avio_wb32(s->pb, item->iloc.extent_offset +
+                         (item->iloc.construction_method ? 0 : extra_offset));
+        avio_wb32(s->pb, item->iloc.extent_length);
+    }
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_grpl_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t pos = avio_tell(s->pb);
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "grpl");
+
+    for (int i = 0; i < heif->nb_grpl; i++) {
+        GroupList *grpl = &heif->grpl[i];
+        int64_t grpl_pos;
+        int ret;
+
+        grpl_pos = avio_tell(s->pb);
+        avio_wb32(s->pb, 0); /* size */
+        ffio_wfourcc(s->pb, av_fourcc2str(grpl->type));
+        avio_wb32(s->pb, 0); /* version + flags */
+        avio_wb32(s->pb, heif->nb_items + i + 1); /* group_id */
+        avio_wb32(s->pb, grpl->nb_entities); /* num_entities_in_group */
+        for (int j = 0; j < grpl->nb_entities; j++)
+            avio_wb32(s->pb, grpl->entities[j]); /* entity_id */
+
+        ret = update_size(s->pb, grpl_pos);
+        if (ret < 0)
+            return ret;
+    }
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_meta_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    int64_t pos = avio_tell(s->pb);
+    int ret;
+
+    avio_wb32(s->pb, 0); /* size */
+    ffio_wfourcc(s->pb, "meta");
+    avio_wb32(s->pb, 0);
+
+    ret = heif_write_hdlr_tag(s);
+    if (ret < 0)
+        return ret;
+    ret = heif_write_iinf_tag(s);
+    if (ret < 0)
+        return ret;
+    ret = heif_write_pitm_tag(s);
+    if (ret < 0)
+        return ret;
+    ret = heif_write_iprp_tag(s);
+    if (ret < 0)
+        return ret;
+    ret = heif_write_idat_tag(s);
+    if (ret < 0)
+        return ret;
+    if (heif->nb_iref) {
+        ret = heif_write_iref_tag(s);
+        if (ret < 0)
+            return ret;
+    }
+    if (heif->nb_grpl) {
+        ret = heif_write_grpl_tag(s);
+        if (ret < 0)
+            return ret;
+    }
+    ret = heif_write_iloc_tag(s);
+    if (ret < 0)
+        return ret;
+
+    return update_size(s->pb, pos);
+}
+
+static int heif_write_mdat_tag(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+    uint8_t *buf;
+    int buf_size = avio_get_dyn_buf(heif->mdat_pb, &buf);
+
+    if (!buf_size)
+        return AVERROR(EINVAL);
+
+    avio_wb32(s->pb, buf_size + 8); /* size */
+    ffio_wfourcc(s->pb, "mdat");
+    avio_write(s->pb, buf, buf_size);
+
+    ffio_free_dyn_buf(&heif->mdat_pb);
+
+    return 0;
+}
+
+static int heif_write_trailer(AVFormatContext *s)
+{
+    int ret;
+
+    ret = heif_write_meta_tag(s);
+    if (ret < 0)
+        return ret;
+    ret = heif_write_mdat_tag(s);
+    if (ret < 0)
+        return ret;
+
+    return 0;
+}
+
+static void heif_deinit(AVFormatContext *s)
+{
+    HEIFMuxContext *heif = s->priv_data;
+
+    for (int i = 0; i < heif->nb_items; i++) {
+        ItemEntry *item = &heif->item[i];
+        av_freep(&item->ipma);
+        av_freep(&item->iref);
+        av_freep(&item->infe.idat_payload);
+        av_freep(&item->infe.name);
+    }
+    av_freep(&heif->item);
+
+    for (int i = 0; i < heif->nb_iprp; i++) {
+        ItemProperties *iprp = &heif->iprp[i];
+        switch (iprp->type) {
+        case MKTAG('a','u','x','C'): {
+            AuxiliaryType *auxc = iprp->ipco;
+            av_freep(&auxc->aux_type);
+            break;
+        }
+        case MKTAG('c','o','l','r'): {
+            ColorInformation *colr = iprp->ipco;
+            av_freep(&colr->icc_profile);
+            break;
+        }
+        case MKTAG('h','v','c','C'): {
+            HEVCConfiguration *hvcc = iprp->ipco;
+            ffio_free_dyn_buf(&hvcc->pb);
+            break;
+        }
+        }
+        av_freep(&iprp->ipco);
+    }
+    av_freep(&heif->iprp);
+
+    for (int i = 0; i < heif->nb_iref; i++) {
+        ItemReference *iref = &heif->iref[i];
+        av_freep(&iref->to_id);
+    }
+    av_freep(&heif->iref);
+
+    for (int i = 0; i < heif->nb_grpl; i++) {
+        GroupList *grpl = &heif->grpl[i];
+        av_freep(&grpl->entities);
+    }
+    av_freep(&heif->grpl);
+
+    ffio_free_dyn_buf(&heif->mdat_pb);
+}
+
+static const AVCodecTag codec_heif_tags[] = {
+    { AV_CODEC_ID_HEVC, MKTAG('h','v','c','1') },
+    { AV_CODEC_ID_MJPEG, MKTAG('j','p','e','g') },
+    { AV_CODEC_ID_NONE, 0 },
+};
+
+const FFOutputFormat ff_heif_muxer = {
+    .p.name            = "heif",
+    .p.long_name       = NULL_IF_CONFIG_SMALL("HEIF"),
+    .p.mime_type       = "image/heif",
+    .p.extensions      = "heic,heif",
+    .priv_data_size    = sizeof(HEIFMuxContext),
+    .p.video_codec     = AV_CODEC_ID_HEVC,
+    .init              = heif_init,
+    .write_header      = heif_write_header,
+    .write_packet      = heif_write_packet,
+    .write_trailer     = heif_write_trailer,
+    .deinit            = heif_deinit,
+    .p.flags           = AVFMT_GLOBALHEADER | AVFMT_NOTIMESTAMPS,
+    .p.codec_tag       = (const AVCodecTag* const []){ codec_heif_tags, NULL },
+};
-- 
2.54.0


>From 5fcb399ce2ee6032837c86a72fea5f262b88c18a Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Thu, 22 Oct 2026 17:12:16 -0300
Subject: [PATCH 2/6] avcodec/encode: add an internal last_frame_props field

With this, encode_set_packet_props() can be called from ff_alloc_packet() and
ff_get_encode_buffer() in both single or threaded frame scenarios.
This is needed for the next change.

Signed-off-by: James Almer <[email protected]>
---
 libavcodec/avcodec.c              |  2 ++
 libavcodec/avcodec_internal.h     |  2 ++
 libavcodec/encode.c               | 58 ++++++++++++++++++++++++++++---
 libavcodec/frame_thread_encoder.c |  1 +
 4 files changed, 58 insertions(+), 5 deletions(-)

diff --git a/libavcodec/avcodec.c b/libavcodec/avcodec.c
index 80f2598a921..a48c565f33d 100644
--- a/libavcodec/avcodec.c
+++ b/libavcodec/avcodec.c
@@ -471,6 +471,8 @@ av_cold void ff_codec_close(AVCodecContext *avctx)
         av_refstruct_pool_uninit(&avci->progress_frame_pool);
         if (av_codec_is_decoder(avctx->codec))
             ff_decode_internal_uninit(avctx);
+        else
+            ff_encode_internal_uninit(avctx);
 
         ff_hwaccel_uninit(avctx);
 
diff --git a/libavcodec/avcodec_internal.h b/libavcodec/avcodec_internal.h
index d0091386659..3d95bf34ac9 100644
--- a/libavcodec/avcodec_internal.h
+++ b/libavcodec/avcodec_internal.h
@@ -85,6 +85,8 @@ struct AVCodecInternal *ff_decode_internal_alloc(void);
 void ff_decode_internal_sync(struct AVCodecContext *dst,
                              const struct AVCodecContext *src);
 void ff_decode_internal_uninit(struct AVCodecContext *avctx);
+void ff_encode_internal_uninit(struct AVCodecContext *avctx);
+int ff_encode_extract_frame_props(struct AVCodecContext *avctx, const AVFrame 
*frame);
 
 struct AVCodecInternal *ff_encode_internal_alloc(void);
 
diff --git a/libavcodec/encode.c b/libavcodec/encode.c
index 82ea3aedbb4..1f0a582b18e 100644
--- a/libavcodec/encode.c
+++ b/libavcodec/encode.c
@@ -52,6 +52,8 @@ typedef struct EncodeContext {
      * potentially padded with silence). Reject all subsequent frames.
      */
     int last_audio_frame;
+
+    AVFrame *last_frame_props;
 } EncodeContext;
 
 static EncodeContext *encode_ctx(AVCodecInternal *avci)
@@ -59,8 +61,12 @@ static EncodeContext *encode_ctx(AVCodecInternal *avci)
     return (EncodeContext*)avci;
 }
 
+static int encode_set_packet_props(AVCodecContext *avctx, AVPacket *avpkt);
+
 int ff_alloc_packet(AVCodecContext *avctx, AVPacket *avpkt, int64_t size)
 {
+    const FFCodec *const codec = ffcodec(avctx->codec);
+    int ret;
     if (size < 0 || size > INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE) {
         av_log(avctx, AV_LOG_ERROR, "Invalid minimum required packet size 
%"PRId64" (max allowed is %d)\n",
                size, INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE);
@@ -69,6 +75,13 @@ int ff_alloc_packet(AVCodecContext *avctx, AVPacket *avpkt, 
int64_t size)
 
     av_assert0(!avpkt->data);
 
+    if (!(avctx->codec->capabilities & AV_CODEC_CAP_DELAY) ||
+        (codec->caps_internal & FF_CODEC_CAP_EOF_FLUSH)) {
+        ret = encode_set_packet_props(avctx, avpkt);
+        if (ret < 0)
+            return ret;
+    }
+
     av_fast_padded_malloc(&avctx->internal->byte_buffer,
                           &avctx->internal->byte_buffer_size, size);
     avpkt->data = avctx->internal->byte_buffer;
@@ -105,6 +118,7 @@ int avcodec_default_get_encode_buffer(AVCodecContext 
*avctx, AVPacket *avpkt, in
 
 int ff_get_encode_buffer(AVCodecContext *avctx, AVPacket *avpkt, int64_t size, 
int flags)
 {
+    const FFCodec *const codec = ffcodec(avctx->codec);
     int ret;
 
     if (size < 0 || size > INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE)
@@ -112,6 +126,13 @@ int ff_get_encode_buffer(AVCodecContext *avctx, AVPacket 
*avpkt, int64_t size, i
 
     av_assert0(!avpkt->data && !avpkt->buf);
 
+    if (!(avctx->codec->capabilities & AV_CODEC_CAP_DELAY) ||
+        (codec->caps_internal & FF_CODEC_CAP_EOF_FLUSH)) {
+        ret = encode_set_packet_props(avctx, avpkt);
+        if (ret < 0)
+            return ret;
+    }
+
     avpkt->size = size;
     ret = avctx->get_encode_buffer(avctx, avpkt, flags);
     if (ret < 0)
@@ -216,9 +237,23 @@ int avcodec_encode_subtitle(AVCodecContext *avctx, uint8_t 
*buf, int buf_size,
     return ret;
 }
 
+int ff_encode_extract_frame_props(AVCodecContext *avctx, const AVFrame *frame)
+{
+    AVCodecInternal *avci = avctx->internal;
+    EncodeContext *ec = encode_ctx(avci);
+    int ret = 0;
+
+    av_frame_unref(ec->last_frame_props);
+    if (frame)
+        ret = av_frame_copy_props(ec->last_frame_props, frame);
+
+    return ret;
+}
+
 int ff_encode_get_frame(AVCodecContext *avctx, AVFrame *frame)
 {
     AVCodecInternal *avci = avctx->internal;
+    int ret;
 
     if (avci->draining)
         return AVERROR_EOF;
@@ -226,15 +261,20 @@ int ff_encode_get_frame(AVCodecContext *avctx, AVFrame 
*frame)
     if (!avci->buffer_frame->buf[0])
         return AVERROR(EAGAIN);
 
+    ret = ff_encode_extract_frame_props(avctx, avci->buffer_frame);
+    if (ret < 0)
+        return ret;
+
     av_frame_move_ref(frame, avci->buffer_frame);
 
     return 0;
 }
 
-static int encode_set_packet_props(AVCodecContext *avctx, AVPacket *avpkt, 
const AVFrame *frame)
+static int encode_set_packet_props(AVCodecContext *avctx, AVPacket *avpkt)
 {
     AVCodecInternal *avci = avctx->internal;
     EncodeContext     *ec = encode_ctx(avci);
+    const AVFrame *frame  = ec->last_frame_props;
 
     if (avpkt->pts == AV_NOPTS_VALUE) {
         avpkt->pts = frame->pts;
@@ -314,10 +354,6 @@ int ff_encode_encode_cb(AVCodecContext *avctx, AVPacket 
*avpkt,
         // encoders with delay have to set the timestamps themselves
         if (!(avctx->codec->capabilities & AV_CODEC_CAP_DELAY) ||
             (frame && (codec->caps_internal & FF_CODEC_CAP_EOF_FLUSH))) {
-            ret = encode_set_packet_props(avctx, avpkt, frame);
-            if (ret < 0)
-                goto unref;
-
             ret = ff_encode_reordered_opaque(avctx, avpkt, frame);
             if (ret < 0)
                 goto unref;
@@ -964,6 +1000,10 @@ int ff_encode_preinit(AVCodecContext *avctx)
             return AVERROR(ENOMEM);
     }
 
+    ec->last_frame_props = av_frame_alloc();
+    if (!ec->last_frame_props)
+        return AVERROR(ENOMEM);
+
     for (int i = 0; ff_sd_global_map[i].packet < AV_PKT_DATA_NB; i++) {
         const enum AVPacketSideDataType type_packet = 
ff_sd_global_map[i].packet;
         const enum AVFrameSideDataType  type_frame  = 
ff_sd_global_map[i].frame;
@@ -1046,6 +1086,14 @@ AVCodecInternal *ff_encode_internal_alloc(void)
     return av_mallocz(sizeof(EncodeContext));
 }
 
+av_cold void ff_encode_internal_uninit(AVCodecContext *avctx)
+{
+    AVCodecInternal *avci = avctx->internal;
+    EncodeContext *ec = encode_ctx(avci);
+
+    av_frame_free(&ec->last_frame_props);
+}
+
 AVCPBProperties *ff_encode_add_cpb_side_data(AVCodecContext *avctx)
 {
     AVPacketSideData *tmp;
diff --git a/libavcodec/frame_thread_encoder.c 
b/libavcodec/frame_thread_encoder.c
index 6af3db722e0..d390ae9f9e7 100644
--- a/libavcodec/frame_thread_encoder.c
+++ b/libavcodec/frame_thread_encoder.c
@@ -105,6 +105,7 @@ static void * attribute_align_arg worker(void *v){
         frame = task->indata;
         pkt   = task->outdata;
 
+        ff_encode_extract_frame_props(avctx, frame);
         ret = ff_encode_encode_cb(avctx, pkt, frame, &task->got_packet);
         pthread_mutex_lock(&c->finished_task_mutex);
         task->return_code = ret;
-- 
2.54.0


>From 63fc49649ad966fe9362dd38ee871986b4f31d3f Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Thu, 22 Oct 2026 20:24:25 -0300
Subject: [PATCH 3/6] avcodec/encode: propagate frame side data

Much like it's done at init for global side data, copy some input frame side
data entries to the output packet.

Signed-off-by: James Almer <[email protected]>
---
 libavcodec/encode.c                        | 41 ++++++++++++++++++++++
 tests/ref/fate/filter-gainmap              |  2 +-
 tests/ref/fate/filter-gainmap-fixed        |  2 +-
 tests/ref/fate/filter-gainmap-luma         |  2 +-
 tests/ref/fate/filter-gainmap-maxrgb       |  2 +-
 tests/ref/fate/jpeg-mpf-gainmap            |  2 +-
 tests/ref/fate/jpg-exif-autorotate         |  2 +-
 tests/ref/fate/jpg-exif-noautorotate       |  2 +-
 tests/ref/fate/jpg-exif-rotation-override  |  2 +-
 tests/ref/fate/tiff-fax-g3                 |  2 +-
 tests/ref/fate/tiff-fax-g3s                |  2 +-
 tests/ref/fate/tiff-icc                    |  2 +-
 tests/ref/fate/tiff-lzw-rgbaf32le          |  2 +-
 tests/ref/fate/tiff-lzw-rgbf32le           |  2 +-
 tests/ref/fate/tiff-uncompressed-rgbaf32le |  2 +-
 tests/ref/fate/tiff-uncompressed-rgbf32le  |  2 +-
 tests/ref/fate/tiff-zip-rgbaf32le          |  2 +-
 tests/ref/fate/tiff-zip-rgbf32le           |  2 +-
 18 files changed, 58 insertions(+), 17 deletions(-)

diff --git a/libavcodec/encode.c b/libavcodec/encode.c
index 1f0a582b18e..d71f42d31b6 100644
--- a/libavcodec/encode.c
+++ b/libavcodec/encode.c
@@ -270,11 +270,52 @@ int ff_encode_get_frame(AVCodecContext *avctx, AVFrame 
*frame)
     return 0;
 }
 
+
+static int side_data_map(AVPacket *dst,
+                         AVFrameSideData *const *sd_src, int nb_sd_src,
+                         const SideDataMap *map)
+
+{
+    for (int i = 0; map[i].packet < AV_PKT_DATA_NB; i++) {
+        const enum AVPacketSideDataType type_packet = map[i].packet;
+        const enum AVFrameSideDataType  type_frame  = map[i].frame;
+        const AVFrameSideData *sd_frame;
+        AVPacketSideData      *sd_packet;
+
+        sd_frame = av_frame_side_data_get(sd_src, nb_sd_src, type_frame);
+        if (!sd_frame)
+            continue;
+
+        sd_packet = av_packet_side_data_new(&dst->side_data, 
&dst->side_data_elems,
+                                            type_packet, sd_frame->size, 0);
+        if (!sd_packet)
+            return AVERROR(ENOMEM);
+
+        memcpy(sd_packet->data, sd_frame->data, sd_frame->size);
+    }
+
+    return 0;
+}
+
 static int encode_set_packet_props(AVCodecContext *avctx, AVPacket *avpkt)
 {
     AVCodecInternal *avci = avctx->internal;
     EncodeContext     *ec = encode_ctx(avci);
     const AVFrame *frame  = ec->last_frame_props;
+    int ret;
+
+    static const SideDataMap sd[] = {
+        { AV_PKT_DATA_EXIF,                        AV_FRAME_DATA_EXIF },
+        { AV_PKT_DATA_GAIN_MAP_PARAMS,             
AV_FRAME_DATA_GAIN_MAP_PARAMS },
+        { AV_PKT_DATA_IAMF_MIX_GAIN_PARAM,         
AV_FRAME_DATA_IAMF_MIX_GAIN_PARAM },
+        { AV_PKT_DATA_IAMF_DEMIXING_INFO_PARAM,    
AV_FRAME_DATA_IAMF_DEMIXING_INFO_PARAM },
+        { AV_PKT_DATA_IAMF_RECON_GAIN_INFO_PARAM,  
AV_FRAME_DATA_IAMF_RECON_GAIN_INFO_PARAM },
+        { AV_PKT_DATA_NB },
+    };
+
+    ret = side_data_map(avpkt, frame->side_data, frame->nb_side_data, sd);
+    if (ret < 0)
+        return ret;
 
     if (avpkt->pts == AV_NOPTS_VALUE) {
         avpkt->pts = frame->pts;
diff --git a/tests/ref/fate/filter-gainmap b/tests/ref/fate/filter-gainmap
index 96d59042072..d6dc920d110 100644
--- a/tests/ref/fate/filter-gainmap
+++ b/tests/ref/fate/filter-gainmap
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 64x48
 #sar 0: 1/1
-0,          0,          0,        1,     9216, 0x91dedb2f
+0,          0,          0,        1,     9216, 0x91dedb2f, S=1, Gain Map 
Parameters,      148, 0x36e61447
diff --git a/tests/ref/fate/filter-gainmap-fixed 
b/tests/ref/fate/filter-gainmap-fixed
index 93b721a20ec..43524fbe6fe 100644
--- a/tests/ref/fate/filter-gainmap-fixed
+++ b/tests/ref/fate/filter-gainmap-fixed
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 64x48
 #sar 0: 1/1
-0,          0,          0,        1,     9216, 0x28fde0dc
+0,          0,          0,        1,     9216, 0x28fde0dc, S=1, Gain Map 
Parameters,      148, 0x4789039c
diff --git a/tests/ref/fate/filter-gainmap-luma 
b/tests/ref/fate/filter-gainmap-luma
index ea6d074487f..7f4db5fcbf0 100644
--- a/tests/ref/fate/filter-gainmap-luma
+++ b/tests/ref/fate/filter-gainmap-luma
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 64x48
 #sar 0: 1/1
-0,          0,          0,        1,     3072, 0xbfac1585
+0,          0,          0,        1,     3072, 0xbfac1585, S=1, Gain Map 
Parameters,      148, 0xf1fe094e
diff --git a/tests/ref/fate/filter-gainmap-maxrgb 
b/tests/ref/fate/filter-gainmap-maxrgb
index 1d521020042..cd767e967ce 100644
--- a/tests/ref/fate/filter-gainmap-maxrgb
+++ b/tests/ref/fate/filter-gainmap-maxrgb
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 64x48
 #sar 0: 1/1
-0,          0,          0,        1,     3072, 0x2253f63c
+0,          0,          0,        1,     3072, 0x2253f63c, S=1, Gain Map 
Parameters,      148, 0x181009a2
diff --git a/tests/ref/fate/jpeg-mpf-gainmap b/tests/ref/fate/jpeg-mpf-gainmap
index e39f5d8d979..3961959c240 100644
--- a/tests/ref/fate/jpeg-mpf-gainmap
+++ b/tests/ref/fate/jpeg-mpf-gainmap
@@ -11,7 +11,7 @@
 #dimensions 1: 64x48
 #sar 1: 1/1
 0,          0,          0,        1,     9216, 0xdfa842b3
-1,          0,          0,        1,     9216, 0x662fcc6a
+1,          0,          0,        1,     9216, 0x662fcc6a, S=1, Gain Map 
Parameters,      148, 0xf4dc167e
 frame|stream_index=0
 frame|stream_index=1|side_datum/gain_map_parameters:side_data_type=Gain map 
parameters|side_datum/gain_map_parameters:version=0|side_datum/gain_map_parameters:use_base_color_space=1|side_datum/gain_map_parameters:base_hdr_headroom=0/1|side_datum/gain_map_parameters:alternate_hdr_headroom=5895489/1048576|component_entry/rgb:gain_map_min=-3392315/4194304|component_entry/rgb:gain_map_max=13278945/2097152|component_entry/rgb:gamma=691797/1048576|component_entry/rgb:base_offset=1/64|component_entry/rgb:alternate_offset=1/64
 stream_group|index=0|nb_streams=2|type=Gain Map (Split base rendition and gain 
map)
diff --git a/tests/ref/fate/jpg-exif-autorotate 
b/tests/ref/fate/jpg-exif-autorotate
index 3210e0a1ee4..4d294dea884 100644
--- a/tests/ref/fate/jpg-exif-autorotate
+++ b/tests/ref/fate/jpg-exif-autorotate
@@ -5,7 +5,7 @@
 #codec_id 0: rawvideo
 #dimensions 0: 300x200
 #sar 0: 1/1
-0,          0,          0,        1,    90000, 0x85315584
+0,          0,          0,        1,    90000, 0x85315584, S=1, EXIF metadata, 
     360, 0x87f72133
 [FRAME]
 media_type=video
 stream_index=0
diff --git a/tests/ref/fate/jpg-exif-noautorotate 
b/tests/ref/fate/jpg-exif-noautorotate
index c18cdfff91b..fdb7afe61ee 100644
--- a/tests/ref/fate/jpg-exif-noautorotate
+++ b/tests/ref/fate/jpg-exif-noautorotate
@@ -5,7 +5,7 @@ e3f1a70f55d680145fca061e4a515133 
*tests/data/fate/jpg-exif-noautorotate.image2
 #codec_id 0: rawvideo
 #dimensions 0: 200x300
 #sar 0: 1/1
-0,          0,          0,        1,   180000, 0x4c43539a
+0,          0,          0,        1,   180000, 0x4c43539a, S=1, EXIF metadata, 
     144, 0xe072076c
 [FRAME]
 media_type=video
 stream_index=0
diff --git a/tests/ref/fate/jpg-exif-rotation-override 
b/tests/ref/fate/jpg-exif-rotation-override
index 40b758dd2b2..a63fb4260aa 100644
--- a/tests/ref/fate/jpg-exif-rotation-override
+++ b/tests/ref/fate/jpg-exif-rotation-override
@@ -5,7 +5,7 @@ e91d7401ba21087b06683672b0cdabd3 
*tests/data/fate/jpg-exif-rotation-override.ima
 #codec_id 0: rawvideo
 #dimensions 0: 200x300
 #sar 0: 1/1
-0,          0,          0,        1,    90000, 0x373a5584
+0,          0,          0,        1,    90000, 0x373a5584, S=1, EXIF metadata, 
     352, 0xca3a1ec0
 [FRAME]
 media_type=video
 stream_index=0
diff --git a/tests/ref/fate/tiff-fax-g3 b/tests/ref/fate/tiff-fax-g3
index ac1fff91420..cb76ad6573b 100644
--- a/tests/ref/fate/tiff-fax-g3
+++ b/tests/ref/fate/tiff-fax-g3
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 2464x3248
 #sar 0: 1/1
-0,          0,          0,        1,  1000384, 0xb758eb8b
+0,          0,          0,        1,  1000384, 0xb758eb8b, S=1, EXIF metadata, 
     255, 0x11f018a0
diff --git a/tests/ref/fate/tiff-fax-g3s b/tests/ref/fate/tiff-fax-g3s
index ac1fff91420..5fc9a58b140 100644
--- a/tests/ref/fate/tiff-fax-g3s
+++ b/tests/ref/fate/tiff-fax-g3s
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 2464x3248
 #sar 0: 1/1
-0,          0,          0,        1,  1000384, 0xb758eb8b
+0,          0,          0,        1,  1000384, 0xb758eb8b, S=1, EXIF metadata, 
     391, 0xebdd2fa6
diff --git a/tests/ref/fate/tiff-icc b/tests/ref/fate/tiff-icc
index f493629c558..245fba874e9 100644
--- a/tests/ref/fate/tiff-icc
+++ b/tests/ref/fate/tiff-icc
@@ -5,7 +5,7 @@ bcbf8f9edb2b96020b8cce6f0e27a34e 
*tests/data/fate/tiff-icc.image2
 #codec_id 0: rawvideo
 #dimensions 0: 1x1
 #sar 0: 1/1
-0,          0,          0,        1,        3, 0x034d0182
+0,          0,          0,        1,        3, 0x034d0182, S=1, EXIF metadata, 
    3348, 0xab6cfbf1
 [FRAME]
 [SIDE_DATA]
 side_data_type=ICC profile
diff --git a/tests/ref/fate/tiff-lzw-rgbaf32le 
b/tests/ref/fate/tiff-lzw-rgbaf32le
index c99aa02ef0d..96dbc7bea3c 100644
--- a/tests/ref/fate/tiff-lzw-rgbaf32le
+++ b/tests/ref/fate/tiff-lzw-rgbaf32le
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 8x8
 #sar 0: 0/1
-0,          0,          0,        1,     1024, 0x877e1d5f
+0,          0,          0,        1,     1024, 0x877e1d5f, S=1, EXIF metadata, 
     354, 0x46071a7a
diff --git a/tests/ref/fate/tiff-lzw-rgbf32le b/tests/ref/fate/tiff-lzw-rgbf32le
index a6d3fabfda1..d6d82cb6c2a 100644
--- a/tests/ref/fate/tiff-lzw-rgbf32le
+++ b/tests/ref/fate/tiff-lzw-rgbf32le
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 8x8
 #sar 0: 0/1
-0,          0,          0,        1,      768, 0xad26ed90
+0,          0,          0,        1,      768, 0xad26ed90, S=1, EXIF metadata, 
     330, 0xf8d819c0
diff --git a/tests/ref/fate/tiff-uncompressed-rgbaf32le 
b/tests/ref/fate/tiff-uncompressed-rgbaf32le
index c99aa02ef0d..afdf54b86e5 100644
--- a/tests/ref/fate/tiff-uncompressed-rgbaf32le
+++ b/tests/ref/fate/tiff-uncompressed-rgbaf32le
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 8x8
 #sar 0: 0/1
-0,          0,          0,        1,     1024, 0x877e1d5f
+0,          0,          0,        1,     1024, 0x877e1d5f, S=1, EXIF metadata, 
     206, 0xdb300a0d
diff --git a/tests/ref/fate/tiff-uncompressed-rgbf32le 
b/tests/ref/fate/tiff-uncompressed-rgbf32le
index a6d3fabfda1..ce98b6fffde 100644
--- a/tests/ref/fate/tiff-uncompressed-rgbf32le
+++ b/tests/ref/fate/tiff-uncompressed-rgbf32le
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 8x8
 #sar 0: 0/1
-0,          0,          0,        1,      768, 0xad26ed90
+0,          0,          0,        1,      768, 0xad26ed90, S=1, EXIF metadata, 
     318, 0xd76b1867
diff --git a/tests/ref/fate/tiff-zip-rgbaf32le 
b/tests/ref/fate/tiff-zip-rgbaf32le
index c99aa02ef0d..2542e2b98cb 100644
--- a/tests/ref/fate/tiff-zip-rgbaf32le
+++ b/tests/ref/fate/tiff-zip-rgbaf32le
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 8x8
 #sar 0: 0/1
-0,          0,          0,        1,     1024, 0x877e1d5f
+0,          0,          0,        1,     1024, 0x877e1d5f, S=1, EXIF metadata, 
     354, 0xe22c1a09
diff --git a/tests/ref/fate/tiff-zip-rgbf32le b/tests/ref/fate/tiff-zip-rgbf32le
index a6d3fabfda1..e30f6a3c306 100644
--- a/tests/ref/fate/tiff-zip-rgbf32le
+++ b/tests/ref/fate/tiff-zip-rgbf32le
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 8x8
 #sar 0: 0/1
-0,          0,          0,        1,      768, 0xad26ed90
+0,          0,          0,        1,      768, 0xad26ed90, S=1, EXIF metadata, 
     330, 0xaacc195d
-- 
2.54.0


>From 00d4b332715f0df29f46785e45b11420778822b6 Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Fri, 23 Oct 2026 12:02:05 -0300
Subject: [PATCH 4/6] fftools/ffmpeg_mux_init: add support for Alpha group type

Signed-off-by: James Almer <[email protected]>
---
 fftools/ffmpeg_mux_init.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c
index 011519f8d0d..5975e57f774 100644
--- a/fftools/ffmpeg_mux_init.c
+++ b/fftools/ffmpeg_mux_init.c
@@ -2648,6 +2648,7 @@ static int of_map_group(Muxer *mux, AVDictionary **dict, 
AVBPrint *bp, const cha
     }
     case AV_STREAM_GROUP_PARAMS_LCEVC:
     case AV_STREAM_GROUP_PARAMS_TREF:
+    case AV_STREAM_GROUP_PARAMS_ALPHA:
     case AV_STREAM_GROUP_PARAMS_DOLBY_VISION:
     case AV_STREAM_GROUP_PARAMS_GAIN_MAP:
         break;
@@ -2679,6 +2680,8 @@ static int of_parse_group_token(Muxer *mux, const char 
*token, char *ptr)
                 { .i64 = AV_STREAM_GROUP_PARAMS_TREF }, .unit = "type" },
             { "gain_map", NULL, 0, AV_OPT_TYPE_CONST,
                 { .i64 = AV_STREAM_GROUP_PARAMS_GAIN_MAP }, .unit = "type" },
+            { "alpha", NULL, 0, AV_OPT_TYPE_CONST,
+                { .i64 = AV_STREAM_GROUP_PARAMS_ALPHA }, .unit = "type" },
         { NULL },
     };
     const AVClass class = {
-- 
2.54.0


>From 3bf1af5ac26cacfb918fb18f9c42c63775126134 Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Fri, 23 Oct 2026 18:27:41 -0300
Subject: [PATCH 5/6] tests/fate/image: add tests for the HEIF muxer

Signed-off-by: James Almer <[email protected]>
---
 tests/fate/image.mak                        | 39 ++++++++++++++++++++-
 tests/filtergraphs/heif_alpha               |  3 ++
 tests/ref/fate/heif-mux-still-image-1-item  |  9 +++++
 tests/ref/fate/heif-mux-still-image-alpha   | 15 ++++++++
 tests/ref/fate/heif-mux-still-image-gainmap | 17 +++++++++
 5 files changed, 82 insertions(+), 1 deletion(-)
 create mode 100644 tests/filtergraphs/heif_alpha
 create mode 100644 tests/ref/fate/heif-mux-still-image-1-item
 create mode 100644 tests/ref/fate/heif-mux-still-image-alpha
 create mode 100644 tests/ref/fate/heif-mux-still-image-gainmap

diff --git a/tests/fate/image.mak b/tests/fate/image.mak
index a42d19861a3..c916a4e5a21 100644
--- a/tests/fate/image.mak
+++ b/tests/fate/image.mak
@@ -641,6 +641,43 @@ fate-jpeg-mpf-gainmap: CMD = transcode lavfi 
testsrc2=s=64x48:r=1:d=1 jpeg_mpf \
 FATE_FFMPEG_FFPROBE += $(FATE_JPEG_MPF-yes)
 fate-jpeg-mpf: $(FATE_JPEG_MPF-yes)
 
+# heif muxing - still image with 1 item.
+FATE_HEIF_SAMPLES-$(call REMUX, HEIF MOV, HEVC_DECODER HEVC_PARSER) \
+                           += fate-heif-mux-still-image-1-item
+fate-heif-mux-still-image-1-item: CMD = stream_remux mov 
$(TARGET_SAMPLES)/heif-conformance/C002.heic "" heif "" "" "-c:v copy"
+
+# heif jpeg muxing - still image with a Gain Map signaled with a tmap derived 
image
+FATE_HEIF-$(call ALLYES, LAVFI_INDEV TESTSRC2_FILTER SCALE_FILTER 
FORMAT_FILTER \
+                         SPLIT_FILTER GAINMAP_FILTER MJPEG_ENCODER \
+                         HEIF_MUXER FILE_PROTOCOL) += 
fate-heif-mux-still-image-gainmap
+fate-heif-mux-still-image-gainmap: tests/data/filtergraphs/jpeg_mpf_gainmap
+fate-heif-mux-still-image-gainmap: CMD = transcode lavfi 
testsrc2=s=64x48:r=1:d=1 heif \
+    "-/filter_complex $(TARGET_PATH)/tests/data/filtergraphs/jpeg_mpf_gainmap \
+     -map [base] -map [gain] -disposition:v:1 dependent -stream_group 
type=gain_map:video_size=64x48:st=0:st=1:el_index=1 \
+     -pix_fmt yuv444p -c:v mjpeg -dct int" "-c:v copy -map 0" \
+    "-of compact -show_entries 
stream_group=index,id,nb_streams,type:stream_group_components:stream_group_disposition:stream_group_tags:stream_group_side_data:stream_group_stream=index,id:stream_group_stream_disposition:stream_group_stream_side_data_list"
+
+# heif jpeg muxing - still image with Alpha channel signaled with an auxl 
reference
+tests/data/heif-mux-still-image-alpha.heic: TAG = GEN
+tests/data/heif-mux-still-image-alpha.heic: tests/data/filtergraphs/heif_alpha
+tests/data/heif-mux-still-image-alpha.heic: ffmpeg$(PROGSSUF)$(EXESUF) | 
tests/data
+       $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+    -i $(TARGET_SAMPLES)/png1/55c99e750a5fd6_50314226.png \
+    -sws_flags +accurate_rnd+bitexact \
+    -/filter_complex $(TARGET_PATH)/tests/data/filtergraphs/heif_alpha \
+    -map "[base]" -map "[alpha]" -disposition:v:1 dependent -stream_group 
"type=alpha:st=0:st=1:el_index=1" \
+    -flags +bitexact -fflags +bitexact -c:v mjpeg -dct int -f heif 
$(TARGET_PATH)/$@ -y 2>/dev/null
+
+FATE_HEIF_SAMPLES-$(call ALLYES, SCALE_FILTER FORMAT_FILTER SPLIT_FILTER 
ALPHAEXTRACT_FILTER \
+                                               PNG_DECODER MJPEG_ENCODER 
IMAGE_PNG_PIPE_DEMUXER HEIF_MUXER FILE_PROTOCOL) += 
fate-heif-mux-still-image-alpha
+fate-heif-mux-still-image-alpha: tests/data/heif-mux-still-image-alpha.heic
+fate-heif-mux-still-image-alpha: CMD = stream_demux mov 
$(TARGET_PATH)/tests/data/heif-mux-still-image-alpha.heic "" "-c:v copy -map 
0:g:0" \
+  "-of compact -show_entries 
stream_group=index,id,nb_streams,type:stream_group_components:stream_group_disposition:stream_group_tags:stream_group_side_data:stream_group_stream=index,id:stream_group_stream_disposition:stream_group_stream_side_data_list"
+
+FATE_FFMPEG_FFPROBE += $(FATE_HEIF-yes)
+FATE_SAMPLES_FFMPEG_FFPROBE += $(FATE_HEIF_SAMPLES-yes)
+fate-heif: $(FATE_HEIF-yes) $(FATE_HEIF_SAMPLES-yes)
+
 FATE_IMG2_MUXER-$(call ALLYES, LAVFI_INDEV COLOR_FILTER FORMAT_FILTER \
     PGM_ENCODER IMAGE2_MUXER IMAGE2_DEMUXER FILE_PROTOCOL FFPROBE) \
     += fate-img2-update-filemtime
@@ -653,4 +690,4 @@ FATE_SAMPLES_FFMPEG += $(FATE_IMAGE)
 FATE_SAMPLES_FFPROBE += $(FATE_IMAGE_PROBE)
 FATE_SAMPLES_FFMPEG_FFPROBE += $(FATE_IMAGE_TRANSCODE)
 
-fate-image: $(FATE_IMAGE) $(FATE_IMAGE_PROBE) $(FATE_IMAGE_TRANSCODE) 
$(FATE_IMG2_MUXER-yes) $(FATE_JPEG_MPF-yes)
+fate-image: $(FATE_IMAGE) $(FATE_IMAGE_PROBE) $(FATE_IMAGE_TRANSCODE) 
$(FATE_IMG2_MUXER-yes) $(FATE_JPEG_MPF-yes) $(FATE_HEIF-yes) 
$(FATE_HEIF_SAMPLES-yes)
diff --git a/tests/filtergraphs/heif_alpha b/tests/filtergraphs/heif_alpha
new file mode 100644
index 00000000000..4ca4c406aee
--- /dev/null
+++ b/tests/filtergraphs/heif_alpha
@@ -0,0 +1,3 @@
+[0:v]split[a][b];
+[a]scale,format=yuv444p[base];
+[b]alphaextract,scale,format=yuv420p[alpha]
diff --git a/tests/ref/fate/heif-mux-still-image-1-item 
b/tests/ref/fate/heif-mux-still-image-1-item
new file mode 100644
index 00000000000..64f1db968f8
--- /dev/null
+++ b/tests/ref/fate/heif-mux-still-image-1-item
@@ -0,0 +1,9 @@
+da9ee2beb0a504c7fec74fbe6aa3b67d 
*tests/data/fate/heif-mux-still-image-1-item.heif
+111923 tests/data/fate/heif-mux-still-image-1-item.heif
+#extradata 0:      100, 0xee3e15e9
+#tb 0: 1/1
+#media_type 0: video
+#codec_id 0: hevc
+#dimensions 0: 1280x720
+#sar 0: 0/1
+0,          0,          0,        1,   111554, 0xa0679859
diff --git a/tests/ref/fate/heif-mux-still-image-alpha 
b/tests/ref/fate/heif-mux-still-image-alpha
new file mode 100644
index 00000000000..62c159424c1
--- /dev/null
+++ b/tests/ref/fate/heif-mux-still-image-alpha
@@ -0,0 +1,15 @@
+#tb 0: 1/1
+#media_type 0: video
+#codec_id 0: mjpeg
+#dimensions 0: 330x330
+#sar 0: 0/1
+#tb 1: 1/1
+#media_type 1: video
+#codec_id 1: mjpeg
+#dimensions 1: 330x330
+#sar 1: 0/1
+0,          0,          0,        1,    20288, 0x486f2063
+1,          0,          0,        1,     5591, 0x402eae5c
+stream_group|index=0|id=0x2|nb_streams=2|type=Alpha (Split color and alpha 
channels)|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0
+stream|index=0|id=0x1|disposition:default=1|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0
+stream|index=1|id=0x2|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0
diff --git a/tests/ref/fate/heif-mux-still-image-gainmap 
b/tests/ref/fate/heif-mux-still-image-gainmap
new file mode 100644
index 00000000000..00cec836bf2
--- /dev/null
+++ b/tests/ref/fate/heif-mux-still-image-gainmap
@@ -0,0 +1,17 @@
+c0fbb770d9404a6346e61d6e60b55f16 
*tests/data/fate/heif-mux-still-image-gainmap.heif
+5108 tests/data/fate/heif-mux-still-image-gainmap.heif
+#tb 0: 1/1
+#media_type 0: video
+#codec_id 0: mjpeg
+#dimensions 0: 64x48
+#sar 0: 1/1
+#tb 1: 1/1
+#media_type 1: video
+#codec_id 1: mjpeg
+#dimensions 1: 64x48
+#sar 1: 1/1
+0,          0,          0,        1,     2659, 0xe03fbe12
+1,          0,          0,        1,     1955, 0xf1c1f1d5
+stream_group|index=0|id=0x3|nb_streams=2|type=Gain Map (Split base rendition 
and gain 
map)|component_entry/gain_map__split_base_rendition_and_gain_map_:el_index=1|component_entry/gain_map__split_base_rendition_and_gain_map_:width=64|component_entry/gain_map__split_base_rendition_and_gain_map_:height=48|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0
+stream|index=0|id=0x1|disposition:default=1|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0
+stream|index=1|id=0x2|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=1|disposition:still_image=0|disposition:multilayer=0|side_datum/gain_map_parameters:side_data_type=Gain
 Map 
Parameters|side_datum/gain_map_parameters:version=0|side_datum/gain_map_parameters:use_base_color_space=1|side_datum/gain_map_parameters:base_hdr_headroom=0/1|side_datum/gain_map_parameters:alternate_hdr_headroom=5895489/1048576|side_datum/gain_map_parameters:nb_channels=1|component_entry/gain_map_channel:gain_map_min=-3392315/4194304|component_entry/gain_map_channel:gain_map_max=13278945/2097152|component_entry/gain_map_channel:gamma=691797/104
 
8576|component_entry/gain_map_channel:base_offset=1/64|component_entry/gain_map_channel:alternate_offset=1/64
-- 
2.54.0


>From f251e571e3694a63adcfde6a0f4de4e9030109ac Mon Sep 17 00:00:00 2001
From: James Almer <[email protected]>
Date: Fri, 23 Oct 2026 18:28:11 -0300
Subject: [PATCH 6/6] tests/fate/ffmpeg: add a test for alpha merging in
 ffmpeg.c

Signed-off-by: James Almer <[email protected]>
---
 tests/fate/ffmpeg.mak                  | 7 +++++++
 tests/ref/fate/ffmpeg-heif-merge-alpha | 6 ++++++
 2 files changed, 13 insertions(+)
 create mode 100644 tests/ref/fate/ffmpeg-heif-merge-alpha

diff --git a/tests/fate/ffmpeg.mak b/tests/fate/ffmpeg.mak
index aa682ba202f..75aa2e0b42a 100644
--- a/tests/fate/ffmpeg.mak
+++ b/tests/fate/ffmpeg.mak
@@ -469,3 +469,10 @@ FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MOV, HEVC, 
HEVC_PARSER) += fate-ffmpeg-heif
 # binding the internal filtegraph with a caller defined filtergraph
 fate-ffmpeg-heif-merge-filtergraph: CMD = framecrc -i 
$(TARGET_SAMPLES)/heif-conformance/C007.heic -filter_complex 
"sws_flags=+accurate_rnd+bitexact\;[0:g:0]scale=w=1280:h=720[out]" -map "[out]"
 FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MOV, HEVC, HEVC_PARSER SCALE_FILTER) += 
fate-ffmpeg-heif-merge-filtergraph
+
+# alpha merging
+fate-ffmpeg-heif-merge-alpha: tests/data/heif-mux-still-image-alpha.heic
+fate-ffmpeg-heif-merge-alpha: CMD = framecrc -auto_conversion_filters -i 
$(TARGET_PATH)/tests/data/heif-mux-still-image-alpha.heic
+FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MOV, MJPEG, ALPHAEXTRACT_FILTER 
ALPHAMERGE_FILTER \
+                                                 SCALE_FILTER FORMAT_FILTER 
SPLIT_FILTER \
+                                                 PNG_DECODER MJPEG_ENCODER 
IMAGE_PNG_PIPE_DEMUXER HEIF_MUXER) += fate-ffmpeg-heif-merge-alpha
diff --git a/tests/ref/fate/ffmpeg-heif-merge-alpha 
b/tests/ref/fate/ffmpeg-heif-merge-alpha
new file mode 100644
index 00000000000..1352293e0e1
--- /dev/null
+++ b/tests/ref/fate/ffmpeg-heif-merge-alpha
@@ -0,0 +1,6 @@
+#tb 0: 1/1
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 330x330
+#sar 0: 0/1
+0,          0,          0,        1,   435600, 0xedd62079
-- 
2.54.0

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

Reply via email to