Commit ebbc9b6600 for ffmpeg

commit ebbc9b6600d36608f523d0d97fcb4c52322870e5
Author: Niklas Haas <git@haasn.dev>
Date:   Fri Aug 28 16:49:27 2026 +0200

    avcodec/mjpegenc: add -xmp_gain_map option

    Controls whether or not to serialize the gain map as an XMP blob in
    addition to an ISO blob. Defaults to -1, meaning to serialize the
    gain map only if compatible.

    Signed-off-by: Niklas Haas <git@haasn.dev>

diff --git a/libavcodec/mjpegenc.c b/libavcodec/mjpegenc.c
index b9ebe3d769..8dc3217acf 100644
--- a/libavcodec/mjpegenc.c
+++ b/libavcodec/mjpegenc.c
@@ -644,11 +644,12 @@ static int amv_encode_picture(AVCodecContext *avctx, AVPacket *pkt,
 #define OFFSET(x) offsetof(MJPEGEncContext, mjpeg.x)
 #define VE AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_ENCODING_PARAM
 static const AVOption options[] = {
-#define AMV_OPTIONS_OFFSET 4
+#define AMV_OPTIONS_OFFSET 5
 { "huffman", "Huffman table strategy", OFFSET(huffman), AV_OPT_TYPE_INT, { .i64 = HUFFMAN_TABLE_OPTIMAL }, 0, NB_HUFFMAN_TABLE_OPTION - 1, VE, .unit = "huffman" },
     { "default", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = HUFFMAN_TABLE_DEFAULT }, INT_MIN, INT_MAX, VE, .unit = "huffman" },
     { "optimal", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = HUFFMAN_TABLE_OPTIMAL }, INT_MIN, INT_MAX, VE, .unit = "huffman" },
 { "force_duplicated_matrix", "Always write luma and chroma matrix for mjpeg, useful for rtp streaming.", OFFSET(force_duplicated_matrix), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, VE },
+{ "xmp_gain_map", "Write Ultra HDR gain map metadata as an XMP packet", OFFSET(xmp_gain_map), AV_OPT_TYPE_BOOL, { .i64 = -1 }, -1, 1, VE },
 FF_MPV_COMMON_OPTS
 { NULL},
 };
