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