Commit 6ae491a26c for ffmpeg
commit 6ae491a26c8320d1bbfdc3f137b1ea1e9989afaa
Author: Niklas Haas <git@haasn.dev>
Date: Fri Aug 28 16:49:36 2026 +0200
avformat/jpegmpfenc: serialize GContainer XMP APP1 if needed
If the gain map image contains an hdrgm XMP blob, we need to serialize
the corresponding GContainer in the base image.
Signed-off-by: Niklas Haas <git@haasn.dev>
diff --git a/libavformat/jpegmpfenc.c b/libavformat/jpegmpfenc.c
index 1467ce1f1a..44d2a3b697 100644
--- a/libavformat/jpegmpfenc.c
+++ b/libavformat/jpegmpfenc.c
@@ -23,22 +23,29 @@
* Muxes several JPEG images into a single JPEG Multi-Picture Format file.
*/
+#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include "libavutil/avassert.h"
+#include "libavutil/avstring.h"
#include "libavutil/gain_map.h"
#include "libavutil/internal.h"
#include "libavutil/mem.h"
#include "libavcodec/bytestream.h"
#include "libavcodec/exif.h"
+#include "libavcodec/gain_map.h"
#include "libavcodec/mjpeg.h"
#include "avformat.h"
#include "avio_internal.h"
#include "mux.h"
+/* Namespace URIs of the GContainer XMP extension */
+#define XMP_CONTAINER_NS "http://ns.google.com/photos/1.0/container/"
+#define XMP_ITEM_NS "http://ns.google.com/photos/1.0/container/item/"
+
/* Individual Image Attribute bits, from CIPA DC-007 Figure 8 */
enum MPFAttribute {
MPF_DEPENDENT_PARENT = 0x80000000,
@@ -62,6 +69,7 @@ typedef enum MPFRole {
/* APP2 extension segments */
typedef enum MPFExt {
+ MPF_EXT_XMP, /* GContainer directory, First Individual Image only */
MPF_EXT_INDEX, /* MP Index IFD, First Individual Image only */
MPF_EXT_ISO21496, /* ISO 21496-1 version fields (in the base image) */
MPF_EXT_NB,
@@ -83,6 +91,8 @@ struct MPFImage {
/* Relative to the packet data (before splicing) */
int app2; /* offset to role-specific APP2 header */
+ int has_xmp; /* image already carries an XMP packet */
+ int has_hdrgm; /* .. containing Ultra HDR gain map metadata */
int splice_pos; /* where in `packet->data` to splice our segments */
/* Relative to the output file (after splicing) */
@@ -97,6 +107,11 @@ struct MPFImage {
typedef struct MPFMuxContext {
MPFImage *images;
+
+ /* Holds the XMP GContainer; if needed */
+#define XMP_GCONTAINER_MAX_LEN 1024 /* see XMP_GCONTAINER_TEMPLATE */
+ char xmp_gcontainer[1024];
+ int xmp_len; /* or 0 */
} MPFMuxContext;
static int register_dep(AVFormatContext *ctx, MPFImage *img, MPFImage *base)
@@ -204,6 +219,16 @@ static int mpf_init(AVFormatContext *ctx)
return 0;
}
+static void parse_app1(MPFImage *img, const GetByteContext *gb, int len)
+{
+ if (len < sizeof(FF_GAIN_MAP_XMP_IDENT) ||
+ memcmp(gb->buffer, FF_GAIN_MAP_XMP_IDENT, sizeof(FF_GAIN_MAP_XMP_IDENT)))
+ return; /* not an XMP packet; e.g. Exif */
+
+ img->has_xmp = 1;
+ img->has_hdrgm = !!av_strnstr(gb->buffer, FF_GAIN_MAP_XMP_NAMESPACE, len);
+}
+
static int parse_app2(AVFormatContext *ctx, MPFImage *img,
const GetByteContext *gb, int len)
{
@@ -275,8 +300,10 @@ static int mpf_write_packet(AVFormatContext *ctx, AVPacket *pkt)
switch (marker) {
case APP0:
- case APP1:
break; /* ignored */
+ case APP1:
+ parse_app1(img, &gb, len);
+ break; /* not spliced past */
case APP2:
ret = parse_app2(ctx, img, &gb, len);
if (ret < 0)
@@ -323,6 +350,80 @@ static void update_offset(AVIOContext *pb, int64_t base, int64_t *offset)
*offset = val;
}
+#define XMP_GCONTAINER_TEMPLATE \
+ "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"Adobe XMP Core 5.1.2\">\n" \
+ " <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n" \
+ " <rdf:Description\n" \
+ " xmlns:Container=\"" XMP_CONTAINER_NS "\"\n" \
+ " xmlns:Item=\"" XMP_ITEM_NS "\"\n" \
+ " xmlns:hdrgm=\"" FF_GAIN_MAP_XMP_NAMESPACE "\"\n" \
+ " hdrgm:Version=\"1.0\">\n" \
+ " <Container:Directory>\n" \
+ " <rdf:Seq>\n" \
+ " <rdf:li rdf:parseType=\"Resource\">\n" \
+ " <Container:Item\n" \
+ " Item:Semantic=\"Primary\"\n" \
+ " Item:Mime=\"image/jpeg\"/>\n" \
+ " </rdf:li>\n" \
+ " <rdf:li rdf:parseType=\"Resource\">\n" \
+ " <Container:Item\n" \
+ " Item:Semantic=\"GainMap\"\n" \
+ " Item:Mime=\"image/jpeg\"\n" \
+ " Item:Length=\"%d\"/>\n" \
+ " </rdf:li>\n" \
+ " </rdf:Seq>\n" \
+ " </Container:Directory>\n" \
+ " </rdf:Description>\n" \
+ " </rdf:RDF>\n" \
+ "</x:xmpmeta>"
+
+static_assert(XMP_GCONTAINER_MAX_LEN > sizeof(XMP_GCONTAINER_TEMPLATE) + 10,
+ "XMP buffer too small for template and values");
+
+static int mpf_build_gcontainer_xmp(AVFormatContext *ctx)
+{
+ MPFMuxContext *s = ctx->priv_data;
+ const MPFImage *base = &s->images[0];
+ const MPFImage *gain = NULL;
+ for (int i = 0; i < base->nb_rev_deps; i++) {
+ if (base->rev_deps[i]->role == MPF_ROLE_GAIN_MAP)
+ gain = base->rev_deps[i];
+ }
+
+ if (!gain || !gain->has_hdrgm)
+ return 0;
+
+ if (base->has_xmp) {
+ av_log(ctx, AV_LOG_WARNING, "The primary image already carries an XMP "
+ "packet; omitting the GContainer directory, as a JPEG file may "
+ "only contain one.\n");
+ return 0;
+ }
+
+ int ret = snprintf(s->xmp_gcontainer, sizeof(s->xmp_gcontainer),
+ XMP_GCONTAINER_TEMPLATE, gain->packet->size);
+ if (ret < 0 || ret >= sizeof(s->xmp_gcontainer))
+ return AVERROR_BUG;
+
+ s->xmp_len = ret;
+ return 0;
+}
+
+static void mpf_write_xmp(AVFormatContext *ctx, AVIOContext *pb, MPFImage *img)
+{
+ const MPFMuxContext *s = ctx->priv_data;
+
+ avio_w8(pb, 0xFF);
+ avio_w8(pb, APP1);
+
+ const int64_t start = avio_tell(pb);
+ avio_wb16(pb, img->ext_size[MPF_EXT_XMP]);
+ avio_write(pb, FF_GAIN_MAP_XMP_IDENT, sizeof(FF_GAIN_MAP_XMP_IDENT));
+ avio_write(pb, s->xmp_gcontainer, s->xmp_len);
+
+ update_offset(pb, start, &img->ext_size[MPF_EXT_XMP]);
+}
+
static int mpf_write_index(AVFormatContext *ctx, AVIOContext *pb, int64_t base,
MPFImage *img)
{
@@ -415,10 +516,15 @@ static void mpf_write_iso21496(AVIOContext *pb, MPFImage *src)
static int mpf_write_extensions(AVFormatContext *ctx, AVIOContext *pb,
int64_t base, MPFImage *img)
{
+ MPFMuxContext *s = ctx->priv_data;
int ret;
switch (img->role) {
case MPF_ROLE_PRIMARY:
+ /* The GContainer directory goes ahead of the MP Extensions, so that
+ * the latter still directly follow the APP1 segments (DC-007 Fig. 1) */
+ if (s->xmp_len)
+ mpf_write_xmp(ctx, pb, img);
if (ctx->nb_streams > 1) {
ret = mpf_write_index(ctx, pb, base, img);
if (ret < 0)
@@ -472,6 +578,9 @@ static int mpf_write_trailer(AVFormatContext *ctx)
}
}
+ if ((ret = mpf_build_gcontainer_xmp(ctx)) < 0)
+ return ret;
+
/* Write the whole file once to a null buffer to settle offsets/sizes */
AVIOContext *null;
if ((ret = ffio_open_null_buf(&null)) < 0)
diff --git a/tests/ref/fate/jpeg-mpf-gainmap b/tests/ref/fate/jpeg-mpf-gainmap
index 999852b117..c90fdd1fe1 100644
--- a/tests/ref/fate/jpeg-mpf-gainmap
+++ b/tests/ref/fate/jpeg-mpf-gainmap
@@ -1 +1 @@
-e7c134ebd17c6d040e8eeaaada4633f2
+7cab095419286aec7affcf4eb697f4c9