| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * JPEG Multi-Picture Format muxer | ||
| 3 | * | ||
| 4 | * This file is part of FFmpeg. | ||
| 5 | * | ||
| 6 | * FFmpeg is free software; you can redistribute it and/or | ||
| 7 | * modify it under the terms of the GNU Lesser General Public | ||
| 8 | * License as published by the Free Software Foundation; either | ||
| 9 | * version 2.1 of the License, or (at your option) any later version. | ||
| 10 | * | ||
| 11 | * FFmpeg is distributed in the hope that it will be useful, | ||
| 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| 14 | * Lesser General Public License for more details. | ||
| 15 | * | ||
| 16 | * You should have received a copy of the GNU Lesser General Public | ||
| 17 | * License along with FFmpeg; if not, write to the Free Software | ||
| 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
| 19 | */ | ||
| 20 | |||
| 21 | /** | ||
| 22 | * @file | ||
| 23 | * Muxes several JPEG images into a single JPEG Multi-Picture Format file. | ||
| 24 | */ | ||
| 25 | |||
| 26 | #include <stdio.h> | ||
| 27 | #include <string.h> | ||
| 28 | #include <stddef.h> | ||
| 29 | |||
| 30 | #include "libavutil/avassert.h" | ||
| 31 | #include "libavutil/avstring.h" | ||
| 32 | #include "libavutil/gain_map.h" | ||
| 33 | #include "libavutil/internal.h" | ||
| 34 | #include "libavutil/mem.h" | ||
| 35 | |||
| 36 | #include "libavcodec/bytestream.h" | ||
| 37 | #include "libavcodec/exif.h" | ||
| 38 | #include "libavcodec/gain_map.h" | ||
| 39 | #include "libavcodec/mjpeg.h" | ||
| 40 | |||
| 41 | #include "avformat.h" | ||
| 42 | #include "avio_internal.h" | ||
| 43 | #include "mux.h" | ||
| 44 | |||
| 45 | /* Namespace URIs of the GContainer XMP extension */ | ||
| 46 | #define XMP_CONTAINER_NS "http://ns.google.com/photos/1.0/container/" | ||
| 47 | #define XMP_ITEM_NS "http://ns.google.com/photos/1.0/container/item/" | ||
| 48 | |||
| 49 | /* Individual Image Attribute bits, from CIPA DC-007 Figure 8 */ | ||
| 50 | enum MPFAttribute { | ||
| 51 | MPF_DEPENDENT_PARENT = 0x80000000, | ||
| 52 | MPF_DEPENDENT_CHILD = 0x40000000, | ||
| 53 | MPF_FORMAT_JPEG = 0x00000000, | ||
| 54 | }; | ||
| 55 | |||
| 56 | /* MP Index IFD tags, from CIPA DC-007 Table 3 */ | ||
| 57 | enum MPFTag { | ||
| 58 | MPF_TAG_MPFVersion = 0xB000, | ||
| 59 | MPF_TAG_NumberOfImages = 0xB001, | ||
| 60 | MPF_TAG_MPEntry = 0xB002, | ||
| 61 | }; | ||
| 62 | |||
| 63 | typedef enum MPFRole { | ||
| 64 | MPF_ROLE_UNKNOWN = 0, | ||
| 65 | MPF_ROLE_PRIMARY, | ||
| 66 | MPF_ROLE_GAIN_MAP, | ||
| 67 | MPF_ROLE_NB, | ||
| 68 | } MPFRole; | ||
| 69 | |||
| 70 | /* APP2 extension segments */ | ||
| 71 | typedef enum MPFExt { | ||
| 72 | MPF_EXT_XMP, /* GContainer directory, First Individual Image only */ | ||
| 73 | MPF_EXT_INDEX, /* MP Index IFD, First Individual Image only */ | ||
| 74 | MPF_EXT_ISO21496, /* ISO 21496-1 version fields (in the base image) */ | ||
| 75 | MPF_EXT_NB, | ||
| 76 | } MPFExt; | ||
| 77 | |||
| 78 | static const struct { | ||
| 79 | const char *name; | ||
| 80 | uint32_t type; /* from CIPA DC-007 Table 4 */ | ||
| 81 | } mpf_roles[MPF_ROLE_NB] = { | ||
| 82 | [MPF_ROLE_PRIMARY] = { "primary image", 0x030000 }, | ||
| 83 | [MPF_ROLE_GAIN_MAP] = { "gain map image", 0x050000 }, | ||
| 84 | }; | ||
| 85 | |||
| 86 | typedef struct MPFImage MPFImage; | ||
| 87 | struct MPFImage { | ||
| 88 | AVPacket *packet; | ||
| 89 | int index; | ||
| 90 | MPFRole role; | ||
| 91 | |||
| 92 | /* Relative to the packet data (before splicing) */ | ||
| 93 | int app2; /* offset to role-specific APP2 header */ | ||
| 94 | int has_xmp; /* image already carries an XMP packet */ | ||
| 95 | int has_hdrgm; /* .. containing Ultra HDR gain map metadata */ | ||
| 96 | int splice_pos; /* where in `packet->data` to splice our segments */ | ||
| 97 | |||
| 98 | /* Relative to the output file (after splicing) */ | ||
| 99 | int64_t start, end; /* start and end of this image in the output stream */ | ||
| 100 | int64_t ext_size[MPF_EXT_NB]; /* APP2 payload sizes */ | ||
| 101 | |||
| 102 | /* Image dependency metadata */ | ||
| 103 | MPFImage *dep; | ||
| 104 | MPFImage *rev_deps[2]; /* maximum 2 permissible by the spec */ | ||
| 105 | int nb_rev_deps; | ||
| 106 | }; | ||
| 107 | |||
| 108 | typedef struct MPFMuxContext { | ||
| 109 | MPFImage *images; | ||
| 110 | |||
| 111 | /* Holds the XMP GContainer; if needed */ | ||
| 112 | #define XMP_GCONTAINER_MAX_LEN 1024 /* see XMP_GCONTAINER_TEMPLATE */ | ||
| 113 | char xmp_gcontainer[1024]; | ||
| 114 | int xmp_len; /* or 0 */ | ||
| 115 | } MPFMuxContext; | ||
| 116 | |||
| 117 | 1 | static int register_dep(AVFormatContext *ctx, MPFImage *img, MPFImage *base) | |
| 118 | { | ||
| 119 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (base->nb_rev_deps == FF_ARRAY_ELEMS(base->rev_deps)) { |
| 120 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d cannot depend on %d, maximum " | |
| 121 | "number of dependents reached (%d).\n", img->index, base->index, | ||
| 122 | (int) FF_ARRAY_ELEMS(base->rev_deps)); | ||
| 123 | ✗ | return AVERROR(EINVAL); | |
| 124 | } | ||
| 125 | |||
| 126 | 1 | base->rev_deps[base->nb_rev_deps++] = img; | |
| 127 | 1 | img->dep = base; | |
| 128 | 1 | return 0; | |
| 129 | } | ||
| 130 | |||
| 131 | 1 | static int init_gain_map(AVFormatContext *ctx, const AVStreamGroup *sg) | |
| 132 | { | ||
| 133 | 1 | MPFMuxContext *s = ctx->priv_data; | |
| 134 | 1 | const AVStreamGroupLayeredVideo *lv = sg->params.layered_video; | |
| 135 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
|
1 | if (sg->nb_streams != 2 || lv->el_index >= sg->nb_streams) |
| 136 | ✗ | return AVERROR(EINVAL); | |
| 137 | |||
| 138 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | const AVStream *bl = sg->streams[!lv->el_index]; |
| 139 | 1 | const AVStream *el = sg->streams[lv->el_index]; | |
| 140 | 1 | MPFImage *base = &s->images[bl->index]; | |
| 141 | 1 | MPFImage *gain = &s->images[el->index]; | |
| 142 | |||
| 143 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (base->index != 0) { |
| 144 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream group %u: the gain map base layer must " | |
| 145 | "be the primary image (stream 0), got stream %d\n", | ||
| 146 | ✗ | sg->index, base->index); | |
| 147 | ✗ | return AVERROR(EINVAL); | |
| 148 | } | ||
| 149 | |||
| 150 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (gain->role != MPF_ROLE_UNKNOWN) { |
| 151 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream group %u: stream %d already used " | |
| 152 | ✗ | "as %s for a different stream group.\n", sg->index, gain->index, | |
| 153 | ✗ | mpf_roles[gain->role].name); | |
| 154 | ✗ | return AVERROR(EINVAL); | |
| 155 | } | ||
| 156 | |||
| 157 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | for (int i = 0; i < base->nb_rev_deps; i++) { |
| 158 | ✗ | const MPFImage *other = base->rev_deps[i]; | |
| 159 | ✗ | if (other->role == MPF_ROLE_GAIN_MAP) { | |
| 160 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream group %u: stream %d already " | |
| 161 | ✗ | "specified as a gain map for stream %d.\n", sg->index, | |
| 162 | ✗ | other->index, base->index); | |
| 163 | ✗ | return AVERROR(EINVAL); | |
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | 1 | gain->role = MPF_ROLE_GAIN_MAP; | |
| 168 | 1 | return register_dep(ctx, gain, base); | |
| 169 | } | ||
| 170 | |||
| 171 | 1 | static int mpf_init(AVFormatContext *ctx) | |
| 172 | { | ||
| 173 | int ret; | ||
| 174 | |||
| 175 | 1 | MPFMuxContext *s = ctx->priv_data; | |
| 176 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (ctx->nb_streams < 2) |
| 177 | ✗ | av_log(ctx, AV_LOG_WARNING, "Specified only a single stream (no-op)\n"); | |
| 178 | |||
| 179 | 1 | s->images = av_calloc(ctx->nb_streams, sizeof(*s->images)); | |
| 180 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (!s->images) |
| 181 | ✗ | return AVERROR(ENOMEM); | |
| 182 | |||
| 183 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
|
3 | for (int i = 0; i < ctx->nb_streams; i++) { |
| 184 | 2 | MPFImage *img = &s->images[i]; | |
| 185 | 2 | img->index = i; | |
| 186 | 2 | img->start = img->end = -1; | |
| 187 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
|
8 | for (int n = 0; n < MPF_EXT_NB; n++) |
| 188 | 6 | img->ext_size[n] = -1; | |
| 189 | 2 | img->packet = av_packet_alloc(); | |
| 190 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (!img->packet) |
| 191 | ✗ | return AVERROR(ENOMEM); | |
| 192 | } | ||
| 193 | |||
| 194 | /* Classify streams by their role */ | ||
| 195 | 1 | s->images[0].role = MPF_ROLE_PRIMARY; | |
| 196 | |||
| 197 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
|
2 | for (unsigned i = 0; i < ctx->nb_stream_groups; i++) { |
| 198 | 1 | const AVStreamGroup *sg = ctx->stream_groups[i]; | |
| 199 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | switch (sg->type) { |
| 200 | 1 | case AV_STREAM_GROUP_PARAMS_GAIN_MAP: | |
| 201 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
|
1 | if ((ret = init_gain_map(ctx, sg)) < 0) |
| 202 | ✗ | return ret; | |
| 203 | 1 | break; | |
| 204 | |||
| 205 | ✗ | default: | |
| 206 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream group %u: unknown type '%s'\n", i, | |
| 207 | ✗ | avformat_stream_group_name(sg->type)); | |
| 208 | ✗ | return AVERROR(EINVAL); | |
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
|
3 | for (int i = 0; i < ctx->nb_streams; i++) { |
| 213 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (s->images[i].role == MPF_ROLE_UNKNOWN) { |
| 214 | ✗ | av_log(ctx, AV_LOG_ERROR, "Missing stream group for stream %d.\n", i); | |
| 215 | ✗ | return AVERROR(EINVAL); | |
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 219 | 1 | return 0; | |
| 220 | } | ||
| 221 | |||
| 222 | 1 | static void parse_app1(MPFImage *img, const GetByteContext *gb, int len) | |
| 223 | { | ||
| 224 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (len < sizeof(FF_GAIN_MAP_XMP_IDENT) || |
| 225 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | memcmp(gb->buffer, FF_GAIN_MAP_XMP_IDENT, sizeof(FF_GAIN_MAP_XMP_IDENT))) |
| 226 | ✗ | return; /* not an XMP packet; e.g. Exif */ | |
| 227 | |||
| 228 | 1 | img->has_xmp = 1; | |
| 229 | 1 | img->has_hdrgm = !!av_strnstr(gb->buffer, FF_GAIN_MAP_XMP_NAMESPACE, len); | |
| 230 | } | ||
| 231 | |||
| 232 | 1 | static int parse_app2(AVFormatContext *ctx, MPFImage *img, | |
| 233 | const GetByteContext *gb, int len) | ||
| 234 | { | ||
| 235 | #define CHECK_IDENT(strc) (len >= sizeof(strc) && !memcmp(gb->buffer, strc, sizeof(strc))) | ||
| 236 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | switch (img->role) { |
| 237 | 1 | case MPF_ROLE_GAIN_MAP: | |
| 238 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | if (CHECK_IDENT(AV_ISO21496_IDENTIFIER)) { |
| 239 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (img->app2) { |
| 240 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d: multiple ISO 21496-1 " | |
| 241 | "APP2 segments\n", img->index); | ||
| 242 | ✗ | return AVERROR_INVALIDDATA; | |
| 243 | } | ||
| 244 | |||
| 245 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (len < sizeof(AV_ISO21496_IDENTIFIER) + sizeof(uint16_t[2])) { |
| 246 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d: ISO 21496-1 payload too " | |
| 247 | "short\n", img->index); | ||
| 248 | ✗ | return AVERROR_INVALIDDATA; | |
| 249 | } | ||
| 250 | 1 | img->app2 = bytestream2_tell(gb); | |
| 251 | } | ||
| 252 | 1 | break; | |
| 253 | } | ||
| 254 | #undef CHECK_IDENT | ||
| 255 | |||
| 256 | 1 | return 0; | |
| 257 | } | ||
| 258 | |||
| 259 | 2 | static int mpf_write_packet(AVFormatContext *ctx, AVPacket *pkt) | |
| 260 | { | ||
| 261 | int ret; | ||
| 262 | 2 | MPFMuxContext *s = ctx->priv_data; | |
| 263 | 2 | MPFImage *img = &s->images[pkt->stream_index]; | |
| 264 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (img->packet->size) { |
| 265 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d: expected a single JPEG image\n", img->index); | |
| 266 | ✗ | return AVERROR(EINVAL); | |
| 267 | } | ||
| 268 | |||
| 269 | GetByteContext gb; | ||
| 270 | 2 | bytestream2_init(&gb, pkt->data, pkt->size); | |
| 271 |
2/4✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
|
4 | if (bytestream2_get_bytes_left(&gb) < 2 || |
| 272 | 2 | bytestream2_get_be16(&gb) != (0xFF00 | SOI)) | |
| 273 | { | ||
| 274 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d: not a JPEG image (no SOI)\n", img->index); | |
| 275 | ✗ | return AVERROR_INVALIDDATA; | |
| 276 | } | ||
| 277 | |||
| 278 | /* Walk the JPEG marker sections up until the start of entropy coded data */ | ||
| 279 |
1/2✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
|
12 | while (bytestream2_get_bytes_left(&gb) >= 4) { |
| 280 | 12 | const int pos = bytestream2_tell(&gb); | |
| 281 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
|
12 | if (bytestream2_get_byte(&gb) != 0xFF) { |
| 282 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d: malformed JPEG marker at 0x%x\n", | |
| 283 | img->index, pos); | ||
| 284 | ✗ | return AVERROR_INVALIDDATA; | |
| 285 | } | ||
| 286 | |||
| 287 | 12 | int marker = bytestream2_get_byte(&gb); | |
| 288 |
6/8✓ Branch 0 taken 10 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 4 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 6 times.
|
12 | if (marker == SOS || marker == TEM || (marker >= RST0 && marker <= EOI)) { |
| 289 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (!img->splice_pos) |
| 290 | ✗ | img->splice_pos = pos; | |
| 291 | 2 | break; /* start of entropy coded data */ | |
| 292 | } | ||
| 293 | |||
| 294 | 10 | int len = bytestream2_get_be16(&gb) - 2; | |
| 295 |
2/4✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
|
10 | if (len < 0 || len > bytestream2_get_bytes_left(&gb)) { |
| 296 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d: truncated marker segment at " | |
| 297 | "0x%x\n", img->index, pos); | ||
| 298 | ✗ | return AVERROR_INVALIDDATA; | |
| 299 | } | ||
| 300 | |||
| 301 |
4/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 6 times.
|
10 | switch (marker) { |
| 302 | 2 | case APP0: | |
| 303 | 2 | break; /* ignored */ | |
| 304 | 1 | case APP1: | |
| 305 | 1 | parse_app1(img, &gb, len); | |
| 306 | 1 | break; /* not spliced past */ | |
| 307 | 1 | case APP2: | |
| 308 | 1 | ret = parse_app2(ctx, img, &gb, len); | |
| 309 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (ret < 0) |
| 310 | ✗ | return ret; | |
| 311 | av_fallthrough; | ||
| 312 | default: | ||
| 313 | /** | ||
| 314 | * Splice position for the MPF metadata shall be the first segment | ||
| 315 | * position after any SOI, APP0 or APP1 (cf. CIPA DC-007) | ||
| 316 | */ | ||
| 317 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 5 times.
|
7 | if (!img->splice_pos) |
| 318 | 2 | img->splice_pos = pos; | |
| 319 | 7 | break; | |
| 320 | } | ||
| 321 | |||
| 322 | 10 | bytestream2_skip(&gb, len); | |
| 323 | } | ||
| 324 | |||
| 325 | /* Verify completeness of all needed information */ | ||
| 326 |
2/3✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
2 | switch (img->role) { |
| 327 | 1 | case MPF_ROLE_PRIMARY: | |
| 328 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (!img->splice_pos) { |
| 329 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d: truncated JPEG data\n", img->index); | |
| 330 | ✗ | return AVERROR_INVALIDDATA; | |
| 331 | } | ||
| 332 | 1 | break; | |
| 333 | 1 | case MPF_ROLE_GAIN_MAP: | |
| 334 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (!img->app2) { |
| 335 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d was declared as a gain map " | |
| 336 | "but is missing ISO 21496-1 metadata\n", img->index); | ||
| 337 | ✗ | return AVERROR_INVALIDDATA; | |
| 338 | } | ||
| 339 | 1 | break; | |
| 340 | } | ||
| 341 | |||
| 342 | 2 | return av_packet_ref(img->packet, pkt); | |
| 343 | } | ||
| 344 | |||
| 345 | 14 | static void update_offset(AVIOContext *pb, int64_t base, int64_t *offset) | |
| 346 | { | ||
| 347 | 14 | int64_t val = avio_tell(pb) - base; | |
| 348 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
|
14 | av_assert0(val >= 0); |
| 349 |
3/4✓ Branch 0 taken 7 times.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
|
14 | av_assert0(*offset < 0 || *offset == val); |
| 350 | 14 | *offset = val; | |
| 351 | 14 | } | |
| 352 | |||
| 353 | #define XMP_GCONTAINER_TEMPLATE \ | ||
| 354 | "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"Adobe XMP Core 5.1.2\">\n" \ | ||
| 355 | " <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n" \ | ||
| 356 | " <rdf:Description\n" \ | ||
| 357 | " xmlns:Container=\"" XMP_CONTAINER_NS "\"\n" \ | ||
| 358 | " xmlns:Item=\"" XMP_ITEM_NS "\"\n" \ | ||
| 359 | " xmlns:hdrgm=\"" FF_GAIN_MAP_XMP_NAMESPACE "\"\n" \ | ||
| 360 | " hdrgm:Version=\"1.0\">\n" \ | ||
| 361 | " <Container:Directory>\n" \ | ||
| 362 | " <rdf:Seq>\n" \ | ||
| 363 | " <rdf:li rdf:parseType=\"Resource\">\n" \ | ||
| 364 | " <Container:Item\n" \ | ||
| 365 | " Item:Semantic=\"Primary\"\n" \ | ||
| 366 | " Item:Mime=\"image/jpeg\"/>\n" \ | ||
| 367 | " </rdf:li>\n" \ | ||
| 368 | " <rdf:li rdf:parseType=\"Resource\">\n" \ | ||
| 369 | " <Container:Item\n" \ | ||
| 370 | " Item:Semantic=\"GainMap\"\n" \ | ||
| 371 | " Item:Mime=\"image/jpeg\"\n" \ | ||
| 372 | " Item:Length=\"%d\"/>\n" \ | ||
| 373 | " </rdf:li>\n" \ | ||
| 374 | " </rdf:Seq>\n" \ | ||
| 375 | " </Container:Directory>\n" \ | ||
| 376 | " </rdf:Description>\n" \ | ||
| 377 | " </rdf:RDF>\n" \ | ||
| 378 | "</x:xmpmeta>" | ||
| 379 | |||
| 380 | static_assert(XMP_GCONTAINER_MAX_LEN > sizeof(XMP_GCONTAINER_TEMPLATE) + 10, | ||
| 381 | "XMP buffer too small for template and values"); | ||
| 382 | |||
| 383 | 1 | static int mpf_build_gcontainer_xmp(AVFormatContext *ctx) | |
| 384 | { | ||
| 385 | 1 | MPFMuxContext *s = ctx->priv_data; | |
| 386 | 1 | const MPFImage *base = &s->images[0]; | |
| 387 | 1 | const MPFImage *gain = NULL; | |
| 388 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
|
2 | for (int i = 0; i < base->nb_rev_deps; i++) { |
| 389 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (base->rev_deps[i]->role == MPF_ROLE_GAIN_MAP) |
| 390 | 1 | gain = base->rev_deps[i]; | |
| 391 | } | ||
| 392 | |||
| 393 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
|
1 | if (!gain || !gain->has_hdrgm) |
| 394 | ✗ | return 0; | |
| 395 | |||
| 396 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (base->has_xmp) { |
| 397 | ✗ | av_log(ctx, AV_LOG_WARNING, "The primary image already carries an XMP " | |
| 398 | "packet; omitting the GContainer directory, as a JPEG file may " | ||
| 399 | "only contain one.\n"); | ||
| 400 | ✗ | return 0; | |
| 401 | } | ||
| 402 | |||
| 403 | 1 | int ret = snprintf(s->xmp_gcontainer, sizeof(s->xmp_gcontainer), | |
| 404 | 1 | XMP_GCONTAINER_TEMPLATE, gain->packet->size); | |
| 405 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
|
1 | if (ret < 0 || ret >= sizeof(s->xmp_gcontainer)) |
| 406 | ✗ | return AVERROR_BUG; | |
| 407 | |||
| 408 | 1 | s->xmp_len = ret; | |
| 409 | 1 | return 0; | |
| 410 | } | ||
| 411 | |||
| 412 | 2 | static void mpf_write_xmp(AVFormatContext *ctx, AVIOContext *pb, MPFImage *img) | |
| 413 | { | ||
| 414 | 2 | const MPFMuxContext *s = ctx->priv_data; | |
| 415 | |||
| 416 | 2 | avio_w8(pb, 0xFF); | |
| 417 | 2 | avio_w8(pb, APP1); | |
| 418 | |||
| 419 | 2 | const int64_t start = avio_tell(pb); | |
| 420 | 2 | avio_wb16(pb, img->ext_size[MPF_EXT_XMP]); | |
| 421 | 2 | avio_write(pb, FF_GAIN_MAP_XMP_IDENT, sizeof(FF_GAIN_MAP_XMP_IDENT)); | |
| 422 | 2 | avio_write(pb, s->xmp_gcontainer, s->xmp_len); | |
| 423 | |||
| 424 | 2 | update_offset(pb, start, &img->ext_size[MPF_EXT_XMP]); | |
| 425 | 2 | } | |
| 426 | |||
| 427 | 2 | static int mpf_write_index(AVFormatContext *ctx, AVIOContext *pb, int64_t base, | |
| 428 | MPFImage *img) | ||
| 429 | { | ||
| 430 | 2 | const MPFMuxContext *s = ctx->priv_data; | |
| 431 | |||
| 432 | 2 | avio_w8(pb, 0xFF); | |
| 433 | 2 | avio_w8(pb, APP2); | |
| 434 | 2 | const int64_t app2_start = avio_tell(pb) - base; | |
| 435 | 2 | avio_wb16(pb, img->ext_size[MPF_EXT_INDEX]); | |
| 436 | 2 | avio_write(pb, "MPF\0", 4); | |
| 437 | |||
| 438 | /* every offset below is relative to this byte */ | ||
| 439 | 2 | int64_t tiff_base = avio_tell(pb) - base; | |
| 440 | #define RELPOS(x) (avio_tell(pb) - base - tiff_base + (x)) | ||
| 441 | |||
| 442 | 2 | avio_write(pb, "MM\0*", 4); /* big endian */ | |
| 443 | 2 | avio_wb32(pb, RELPOS(4)); /* offset to first IFD */ | |
| 444 | |||
| 445 | /* First (and only) IFD */ | ||
| 446 | 2 | const int nb_tags = 3; /* MPFVersion, NumberOfImages, MPEntry */ | |
| 447 | 2 | avio_wb16(pb, nb_tags); /* number of IFD tags */ | |
| 448 | |||
| 449 | /* MPFVersion tag */ | ||
| 450 | 2 | avio_wb16(pb, MPF_TAG_MPFVersion); | |
| 451 | 2 | avio_wb16(pb, AV_TIFF_UNDEFINED); | |
| 452 | 2 | avio_wb32(pb, 4); /* count */ | |
| 453 | 2 | avio_write(pb, "0100", 4); | |
| 454 | |||
| 455 | /* NumberOfImages tag */ | ||
| 456 | 2 | avio_wb16(pb, MPF_TAG_NumberOfImages); | |
| 457 | 2 | avio_wb16(pb, AV_TIFF_LONG); | |
| 458 | 2 | avio_wb32(pb, 1); | |
| 459 | 2 | avio_wb32(pb, ctx->nb_streams); | |
| 460 | |||
| 461 | /* MPEntry tag */ | ||
| 462 | 2 | const int64_t entry_size = sizeof(int32_t[3]) + sizeof(int16_t[2]); | |
| 463 | 2 | avio_wb16(pb, MPF_TAG_MPEntry); | |
| 464 | 2 | avio_wb16(pb, AV_TIFF_UNDEFINED); | |
| 465 | 2 | avio_wb32(pb, ctx->nb_streams * entry_size); | |
| 466 | 2 | avio_wb32(pb, RELPOS(4 + 4)); /* offset to MPEntry data */ | |
| 467 | 2 | avio_wb32(pb, 0); /* no MP Attribute IFD, per DC-007 6.1.1.1 */ | |
| 468 | |||
| 469 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
|
6 | for (int i = 0; i < ctx->nb_streams; i++) { |
| 470 | 4 | const MPFImage *entry = &s->images[i]; | |
| 471 | 4 | int64_t size = entry->end - entry->start; | |
| 472 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | av_assert0(size <= UINT32_MAX); /* would overflow null buffer anyways */ |
| 473 | 4 | uint32_t attr = MPF_FORMAT_JPEG | mpf_roles[entry->role].type; | |
| 474 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
4 | if (entry->nb_rev_deps) |
| 475 | 2 | attr |= MPF_DEPENDENT_PARENT; | |
| 476 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
4 | if (entry->dep) |
| 477 | 2 | attr |= MPF_DEPENDENT_CHILD; | |
| 478 | |||
| 479 | 4 | avio_wb32(pb, attr); | |
| 480 | 4 | avio_wb32(pb, size); | |
| 481 | |||
| 482 | /* The data offset of the First Individual Image is always zero */ | ||
| 483 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
4 | int64_t offset = i ? entry->start - tiff_base : 0; |
| 484 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | av_assert0(offset <= UINT32_MAX); |
| 485 | 4 | avio_wb32(pb, offset); | |
| 486 | |||
| 487 | /* Indices are 1-based; 0 means "no dependent image" */ | ||
| 488 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 4 times.
|
12 | for (int j = 0; j < FF_ARRAY_ELEMS(entry->rev_deps); j++) { |
| 489 | 8 | const MPFImage *rdep = entry->rev_deps[j]; | |
| 490 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
|
8 | avio_wb16(pb, rdep ? rdep->index + 1 : 0); |
| 491 | } | ||
| 492 | } | ||
| 493 | #undef RELPOS | ||
| 494 | |||
| 495 | 2 | update_offset(pb, base + app2_start, &img->ext_size[MPF_EXT_INDEX]); | |
| 496 | 2 | return 0; | |
| 497 | } | ||
| 498 | |||
| 499 | 2 | static void mpf_write_iso21496(AVIOContext *pb, MPFImage *src) | |
| 500 | { | ||
| 501 | 2 | avio_w8(pb, 0xFF); | |
| 502 | 2 | avio_w8(pb, APP2); | |
| 503 | |||
| 504 | 2 | const int64_t start = avio_tell(pb); | |
| 505 | 2 | avio_wb16(pb, src->ext_size[MPF_EXT_ISO21496]); | |
| 506 | |||
| 507 | /* The primary image contains a subset of the ISO 21496-1 struct, carrying | ||
| 508 | * just the version header (two 16-bit integers), but no payload */ | ||
| 509 | 2 | avio_write(pb, AV_ISO21496_IDENTIFIER, sizeof(AV_ISO21496_IDENTIFIER)); | |
| 510 | 2 | avio_write(pb, src->packet->data + src->app2 + sizeof(AV_ISO21496_IDENTIFIER), | |
| 511 | sizeof(uint16_t[2])); | ||
| 512 | |||
| 513 | 2 | update_offset(pb, start, &src->ext_size[MPF_EXT_ISO21496]); | |
| 514 | 2 | } | |
| 515 | |||
| 516 | 4 | static int mpf_write_extensions(AVFormatContext *ctx, AVIOContext *pb, | |
| 517 | int64_t base, MPFImage *img) | ||
| 518 | { | ||
| 519 | 4 | MPFMuxContext *s = ctx->priv_data; | |
| 520 | int ret; | ||
| 521 | |||
| 522 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
4 | switch (img->role) { |
| 523 | 2 | case MPF_ROLE_PRIMARY: | |
| 524 | /* The GContainer directory goes ahead of the MP Extensions, so that | ||
| 525 | * the latter still directly follow the APP1 segments (DC-007 Fig. 1) */ | ||
| 526 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if (s->xmp_len) |
| 527 | 2 | mpf_write_xmp(ctx, pb, img); | |
| 528 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if (ctx->nb_streams > 1) { |
| 529 | 2 | ret = mpf_write_index(ctx, pb, base, img); | |
| 530 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (ret < 0) |
| 531 | ✗ | return ret; | |
| 532 | } | ||
| 533 | 2 | break; | |
| 534 | } | ||
| 535 | |||
| 536 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
|
6 | for (int i = 0; i < img->nb_rev_deps; i++) { |
| 537 | 2 | MPFImage *rdep = img->rev_deps[i]; | |
| 538 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | switch (rdep->role) { |
| 539 | 2 | case MPF_ROLE_GAIN_MAP: | |
| 540 | 2 | mpf_write_iso21496(pb, rdep); | |
| 541 | 2 | break; | |
| 542 | } | ||
| 543 | } | ||
| 544 | |||
| 545 | 4 | return 0; | |
| 546 | } | ||
| 547 | |||
| 548 | 2 | static int mpf_write(AVFormatContext *ctx, AVIOContext *pb) | |
| 549 | { | ||
| 550 | 2 | MPFMuxContext *s = ctx->priv_data; | |
| 551 | 2 | const int64_t base = avio_tell(pb); | |
| 552 | int ret; | ||
| 553 | |||
| 554 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
|
6 | for (int i = 0; i < ctx->nb_streams; i++) { |
| 555 | 4 | MPFImage *img = &s->images[i]; | |
| 556 | 4 | const AVPacket *pkt = img->packet; | |
| 557 | |||
| 558 | 4 | update_offset(pb, base, &img->start); | |
| 559 | 4 | avio_write(pb, pkt->data, img->splice_pos); | |
| 560 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
|
4 | if ((ret = mpf_write_extensions(ctx, pb, base, img)) < 0) |
| 561 | ✗ | return ret; | |
| 562 | 4 | avio_write(pb, pkt->data + img->splice_pos, pkt->size - img->splice_pos); | |
| 563 | 4 | update_offset(pb, base, &img->end); | |
| 564 | } | ||
| 565 | |||
| 566 | 2 | return 0; | |
| 567 | } | ||
| 568 | |||
| 569 | 1 | static int mpf_write_trailer(AVFormatContext *ctx) | |
| 570 | { | ||
| 571 | 1 | MPFMuxContext *s = ctx->priv_data; | |
| 572 | int ret; | ||
| 573 | |||
| 574 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
|
3 | for (int i = 0; i < ctx->nb_streams; i++) { |
| 575 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (!s->images[i].packet->size) { |
| 576 | ✗ | av_log(ctx, AV_LOG_ERROR, "No image was supplied on stream %d\n", i); | |
| 577 | ✗ | return AVERROR(EINVAL); | |
| 578 | } | ||
| 579 | } | ||
| 580 | |||
| 581 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
|
1 | if ((ret = mpf_build_gcontainer_xmp(ctx)) < 0) |
| 582 | ✗ | return ret; | |
| 583 | |||
| 584 | /* Write the whole file once to a null buffer to settle offsets/sizes */ | ||
| 585 | AVIOContext *null; | ||
| 586 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
|
1 | if ((ret = ffio_open_null_buf(&null)) < 0) |
| 587 | ✗ | return ret; | |
| 588 | 1 | ret = mpf_write(ctx, null); | |
| 589 | 1 | int size = ffio_close_null_buf(null); | |
| 590 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (ret < 0) |
| 591 | ✗ | return ret; | |
| 592 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | else if (size < 0) |
| 593 | ✗ | return size; | |
| 594 | |||
| 595 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
|
3 | for (int i = 0; i < ctx->nb_streams; i++) { |
| 596 | 2 | MPFImage *img = &s->images[i]; | |
| 597 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
|
8 | for (int j = 0; j < MPF_EXT_NB; j++) { |
| 598 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
|
6 | if (img->ext_size[j] > UINT16_MAX) { |
| 599 | ✗ | av_log(ctx, AV_LOG_ERROR, "Stream %d: APP2 segment %d is too " | |
| 600 | "large (%"PRId64" bytes)\n", img->index, j, img->ext_size[j]); | ||
| 601 | ✗ | return AVERROR(EINVAL); | |
| 602 | } | ||
| 603 | } | ||
| 604 | } | ||
| 605 | |||
| 606 | 1 | return mpf_write(ctx, ctx->pb); | |
| 607 | } | ||
| 608 | |||
| 609 | 1 | static void mpf_uninit(AVFormatContext *ctx) | |
| 610 | { | ||
| 611 | 1 | MPFMuxContext *s = ctx->priv_data; | |
| 612 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (!s->images) |
| 613 | ✗ | return; | |
| 614 | |||
| 615 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
|
3 | for (int i = 0; i < ctx->nb_streams; i++) |
| 616 | 2 | av_packet_free(&s->images[i].packet); | |
| 617 | |||
| 618 | 1 | av_freep(&s->images); | |
| 619 | } | ||
| 620 | |||
| 621 | const FFOutputFormat ff_jpeg_mpf_muxer = { | ||
| 622 | .p.name = "jpeg_mpf", | ||
| 623 | .p.long_name = NULL_IF_CONFIG_SMALL("JPEG Multi-Picture Format"), | ||
| 624 | .p.mime_type = "image/jpeg", | ||
| 625 | .p.extensions = "jpg,jpeg", | ||
| 626 | .priv_data_size = sizeof(MPFMuxContext), | ||
| 627 | .p.flags = AVFMT_NOTIMESTAMPS | AVFMT_NODIMENSIONS, | ||
| 628 | .p.video_codec = AV_CODEC_ID_MJPEG, | ||
| 629 | .flags_internal = FF_OFMT_FLAG_ONLY_DEFAULT_CODECS, | ||
| 630 | .init = mpf_init, | ||
| 631 | .deinit = mpf_uninit, | ||
| 632 | .write_packet = mpf_write_packet, | ||
| 633 | .write_trailer = mpf_write_trailer, | ||
| 634 | .interleave_packet = ff_interleave_packet_passthrough, | ||
| 635 | }; | ||
| 636 |