Commit 7db3357abf for ffmpeg

commit 7db3357abf7dc9c055393b5e3dcba62a2f38f97b
Author: Niklas Haas <git@haasn.dev>
Date:   Fri Aug 28 14:32:44 2026 +0200

    avcodec/gain_map: add support for serializing XMP metadata

    This is strictly worse than the ISO21496 blob, but apparently Google
    tools refuse to identify Jpeg Ultra HDR images as such without this
    stupid XML blob.

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

diff --git a/libavcodec/gain_map.c b/libavcodec/gain_map.c
index bdb455adca..172adc218b 100644
--- a/libavcodec/gain_map.c
+++ b/libavcodec/gain_map.c
@@ -16,7 +16,10 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */

+#include <stdio.h>
+
 #include "libavutil/avassert.h"
+#include "libavutil/common.h"
 #include "libavutil/error.h"
 #include "libavutil/gain_map.h"
 #include "libavutil/mem.h"
@@ -166,3 +169,73 @@ int ff_gain_map_params_to_iso21496(const AVGainMapParams *p, uint8_t *buf)

     return bytestream2_tell_p(&pb);
 }
+
+int ff_gain_map_params_check_xmp(const AVGainMapParams *p)
+{
+    if (av_gain_map_params_validate(p) < 0)
+        return 0;
+
+    if (p->version > FF_GAIN_MAP_VERSION)
+        return 0; /* e.g. libraries out of sync */
+
+    /* XMP gain maps only store one set of parameters (shared across channels),
+     * have no way to describe a map applied in the alternate rendition's
+     * color space, and explicitly reject no-op parameters */
+    return (p->nb_channels == 1 || av_gain_map_channels_identical(p)) &&
+           p->use_base_color_space &&
+           av_cmp_q(p->alternate_hdr_headroom, p->base_hdr_headroom) != 0;
+}
+
+#define XMP_GAIN_MAP_TEMPLATE                                                 \
+    "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 5.5.0\">\n"     \
+    "  <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n" \
+    "    <rdf:Description rdf:about=\"\"\n"                                   \
+    "     xmlns:hdrgm=\"" FF_GAIN_MAP_XMP_NAMESPACE "\"\n"                    \
+    "     hdrgm:Version=\"1.0\"\n"                                            \
+    "     hdrgm:GainMapMin=\"%g\"\n"                                          \
+    "     hdrgm:GainMapMax=\"%g\"\n"                                          \
+    "     hdrgm:Gamma=\"%g\"\n"                                               \
+    "     hdrgm:OffsetSDR=\"%g\"\n"                                           \
+    "     hdrgm:OffsetHDR=\"%g\"\n"                                           \
+    "     hdrgm:HDRCapacityMin=\"%g\"\n"                                      \
+    "     hdrgm:HDRCapacityMax=\"%g\"\n"                                      \
+    "     hdrgm:BaseRenditionIsHDR=\"%s\"/>\n"                                \
+    "  </rdf:RDF>\n"                                                          \
+    "</x:xmpmeta>"
+
+static_assert(FF_GAIN_MAP_XMP_MAX_LEN > sizeof(XMP_GAIN_MAP_TEMPLATE) + 8 * 10,
+              "XMP buffer too small for template and values");
+
+int ff_gain_map_params_to_xmp(const AVGainMapParams *p, char *buf)
+{
+    if (!p || !buf)
+        return AVERROR(EINVAL);
+
+    if (!ff_gain_map_params_check_xmp(p))
+        return AVERROR(ENOTSUP);
+
+    const struct AVGainMapChannel *const ch = &p->channels[0];
+    AVRational offset_sdr  = ch->base_offset;
+    AVRational offset_hdr  = ch->alternate_offset;
+    AVRational hdr_cap_min = p->base_hdr_headroom;
+    AVRational hdr_cap_max = p->alternate_hdr_headroom;
+
+    const int base_is_hdr = av_cmp_q(p->base_hdr_headroom,
+                                     p->alternate_hdr_headroom) > 0;
+    if (base_is_hdr) {
+        FFSWAP(AVRational, offset_sdr, offset_hdr);
+        FFSWAP(AVRational, hdr_cap_min, hdr_cap_max);
+    }
+
+    const size_t size = FF_GAIN_MAP_XMP_MAX_LEN;
+    int ret = snprintf(buf, size, XMP_GAIN_MAP_TEMPLATE,
+                       av_q2d(ch->gain_map_min), av_q2d(ch->gain_map_max),
+                       av_q2d(ch->gamma),
+                       av_q2d(offset_sdr), av_q2d(offset_hdr),
+                       av_q2d(hdr_cap_min), av_q2d(hdr_cap_max),
+                       base_is_hdr ? "True" : "False");
+    if (ret < 0 || ret >= size)
+        return AVERROR_BUG;
+
+    return ret;
+}
diff --git a/libavcodec/gain_map.h b/libavcodec/gain_map.h
index 0889939e35..53207e448b 100644
--- a/libavcodec/gain_map.h
+++ b/libavcodec/gain_map.h
@@ -33,6 +33,11 @@
 #define FF_GAIN_MAP_VERSION          0
 #define FF_GAIN_MAP_MAX_PAYLOAD_SIZE 141 /* = 5 + 2*8 + 3*5*8 */

+/* For XMP-based encoding */
+#define FF_GAIN_MAP_XMP_IDENT     "http://ns.adobe.com/xap/1.0/"
+#define FF_GAIN_MAP_XMP_NAMESPACE "http://ns.adobe.com/hdr-gain-map/1.0/"
+#define FF_GAIN_MAP_XMP_MAX_LEN   1024
+
 /**
  * Parse the payload of an ISO 21496-1 gain map metadata blob.
  *
@@ -62,4 +67,23 @@ int ff_gain_map_params_from_iso21496(AVGainMapParams *p, const uint8_t *data,
  */
 int ff_gain_map_params_to_iso21496(const AVGainMapParams *p, uint8_t *buf);

+/**
+ * Returns 1 if the gain map parameters are compatible with the XMP-based
+ * encoding, or 0 otherwise.
+ */
+int ff_gain_map_params_check_xmp(const AVGainMapParams *p);
+
+/**
+ * Serialize an AVGainMapParams as an Ultra HDR XMP packet.
+ *
+ * @param p    parameters to serialize
+ * @param buf  Byte buffer to fill with the serialized data. Must contain at
+ *             least FF_GAIN_MAP_MAX_XMP_LEN bytes.
+ *
+ * @return Number of bytes written on success, or a negative AVERROR on failure
+ *
+ * @note Always succeeds if ff_gain_map_params_check_xmp() returns 1.
+ */
+int ff_gain_map_params_to_xmp(const AVGainMapParams *p, char *buf);
+
 #endif /* AVCODEC_GAIN_MAP_H */