diff --git a/libavcodec/mjpegenc.h b/libavcodec/mjpegenc.h
index 92feed28b4..865f13f50e 100644
--- a/libavcodec/mjpegenc.h
+++ b/libavcodec/mjpegenc.h
@@ -45,6 +45,9 @@ typedef struct MJpegContext {
     int huffman;
     /* Force duplication of mjpeg matrices, useful for rtp streaming */
     int force_duplicated_matrix;
+    /* Tristate controlling the Ultra HDR gain map XMP packet */
+    int xmp_gain_map;
+    int xmp_warned;
     //FIXME use array [3] instead of lumi / chroma, for easier addressing
     uint8_t huff_size_dc_luminance[12];     ///< DC luminance Huffman table size.
     uint16_t huff_code_dc_luminance[12];    ///< DC luminance Huffman table codes.
diff --git a/libavcodec/mjpegenc_common.c b/libavcodec/mjpegenc_common.c
index a4464c3f90..235a46799a 100644
--- a/libavcodec/mjpegenc_common.c
+++ b/libavcodec/mjpegenc_common.c
@@ -160,8 +160,14 @@ int ff_mjpeg_add_icc_profile_size(AVCodecContext *avctx, const AVFrame *frame,
     return 0;
 }

-int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, const AVFrame *frame,
-                               size_t *max_pkt_size)
+static int want_gain_map_xmp(const struct MJpegContext *m,
+                             const AVGainMapParams *p)
+{
+    return m && m->xmp_gain_map && ff_gain_map_params_check_xmp(p);
+}
+
+int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, struct MJpegContext *m,
+                               const AVFrame *frame, size_t *max_pkt_size)
 {
     if (avctx->codec_id != AV_CODEC_ID_MJPEG)
         return 0;
@@ -176,8 +182,20 @@ int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, const AVFrame *frame,
     if (ret < 0)
         return ret;

-    size_t app2_size = 4 + sizeof(AV_ISO21496_IDENTIFIER) + FF_GAIN_MAP_MAX_PAYLOAD_SIZE;
-    size_t new_pkt_size = *max_pkt_size + app2_size;
+    size_t extra_size = 4 + sizeof(AV_ISO21496_IDENTIFIER) + FF_GAIN_MAP_MAX_PAYLOAD_SIZE;
+    if (m && m->xmp_gain_map) {
+        if (!ff_gain_map_params_check_xmp(params)) {
+            const int level = m->xmp_gain_map > 0 ? AV_LOG_ERROR : AV_LOG_WARNING;
+            av_log_once(avctx, level, AV_LOG_VERBOSE, &m->xmp_warned,
+                        "Gain map metadata is incompatible with Ultra HDR XMP\n");
+            if (m->xmp_gain_map > 0)
+                return AVERROR(ENOTSUP);
+        } else {
+            extra_size += 4 + sizeof(FF_GAIN_MAP_XMP_IDENT) + FF_GAIN_MAP_XMP_MAX_LEN;
+        }
+    }
+
+    size_t new_pkt_size = *max_pkt_size + extra_size;
     if (new_pkt_size < *max_pkt_size) /* overflow */
         return AVERROR_INVALIDDATA;
     *max_pkt_size = new_pkt_size;
@@ -185,7 +203,7 @@ int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, const AVFrame *frame,
 }

 static void jpeg_put_comments(AVCodecContext *avctx, PutBitContext *p,
-                              const AVFrame *frame)
+                              const AVFrame *frame, const struct MJpegContext *m)
 {
     const AVFrameSideData *sd = NULL;
     int size;
@@ -246,9 +264,27 @@ static void jpeg_put_comments(AVCodecContext *avctx, PutBitContext *p,
         av_assert1(!remaining);
     }

-    /* ISO 21496-1 gain map metadata */
+    /* Gain map metadata; the ISO 21496-1 blob is written directly after the
+     * XMP packet, as recommended by the Ultra HDR specification */
     sd = avctx->codec_id == AV_CODEC_ID_MJPEG ?
          av_frame_get_side_data(frame, AV_FRAME_DATA_GAIN_MAP_PARAMS) : NULL;
+
+    const AVGainMapParams *gmp = sd ? (const AVGainMapParams *) sd->data : NULL;
+    if (sd && sd->size >= sizeof(AVGainMapParams) && want_gain_map_xmp(m, gmp)) {
+        put_marker(p, APP1);
+        flush_put_bits(p);
+        ptr = put_bits_ptr(p);
+        put_bits(p, 16, 0); /* patched later */
+        ff_put_string(p, FF_GAIN_MAP_XMP_IDENT, 1);
+        flush_put_bits(p);
+
+        /* pre-validated by ff_mjpeg_add_gain_map_size() */
+        size = ff_gain_map_params_to_xmp(gmp, put_bits_ptr(p));
+        av_assert0(size >= 0);
+        skip_put_bytes(p, size);
+        AV_WB16(ptr, 2 + sizeof(FF_GAIN_MAP_XMP_IDENT) + size);
+    }
+
     if (sd && sd->size >= sizeof(AVGainMapParams)) {
         put_marker(p, APP2);
         flush_put_bits(p);
@@ -258,8 +294,7 @@ static void jpeg_put_comments(AVCodecContext *avctx, PutBitContext *p,
         flush_put_bits(p);

         /* pre-validated by ff_mjpeg_add_gain_map_size() */
-        const AVGainMapParams *params = (const AVGainMapParams *) sd->data;
-        size = ff_gain_map_params_to_iso21496(params, put_bits_ptr(p));
+        size = ff_gain_map_params_to_iso21496(gmp, put_bits_ptr(p));
         av_assert0(size >= 0);
         skip_put_bytes(p, size);
         AV_WB16(ptr, 2 + sizeof(AV_ISO21496_IDENTIFIER) + size);
@@ -336,7 +371,7 @@ void ff_mjpeg_encode_picture_header(AVCodecContext *avctx, PutBitContext *pb,
     if (avctx->codec_id == AV_CODEC_ID_AMV)
         return;

-    jpeg_put_comments(avctx, pb, frame);
+    jpeg_put_comments(avctx, pb, frame, m);

     chroma_matrix = !lossless && !!memcmp(luma_intra_matrix,
                                           chroma_intra_matrix,
diff --git a/libavcodec/mjpegenc_common.h b/libavcodec/mjpegenc_common.h
index 4d1e2a5477..20e3902c03 100644
--- a/libavcodec/mjpegenc_common.h
+++ b/libavcodec/mjpegenc_common.h
@@ -30,8 +30,8 @@ struct MJpegContext;

 int ff_mjpeg_add_icc_profile_size(AVCodecContext *avctx, const AVFrame *frame,
                                   size_t *max_pkt_size);
-int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, const AVFrame *frame,
-                               size_t *max_pkt_size);
+int ff_mjpeg_add_gain_map_size(AVCodecContext *avctx, struct MJpegContext *m,
+                               const AVFrame *frame, size_t *max_pkt_size);
 void ff_mjpeg_encode_picture_header(AVCodecContext *avctx, PutBitContext *pb,
                                     const AVFrame *frame, const struct MJpegContext *m,
                                     const uint8_t intra_matrix_permutation[64],
diff --git a/libavcodec/mpegvideo_enc.c b/libavcodec/mpegvideo_enc.c
index 592334473a..3908628504 100644
--- a/libavcodec/mpegvideo_enc.c
+++ b/libavcodec/mpegvideo_enc.c
@@ -1939,7 +1939,7 @@ int ff_mpv_encode_picture(AVCodecContext *avctx, AVPacket *pkt,
             ret = ff_mjpeg_add_icc_profile_size(avctx, s->new_pic, &pkt_size);
             if (ret < 0)
                 return ret;
-            ret = ff_mjpeg_add_gain_map_size(avctx, s->new_pic, &pkt_size);
+            ret = ff_mjpeg_add_gain_map_size(avctx, s->mjpeg_ctx, s->new_pic, &pkt_size);
             if (ret < 0)
                 return ret;
         }
diff --git a/tests/ref/fate/jpeg-mpf-gainmap b/tests/ref/fate/jpeg-mpf-gainmap
index 2fee05c244..999852b117 100644
--- a/tests/ref/fate/jpeg-mpf-gainmap
+++ b/tests/ref/fate/jpeg-mpf-gainmap
@@ -1 +1 @@
-50dc4be69672174d4ff3ca8dbdaaa9d3
+e7c134ebd17c6d040e8eeaaada4633f2