This is an automated email from the git hooks/post-receive script. Git pushed a commit to branch master in repository ffmpeg.
commit 6ae491a26c8320d1bbfdc3f137b1ea1e9989afaa Author: Niklas Haas <[email protected]> AuthorDate: Fri Aug 28 16:49:36 2026 +0200 Commit: Niklas Haas <[email protected]> CommitDate: Wed Sep 30 14:37:59 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 <[email protected]> --- libavformat/jpegmpfenc.c | 111 +++++++++++++++++++++++++++++++++++++++- tests/ref/fate/jpeg-mpf-gainmap | 2 +- 2 files changed, 111 insertions(+), 2 deletions(-) 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 -- To stop receiving notification emails like this one, please contact [email protected]. _______________________________________________ ffmpeg-cvslog mailing list -- [email protected] To unsubscribe send an email to [email protected]
